01Что умеет API
API для серверов компании, которая подключена к xWires: кадровой системы, учёта, согласований, своих сервисов. Почти всё, что система делает через API, админ компании может и в кабинете («Компания»); подписку по session сотрудника там оформляет оператор xWires:
| В кабинете | Через API |
|---|---|
| Проверить ключ и узнать свою компанию | GET /me |
| «Сотрудники»: кто в компании, их подписки | GET /employeesGET /employees/{id}POST /employees/search |
| «Уволился» у сотрудника (не у админа) | POST /employees/{id}/offboard |
| «Ждут подтверждения»: «Подтвердить» и «Отклонить» | POST /orders/{id}/approvePOST /orders/{id}/reject |
| Подписка сотруднику по его session — в кабинете её оформляет оператор xWires | POST /subscriptions |
| «Последние заказы» | GET /ordersGET /orders/{id} |
| «Добавить почты», вручную или файлом | POST /approved-emails |
| «Почты без заказа» и «Удалить» | GET /approved-emailsDELETE /approved-emails/{id} |
| «Не оплачивать, только пригласить», приглашения и «Отозвать» | POST /invitesGET /invitesDELETE /invites/{id} |
| Что изменилось: заказы, сотрудники, подписки | GET /events |
Об изменениях API сообщает лентой событий, а вебхуки присылают их на адреса ваших систем (раздел 13). Только в кабинете остаются ключи API и адреса вебхуков, назначение и снятие админов, отметка «Уволился» у админа, ссылка для сотрудников и номера для подтверждения ChatGPT.
Это не API к моделям. OpenAI-совместимый API к DeepSeek, GLM и Kimi — другой продукт со своими ключами, он описан на странице «API к моделям». Ключ API управления к нему не подходит, и наоборот.
02Быстрый старт
- Админ компании открывает «Компания → Интеграции» и в «Ключах API» создаёт ключ: название, например «1С: кадры», и доступ — «Только чтение» или «Чтение и действия». Ключ показывается один раз, скопируйте его сразу. Всем админам компании придёт письмо о новом ключе.
- Положите ключ в секреты сервера, который ходит в API, под именем
XWIRES_COMPANY_API_KEY. В код, репозиторий, браузер и мобильное приложение ключ не попадает. - Проверьте ключ:
curl -sS https://api.xwires.ai/company/v1/me \ -H "Authorization: Bearer $XWIRES_COMPANY_API_KEY"
{
"key": {
"id": "0b7d9e2a-5c13-4f6e-8a21-9d4c3b7e1f60",
"name": "1С: кадры",
"access": "manage",
"last4": "a91f",
"created_at": "2026-10-15T09:12:40.118204+00:00"
},
"company": {
"id": "6f1c2b9e-3d4a-4c71-9a5e-2b8f0d1c7a43",
"name": "Ромашка"
}
}Postman, Insomnia и генераторы клиентов берут описание API по ссылке: Import → Link → https://api.xwires.ai/company/v1/openapi.json. Ключ укажите в Authorization как Bearer Token. Статусы, типы событий и другие значения в ответах в v1 только добавляются, поэтому в описании они перечислены в x-extensible-enum, а не в закрытом enum: генератор делает из них строки, и новое значение не ломает разбор ответа. Событие нового типа подходит под схему UnknownEvent, последнюю в Event и WebhookEvent.
Тот же запрос и список заказов, которые ждут подтверждения, из кода:
const BASE = "https://api.xwires.ai/company/v1";
// Один запрос к API: ответ как объект, ошибка как исключение с status, code и request_id.
// error.retryAfter — через сколько секунд повторить, когда повтор поможет: 429 rate_limited, 503,
// сбой сети и ответ не от API (прокси, пока xWires перезапускается).
async function xwires(method, path, body) {
let response;
try {
response = await fetch(BASE + path, {
method,
headers: {
Authorization: `Bearer ${process.env.XWIRES_COMPANY_API_KEY}`,
...(body === undefined ? {} : { "Content-Type": "application/json" }),
},
body: body === undefined ? undefined : JSON.stringify(body),
});
} catch (cause) {
throw Object.assign(new Error(`xWires не ответил: ${cause.message}`), { retryAfter: 5 });
}
if (response.status === 204) return null;
const data = await response.json().catch(() => null);
if (response.ok) return data;
const retryAfter = Number(response.headers.get("Retry-After")) || null;
if (!data?.error) {
// Не ответ API: 405 у адреса без такого метода или 502 и 504 прокси, пока xWires перезапускается.
throw Object.assign(new Error(`${response.status} без ответа API`), {
status: response.status,
retryAfter: retryAfter ?? (response.status >= 500 ? 5 : null),
});
}
const { code, message, request_id } = data.error;
throw Object.assign(new Error(`${response.status} ${code}: ${message} (request_id ${request_id})`), {
status: response.status,
code,
retryAfter,
});
}
const me = await xwires("GET", "/me");
console.log(me.company.name, me.key.access);
const waiting = await xwires("GET", "/orders?status=awaiting_approval");
for (const order of waiting.data) console.log(order.id, order.plan, order.employee?.email);# pip install requests
import os
import requests
BASE = "https://api.xwires.ai/company/v1"
session = requests.Session()
session.headers["Authorization"] = "Bearer " + os.environ["XWIRES_COMPANY_API_KEY"]
class XWiresError(Exception):
# status и code — из ответа API. retry_after — через сколько секунд повторить, когда повтор поможет:
# 429 rate_limited, 503, сбой сети и ответ не от API (прокси, пока xWires перезапускается).
def __init__(self, message, status=None, code=None, retry_after=None):
super().__init__(message)
self.status = status
self.code = code
self.retry_after = retry_after
# Один запрос к API: ответ как словарь, ошибка как XWiresError с кодом и request_id.
def xwires(method, path, body=None, params=None):
try:
response = session.request(method, BASE + path, json=body, params=params, timeout=30)
except requests.RequestException as error:
raise XWiresError(f"xWires не ответил: {error}", retry_after=5) from error
if response.status_code == 204:
return None
try:
data = response.json()
except ValueError:
data = None
if response.ok:
return data
header = response.headers.get("Retry-After", "")
retry_after = int(header) if header.isdigit() else None
if not isinstance(data, dict) or "error" not in data:
# Не ответ API: 405 у адреса без такого метода или 502 и 504 прокси, пока xWires перезапускается.
if retry_after is None and response.status_code >= 500:
retry_after = 5
raise XWiresError(f"{response.status_code} без ответа API", response.status_code, None, retry_after)
error = data["error"]
raise XWiresError(
f'{response.status_code} {error["code"]}: {error["message"]} (request_id {error["request_id"]})',
response.status_code,
error["code"],
retry_after,
)
me = xwires("GET", "/me")
print(me["company"]["name"], me["key"]["access"])
waiting = xwires("GET", "/orders", params={"status": "awaiting_approval"})
for order in waiting["data"]:
print(order["id"], order["plan"], (order["employee"] or {}).get("email"))03Ключи и доступ
Ключ — xwc_ и 64 шестнадцатеричных символа, ключ песочницы — xwc_test_ и 64 (раздел 11). Передавайте его только в заголовке Authorization: Bearer xwc_…. Без ключа, с испорченным или незнакомым ключом ответ — 401 invalid_api_key.
| Доступ | Что можно |
|---|---|
| Только чтение | Все запросы на чтение (GET) и поиск сотрудника по почте (POST /employees/search). |
| Чтение и действия | То же и 9 действий: отметить уволившимся, отключить автопродление, подтвердить заказ, отклонить заказ, оформить подписку сотруднику, добавить почты, удалить почту из списка, пригласить и отозвать приглашение. Ключ только для чтения на них получает 403 api_key_read_only. |
Доступ задают при выпуске, изменить его нельзя: выпустите новый ключ.
Ключ принадлежит компании
Ключ действует от имени компании, а не админа, который его выпустил, и работает, пока его не отзовут: и когда этого админа сняли с роли, отметили уволившимся или удалили, и когда у компании не осталось ни одного админа. В заказе, почте и приглашении, которые сделал ключ, записан ключ, а не человек.
Отзыв и смена
- Отозвать ключ может любой админ компании в «Интеграциях» и оператор xWires, навсегда. Запросы, которые идут в момент отзыва, успеют завершиться, следующие получат 401 api_key_revoked.
- Отзывая ключ, можно удалить и неиспользованные почты и приглашения, которые он добавил. Снимая человека с роли админа или отмечая уволившимся, админ может сразу отозвать выпущенные им ключи.
- Сменить ключ: выпустите новый, переключите на него систему, отзовите старый. У компании может быть до 10 действующих ключей, так что старый и новый поработают вместе.
- Уходит человек, у которого могла остаться копия ключа, — админ, разработчик интеграции, подрядчик: смените ключ.
Только с сервера
Запросы к API — только с ваших серверов: из браузера API не вызвать, заголовков CORS нет и cookies не читаются. В адресе запроса ключ не передавайте: адреса попадают в логи, такой запрос получит 400 api_key_in_url, а ключ стоит отозвать.
Свои действия
У заказа, который подтвердил ключ, approved_via: "api_key" и approved_by_api_key_id; у события, которое вызвал запрос ключа, — actor.api_key_id. Сравнивайте их с key.id из GET /me: так система отличит эхо своих действий от чужих.
В кабинете ключ виден как xwc_…a91f (последние символы — key.last4), с временем последнего успешного запроса, а «Что сделали ключи» показывает последние изменения через API. Чтение не записывается, IP-адреса мы не храним.
04Запросы и ответы
- Тело запроса — JSON в UTF-8 с
Content-Type: application/json, до 256 КБ, и приходит целиком за 10 секунд, иначе 415 unsupported_media_type, 413 payload_too_large или 408 request_timeout. Подтверждение, отказ и увольнение шлите без тела или с{}. - Ответ — JSON в UTF-8, кроме
204без тела у удаления и отзыва. Ответы не кэшируются (Cache-Control: no-store). - Идентификаторы — uuid. Время — RFC 3339 (ISO 8601) со смещением
+00:00, как его отдаёт база:2026-10-15T09:30:12.345678+00:00. В доле секунды от 0 до 6 цифр: нули в конце база отбрасывает. Разбирайте время полным разбором RFC 3339 —Date.parseв Node.js,datetime.fromisoformatв Python 3.11+,dateutil.parser.isoparseв более ранних, — а не шаблоном фиксированной длины. Цен в API нет: стоимость подписок xWires согласует с компанией отдельно. - Поля, значения и типы событий в v1 только добавляются. Незнакомые поля и типы событий пропускайте, незнакомый статус заказа считайте «в работе».
- Каждый ответ, и ошибка тоже, несёт заголовок
X-Request-Id: назовите его поддержке, если что-то пошло не так. Ответы на запросы с ключом несут иRateLimit-*(раздел 8). - Почты в адресах не передаются: поиск сотрудника по почте — POST /employees/search с почтой в теле.
05Ошибки
Ошибка — JSON с кодом. Решения принимайте по code: message написан для человека и может меняться.
{
"error": {
"code": "order_not_awaiting_approval",
"message": "Заказ не ждёт подтверждения: его уже подтвердили, отклонили или отменили. Прочитайте заказ.",
"request_id": "d2f6b8a4-9e1c-4a37-85d0-6c3b7e9f1a42"
}
}details есть только у invalid_request: пути полей и коды проверки, без присланных значений.
{
"error": {
"code": "invalid_request",
"message": "Запрос не прошёл проверку: details.issues называет поля и что с ними не так.",
"request_id": "d2f6b8a4-9e1c-4a37-85d0-6c3b7e9f1a42",
"details": {
"issues": [
{
"path": "state",
"issue": "unrecognized_keys"
}
]
}
}
}| Код | HTTP | Что значит и что делать |
|---|---|---|
invalid_request | HTTP400 | Что значит и что делатьЗапрос не прошёл проверку: details.issues называет поля и что с ними не так. |
invalid_session | HTTP400 | Что значит и что делатьSession не подходит, details.issues называет почему: too_large — длиннее 32 КБ, wrong_shape — это не ответ chatgpt.com/api/auth/session (нет user.email, expires или accessToken), email_mismatch — accessToken от другого аккаунта, expired — истёк. Попросите сотрудника войти в свой ChatGPT, открыть chatgpt.com/api/auth/session и прислать ответ целиком. |
invalid_cursor | HTTP400 | Что значит и что делатьКурсор не подходит: он испорчен или получен с другими фильтрами. Начните список сначала, без cursor. |
api_key_in_url | HTTP400 | Что значит и что делатьКлюч API передан в адресе запроса, а адреса попадают в логи. Отзовите этот ключ в кабинете и передавайте новый только в заголовке Authorization: Bearer. |
invalid_email | HTTP400 | Что значит и что делатьПроверьте почту: нужен адрес вида name@example.ru. |
invalid_plan | HTTP400 | Что значит и что делатьУкажите план, который оплачивает компания: plus, pro100, pro200, pro500. |
session_token_required | HTTP400 | Что значит и что делатьВ session нет sessionToken, а подписку сотрудника продлевают каждый месяц по session: пришлите ответ chatgpt.com/api/auth/session целиком. |
session_expires_too_soon | HTTP400 | Что значит и что делатьSession истечёт раньше, чем заказ уйдёт партнёру: продление уходит, когда закончится оплаченный период. Пришлите свежий session ближе к этой дате. |
no_emails | HTTP400 | Что значит и что делатьПришлите хотя бы одну почту. |
too_many_emails | HTTP400 | Что значит и что делатьНе больше 500 почт в одном запросе. |
invalid_api_key | HTTP401 | Что значит и что делатьКлюч API не подходит: передайте его в заголовке Authorization: Bearer xwc_…. |
api_key_revoked | HTTP401 | Что значит и что делатьКлюч API отозван. Выпустите новый в кабинете: Компания → Интеграции. |
api_key_read_only | HTTP403 | Что значит и что делатьКлюч API только для чтения: для этого запроса нужен ключ с правом действий. |
not_found | HTTP404 | Что значит и что делатьТакого адреса в API нет: проверьте путь и метод. |
order_not_found | HTTP404 | Что значит и что делатьЗаказа с таким id у компании нет. |
employee_not_found | HTTP404 | Что значит и что делатьСотрудника с таким id в компании нет: возможно, он уже ушёл. |
approved_email_not_found | HTTP404 | Что значит и что делатьПочты с таким id в списке нет: её уже удалили, или она уже подтвердила заказ. |
invite_not_found | HTTP404 | Что значит и что делатьПриглашения с таким id нет: его уже отозвали или приняли. |
request_timeout | HTTP408 | Что значит и что делатьТело запроса не пришло целиком за 10 секунд. Повторите запрос. |
order_not_awaiting_approval | HTTP409 | Что значит и что делатьЗаказ не ждёт подтверждения: его уже подтвердили, отклонили или отменили. Прочитайте заказ. |
already_employee | HTTP409 | Что значит и что делатьЧеловек с этой почтой уже сотрудник компании. |
open_order_exists | HTTP409 | Что значит и что делатьУ сотрудника уже есть открытый заказ: следите за ним в GET /orders?employee_id=… или в ленте событий. В песочнице его можно довести до конца (POST /sandbox/orders/{id}/advance) или создать другого сотрудника. |
chatgpt_email_taken | HTTP409 | Что значит и что делатьЭтот ChatGPT-аккаунт в xWires уже у другого человека: проверьте, что session от аккаунта сотрудника. |
chatgpt_email_mismatch | HTTP409 | Что значит и что делатьПодписка сотрудника — на другой ChatGPT-аккаунт, а продление идёт на тот же: пришлите session того аккаунта (subscription.chatgpt_email в GET /employees/{id}). |
plan_mismatch | HTTP409 | Что значит и что делатьПодписка сотрудника — на другой план, а продление идёт на тот же: укажите plan его подписки (GET /employees/{id}). |
auto_renewal_not_on | HTTP409 | Что значит и что делатьАвтопродление сотрудника не включено или его оплачивает не компания: отключать нечего. |
auto_renewal_on | HTTP409 | Что значит и что делатьПодписку сотрудника каждый месяц продлевает автопродление: новый заказ не нужен. Чтобы продлить вручную, сначала отключите его (stopAutoRenewal). |
auto_renewal_closed | HTTP409 | Что значит и что делатьАвтопродление для компании пока закрыто: подтвердите разовую активацию — без renewal или с renewal: "once". |
cannot_offboard_admin | HTTP409 | Что значит и что делатьАдмина компании через API уволившимся не отметить: это делают в кабинете. |
membership_not_found | HTTP409 | Что значит и что делатьУ сотрудника нет подписки: заказ создаётся для придуманного сотрудника, POST /sandbox/employees. |
sandbox_invalid_transition | HTTP409 | Что значит и что делатьИз нынешнего статуса заказ в этот не перевести: подтверждает и отклоняет компания, а из succeeded, failed, rejected и cancelled дальше пути нет. Прочитайте заказ. |
subscription_not_active | HTTP409 | Что значит и что делатьПодписка сотрудника не активна: завершить нечего. |
events_cursor_expired | HTTP410 | Что значит и что делатьСобытий сразу после этой позиции нет: мы храним их 30 дней, или ленту компании восстановили из резервной копии. Запросите after=latest и сохраните next_after, затем перечитайте списки и продолжайте с сохранённой позиции. |
payload_too_large | HTTP413 | Что значит и что делатьТело запроса больше 256 КБ. |
unsupported_media_type | HTTP415 | Что значит и что делатьТело запроса должно быть JSON с Content-Type: application/json. |
rate_limited | HTTP429 | Что значит и что делатьСлишком много запросов с этим ключом: не больше 60 в минуту и 2 одновременно. Повторите после Retry-After. |
api_email_quota_exceeded | HTTP429 | Что значит и что делатьЗа сутки через API прислали 5000 почт и приглашений компании. Повторите позже или добавьте почты в кабинете. |
internal_error | HTTP500 | Что значит и что делатьВнутренняя ошибка xWires. Прочитайте данные GET-запросом и повторите; если ошибка не проходит, напишите в поддержку с request_id. |
service_unavailable | HTTP503 | Что значит и что делатьxWires сейчас не может ответить. Повторите через секунду: запрос можно безопасно повторить. |
405без нашего JSON — у адреса нет такого метода.429: наrate_limitedподождите столько секунд, сколько вRetry-After. Уapi_email_quota_exceededзаголовка нет: суточный лимит освобождается постепенно.500— ошибка у нас: сначала прочитайте данные, потом повторяйте. Не проходит — напишите в поддержку сrequest_id.503приходит сRetry-After: 1: повторите через секунду.
06Списки и синхронизация
Страницы
Списки отдают до limit строк (от 1 до 200, по умолчанию 100) и next_cursor. Следующая страница — тот же запрос с cursor=<next_cursor>; null — страниц больше нет. Курсор непрозрачный, до 512 символов, и действует только с теми же фильтрами, иначе 400 invalid_cursor. Незнакомый или повторённый параметр — 400 invalid_request.
Сотрудники идут по id, заказы — по времени последнего изменения (updated_at, затем id), почты и приглашения — по времени добавления.
Как держать данные в актуальном виде
Главный способ узнавать об изменениях — лента событий GET /events (раздел 13): в ней те же события, что приходят на вебхуки.
- Запросите
GET /events?after=latestи сохранитеnext_after, затем один раз прочитайте нужные списки целиком. Именно в таком порядке: изменения, сделанные, пока вы читаете списки, придут и в ленте, а обработчик, которому повтор не вредит, примет их ещё раз без последствий. - Раз в 10–60 секунд или на каждый вебхук запрашивайте
GET /events?after=<next_after>, обработайте события и сохраните новыйnext_after. Покаhas_more: true, спрашивайте сразу; на 429 rate_limited и 503 service_unavailable подождитеRetry-Afterи повторите. - На 410 events_cursor_expired начните заново, как в первом шаге: сначала
after=latestиnext_after, потом списки, и продолжайте с сохранённой позиции. - Раз в сутки, например ночью, читайте весь GET /employees для сверки.
// xwires() — из примера выше. loadAfter, saveAfter, handle и resyncFromLists — ваши:
// храните позицию в своей базе, обрабатывайте событие так, чтобы повтор с тем же id ничего не менял.
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));
let after = await loadAfter(); // null — ленту ещё не читали
for (;;) {
try {
if (after === null) {
// Сначала позиция, потом списки: изменения, сделанные во время чтения, придут и в ленте.
const { next_after } = await xwires("GET", "/events?after=latest");
await resyncFromLists();
after = String(next_after);
await saveAfter(after);
}
const page = await xwires("GET", `/events?after=${after}&limit=200`);
for (const event of page.data) await handle(event);
after = String(page.next_after);
await saveAfter(after);
if (!page.has_more) await sleep(30);
} catch (error) {
if (error.code === "events_cursor_expired") {
// Событий после позиции уже нет: они старше 30 дней, или базу xWires восстановили из копии.
after = null;
} else if (error.retryAfter) {
// 429 и 503, сбой сети или прокси, пока xWires перезапускается: подождите и повторите.
await sleep(error.retryAfter);
} else {
throw error;
}
}
}Дозагрузить заказы помогает updated_since: берите его на минуту раньше последнего updated_at, который видели, и убирайте повторы по паре id и updated_at. + в смещении времени кодируйте как %2B.
curl -sS "https://api.xwires.ai/company/v1/orders?status=succeeded&updated_since=2026-10-01T00:00:00%2B03:00" \ -H "Authorization: Bearer $XWIRES_COMPANY_API_KEY"
07Повторы
Заголовка Idempotency-Key нет: каждое действие и так безопасно повторить.
| Действие | Что ответит повтор |
|---|---|
| POST /orders/{id}/approvePOST /orders/{id}/reject | 409 order_not_awaiting_approval |
| POST /approved-emails | Почты из списка и почты, которые уже подтвердили незавершённый заказ, считаются в skipped, но идут в суточный лимит. |
| POST /invites | 200 и created: false, письмо второй раз не уходит. |
| POST /subscriptions | 409 open_order_exists: заказ уже создан, найдите его в GET /orders?employee_id=…. Повтор после 202 снова ответит 202, письмо второй раз не уходит. Каждый повтор идёт в суточный лимит. |
| POST /employees/{id}/offboard | 404 employee_not_found |
| DELETE /approved-emails/{id} | 404 approved_email_not_found |
| DELETE /invites/{id} | 404 invite_not_found |
После таймаута или ответа 5xx не повторяйте вслепую: сначала прочитайте заказ, список или сотрудника, потом решайте. Повтор не навредит, но ответ на него скажет больше, если знать, что уже сделано.
08Лимиты
| Что | Сколько |
|---|---|
| Запросы с одного ключа | 60 в минуту и 2 одновременно, лента событий тоже. Сверх — 429 rate_limited с Retry-After. |
| Тело запроса | до 256 КБ, целиком за 10 секунд |
| Почт в одном запросе | до 500 |
| Почт и приглашений через API за сутки | 5000 у компании: каждая присланная почта, и та, что уже есть в списке, и каждое приглашение. Сверх — 429 api_email_quota_exceeded. Кабинет не считается. |
| Строк на странице | от 1 до 200, по умолчанию 100 |
| Действующих ключей у компании | до 10 |
| Хранение событий | 30 дней |
| Адресов вебхуков у компании | до 5 |
| Типов событий у адреса | все, включая будущие, или от 1 до 19 выбранных |
| Адрес и описание | до 2048 и до 100 символов |
| Новые адреса, смены адреса и секрета | до 20 за сутки у компании |
| Тестовые события | не чаще раза в 10 секунд на адрес и до 30 в час у компании |
| Ответ на вебхук | 2xx за 10 секунд |
| Попытки доставки | 10 примерно за 22 часа; событие, не доставленное за 3 дня с первой попытки, больше не отправляется |
RateLimit-Limit — сколько запросов с ключа можно за минуту, RateLimit-Remaining — сколько осталось, RateLimit-Reset — через сколько секунд минута начнётся заново. 429 rate_limited бывает и когда xWires занят запросами других компаний: просто подождите Retry-After.
09Сценарии
Новый сотрудник
Кадровая система добавляет почту нового сотрудника в список с планом, который оплачивает компания. Подходит почта, с которой он входит в xWires, или почта его аккаунта ChatGPT.
curl -sS -X POST https://api.xwires.ai/company/v1/approved-emails \
-H "Authorization: Bearer $XWIRES_COMPANY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"emails":["ivan.sidorov@example.ru","maria.kuznetsova@example.ru"],"plan":"plus"}'{
"approved": 1,
"remembered": 1,
"skipped": 0,
"invalid": 0,
"approved_order_ids": [
"1d6f8a3c-7e2b-4c95-a0d4-6b9e1f3a7c52"
],
"renews_monthly": false
}approved: почта сразу подтвердила заказ, который уже ждал (approved_order_ids);remembered: почта ждёт заказа и подтвердит первый заказ этого плана;skipped: почта уже в списке, уже подтвердила незавершённый заказ этого плана или повторяется в запросе;invalid: не похоже на почту.- Почта подтверждает один заказ. Пока он не завершён, та же почта или другая почта того же сотрудника с этим планом пропускается, поэтому повтор запроса лишнего заказа не оплатит. Чтобы оплатить и следующий заказ, добавьте почту, когда этот завершится, или подтвердите следующий заказ, когда он придёт.
- Если почта не принадлежит сотруднику компании, на неё уходит письмо: компания оплатит подписку, можно входить в кабинет, — и приглашение в компанию.
- Почта и приглашение удаляются отдельно: DELETE /approved-emails/{id} не отзывает приглашение, для этого есть DELETE /invites/{id}. Принять в компанию, ничего не оплачивая, — POST /invites.
- Заказ с
renewal_mode: monthlyпочта подтверждает, только если её добавили сrenews_monthly: true: это согласие компании, что подписка будет продлеваться каждый месяц за её счёт, пока её не отключат.renews_monthlyответа говорит, записано ли согласие:falseприtrueв запросе значит, что подписки компании сейчас не продлеваются каждый месяц. Какие почты списка его несут, показывает GET /approved-emails.
Согласование в вашей системе
Заказ сотрудника ждёт подтверждения компании. Узнайте о нём из события order.awaiting_approval (вебхук или лента) или спрашивайте список раз в несколько минут:
curl -sS "https://api.xwires.ai/company/v1/orders?status=awaiting_approval" \ -H "Authorization: Bearer $XWIRES_COMPANY_API_KEY"
renewal_mode заказа — что выбрал сотрудник, он есть и в событии: once — разовая активация на месяц, monthly — с автопродлением. Решает компания: без тела заказ подтверждается разовым, что бы ни выбрал сотрудник. {"renewal": "monthly"} — согласие компании, что подписка будет продлеваться каждый месяц за её счёт, пока её не отключат; его можно дать и заказу, где сотрудник выбрал разовую активацию. Автопродление открывает компании xWires; пока оно закрыто, ответ на monthly — 409 auto_renewal_closed. Окончательным режим станет, когда сотрудник пришлёт session: без его согласия на продление заказ будет разовым.
Когда в вашей системе решили, подтвердите или отклоните заказ:
curl -sS -X POST https://api.xwires.ai/company/v1/orders/9c2e4b71-6a3f-4d58-b1e0-7f3a2c9d5e14/approve \ -H "Authorization: Bearer $XWIRES_COMPANY_API_KEY"
curl -sS -X POST https://api.xwires.ai/company/v1/orders/9c2e4b71-6a3f-4d58-b1e0-7f3a2c9d5e14/approve \
-H "Authorization: Bearer $XWIRES_COMPANY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"renewal": "monthly"}'curl -sS -X POST https://api.xwires.ai/company/v1/orders/9c2e4b71-6a3f-4d58-b1e0-7f3a2c9d5e14/reject \ -H "Authorization: Bearer $XWIRES_COMPANY_API_KEY"
{
"id": "9c2e4b71-6a3f-4d58-b1e0-7f3a2c9d5e14",
"status": "approved",
"is_open": true,
"kind": "renewal",
"plan": "plus",
"renewal_mode": "once",
"origin": "manual",
"created_at": "2026-10-15T09:28:04.551902+00:00",
"updated_at": "2026-10-15T09:30:12.345678+00:00",
"approved_at": "2026-10-15T09:30:12.345678+00:00",
"approved_by_operator": false,
"approved_via": "api_key",
"approved_by_api_key_id": "0b7d9e2a-5c13-4f6e-8a21-9d4c3b7e1f60",
"scheduled_for": null,
"completed_at": null,
"employee": {
"id": "3a9e5c71-8b2d-4f04-a6c9-1e7b2d5f8a90",
"email": "anna.petrova@example.ru",
"name": "Анна Петрова",
"chatgpt_email": "a.petrova@example.ru"
}
}renewal_mode ответа — режим, с которым компания подтвердила заказ. Окончательный режим виден с awaiting_check.
После подтверждения сотрудника просят прислать session ChatGPT, оператор xWires проверяет заказ и отправляет его партнёру. Если заказ тем временем решил админ в кабинете, ответ — 409 order_not_awaiting_approval: прочитайте заказ, чтобы узнать итог.
Подписка по session сотрудника
Сотрудник присылает в вашу систему session своего ChatGPT: войдя в свой аккаунт, открывает chatgpt.com/api/auth/session и копирует ответ целиком. Одним запросом система оформляет ему подписку, как оператор xWires в своём кабинете:
{
"email": "oleg.smirnov@example.ru",
"plan": "plus",
"name": "Олег Смирнов",
"session": {
"user": {
"id": "user-Xq3vT8LmN2",
"name": "Олег Смирнов",
"email": "o.smirnov@example.ru"
},
"expires": "2027-01-16T10:04:12.000Z",
"accessToken": "eyJhbGciOiJSUzI1NiIs…",
"sessionToken": "eyJhbGciOiJkaXIiLCJlbmMiOi…"
}
}curl -sS -X POST https://api.xwires.ai/company/v1/subscriptions \ -H "Authorization: Bearer $XWIRES_COMPANY_API_KEY" \ -H "Content-Type: application/json" \ -d @subscription.json
{
"outcome": "ordered",
"account_created": true,
"order": {
"id": "a4c7e1b9-5d2f-4a83-b6e0-2f9c7d1a3e58",
"status": "awaiting_check",
"is_open": true,
"kind": "activation",
"plan": "plus",
"renewal_mode": "once",
"origin": "manual",
"created_at": "2026-10-18T10:15:31.402811+00:00",
"updated_at": "2026-10-18T10:15:31.402811+00:00",
"approved_at": "2026-10-18T10:15:31.402811+00:00",
"approved_by_operator": false,
"approved_via": "api_key",
"approved_by_api_key_id": "0b7d9e2a-5c13-4f6e-8a21-9d4c3b7e1f60",
"scheduled_for": null,
"completed_at": null,
"employee": {
"id": "5e8b2d94-3c7a-4f1e-9b06-8a2d4c6e1f37",
"email": "oleg.smirnov@example.ru",
"name": "Олег Смирнов",
"chatgpt_email": "o.smirnov@example.ru"
}
}
}- Почты нет в xWires — создаётся учётная запись в компании (
account_created: true): сотрудник входит кодом на эту почту и видит подписку. У сотрудника компании активная или истёкшая подписка продлевается на тот же аккаунт ChatGPT и план. - Заказ подтверждён ключом и ждёт проверки оператора xWires (
awaiting_check), потом уходит партнёру; сотруднику приходит письмо. Почту ChatGPT мы берём из session. - Отправляя session, система заверяет, что сотрудник согласился передать его партнёру. Без поля
renewalподписка разовая. С"renewal": "monthly"она будет продлеваться каждый месяц за счёт компании: система заверяет и согласие сотрудника на это. Так можно, только пока автопродление для компании открыто — иначе 409 auto_renewal_closed, — и в session нуженsessionToken— 400 session_token_required. Пока автопродление сотрудника включено, заказывать продление не нужно: 409 auto_renewal_on. - Если человек с этой почтой не в компании — личный клиент xWires или сотрудник другой компании, — или почта на домене другой компании, ответ
202сoutcome: invited. Ему уходит приглашение, почта ждёт в списке с планом, а session не сохраняется: приняв приглашение, человек сам оформит заказ, и почта его подтвердит. - Session не подошёл — 400 invalid_session, причина в
details.issues; наexpiredпопросите прислать свежий.
Автопродление
Подписку с автопродлением (заказ с renewal_mode: monthly) каждый месяц продлевает сам OpenAI за счёт компании, пока его не отключат. Состояние — в subscription.auto_renewal сотрудника (GET /employees/{id}): state, дата следующего продления next_renewal_at, почему выключено (stop_reason) и может ли компания его отключить (can_stop). Включается оно, когда заказ с автопродлением выполнен, — событие subscription.auto_renewal_started; отключается — subscription.auto_renewal_stopped с stop_reason.
curl -sS -X POST https://api.xwires.ai/company/v1/employees/3a9e5c71-8b2d-4f04-a6c9-1e7b2d5f8a90/auto-renewal/stop \ -H "Authorization: Bearer $XWIRES_COMPANY_API_KEY"
{
"id": "3a9e5c71-8b2d-4f04-a6c9-1e7b2d5f8a90",
"email": "anna.petrova@example.ru",
"name": "Анна Петрова",
"role": "employee",
"subscription": {
"chatgpt_email": "a.petrova@example.ru",
"plan": "plus",
"status": "active",
"expires_at": "2026-11-14T08:03:51.207114+00:00",
"auto_renewal": {
"state": "off",
"next_renewal_at": null,
"stop_reason": "api_key",
"can_stop": false
}
}
}- Оплаченный месяц остаётся у сотрудника до
expires_at, потом подписка не продлится. Продление у OpenAI xWires отменит сам, сотруднику и админам компании придёт письмо. Включить снова доexpires_atнельзя; новый заказ сrenewal: monthlyуйдёт партнёру, когда оплаченный месяц кончится. - Отключить можно, только пока автопродление включено и платит за него ваша компания (
can_stop: true), иначе 409 auto_renewal_not_on; повтор отвечает так же. - Отключить его может и сам сотрудник, админ компании в кабинете и оператор xWires:
stop_reasonскажет, кто. Увольнение отключает его само (stop_reason: offboarded).
Увольнение
Найдите сотрудника по почте и отметьте уволившимся:
curl -sS -X POST https://api.xwires.ai/company/v1/employees/search \
-H "Authorization: Bearer $XWIRES_COMPANY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"anna.petrova@example.ru"}'{
"data": [
{
"id": "3a9e5c71-8b2d-4f04-a6c9-1e7b2d5f8a90",
"email": "anna.petrova@example.ru",
"name": "Анна Петрова",
"role": "employee",
"subscription": {
"chatgpt_email": "a.petrova@example.ru",
"plan": "plus",
"status": "active",
"expires_at": "2026-11-14T08:03:51.207114+00:00",
"auto_renewal": {
"state": "on",
"next_renewal_at": "2026-11-14T08:03:51.207114+00:00",
"stop_reason": null,
"can_stop": true
}
}
}
],
"next_cursor": null
}curl -sS -X POST https://api.xwires.ai/company/v1/employees/3a9e5c71-8b2d-4f04-a6c9-1e7b2d5f8a90/offboard \ -H "Authorization: Bearer $XWIRES_COMPANY_API_KEY"
{
"employee_id": "3a9e5c71-8b2d-4f04-a6c9-1e7b2d5f8a90",
"cancelled_order_ids": [
"9c2e4b71-6a3f-4d58-b1e0-7f3a2c9d5e14"
]
}Человек перестаёт быть сотрудником компании, а его заказы за счёт компании, которые ещё не ушли партнёру, отменяются (cancelled_order_ids). Заказ, который партнёру как раз передают (submitting), остаётся и доходит до итога: если партнёр его так и не получил, он отменится позже, и придёт order.cancelled. Админа так не отметить — 409 cannot_offboard_admin: это делают в кабинете. Автопродление его подписки за счёт компании отключается (subscription.auto_renewal_stopped с stop_reason: offboarded). Вернуть человека можно новым приглашением. Если у ушедшего мог остаться ключ API или секрет вебхука, смените их.
Отчёт для бухгалтерии
Выполненные заказы приходят событиями order.succeeded: подписка на них у адреса вебхука или фильтр ленты. Сумму и план берите из заказа: GET /orders/{id}.
curl -sS "https://api.xwires.ai/company/v1/events?after=1041&types=order.succeeded" \ -H "Authorization: Bearer $XWIRES_COMPANY_API_KEY"
Или списком за период, по времени последнего изменения:
curl -sS "https://api.xwires.ai/company/v1/orders?status=succeeded&updated_since=2026-10-01T00:00:00%2B03:00" \ -H "Authorization: Bearer $XWIRES_COMPANY_API_KEY"
10Статусы заказа и подписки
Заказ
| status | В кабинете | Что значит |
|---|---|---|
awaiting_approval | В кабинетеЖдёт подтверждения | Что значитСотрудник оформил заказ, компания должна подтвердить или отклонить его. |
approved | В кабинетеПодтверждён, ждём session | Что значитКомпания или оператор xWires подтвердили заказ; ждём от сотрудника session ChatGPT. |
awaiting_check | В кабинетеПроверяет оператор | Что значитSession получен, оператор xWires проверяет заказ перед отправкой партнёру. |
scheduled | В кабинетеЗапланирован | Что значитЗаказ проверен и уйдёт партнёру в scheduled_for: продление — когда закончится оплаченный период. |
submitting | В кабинетеОтправляется | Что значитЗаказ передан партнёру, оплата идёт; результата ещё нет. |
sent | В кабинетеОтправлен партнёру | Что значитЗаказ у партнёра, ждём результат. |
review_required | В кабинетеНа разборе у оператора | Что значитПартнёр не принял заказ, его разбирает оператор xWires. |
succeeded | В кабинетеВыполнен | Что значитПодписка пополнена. |
failed | В кабинетеНе выполнен | Что значитПартнёр не выполнил заказ. |
rejected | В кабинетеОтклонён | Что значитКомпания или оператор xWires отклонили заказ. |
cancelled | В кабинетеОтменён | Что значитЗаказ отменён: сотрудника отметили уволившимся или оператор xWires отменил заказ. |
Статус может вернуться назад:
failed → sent: партнёр всё-таки повторил заказ;awaiting_check → approved: оператор попросил у сотрудника новый session;review_required → approved: оператор повторяет заказ, а session уже нет.
Из review_required заказ может уйти и в scheduled или sent, когда снова в работе. Незнакомый статус считайте заказом в работе.
| kind | В кабинете | Что значит |
|---|---|---|
activation | В кабинетеАктивация | Что значитПервый месяц подписки на этот аккаунт ChatGPT. |
renewal | В кабинетеПродление | Что значитСледующий месяц той же подписки. |
| renewal_mode | Что значит |
|---|---|
monthly | Подписку xWires продлевает каждый месяц за счёт компании, пока её не отключат. Заказ подтверждается только с согласием компании: renewal: monthly или почтой с renews_monthly: true. |
once | Оплачивается один месяц; следующий — новым заказом. |
Подписка
| status | В кабинете | Что значит |
|---|---|---|
new | В кабинетеНе активирована | Что значитПлан и аккаунт ChatGPT выбраны, подписку ещё не оплачивали. |
processing | В кабинетеОформляется | Что значитЗаказ у партнёра. |
active | В кабинетеАктивна | Что значитПодписка действует до expires_at. |
expired | В кабинетеИстекла | Что значитСрок прошёл, продления не было. |
cancelled | В кабинетеОтменена | Что значитОт подписки отказались. |
Автопродление
| auto_renewal.state | В кабинете | Что значит |
|---|---|---|
on | В кабинетеВключено | Что значитВключено: подписка продлится в next_renewal_at за счёт компании. |
needs_session | В кабинетеНужен новый session | Что значитДля следующего продления нужен свежий session сотрудника. Появится вместе с самими продлениями. |
in_progress | В кабинетеПродлеваем | Что значитПодписку продлеваем. Появится вместе с самими продлениями. |
attention | В кабинетеНа разборе | Что значитПродление разбирает оператор xWires. Появится вместе с самими продлениями. |
off | В кабинетеВыключено | Что значитВыключено или не включалось; почему выключено — в stop_reason. |
Незнакомое состояние считайте включённым.
| stop_reason | Что значит |
|---|---|
user | Отключил сам сотрудник в кабинете. |
company_admin | Отключил админ компании в кабинете. |
api_key | Отключила система компании: stopAutoRenewal. |
operator | Отключил оператор xWires. |
offboarded | Сотрудника отметили уволившимся: компания больше не платит. |
payer_changed | Сменился плательщик: сотрудник перешёл в другую компанию или ушёл из неё. |
Планы
| plan | План |
|---|---|
plus | ChatGPT Plus |
pro100 | ChatGPT Pro 100 |
pro200 | ChatGPT Pro 200 |
pro500 | ChatGPT Pro 500 |
11Песочница
Интеграцию проверяйте в песочнице: это тот же API на тестовых данных, по адресу https://api.sandbox.xwires.ai/company/v1. В ней ничего не оплачивается и никому не пишется: заказы не уходят партнёру, письма не получают ни сотрудники, ни приглашённые. Данные песочницы отдельные и в рабочий xWires не попадают.
- Откройте sandbox.xwires.ai и войдите с любой почтой: код придёт на неё. У вас будет своя компания «Песочница», вы — её админ.
- В «Интеграциях» песочницы выпустите ключ, он начинается с
xwc_test_, и, если нужны вебхуки, добавьте адрес. События приходят на него так же, как в рабочем xWires, подписаны так же и с того же IP. - Направьте систему на адрес песочницы. Все запросы этого руководства работают и там, примеры — тоже: замените адрес и ключ.
Что уже есть в компании
| Сотрудник | План | Заказ |
|---|---|---|
| Анна Смирнова | ПланChatGPT Plus | Заказawaiting_approval |
| Борис Иванов | ПланChatGPT Pro 100 | Заказawaiting_approval |
| Вера Козлова | ПланChatGPT Plus | Заказapproved: ждёт session, автопилот его не трогает |
| Глеб Новиков | ПланChatGPT Plus | Заказsucceeded: подписка активна |
| Дина Орлова | ПланChatGPT Pro 200 | Заказfailed: партнёр отказал |
Это придуманные люди: их почты — под .invalid, войти за них нельзя. Подтвердите или отклоните заказы Анны и Бориса, как в рабочем API, и посмотрите, что придёт вашей системе.
Что в песочнице иначе
- Ключ песочницы в рабочем API и рабочий ключ в песочнице получают 401 invalid_api_key, а в
messageсказано, куда этот ключ отправлять. - Шаги, которые в рабочем xWires делают сотрудник, оператор и партнёр, делает песочница: сама (автопилот) или по вашему запросу. В событиях этих шагов
actor—null, как у действий людей, поэтому система, которая отбрасывает эхо своих запросов поactor.api_key_id, их увидит. - Лимиты, ошибки и форма ответов — те же, что в рабочем API.
Запросы песочницы
В рабочем API их нет: там они отвечают 404 not_found. Нужен ключ с правом действий. Те же действия есть кнопками в «Интеграциях» песочницы.
| Запрос | Что делает |
|---|---|
| POST /sandbox/employees | Новый сотрудник компании с подпиской в статусе new на план из тела: событие employee.joined. Его почты под .invalid, войти за него нельзя. Его заказ — POST /sandbox/orders. |
| POST /sandbox/orders | Сотрудник заказывает месяц подписки, как в своём кабинете: заказ awaiting_approval, или сразу approved, если его почта в списке компании. Активация для новой подписки, продление для активной или истёкшей. Подтверждённый заказ доводит до succeeded автопилот, если его не остановить (POST /sandbox/orders/{id}/advance). |
| POST /sandbox/orders/{id}/advance | Заказ идёт в статус to шаг за шагом, за сотрудника, оператора и партнёра, как в проде: approved → awaiting_check → scheduled → sent → succeeded или failed; review_required из scheduled, cancelled из awaiting_check и review_required, approved из awaiting_check. Каждый шаг — своё событие, без actor. Подтверждают и отклоняют заказ, как всегда, POST /orders/{id}/approve и /reject. После вызова автопилот этот заказ больше не трогает; to, равный нынешнему статусу, только останавливает его. |
| POST /sandbox/employees/{id}/expire-subscription | Активная подписка сотрудника истекает сейчас: событие subscription.expired. Его следующий заказ — продление. Тело пустое или {}. |
Автопилот
Подтверждённый заказ, который вы не переводили вручную, песочница ведёт дальше сама: awaiting_check → scheduled → sent → succeeded, шаг примерно раз в 10 секунд, весь путь — меньше минуты. На каждый шаг — событие order.*, на успех — subscription.activated. Заказ, который вы перевели запросом POST /sandbox/orders/{id}/advance, автопилот больше не трогает. Чтобы он не тронул заказ и после подтверждения, переведите его в нынешний статус, {"to": "awaiting_approval"}, до подтверждения.
Пример: партнёр отказал
Новый сотрудник:
curl -sS -X POST https://api.sandbox.xwires.ai/company/v1/sandbox/employees \
-H "Authorization: Bearer $XWIRES_COMPANY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"plan":"plus"}'{
"id": "5f3a9c21-7d4e-4b86-a1c2-9e8d7f6b5a43",
"email": "employee-5f3a9c21@employees.sandbox.invalid",
"name": "Мария Соколова",
"role": "employee",
"subscription": {
"chatgpt_email": "employee-5f3a9c21@chatgpt.sandbox.invalid",
"plan": "plus",
"status": "new",
"expires_at": null,
"auto_renewal": {
"state": "off",
"next_renewal_at": null,
"stop_reason": null,
"can_stop": false
}
}
}Его заказ, ваше подтверждение и отказ партнёра:
curl -sS -X POST https://api.sandbox.xwires.ai/company/v1/sandbox/orders \
-H "Authorization: Bearer $XWIRES_COMPANY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"employee_id":"5f3a9c21-7d4e-4b86-a1c2-9e8d7f6b5a43"}'curl -sS -X POST https://api.sandbox.xwires.ai/company/v1/orders/8e1b4d7a-2c9f-4a63-b5e0-3d7c1f9a2b68/approve \ -H "Authorization: Bearer $XWIRES_COMPANY_API_KEY"
curl -sS -X POST https://api.sandbox.xwires.ai/company/v1/sandbox/orders/8e1b4d7a-2c9f-4a63-b5e0-3d7c1f9a2b68/advance \
-H "Authorization: Bearer $XWIRES_COMPANY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"to":"failed"}'Ваша система получит order.awaiting_approval, order.approved с actor вашего ключа, затем order.awaiting_check, order.scheduled, order.sent и order.failed без него. Завершить активную подписку и проверить subscription.expired — POST /sandbox/employees/{id}/expire-subscription.
12Справочник
Справочник собран из того же описания, что и openapi.json (OpenAPI 3.0.3), поэтому совпадает с тем, что API делает. Базовый адрес — https://api.xwires.ai/company/v1, у каждого запроса заголовок Authorization: Bearer с ключом.
Ключ
/meКлюч и компания
Проверяет, что ключ действует, и называет его доступ и компанию. key.id сравнивайте с actor.api_key_id событий, чтобы узнавать свои действия.
Ключ: Только чтение
- 200
- Ключ и его компания. Me
- 429
- rate_limited
Сотрудники
/employeesСотрудники
Текущие пользователи компании, админы тоже, с подписками, по id. Ушедших в списке нет.
Ключ: Только чтение
| Параметр | Где | Тип | Описание |
|---|---|---|---|
limit | Гдев запросе | Типinteger | ОписаниеСколько строк на странице, от 1 до 200. (необязательное, по умолчанию 100) |
cursor | Гдев запросе | Типstring | Описаниеnext_cursor предыдущей страницы, с теми же фильтрами. (необязательное, 1–512 символов) |
- 200
- Страница сотрудников. EmployeesPage
- 429
- rate_limited
/employees/searchНайти сотрудника по почте
Ищет по почте входа и по почте ChatGPT-аккаунта, без учёта регистра: 0, 1 или 2 сотрудника. POST, чтобы почта не попадала в адреса и логи.
Ключ: Только чтение
Тело: SearchEmployeesRequest
- 200
- Найденные сотрудники; next_cursor всегда null. EmployeesPage
- 429
- rate_limited
/employees/{id}Сотрудник
Текущий пользователь компании с подпиской. Ушедший или чужой — 404.
Ключ: Только чтение
| Параметр | Где | Тип | Описание |
|---|---|---|---|
id | Гдев пути | Типuuid | Описаниеid сотрудника. |
- 200
- Сотрудник. Employee
- 429
- rate_limited
/employees/{id}/offboardОтметить уволившимся
Человек перестаёт быть сотрудником компании, его заказы за счёт компании, ещё не ушедшие партнёру, отменяются. Заказ, который партнёру как раз передают (submitting), остаётся и доходит до итога; если партнёр его так и не получил, он отменится позже, событием order.cancelled. Админа так не отметить (409 cannot_offboard_admin): это делают в кабинете. Тело пустое или {}. Автопродление его подписки за счёт компании отключается (subscription.auto_renewal_stopped, stop_reason offboarded). Вернуть человека можно новым приглашением.
Ключ: Чтение и действия
| Параметр | Где | Тип | Описание |
|---|---|---|---|
id | Гдев пути | Типuuid | Описаниеid сотрудника. |
Тело: пустое или {}
- 200
- Сотрудник ушёл. OffboardResult
- 429
- rate_limited
/employees/{id}/auto-renewal/stopОтключить автопродление
Подписка сотрудника больше не продлевается каждый месяц за счёт компании: оплаченный месяц остаётся до expires_at, после него подписка истекает. Продление у OpenAI xWires отменит сам; сотруднику и админам компании придёт письмо, в ленте — событие subscription.auto_renewal_stopped с stop_reason api_key. Отключить можно, только пока auto_renewal.state сотрудника не off и платит ваша компания (auto_renewal.can_stop), иначе 409 auto_renewal_not_on, повтор тоже. Включить снова до expires_at нельзя; новый заказ с renewal: monthly уйдёт партнёру, когда оплаченный месяц кончится. Тело пустое или {}.
Ключ: Чтение и действия
| Параметр | Где | Тип | Описание |
|---|---|---|---|
id | Гдев пути | Типuuid | Описаниеid сотрудника. |
Тело: пустое или {}
- 200
- Сотрудник после отключения. Employee
- 429
- rate_limited
Заказы
/ordersЗаказы
Заказы, которые оплачивает компания, также ушедших сотрудников (employee: null), по времени последнего изменения. Для синхронизации есть лента событий; updated_since — для дозагрузки: берите запас в минуту и убирайте повторы по id и updated_at. Курсор действует только с теми же фильтрами.
Ключ: Только чтение
| Параметр | Где | Тип | Описание |
|---|---|---|---|
status | Гдев запросе | Типstring | ОписаниеСтатусы через запятую, например awaiting_approval,approved. Значения: awaiting_approval, approved, awaiting_check, scheduled, submitting, sent, review_required, succeeded, failed, rejected, cancelled. (необязательное) |
updated_since | Гдев запросе | Типstring | ОписаниеЗаказы, изменённые в этот момент или позже: ISO 8601 со смещением, + кодируйте как %2B. (необязательное) |
employee_id | Гдев запросе | Типuuid | ОписаниеЗаказы одного сотрудника, пока он в компании. (необязательное) |
limit | Гдев запросе | Типinteger | ОписаниеСколько строк на странице, от 1 до 200. (необязательное, по умолчанию 100) |
cursor | Гдев запросе | Типstring | Описаниеnext_cursor предыдущей страницы, с теми же фильтрами. (необязательное, 1–512 символов) |
- 200
- Страница заказов. OrdersPage
- 429
- rate_limited
/orders/{id}Заказ
Заказ компании.
Ключ: Только чтение
| Параметр | Где | Тип | Описание |
|---|---|---|---|
id | Гдев пути | Типuuid | Описаниеid заказа. |
- 200
- Заказ. Order
- 429
- rate_limited
/orders/{id}/approveПодтвердить заказ
Заказ в статусе awaiting_approval: компания его оплачивает, а сотрудника просят прислать session ChatGPT. В заказе записан ключ: approved_via api_key и approved_by_api_key_id. Тело пустое, {} или с renewal: monthly либо once; без renewal — once: согласия на ежемесячное продление API никогда не даёт само. monthly — согласие компании, что подписка будет продлеваться каждый месяц за её счёт, пока её не отключат; только пока автопродление для компании открыто, иначе 409 auto_renewal_closed. Компания может выбрать monthly и для заказа, где сотрудник выбрал разовую активацию. Окончательным режим становится, когда сотрудник прислал session и согласился на продление: без его согласия заказ разовый. Режим сейчас — renewal_mode ответа. Повтор — 409 order_not_awaiting_approval.
Ключ: Чтение и действия
| Параметр | Где | Тип | Описание |
|---|---|---|---|
id | Гдев пути | Типuuid | Описаниеid заказа. |
Тело: ApproveOrderRequest (необязательное)
- 200
- Заказ после подтверждения. Order
- 429
- rate_limited
/orders/{id}/rejectОтклонить заказ
Заказ в статусе awaiting_approval отклоняется, сотрудник видит это в кабинете. Тело пустое или {}. Повтор — 409 order_not_awaiting_approval.
Ключ: Чтение и действия
| Параметр | Где | Тип | Описание |
|---|---|---|---|
id | Гдев пути | Типuuid | Описаниеid заказа. |
Тело: пустое или {}
- 200
- Заказ после отказа. Order
- 429
- rate_limited
/subscriptionsОформить подписку сотруднику
Месяц подписки за один запрос, как его оформляет оператор xWires: почта входа сотрудника, план и session его ChatGPT-аккаунта. Почты нет в xWires — создаём учётную запись в компании: войти в неё можно кодом на эту почту. Заказ подтверждён ключом (approved_via api_key) и ждёт проверки xWires (awaiting_check), затем уходит партнёру; сотруднику приходит письмо. Новая подписка активируется, активная или истёкшая продлевается на тот же ChatGPT-аккаунт и план. Отправляя session, ключ заверяет, что сотрудник согласился передать его партнёру. Если человек с этой почтой не в компании (личный клиент или сотрудник другой), или почта на домене другой компании — 202 invited: ему уходит приглашение, почта ждёт в списке с планом (с renewal: monthly — как renews_monthly), session не сохраняется. renewal: monthly — подписка с автопродлением, только пока оно открыто для компании: иначе 409 auto_renewal_closed, и ни заказа, ни приглашения. Заказ он сделает сам, приняв приглашение, и почта его подтвердит. Повтор после обрыва — 409 open_order_exists: найдите заказ в GET /orders?employee_id=…. Идёт в суточный лимит (5000 почт и приглашений через API).
Ключ: Чтение и действия
Тело: OrderSubscriptionRequest
- 201
- Заказ создан и ждёт проверки xWires. OrderSubscriptionResult
- 202
- Человек не в компании: приглашение и почта в списке, без заказа. OrderSubscriptionResult
- 400
- invalid_requestapi_key_in_urlinvalid_emailinvalid_planinvalid_sessionsession_token_requiredsession_expires_too_soon
Список почт
/approved-emailsСписок почт
Почты, которые подтверждают заказы сотрудников, ещё не использованные, по времени добавления.
Ключ: Только чтение
| Параметр | Где | Тип | Описание |
|---|---|---|---|
limit | Гдев запросе | Типinteger | ОписаниеСколько строк на странице, от 1 до 200. (необязательное, по умолчанию 100) |
cursor | Гдев запросе | Типstring | Описаниеnext_cursor предыдущей страницы, с теми же фильтрами. (необязательное, 1–512 символов) |
- 200
- Страница списка. ApprovedEmailsPage
- 429
- rate_limited
/approved-emailsДобавить почты
От 1 до 500 почт с планом, который оплачивает компания. Заказ этого плана от сотрудника с такой почтой входа или ChatGPT подтверждается сам; заказ, который уже ждёт, подтверждается сразу (approved_order_ids). Человеку, которого ещё нет в компании, уходит письмо. Почта с renews_monthly: true подтверждает заказ в режиме, который выбрал сотрудник, почта без согласия — разовым (renewal_mode once). Согласие записывается, только пока автопродление для компании открыто: записано ли оно, говорит renews_monthly ответа. Повтор безопасен: почты из списка и почты, которые уже подтвердили незавершённый заказ этого плана, считаются в skipped. В суточный лимит (5000 почт и приглашений через API) идёт каждая присланная почта.
Ключ: Чтение и действия
Тело: AddApprovedEmailsRequest
- 200
- Что сделали почты. AddApprovedEmailsResult
/approved-emails/{id}Удалить почту из списка
Почта больше не подтверждает заказы. Уже подтверждённые заказы остаются. Повтор — 404.
Ключ: Чтение и действия
| Параметр | Где | Тип | Описание |
|---|---|---|---|
id | Гдев пути | Типuuid | Описаниеid почты в списке. |
- 204
- Почта удалена.
- 429
- rate_limited
Приглашения
/invitesПриглашения
Приглашения, которые ещё не приняли, по времени. Входил ли приглашённый в xWires, API не говорит.
Ключ: Только чтение
| Параметр | Где | Тип | Описание |
|---|---|---|---|
limit | Гдев запросе | Типinteger | ОписаниеСколько строк на странице, от 1 до 200. (необязательное, по умолчанию 100) |
cursor | Гдев запросе | Типstring | Описаниеnext_cursor предыдущей страницы, с теми же фильтрами. (необязательное, 1–512 символов) |
- 200
- Страница приглашений. InvitesPage
- 429
- rate_limited
/invitesПригласить
Приглашает почту входа человека с личной почтой: он станет сотрудником, приняв приглашение в кабинете. Заказ не подтверждает: для этого добавьте почту в список. Новое приглашение — 201 и письмо человеку, существующее — 200 без письма. Идёт в суточный лимит (5000 почт и приглашений через API).
Ключ: Чтение и действия
Тело: CreateInviteRequest
- 200
- Такое приглашение уже было. InviteResult
- 201
- Новое приглашение. InviteResult
/invites/{id}Отозвать приглашение
Приглашение больше не действует. Повтор — 404.
Ключ: Чтение и действия
| Параметр | Где | Тип | Описание |
|---|---|---|---|
id | Гдев пути | Типuuid | Описаниеid приглашения. |
- 204
- Приглашение отозвано.
- 429
- rate_limited
События
/eventsЛента событий
Изменения заказов, сотрудников и подписок компании, те же, что приходят на вебхуки, по position. Храните next_after и спрашивайте after=<next_after> раз в 10–60 секунд или на каждый вебхук. after=0 — с самого старого хранимого события, after=latest — с текущего момента. 410 events_cursor_expired: событий сразу после этой позиции уже нет (они истекли, или базу xWires восстановили из копии): запросите after=latest и сохраните next_after, затем перечитайте списки и продолжайте с сохранённой позиции.
Ключ: Только чтение
| Параметр | Где | Тип | Описание |
|---|---|---|---|
after | Гдев запросе | Типstring | Описание0 — с самого старого хранимого события, latest — только новые, число — после этой позиции (next_after). (необязательное, по умолчанию "0") Шаблон: ^(latest|0|[1-9][0-9]{0,17})$ |
types | Гдев запросе | Типstring | ОписаниеТипы событий через запятую; без него — все. Значения: order.awaiting_approval, order.approved, order.awaiting_check, order.scheduled, order.submitting, order.sent, order.succeeded, order.failed, order.review_required, order.rejected, order.cancelled, employee.joined, employee.left, employee.role_changed, subscription.activated, subscription.renewed, subscription.expired, subscription.auto_renewal_started, subscription.auto_renewal_stopped. (необязательное) |
limit | Гдев запросе | Типinteger | ОписаниеСколько строк на странице, от 1 до 200. (необязательное, по умолчанию 100) |
- 200
- Страница ленты. EventsPage
- 429
- rate_limited
Песочница
/sandbox/employeesПридуманный сотрудник
Только в песочнице. Новый сотрудник компании с подпиской в статусе new на план из тела: событие employee.joined. Его почты под .invalid, войти за него нельзя. Его заказ — POST /sandbox/orders.
Ключ: Чтение и действия
Тело: CreateSandboxEmployeeRequest
- 201
- Сотрудник. Employee
- 404
- not_found
- 429
- rate_limited
/sandbox/ordersЗаказ сотрудника
Только в песочнице. Сотрудник заказывает месяц подписки, как в своём кабинете: заказ awaiting_approval, или сразу approved, если его почта в списке компании. Активация для новой подписки, продление для активной или истёкшей. Подтверждённый заказ доводит до succeeded автопилот, если его не остановить (POST /sandbox/orders/{id}/advance).
Ключ: Чтение и действия
Тело: CreateSandboxOrderRequest
- 201
- Заказ. Order
- 429
- rate_limited
/sandbox/orders/{id}/advanceПеревести заказ
Только в песочнице. Заказ идёт в статус to шаг за шагом, за сотрудника, оператора и партнёра, как в проде: approved → awaiting_check → scheduled → sent → succeeded или failed; review_required из scheduled, cancelled из awaiting_check и review_required, approved из awaiting_check. Каждый шаг — своё событие, без actor. Подтверждают и отклоняют заказ, как всегда, POST /orders/{id}/approve и /reject. После вызова автопилот этот заказ больше не трогает; to, равный нынешнему статусу, только останавливает его.
Ключ: Чтение и действия
| Параметр | Где | Тип | Описание |
|---|---|---|---|
id | Гдев пути | Типuuid | Описаниеid заказа. |
Тело: AdvanceSandboxOrderRequest
- 200
- Заказ в новом статусе. Order
- 429
- rate_limited
/sandbox/employees/{id}/expire-subscriptionЗавершить подписку
Только в песочнице. Активная подписка сотрудника истекает сейчас: событие subscription.expired. Его следующий заказ — продление. Тело пустое или {}.
Ключ: Чтение и действия
| Параметр | Где | Тип | Описание |
|---|---|---|---|
id | Гдев пути | Типuuid | Описаниеid сотрудника. |
Тело: пустое или {}
- 200
- Сотрудник с истёкшей подпиской. Employee
- 429
- rate_limited
Схемы
Поля ответа обязательны и приходят всегда, иногда со значением null; «необязательное» — поле, которого может не быть. В v1 поля только добавляются.
Me
Ключ, которым сделан запрос, и его компания.
| Поле | Тип | Описание |
|---|---|---|
key | Типobject | Описание |
key.id | Типuuid | Описаниеid ключа: его же несут actor.api_key_id событий и approved_by_api_key_id заказов, которые он подтвердил. |
key.name | Типstring | ОписаниеНазвание, которое дал ключу админ. |
key.access | Тип"read" | "manage" | … | Описаниеread: только чтение; manage: чтение и действия. |
key.last4 | Типstring | ОписаниеПоследние 4 символа ключа: в кабинете он показан как xwc_…last4. Шаблон: ^[0-9a-f]{4}$ |
key.created_at | Типdate-time | ОписаниеКогда ключ выпустили. |
company | Типobject | Описание |
company.id | Типuuid | Описаниеid компании. |
company.name | Типstring | ОписаниеНазвание компании. |
AutoRenewal
Автопродление подписки: каждый месяц её продлевает сам OpenAI за счёт плательщика, пока его не отключат.
| Поле | Тип | Описание |
|---|---|---|
state | Тип"on" | "needs_session" | "in_progress" | "attention" | "off" | … | Описаниеon: включено; off: выключено или не включалось; needs_session, in_progress, attention — появятся вместе с самими продлениями: нужен новый session, продлеваем, на разборе у xWires. Незнакомое значение считайте включённым. |
next_renewal_at | Типdate-time | null | ОписаниеКогда подписка продлится; null, если автопродление выключено. |
stop_reason | Тип"user" | "company_admin" | "api_key" | "operator" | "offboarded" | "payer_changed" | … | null | ОписаниеПочему выключено: user — сотрудник, company_admin — админ компании, api_key — система компании, operator — оператор xWires, offboarded — сотрудник ушёл, payer_changed — сменился плательщик. null, если включено или не включалось. |
can_stop | Типboolean | ОписаниеКомпания может отключить его (stopAutoRenewal): оно включено, и платит она. |
Subscription
Подписка ChatGPT сотрудника.
| Поле | Тип | Описание |
|---|---|---|
chatgpt_email | Типstring | ОписаниеПочта ChatGPT-аккаунта, который пополняется. |
plan | Тип"plus" | "pro100" | "pro200" | "pro500" | … | ОписаниеПлан подписки. |
status | Тип"new" | "processing" | "active" | "expired" | "cancelled" | … | Описаниеnew: план выбран, ещё не оплачен; processing: заказ у партнёра; active: действует; expired: срок прошёл; cancelled: отказ. |
expires_at | Типdate-time | null | ОписаниеДо какого момента оплачена; null, пока её ни разу не активировали. |
auto_renewal | ТипAutoRenewal | Описание |
Employee
Текущий пользователь компании, админ тоже.
| Поле | Тип | Описание |
|---|---|---|
id | Типuuid | Описаниеid сотрудника. |
email | Типstring | ОписаниеПочта, с которой он входит в xWires. |
name | Типstring | null | ОписаниеИмя, если сотрудник его указал. |
role | Тип"employee" | "admin" | … | Описаниеemployee или admin. |
subscription | ТипSubscription | null | ОписаниеПодписка; null, пока сотрудник не выбрал план и ChatGPT-аккаунт. |
OrderEmployee
Сотрудник, чей это заказ, пока он в компании.
| Поле | Тип | Описание |
|---|---|---|
id | Типuuid | Описаниеid сотрудника. |
email | Типstring | ОписаниеПочта входа. |
name | Типstring | null | ОписаниеИмя, если указано. |
chatgpt_email | Типstring | ОписаниеПочта ChatGPT-аккаунта подписки. |
Order
Заказ на месяц подписки, который оплачивает компания.
| Поле | Тип | Описание |
|---|---|---|
id | Типuuid | Описаниеid заказа. |
status | Тип"awaiting_approval" | "approved" | "awaiting_check" | "scheduled" | "submitting" | "sent" | "review_required" | "succeeded" | "failed" | "rejected" | "cancelled" | … | ОписаниеСтатус. Может вернуться назад: failed → sent, awaiting_check → approved, review_required → approved. Незнакомый статус считайте заказом в работе. |
is_open | Типboolean | ОписаниеЗаказ ещё не завершён: у сотрудника не может быть второго открытого. |
kind | Тип"activation" | "renewal" | … | Описаниеactivation: первый месяц; renewal: продление. |
plan | Тип"plus" | "pro100" | "pro200" | "pro500" | … | ОписаниеПлан. |
renewal_mode | Тип"monthly" | "once" | … | Описаниеmonthly: подписка будет продлеваться каждый месяц за счёт компании, пока её не отключат; once: оплачивается один месяц. Пока заказ ждёт подтверждения — выбор сотрудника; после подтверждения — выбор компании (renewal в approveOrder); окончательный — с awaiting_check: monthly, только если и сотрудник согласился на продление, присылая session, иначе once. |
origin | Тип"manual" | "auto_renewal" | … | Описаниеmanual: заказ оформили сотрудник, компания или оператор; auto_renewal: продление по автопродлению. |
created_at | Типdate-time | ОписаниеКогда заказ создан. |
updated_at | Типdate-time | ОписаниеПоследнее изменение: по нему идут список и фильтр updated_since. |
approved_at | Типdate-time | null | ОписаниеКогда подтверждён; null, пока нет. |
approved_by_operator | Типboolean | ОписаниеПодтвердил оператор xWires за компанию. |
approved_via | Тип"company_admin" | "operator" | "api_key" | … | null | ОписаниеКто подтвердил, кнопкой или почтой из списка: company_admin, operator или api_key; null, пока никто. |
approved_by_api_key_id | Типuuid | null | ОписаниеКлюч, который подтвердил заказ или добавил почту, которая его подтвердила. |
scheduled_for | Типdate-time | null | ОписаниеКогда заказ уйдёт партнёру; null, пока не назначено. |
completed_at | Типdate-time | null | ОписаниеКогда партнёр выполнил заказ (succeeded) или отказал в нём (failed); null у остальных статусов, также у rejected и cancelled. Закрыт ли заказ, показывает is_open. |
employee | ТипOrderEmployee | null | Описаниеnull, если сотрудник уже ушёл из компании. |
ApprovedEmail
Почта из списка компании, ещё не подтвердившая заказ.
| Поле | Тип | Описание |
|---|---|---|
id | Типuuid | Описаниеid почты в списке. |
email | Типstring | ОписаниеПочта, в нижнем регистре. |
plan | Тип"plus" | "pro100" | "pro200" | "pro500" | … | null | ОписаниеПлан, заказ которого она подтверждает; null — любой (почты, добавленные до планов). |
renews_monthly | Типboolean | ОписаниеПочта несёт согласие компании, что подписка будет продлеваться каждый месяц за её счёт: она подтверждает заказ в режиме, который выбрал сотрудник, почта без согласия — разовым. |
created_at | Типdate-time | ОписаниеКогда добавлена. |
added_by_operator | Типboolean | ОписаниеДобавил оператор xWires. |
Invite
Приглашение, которое ещё не приняли.
| Поле | Тип | Описание |
|---|---|---|
id | Типuuid | Описаниеid приглашения. |
email | Типstring | ОписаниеПриглашённая почта входа, в нижнем регистре. |
created_at | Типdate-time | ОписаниеКогда приглашён. |
EmployeesPage
Страница сотрудников, по id.
| Поле | Тип | Описание |
|---|---|---|
data | ТипEmployee[] | Описание |
next_cursor | Типstring | null | ОписаниеПередайте его в cursor, чтобы получить следующую страницу; null — страниц больше нет. |
OrdersPage
Страница заказов, по updated_at и id.
| Поле | Тип | Описание |
|---|---|---|
data | ТипOrder[] | Описание |
next_cursor | Типstring | null | ОписаниеПередайте его в cursor, чтобы получить следующую страницу; null — страниц больше нет. |
ApprovedEmailsPage
Страница списка почт, по created_at и id.
| Поле | Тип | Описание |
|---|---|---|
data | ТипApprovedEmail[] | Описание |
next_cursor | Типstring | null | ОписаниеПередайте его в cursor, чтобы получить следующую страницу; null — страниц больше нет. |
InvitesPage
Страница приглашений, по created_at и id.
| Поле | Тип | Описание |
|---|---|---|
data | ТипInvite[] | Описание |
next_cursor | Типstring | null | ОписаниеПередайте его в cursor, чтобы получить следующую страницу; null — страниц больше нет. |
AddApprovedEmailsResult
Что сделали присланные почты.
| Поле | Тип | Описание |
|---|---|---|
approved | Типinteger | ОписаниеСколько почт сразу подтвердили ждущий заказ. |
remembered | Типinteger | ОписаниеСколько почт добавлены в список и ждут заказа. |
skipped | Типinteger | ОписаниеСколько уже были в списке, уже подтвердили незавершённый заказ этого плана или повторялись в запросе. |
invalid | Типinteger | ОписаниеСколько не похожи на почту. |
approved_order_ids | Типuuid[] | ОписаниеЗаказы, которые подтвердились сразу. |
renews_monthly | Типboolean | ОписаниеПочты запроса несут согласие на ежемесячное продление. false при renews_monthly: true в запросе — автопродление для компании пока закрыто, и согласие не записано. |
InviteResult
Приглашение и создано ли оно этим запросом.
| Поле | Тип | Описание |
|---|---|---|
invite | ТипInvite | Описание |
created | Типboolean | Описаниеtrue: новое, человеку ушло письмо; false: такое приглашение уже было. |
OrderSubscriptionResult
Что сделал запрос: заказ сотрудника или приглашение человеку вне компании.
| Поле | Тип | Описание |
|---|---|---|
outcome | Тип"ordered" | "invited" | … | Описаниеordered (201): заказ создан и ждёт проверки xWires; invited (202): человек не в компании, ему ушло приглашение, почта добавлена в список с планом, session не сохранён. |
account_created | Типboolean | Описаниеtrue: учётную запись xWires для этой почты создал этот запрос; у invited всегда false. |
order | ТипOrder | null | ОписаниеЗаказ; null у invited. |
OffboardResult
Сотрудник ушёл; какие его заказы отменены.
| Поле | Тип | Описание |
|---|---|---|
employee_id | Типuuid | Описаниеid ушедшего сотрудника. |
cancelled_order_ids | Типuuid[] | ОписаниеЗаказы за счёт компании, отменённые потому, что ещё не ушли партнёру. |
EventActor
Ключ компании, чей запрос к API вызвал событие; null — кто угодно другой: админ, сотрудник, xWires. Сравните с key.id из GET /me, чтобы узнать свои действия.
| Поле | Тип | Описание |
|---|---|---|
api_key_id | Типuuid | Описание |
OrderEvent
order.<status>: заказ компании перешёл в статус status.
| Поле | Тип | Описание |
|---|---|---|
id | Типuuid | Описаниеid события; он же заголовок webhook-id: повторы отбрасывайте по нему. |
timestamp | Типdate-time | ОписаниеКогда это случилось. |
position | Типinteger | ОписаниеНомер события в компании, без пропусков по всем типам; в одном изменении номера могут идти не подряд. |
company_id | Типuuid | Описаниеid компании. |
actor | ТипEventActor | null | Описание |
type | Тип"order.awaiting_approval" | "order.approved" | "order.awaiting_check" | "order.scheduled" | "order.submitting" | "order.sent" | "order.succeeded" | "order.failed" | "order.review_required" | "order.rejected" | "order.cancelled" | … | Описание |
data | Типobject | Описание |
data.order_id | Типuuid | Описаниеid заказа: подробности — GET /orders/{id}. |
data.status | Тип"awaiting_approval" | "approved" | "awaiting_check" | "scheduled" | "submitting" | "sent" | "review_required" | "succeeded" | "failed" | "rejected" | "cancelled" | … | ОписаниеСтатус после изменения. |
data.previous_status | Тип"awaiting_approval" | "approved" | "awaiting_check" | "scheduled" | "submitting" | "sent" | "review_required" | "succeeded" | "failed" | "rejected" | "cancelled" | … | null | ОписаниеСтатус до; null, если заказ только что стал заказом компании. |
data.kind | Тип"activation" | "renewal" | … | Описание |
data.plan | Тип"plus" | "pro100" | "pro200" | "pro500" | … | Описание |
data.renewal_mode | Тип"monthly" | "once" | … | ОписаниеРежим продления после изменения, как renewal_mode в Order. |
data.employee_id | Типuuid | null | ОписаниеСотрудник; null, если он уже ушёл из компании. |
data.approved_via | Тип"company_admin" | "operator" | "api_key" | … | null | ОписаниеКто подтвердил заказ, как в Order. |
data.approved_by_api_key_id | Типuuid | null | ОписаниеКлюч, который подтвердил заказ или добавил подтвердившую его почту. |
EmployeeEvent
employee.joined, employee.left, employee.role_changed: человек пришёл в компанию, ушёл или сменил роль.
| Поле | Тип | Описание |
|---|---|---|
id | Типuuid | Описаниеid события; он же заголовок webhook-id: повторы отбрасывайте по нему. |
timestamp | Типdate-time | ОписаниеКогда это случилось. |
position | Типinteger | ОписаниеНомер события в компании, без пропусков по всем типам; в одном изменении номера могут идти не подряд. |
company_id | Типuuid | Описаниеid компании. |
actor | ТипEventActor | null | Описание |
type | Тип"employee.joined" | "employee.left" | "employee.role_changed" | … | Описание |
data | Типobject | Описание |
data.employee_id | Типuuid | Описаниеid сотрудника. |
data.role | Тип"employee" | "admin" | … | ОписаниеРоль после изменения; у ушедшего — роль, с которой он ушёл. |
data.previous_role | Тип"employee" | "admin" | … | null | ОписаниеРоль до; только у employee.role_changed. (необязательное) |
SubscriptionEvent
subscription.activated, subscription.renewed, subscription.expired, subscription.auto_renewal_started, subscription.auto_renewal_stopped: подписка сотрудника, который сейчас в компании.
| Поле | Тип | Описание |
|---|---|---|
id | Типuuid | Описаниеid события; он же заголовок webhook-id: повторы отбрасывайте по нему. |
timestamp | Типdate-time | ОписаниеКогда это случилось. |
position | Типinteger | ОписаниеНомер события в компании, без пропусков по всем типам; в одном изменении номера могут идти не подряд. |
company_id | Типuuid | Описаниеid компании. |
actor | ТипEventActor | null | Описание |
type | Тип"subscription.activated" | "subscription.renewed" | "subscription.expired" | "subscription.auto_renewal_started" | "subscription.auto_renewal_stopped" | … | Описание |
data | Типobject | Описание |
data.employee_id | Типuuid | Описаниеid сотрудника. |
data.status | Тип"new" | "processing" | "active" | "expired" | "cancelled" | … | ОписаниеСтатус подписки после изменения. |
data.previous_status | Тип"new" | "processing" | "active" | "expired" | "cancelled" | … | null | ОписаниеСтатус до. |
data.plan | Тип"plus" | "pro100" | "pro200" | "pro500" | … | Описание |
data.expires_at | Типdate-time | null | ОписаниеДо какого момента оплачена. |
data.stop_reason | Тип"user" | "company_admin" | "api_key" | "operator" | "offboarded" | "payer_changed" | … | ОписаниеТолько у subscription.auto_renewal_stopped: почему выключено, как stop_reason в AutoRenewal. (необязательное) |
UnknownEvent
Событие типа, которого в этом описании ещё нет: в v1 типы только добавляются. Конверт тот же, что у других событий; такое событие пропустите.
| Поле | Тип | Описание |
|---|---|---|
id | Типuuid | Описаниеid события; он же заголовок webhook-id: повторы отбрасывайте по нему. |
timestamp | Типdate-time | ОписаниеКогда это случилось. |
position | Типinteger | null | ОписаниеНомер события в компании; null — у события только для вебхуков, как у webhook.test. |
company_id | Типuuid | Описаниеid компании. |
actor | ТипEventActor | null | Описание |
type | Типstring | ОписаниеТип события. |
data | Типobject | ОписаниеДанные события, свои у каждого типа. |
Event
Событие компании: в ленте GET /events и в теле вебхука. Незнакомые типы и поля пропускайте.
Одна из схем, по полю type: OrderEvent | EmployeeEvent | SubscriptionEvent | UnknownEvent
WebhookTestEvent
webhook.test: тестовое событие, которое админ отправил из кабинета. Только в вебхуках, не в ленте.
| Поле | Тип | Описание |
|---|---|---|
id | Типuuid | Описаниеid тестовой отправки; он же webhook-id. |
type | Тип"webhook.test" | … | Описание |
timestamp | Типdate-time | ОписаниеКогда отправлено. |
position | Тип… | null | Описание |
company_id | Типuuid | Описаниеid компании. |
actor | Тип… | null | Описание |
data | Типobject | Описание |
data.endpoint_id | Типuuid | ОписаниеАдрес вебхука, который проверяли. |
WebhookEvent
Тело вебхука: событие компании или тестовое.
Одна из схем, по полю type: OrderEvent | EmployeeEvent | SubscriptionEvent | WebhookTestEvent | UnknownEvent
EventsPage
Страница ленты событий, по position.
| Поле | Тип | Описание |
|---|---|---|
data | ТипEvent[] | Описание |
next_after | Типinteger | ОписаниеПередайте его в after следующего запроса. Без новых событий он равен последней позиции компании. |
has_more | Типboolean | Описаниеtrue: за next_after уже есть события, спросите сразу. |
Error
Ошибка. Решения принимайте по code; message — для человека.
| Поле | Тип | Описание |
|---|---|---|
error | Типobject | Описание |
error.code | Тип"invalid_request" | "invalid_session" | "invalid_cursor" | "api_key_in_url" | "not_found" | "request_timeout" | "payload_too_large" | "unsupported_media_type" | "rate_limited" | "internal_error" | "service_unavailable" | "invalid_api_key" | "api_key_revoked" | "api_key_read_only" | "invalid_email" | "invalid_plan" | "session_token_required" | "session_expires_too_soon" | "no_emails" | "too_many_emails" | "order_not_found" | "employee_not_found" | "approved_email_not_found" | "invite_not_found" | "order_not_awaiting_approval" | "already_employee" | "open_order_exists" | "chatgpt_email_taken" | "chatgpt_email_mismatch" | "plan_mismatch" | "auto_renewal_not_on" | "auto_renewal_on" | "auto_renewal_closed" | "cannot_offboard_admin" | "events_cursor_expired" | "api_email_quota_exceeded" | "membership_not_found" | "sandbox_invalid_transition" | "subscription_not_active" | … | Описание |
error.message | Типstring | ОписаниеЧто случилось и что делать, по-русски. |
error.request_id | Типuuid | ОписаниеОн же заголовок X-Request-Id: назовите его поддержке. |
error.details | Типobject | ОписаниеУ invalid_request: какие поля не прошли проверку; у invalid_session: path session и причина. Значения не повторяются. (необязательное) |
error.details.issues | Типobject[] | Описание |
error.details.issues[].path | Типstring | ОписаниеПоле, через точку; пусто — весь запрос. |
error.details.issues[].issue | Типstring | ОписаниеКод проблемы zod (invalid_type, too_big, unrecognized_keys…) или у invalid_session — причина: too_large, wrong_shape, email_mismatch, expired. |
SearchEmployeesRequest
Почта для поиска.
| Поле | Тип | Описание |
|---|---|---|
email | Типstring | ОписаниеПочта входа или ChatGPT-аккаунта, в любом регистре. (до 320 символов) |
AddApprovedEmailsRequest
Почты и план, который оплачивает компания.
| Поле | Тип | Описание |
|---|---|---|
emails | Типstring[] | ОписаниеПочты входа или ChatGPT-аккаунтов сотрудников, в любом регистре; повторы считаются в skipped. (от 1 до 500 элементов) |
plan | Тип"plus" | "pro100" | "pro200" | "pro500" | ОписаниеПлан: почта подтверждает заказ только этого плана. |
renews_monthly | Типboolean | ОписаниеКомпания согласна, что подписка, которую подтвердит почта, будет продлеваться каждый месяц за её счёт, пока её не отключат. Почта с согласием подтверждает заказ в режиме, который выбрал сотрудник, без него — разовым. Согласие записывается, только пока автопродление для компании открыто: записано ли оно, говорит renews_monthly ответа. (необязательное) |
CreateInviteRequest
Почта, которую пригласить.
| Поле | Тип | Описание |
|---|---|---|
email | Типstring | ОписаниеПочта, с которой человек входит или войдёт в xWires. (до 320 символов) |
ApproveOrderRequest
Подтверждение заказа; тело может быть пустым.
| Поле | Тип | Описание |
|---|---|---|
renewal | Тип"monthly" | "once" | Описаниеmonthly — компания согласна, что подписка будет продлеваться каждый месяц за её счёт, пока её не отключат; once — разовая активация. Без поля — once, что бы ни выбрал сотрудник. monthly — только пока автопродление для компании открыто, иначе 409 auto_renewal_closed. Окончательно заказ станет monthly, когда сотрудник согласится на продление, присылая session. (необязательное) |
OrderSubscriptionRequest
Сотрудник, план и session его ChatGPT-аккаунта.
| Поле | Тип | Описание |
|---|---|---|
email | Типstring | ОписаниеПочта, с которой человек входит или войдёт в xWires, в любом регистре. (до 320 символов) |
plan | Тип"plus" | "pro100" | "pro200" | "pro500" | ОписаниеПлан, который оплачивает компания. |
session | Типobject | ОписаниеОтвет https://chatgpt.com/api/auth/session целиком, JSON-объектом: его открывает сотрудник, войдя в свой ChatGPT-аккаунт. Почта аккаунта берётся из него. Мы храним его зашифрованным и передаём только партнёру, который оплачивает подписку. |
name | Типstring | ОписаниеИмя для учётной записи, если её ещё нет; имя существующей не меняется. (необязательное, 1–100 символов) |
renewal | Тип"monthly" | "once" | Описаниеmonthly — компания согласна, что подписка будет продлеваться каждый месяц за её счёт, пока её не отключат, а сотрудник — что для этого хранится его session; once — разовая активация. Без поля — once. monthly — только пока автопродление для компании открыто, иначе 409 auto_renewal_closed. (необязательное) |
CreateSandboxEmployeeRequest
Придуманный сотрудник песочницы.
| Поле | Тип | Описание |
|---|---|---|
name | Типstring | ОписаниеИмя; без него — одно из придуманных. (необязательное, 1–100 символов) |
plan | Тип"plus" | "pro100" | "pro200" | "pro500" | ОписаниеПлан его подписки: его заказы будут на этот план. |
CreateSandboxOrderRequest
Чей заказ.
| Поле | Тип | Описание |
|---|---|---|
employee_id | Типuuid | Описаниеid сотрудника с подпиской и без открытого заказа. |
AdvanceSandboxOrderRequest
Куда перевести заказ.
| Поле | Тип | Описание |
|---|---|---|
to | Тип"awaiting_approval" | "approved" | "awaiting_check" | "scheduled" | "submitting" | "sent" | "review_required" | "succeeded" | "failed" | "rejected" | "cancelled" | ОписаниеСтатус, в который заказ придёт шаг за шагом: approved, awaiting_check, scheduled, sent, review_required, succeeded, failed или cancelled. Нынешний статус только останавливает автопилот. |
Заголовки ответа
| Заголовок | Что значит |
|---|---|
X-Request-Id | id запроса: назовите его поддержке. |
RateLimit-Limit | Сколько запросов с ключа можно за минуту. |
RateLimit-Remaining | Сколько из них осталось в этой минуте. |
RateLimit-Reset | Через сколько секунд минута начнётся заново. |
Retry-After | Через сколько секунд повторить. |
13Лента событий и вебхуки
События — это изменения заказов, сотрудников и подписок компании. Их можно читать лентой GET /events и получать вебхуками на адреса HTTPS ваших систем. В событии только идентификаторы, статусы, план, вид заказа, роль, время и ключ, если действовал он; подробности система берёт через API.
Типы
| type | Что случилось |
|---|---|
order.awaiting_approval | Заказ ждёт подтверждения |
order.approved | Заказ подтверждён |
order.awaiting_check | Заказ проверяет оператор |
order.scheduled | Заказ запланирован |
order.submitting | Заказ отправляется партнёру |
order.sent | Заказ отправлен партнёру |
order.succeeded | Заказ выполнен |
order.failed | Заказ не выполнен |
order.review_required | Заказ на разборе у оператора |
order.rejected | Заказ отклонён |
order.cancelled | Заказ отменён |
employee.joined | Сотрудник присоединился |
employee.left | Сотрудник ушёл |
employee.role_changed | Роль сотрудника изменилась |
subscription.activated | Подписка активирована |
subscription.renewed | Подписка продлена |
subscription.expired | Подписка истекла |
subscription.auto_renewal_started | Автопродление включено |
subscription.auto_renewal_stopped | Автопродление выключено |
webhook.test | Тестовое событие из кабинета, только в вебхуках |
order.<status>— заказ, который оплачивает компания, перешёл в этот статус.previous_status: null— заказ только что стал заказом компании. После ухода сотрудника события его заказа приходят сemployee_id: null.employee.joined— человек стал сотрудником: вошёл с почтой домена компании, принял приглашение, открыл ссылку для сотрудников или его добавил оператор.employee.left— его отметили уволившимся, он перешёл в другую компанию или удалён.employee.role_changed— стал админом или перестал им быть (previous_role).subscription.activated,subscription.renewed,subscription.expired— подписка сотрудника заработала, продлена (новыйexpires_at) или истекла. Приходят компании, в которой человек сейчас.subscription.auto_renewal_started— заказ с автопродлением выполнен, подписка будет продлеваться сама;subscription.auto_renewal_stopped— автопродление выключили,stop_reasonговорит, кто или почему. Приходят компании, в которой человек сейчас.- Личные заказы и подписки людей вне компании в события не попадают никогда.
Событие
Конверт одинаков у всех типов: id, type, timestamp (когда это случилось), position, company_id, actor и data своей группы (схемы Event и WebhookEvent). actor.api_key_id — ключ компании, чей запрос к API вызвал событие; null — кто угодно другой: админ в кабинете, сотрудник, xWires. Заказ, который почта, добавленная ключом, подтвердит позже, когда сотрудник его оформит, придёт с actor: null и approved_via: "api_key". Заказ, который подтвердил сам запрос POST /approved-emails (он есть в approved_order_ids), придёт с actor.api_key_id этого ключа.
{
"id": "e5b1a7c3-2d9f-4e60-8b4a-1c7e3f9d2a85",
"type": "order.approved",
"timestamp": "2026-10-15T09:30:12.345678+00:00",
"position": 1042,
"company_id": "6f1c2b9e-3d4a-4c71-9a5e-2b8f0d1c7a43",
"actor": {
"api_key_id": "0b7d9e2a-5c13-4f6e-8a21-9d4c3b7e1f60"
},
"data": {
"order_id": "9c2e4b71-6a3f-4d58-b1e0-7f3a2c9d5e14",
"status": "approved",
"previous_status": "awaiting_approval",
"kind": "renewal",
"plan": "plus",
"renewal_mode": "once",
"employee_id": "3a9e5c71-8b2d-4f04-a6c9-1e7b2d5f8a90",
"approved_via": "api_key",
"approved_by_api_key_id": "0b7d9e2a-5c13-4f6e-8a21-9d4c3b7e1f60"
}
}{
"id": "4f2a9c61-0b8e-4d37-9e5c-3a1d7b6f2e08",
"type": "employee.role_changed",
"timestamp": "2026-10-15T11:02:47.902316+00:00",
"position": 1043,
"company_id": "6f1c2b9e-3d4a-4c71-9a5e-2b8f0d1c7a43",
"actor": null,
"data": {
"employee_id": "3a9e5c71-8b2d-4f04-a6c9-1e7b2d5f8a90",
"role": "admin",
"previous_role": "employee"
}
}{
"id": "b8e3d1f7-6c2a-4b90-8f15-2e7c9a4d6b31",
"type": "subscription.renewed",
"timestamp": "2026-10-15T12:41:09.664051+00:00",
"position": 1044,
"company_id": "6f1c2b9e-3d4a-4c71-9a5e-2b8f0d1c7a43",
"actor": null,
"data": {
"employee_id": "3a9e5c71-8b2d-4f04-a6c9-1e7b2d5f8a90",
"status": "active",
"previous_status": "active",
"plan": "plus",
"expires_at": "2026-12-14T08:03:51.207114+00:00"
}
}{
"id": "2c9f4e7a-1d3b-4a68-9e05-7b1c3d5f8a24",
"type": "subscription.auto_renewal_stopped",
"timestamp": "2026-10-19T10:15:32.402917+00:00",
"position": 1051,
"company_id": "6f1c2b9e-3d4a-4c71-9a5e-2b8f0d1c7a43",
"actor": {
"api_key_id": "0b7d9e2a-5c13-4f6e-8a21-9d4c3b7e1f60"
},
"data": {
"employee_id": "3a9e5c71-8b2d-4f04-a6c9-1e7b2d5f8a90",
"status": "active",
"previous_status": null,
"plan": "plus",
"expires_at": "2026-11-14T08:03:51.207114+00:00",
"stop_reason": "api_key"
}
}Позиция
position — номер события в компании: по всем типам, без пропусков, 1, 2, 3… События одного изменения могут получить номера не подряд, если в то же время менялось что-то ещё. После восстановления базы xWires из резервной копии позиции уходят скачком вперёд, а на позицию, которой больше нет, лента отвечает 410 events_cursor_expired — с фильтром types и без, при первом же запросе: перечитайте списки, как на эту ошибку. С фильтром types позиции, конечно, идут с пропусками.
Лента
after=0 — с самого старого хранимого события (мы храним их 30 дней), after=latest — только новые, число — после этой позиции. types — типы через запятую, limit — до 200. Лента работает независимо от вебхуков, также когда они выключены.
curl -sS "https://api.xwires.ai/company/v1/events?after=1041" \ -H "Authorization: Bearer $XWIRES_COMPANY_API_KEY"
{
"data": [
{
"id": "e5b1a7c3-2d9f-4e60-8b4a-1c7e3f9d2a85",
"type": "order.approved",
"timestamp": "2026-10-15T09:30:12.345678+00:00",
"position": 1042,
"company_id": "6f1c2b9e-3d4a-4c71-9a5e-2b8f0d1c7a43",
"actor": {
"api_key_id": "0b7d9e2a-5c13-4f6e-8a21-9d4c3b7e1f60"
},
"data": {
"order_id": "9c2e4b71-6a3f-4d58-b1e0-7f3a2c9d5e14",
"status": "approved",
"previous_status": "awaiting_approval",
"kind": "renewal",
"plan": "plus",
"renewal_mode": "once",
"employee_id": "3a9e5c71-8b2d-4f04-a6c9-1e7b2d5f8a90",
"approved_via": "api_key",
"approved_by_api_key_id": "0b7d9e2a-5c13-4f6e-8a21-9d4c3b7e1f60"
}
},
{
"id": "4f2a9c61-0b8e-4d37-9e5c-3a1d7b6f2e08",
"type": "employee.role_changed",
"timestamp": "2026-10-15T11:02:47.902316+00:00",
"position": 1043,
"company_id": "6f1c2b9e-3d4a-4c71-9a5e-2b8f0d1c7a43",
"actor": null,
"data": {
"employee_id": "3a9e5c71-8b2d-4f04-a6c9-1e7b2d5f8a90",
"role": "admin",
"previous_role": "employee"
}
},
{
"id": "b8e3d1f7-6c2a-4b90-8f15-2e7c9a4d6b31",
"type": "subscription.renewed",
"timestamp": "2026-10-15T12:41:09.664051+00:00",
"position": 1044,
"company_id": "6f1c2b9e-3d4a-4c71-9a5e-2b8f0d1c7a43",
"actor": null,
"data": {
"employee_id": "3a9e5c71-8b2d-4f04-a6c9-1e7b2d5f8a90",
"status": "active",
"previous_status": "active",
"plan": "plus",
"expires_at": "2026-12-14T08:03:51.207114+00:00"
}
}
],
"next_after": 1044,
"has_more": false
}410 events_cursor_expired: событий сразу после этой позиции уже нет — они старше 30 дней, или базу xWires восстановили из резервной копии и они потерялись. Запросите after=latest и сохраните next_after, перечитайте списки и продолжайте с сохранённой позиции.
Вебхуки
Адреса добавляет админ компании: «Компания → Интеграции → Вебхуки». У адреса — URL, описание и типы событий: все, включая будущие, или выбранные. Секрет whsec_… показывается один раз. О каждом новом адресе и смене адреса письмом узнают все админы компании, история изменений хранится. Ключ API адресами не управляет.
- Адрес —
https://на порту 443, имя с A-записью (IPv4) и сертификатом из публично доверенных; сертификат от Russian Trusted Root CA (Минцифры) не подойдёт. Перенаправления не выполняются: ответ 3xx — неудача. Адреса в локальных и частных сетях и на доменах xWires не принимаются. - Запросы приходят с IP
135.106.174.63: разрешите его на своей стороне. Смену адреса объявим за 30 дней. - Ответьте
2xxза 10 секунд: примите событие, ответьте, а обработайте потом, например в своей очереди. - Доставка «хотя бы раз»: событие может прийти дважды, повторы отбрасывайте по
webhook-id. На один адрес запросы идут по одному, обычно в течение минуты после события, первые попытки — по порядку позиций. Повторы, тесты и «Повторить» в кабинете порядок меняют, поэтому действуйте по данным API: статус заказа берите из GET /orders/{id}, а не из порядка событий. - Не получилось — повторим с паузами в 1, 5, 15, 30, 60, 120, 240, 360 и 480 минут: до 10 попыток, примерно за 22 часа. После 3 неудач подряд адрес «не принимает события»: туда идут только пробные запросы по тому же расписанию, а как только пробный пройдёт, уйдёт и всё накопленное.
- Через 1 час неудач админам придёт письмо. Событие, которое не удалось доставить за 3 дня с первой попытки, мы больше не отправляем. События, накопившиеся, пока адрес не принимал запросы или оператор xWires останавливал вебхуки компании, придут позже, по порядку, и могут быть старше 3 дней: когда это случилось, смотрите в
timestamp. Через 5 дней без успешной доставки адрес выключается, и админам снова придёт письмо. - Включённый снова адрес не получит событий, случившихся, пока он был выключен: заберите их из ленты. Событие, которое не удалось доставить до выключения, после включения мы ещё попробуем отправить, если с первой попытки прошло меньше 3 дней.
Заголовки и подпись
Подпись — по Standard Webhooks: подходят его библиотеки (standardwebhooks в npm и PyPI) или код ниже.
| Заголовок | Что значит |
|---|---|
webhook-id | id события, одинаковый во всех попытках: повторы отбрасывайте по нему. |
webhook-timestamp | Время попытки в секундах Unix. Отклоняйте запросы, которые расходятся с вашими часами больше чем на 300 секунд. |
webhook-signature | v1, и base64 от HMAC-SHA256 строки <webhook-id>.<webhook-timestamp>.<тело> на байтах секрета (base64 после whsec_). После смены секрета 24 часа подписей две, через пробел: подходит любая. |
content-type | application/json |
user-agent | xWires-Webhooks/1 (+https://xwires.ai/docs/company-api#webhooks) |
Проверяйте подпись по телу запроса как оно пришло, до разбора JSON: тот же JSON, записанный заново, не сойдётся. Секрет храните на сервере, как ключ API, например в XWIRES_WEBHOOK_SECRET.
import { createHmac, timingSafeEqual } from "node:crypto";
// secret: whsec_… из «Компания → Интеграции»; headers: заголовки запроса, имена в нижнем
// регистре, как их даёт Node; body: тело запроса как пришло (строка или Buffer), до JSON.parse.
export function verifyWebhook(secret, headers, body) {
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const signatures = headers["webhook-signature"];
if (!id || !signatures || !/^\d{1,12}$/.test(timestamp ?? "")) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const key = Buffer.from(secret.slice("whsec_".length), "base64");
const expected = createHmac("sha256", key).update(`${id}.${timestamp}.`).update(body).digest();
return signatures.split(" ").some((entry) => {
const [version, signature] = entry.split(",");
const given = Buffer.from(signature ?? "", "base64");
return version === "v1" && given.length === expected.length && timingSafeEqual(given, expected);
});
}import base64
import hashlib
import hmac
import time
# secret: whsec_… из «Компания → Интеграции»; headers: заголовки запроса (во Flask и Django
# имена без учёта регистра); body: тело запроса как пришло, в байтах, до json.loads.
def verify_webhook(secret: str, headers, body: bytes) -> bool:
msg_id = headers.get("webhook-id") or ""
timestamp = headers.get("webhook-timestamp") or ""
signatures = headers.get("webhook-signature") or ""
if not msg_id or not signatures:
return False
if not (timestamp.isascii() and timestamp.isdigit() and len(timestamp) <= 12):
return False
if abs(time.time() - int(timestamp)) > 300:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{msg_id}.{timestamp}.".encode() + body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest())
for entry in signatures.split(" "):
version, _, signature = entry.partition(",")
if version == "v1" and hmac.compare_digest(signature.encode(), expected):
return True
return FalseСмена секрета
«Выпустить новый секрет» в кабинете: прежний подписывает ещё 24 часа, и в это время у запроса две подписи. Перенесите новый секрет в свою систему. Вторая смена за эти сутки выводит самый старый секрет сразу. Смените секрет, когда уходит человек, который мог его видеть: админ, разработчик, подрядчик.
Тестовое событие
«Отправить тестовое событие» в кабинете присылает webhook.test: без позиции и ключа, в data — адрес, который проверяли. Оно уходит и на адрес, который не принимает события. У теста одна попытка, и не доставленный за 10 минут тест считается неудачным. В ленте тестовых событий нет.
{
"id": "7a3c5e91-2f4b-4d68-b0a7-9c1e3f5d8b26",
"type": "webhook.test",
"timestamp": "2026-10-15T10:05:00.000+00:00",
"position": null,
"company_id": "6f1c2b9e-3d4a-4c71-9a5e-2b8f0d1c7a43",
"actor": null,
"data": {
"endpoint_id": "c4d8f2a6-1b3e-4a75-9c0d-5e2f7a1b3c69"
}
}Что и когда ушло на каждый адрес, видно в кабинете в «Журнале доставки»; там же неудачную доставку можно повторить. Оператор xWires может остановить вебхуки компании, тогда в кабинете об этом будет написано.
14Безопасность и персональные данные
- Компания — оператор персональных данных своих сотрудников, xWires обрабатывает их по её поручению (152-ФЗ, ст. 6 ч. 3). Ключ и адрес вебхука, которые создал админ компании, — поручение передавать эти данные в системы компании.
- Если ваша система или адрес вебхука за пределами России, это трансграничная передача, и оформляет её компания.
- Ответы API — персональные данные: почты и имена сотрудников. Храните их и логи интеграции так же, как другие данные сотрудников. Ключи и секреты — только на сервере, в хранилище секретов.
- В событиях почт, имён и цен нет: только идентификаторы, статусы, план, вид заказа, роль, время и ключ, если действовал он.
- У нас тела запросов и ответов в логи не пишутся, почты не попадают в адреса, IP-адреса не хранятся. Ключ хранится только как отпечаток, секрет вебхука — зашифрованным. Адрес вебхука не попадает в логи и письма, только имя сервера; на какие серверы и с какого времени уходили события компании, хранится в истории адресов.
- Session сотрудника — доступ к его ChatGPT. Принимайте его только от самого сотрудника, передавайте только в теле POST /subscriptions, не храните и не пишите в логи. У нас сайт сразу шифрует его так, что расшифровать может только сервис, который передаёт заказ партнёру; в ответах, событиях и логах его нет.
- Письма тем, чьи почты добавила система, уходят постепенно: на большой список — несколько минут. Суточный лимит в 5000 почт и приглашений не даёт утёкшему ключу превратить xWires в рассылку.
- Уходит человек, который мог сохранить ключ или секрет, — смените их: ключ, секрет.
15Изменения
| Версия | Что нового |
|---|---|
| v1, октябрь 2026 | Первая версия: 16 запросов, лента событий и вебхуки. |
| v1, 17 октября 2026 | Новый адрес API — https://api.xwires.ai/company/v1. Прежний, https://xwires.ai/api/company/v1, отвечает так же: менять его в работающих системах не обязательно. |
| v1, 17 октября 2026 | Песочница для проверки интеграций: https://api.sandbox.xwires.ai/company/v1, ключи xwc_test_… и 4 запроса раздела «Песочница» (раздел 11). В рабочем API их нет. |
| v1, 18 октября 2026 | У заказа больше нет полей price и currency: стоимость подписок xWires согласует с компанией отдельно, вне API. Это единственное исключение из правила, что v1 только растёт. |
| v1, 18 октября 2026 | Подписку некоторых заказов xWires продлевает сам каждый месяц за счёт компании, пока её не отключат. Такой заказ POST /orders/{id}/approve подтверждает только с {"renewal": "monthly"}, иначе ответ 409 renewal_consent_required. Остальные заказы подтверждаются и без поля: по умолчанию renewal — once. Почта из POST /approved-emails подтверждает его, только если её добавили с renews_monthly: true. Согласие API никогда не даёт само. |
| v1, 18 октября 2026 | У заказа и в данных событий order.* — renewal_mode, monthly или once: видно заранее, нужен ли для подтверждения {"renewal": "monthly"}, и с каким режимом заказ подтверждён. У почты списка и в ответе POST /approved-emails — renews_monthly: несёт ли почта согласие на ежемесячное продление. |
| v1, 18 октября 2026 | Почта в POST /approved-emails, которая уже подтвердила заказ, пока он не завершён, считается в skipped. Раньше повтор запроса запоминал её снова, и она подтверждала следующий заказ сотрудника. Чтобы оплатить и следующий, добавьте почту, когда этот завершится. |
| v1, 18 октября 2026 | POST /subscriptions: подписка сотруднику по его session одним запросом, с учётной записью, если её нет. Человеку вне компании — приглашение и почта в списке (202). Новые коды 400 invalid_session, 400 session_token_required, 400 session_expires_too_soon, 409 chatgpt_email_taken, 409 chatgpt_email_mismatch, 409 plan_mismatch; 409 open_order_exists теперь и вне песочницы. |
| v1, 18 октября 2026 | Режим заказа выбирает компания. POST /orders/{id}/approve без renewal подтверждает любой заказ разовым и больше не отвечает 409 renewal_consent_required; {"renewal": "monthly"} — согласие на автопродление, и его можно дать любому заказу, пока автопродление для компании открыто, иначе 409 auto_renewal_closed. Так же renewal в POST /subscriptions. Почта без renews_monthly подтверждает заказ разовым, с ним — в режиме сотрудника. Окончательный renewal_mode — с awaiting_check. У заказа новое поле origin. |
| v1, 19 октября 2026 | Автопродление сотрудника: subscription.auto_renewal у сотрудника, его отключение POST /employees/{id}/auto-renewal/stop (409 auto_renewal_not_on), события subscription.auto_renewal_started и subscription.auto_renewal_stopped с stop_reason. POST /subscriptions отвечает 409 auto_renewal_on, пока автопродление включено. Увольнение отключает автопродление за счёт компании. |
В v1 изменения только добавляются: новые поля, значения статусов и типы событий. Несовместимые изменения выйдут как v2 по новому адресу, а v1 проработает рядом не меньше 6 месяцев. Смену IP-адреса вебхуков объявим за 30 дней.