Сообщения и диалоги

Базовый URL: https://api.apx.chat/api/v1/messaging. Справочник, ключи и права, ошибки и восстановление, Postman всех доменов и environment.

Примеры переключаются между cURL, JavaScript и Python. Замените tok_xxxxx, UUID и CURSOR_FROM_SERVER значениями своего приложения. Каждый write-пример — отдельный запрос со свежими версиями из чтения, а не последовательность с заранее вычисленным счётчиком. В Postman выберите папку Messaging, задайте baseUrl, credential и ID.

Для запросов с Idempotency-Key сохраните ключ и исходное тело до первой попытки, затем сохраните полученный результат. При потере ответа повторите прежний ключ и тело. После конфликта версии перечитайте диалог; новое решение человека отправляйте с новым ключом и свежим маркером версии.

Приложение, сотрудник и scopes

Владелец явно разрешает каналы и настоящих сотрудников APX. Пустой список каналов запрещает доступ. automation — внешняя автоматизация; operator — рабочее место сотрудника. Режим неизменяем. Удаление сотрудника из approved списка, отзыв ключа или текущих прав запрещает новые попытки.

ScopeДействие
messaging_readКаналы, диалоги, публичная история и вложения
messaging_events_readFeed и webhook, дополнительно к чтению
messaging_sendОтвет в существующий диалог
messaging_startПервый контакт, дополнительно к send
messaging_send_as_employeeОтправка от approved actor; нужны его текущие read/reply права
messaging_uploadФайлы для последующей отправки
messaging_handoffОстановить бота и запросить человека
messaging_response_resolveЯвно снять покрытое ожидание после подтверждённой отправки
messaging_claim / messaging_assign / messaging_close / messaging_reopenУправление от approved actor в operator mode; проверяется конкретная capability, send scope не требуется

Возможности и состояние канала

Права: действующий ключ; для списка каналов messaging_read. Каналы: только явно разрешённые tenant/app каналы; capabilities зависят от транспорта и текущего подключения.

const url = "https://api.apx.chat/api/v1/messaging/capabilities";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Api-Token": "tok_xxxxx",
  },
});

const data = await response.json();
console.log(data);
const url = "https://api.apx.chat/api/v1/messaging/channels";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Api-Token": "tok_xxxxx",
  },
});

const data = await response.json();
console.log(data);

Проверьте operational.state/reason и capabilities.mediaKinds/mediaFormats/writeFirst по матрице каналов. active — сохранённый совместимый флаг. API не выдаёт provider credentials/QR и не подключает или reconnect-ит аккаунт. Ошибки: 401/403 — исправить ключ/scopes/разрешённые каналы; недоступный транспорт — предложить владельцу восстановить подключение через APX. Дальше: выберите разрешённый channelId и получите диалоги. Контракт метода.

Ручная очередь и discovery

Права: messaging_read; discovery дополнительно требует approved authorUserId и его текущий доступ. Каналы: все разрешённые с диалогами; ручная очередь требует «Взять в работу», даже если история ещё пуста.

const url = "https://api.apx.chat/api/v1/messaging/dialogs?queue=manual&channelId=33333333-3333-4333-8333-333333333333&limit=30";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Api-Token": "tok_xxxxx",
  },
});

const data = await response.json();
console.log(data);
const url = "https://api.apx.chat/api/v1/messaging/dialogs/22222222-2222-4222-8222-222222222222";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Api-Token": "tok_xxxxx",
  },
});

const data = await response.json();
console.log(data);
const url = "https://api.apx.chat/api/v1/messaging/dialogs/22222222-2222-4222-8222-222222222222/actions?authorUserId=11111111-1111-4111-8111-111111111111";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Api-Token": "tok_xxxxx",
  },
});

const data = await response.json();
console.log(data);

Ответ discovery содержит automationControlVersion, allowed/reason для действий и безопасный список операторов id/name/available/reason. Недоступного кандидата выбирать нельзя; email и private notes не возвращаются. Для следующей страницы используйте meta.nextCursor с прежними queue/channelId. Ошибки: 403/404 — проверить actor и текущий доступ;400 invalid_cursor — начать список заново с тем же фильтром. Дальше: при allowed claim выполните claim; если нельзя — покажите reason и предложите разрешённое назначение. Контракт метода.

Durable outbound и проверка результата

Ручная очередь диалогов и durable задания отправки имеют разный смысл. До claim ответ запрещён с dialog_in_queue. HTTP202 и status=accepted подтверждают сохранение message+command. Далее pending → processing → sent; receipt может дать delivered/read. failed — подтверждённая ошибка; partial — отправлена часть физических сообщений; unknown — исход неоднозначен. Не создавайте новую отправку для unknown/partial: сначала сверка с провайдером и человеком. Время исполнения и delivery SLA не гарантируются.

Права: messaging_read. Каналы: любой разрешённый; delivered/read доступны лишь при поддержке receipt транспортом. messageId берите из принятой отправки, а не provider ID.

const url = "https://api.apx.chat/api/v1/messaging/messages/44444444-4444-4444-8444-444444444444";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Api-Token": "tok_xxxxx",
  },
});

const data = await response.json();
console.log(data);

Ошибки: 403/404 — текущий доступ или сообщение изменились;429 — снизить параллельность и учитывать Retry-After. Дальше: обновлять карточку по feed/webhook; failed показать оператору, unknown/partial направить на сверку. Контракт метода.

Ответ сотрудника

Права: messaging_send + messaging_send_as_employee; для предварительного чтения messaging_read. Требуются approved HUMAN actor, его текущие read/reply и capability ответа. Каналы: поддерживающие sendText/reply; в ручной очереди сначала claim.

externalMessageId — ID сообщения в вашей интеграции, который задаёте вы. Сгенерируйте UUID или используйте ID конкретного уведомления, например notification-order-42-created: непустую строку до 200 символов, уникальную в пределах API-приложения. Для нового сообщения нужен новый ID; при повторе сохраняйте прежние ID, Idempotency-Key и тело. Тот же ID с другим телом вернёт 409 idempotency_conflict. Это не ID клиента или сообщения у провайдера и не замена обязательному заголовку Idempotency-Key. APX вернёт собственный messageId — по нему читайте результат отправки.

const url = "https://api.apx.chat/api/v1/messaging/messages";

const payload = {
  "externalMessageId": "crm-reply-42",
  "authorUserId": "11111111-1111-4111-8111-111111111111",
  "dialogId": "22222222-2222-4222-8222-222222222222",
  "text": "Здравствуйте! Уточните номер заказа."
};

const response = await fetch(url, {
  method: "POST",
  headers: {
    "X-Api-Token": "tok_xxxxx",
    "Content-Type": "application/json",
    "Idempotency-Key": "reply-42",
  },
  body: JSON.stringify(payload),
});

const data = await response.json();
console.log(data);

По умолчанию отправка сохраняет ожидание ответа. Чтобы снять покрытое ожидание после подтверждённой отправки, дополнительно нужны messaging_response_resolve, responseEffect=resolve и свежий expectedResponseVersion из диалога:

const url = "https://api.apx.chat/api/v1/messaging/messages";

const payload = {
  "externalMessageId": "crm-resolve-42",
  "authorUserId": "11111111-1111-4111-8111-111111111111",
  "dialogId": "22222222-2222-4222-8222-222222222222",
  "text": "Спасибо, вопрос решён.",
  "responseEffect": "resolve",
  "expectedResponseVersion": 4
};

const response = await fetch(url, {
  method: "POST",
  headers: {
    "X-Api-Token": "tok_xxxxx",
    "Content-Type": "application/json",
    "Idempotency-Key": "resolve-42",
  },
  body: JSON.stringify(payload),
});

const data = await response.json();
console.log(data);

Более позднее входящее останется ожидающим. Ошибки: dialog_in_queue — сначала claim;409 response_version_conflict — перечитать диалог и получить новое решение;403 — проверить actor/scopes/capability;400 — проверить текст/вложения. Дальше: по messageId прочитать результат;202 не означает доставку и не закрывает диалог. Контракт метода.

Первый контакт и транспорт

Права: messaging_send + messaging_start; operator mode дополнительно требует authorUserId, messaging_send_as_employee и доступность actor для назначения. Пример ниже относится к automation mode, без actor. Каналы: Mail принимает email; WhatsApp Personal/MAX Personal — международный phone; Telegram Personal/MAX Personal — прежний provider_user_id. Боты обычно требуют первого входящего клиента.

const url = "https://api.apx.chat/api/v1/messaging/messages";

const payload = {
  "externalMessageId": "notification-order-42-created",
  "channelId": "33333333-3333-4333-8333-333333333333",
  "recipient": {
    "type": "phone",
    "value": "+79990001122"
  },
  "text": "Здравствуйте!"
};

const response = await fetch(url, {
  method: "POST",
  headers: {
    "X-Api-Token": "tok_xxxxx",
    "Content-Type": "application/json",
    "Idempotency-Key": "contact-42",
  },
  body: JSON.stringify(payload),
});

const data = await response.json();
console.log(data);

Номер MAX не является MAX ID: сервер разрешает phone через подключённый аккаунт и фиксирует peer до dispatch. Ошибки: write_first_unsupported/recipient_not_supported — выбрать допустимый канал и адресацию;recipient_not_found — проверить номер;recipient_lookup_failed/channel_reconnect_required — сверить состояние подключения;403 — текущие права actor/app. Дальше: сохранить полученные dialogId/messageId и проверять durable outcome. Контракт метода.

Вложения: загрузка, отправка и скачивание

Права: messaging_upload; operator mode требует authorUserId и существующие messaging_send_as_employee/read/reply проверки. Для отправки нужны send-права предыдущего сценария, для истории/скачивания — messaging_read. Каналы: upload ещё не означает, что выбранный канал поддерживает формат: проверьте mediaKinds/mediaFormats. До 10 файлов по 10 МиБ.

Замените ./attachment.pdf локальным файлом. JavaScript пример предназначен для браузера: fileInput — ваш input type=file. Поле files передаёт файл, authorUserId — отдельное текстовое поле multipart. Content-Type/boundary создаёт библиотека; не задавайте их вручную.

const url = "https://api.apx.chat/api/v1/messaging/uploads";

const formData = new FormData();
formData.append("files", fileInput.files[0]); // fileInput: <input type="file" />
formData.append("authorUserId", "11111111-1111-4111-8111-111111111111");

const response = await fetch(url, {
  method: "POST",
  headers: {
    "X-Api-Token": "tok_xxxxx",
  },
  body: formData,
});

const data = await response.json();
console.log(data);

Возьмите uploadId из ответа; он привязан к приложению и живёт 24 часа. Передайте его в uploadIds:

const url = "https://api.apx.chat/api/v1/messaging/messages";

const payload = {
  "externalMessageId": "crm-file-42",
  "authorUserId": "11111111-1111-4111-8111-111111111111",
  "dialogId": "22222222-2222-4222-8222-222222222222",
  "text": "Прикрепляю документ.",
  "uploadIds": [
    "55555555-5555-4555-8555-555555555555"
  ]
};

const response = await fetch(url, {
  method: "POST",
  headers: {
    "X-Api-Token": "tok_xxxxx",
    "Content-Type": "application/json",
    "Idempotency-Key": "file-42",
  },
  body: JSON.stringify(payload),
});

const data = await response.json();
console.log(data);

Для скачивания сначала получите публичную историю. В URL нужны внутренние messageId и fileId из этой истории, а не uploadId или provider ID:

const url = "https://api.apx.chat/api/v1/messaging/dialogs/22222222-2222-4222-8222-222222222222/messages?limit=30";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Api-Token": "tok_xxxxx",
  },
});

const data = await response.json();
console.log(data);
const url = "https://api.apx.chat/api/v1/messaging/messages/44444444-4444-4444-8444-444444444444/attachments/55555555-5555-4555-8555-555555555555";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Api-Token": "tok_xxxxx",
  },
});

if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.blob();
console.log(data);

cURL/Python сохраняют attachment.bin; JavaScript получает Blob для вашего download UI. Ошибки: 400/413 — поле files, число/размер/формат;403 — actor/app;404 — чужой, удалённый или private файл;400 при истёкшем/чужом upload — загрузить заново. Upload не поддерживает Idempotency-Key и поиск результата по external ID. Если ответ с uploadId потерян, выполните ограниченный повтор загрузки: он создаст новый upload, а прежний неиспользованный станет недействителен через 24 часа. Отправляйте сообщение только с подтверждённым uploadId; сам send после потери ответа повторяйте с прежними Idempotency-Key и payload. Дальше: после send следите за message outcome; скачивайте через API с повторной проверкой прав, не сохраняйте исторический S3 URL. Контракт multipart, контракт download.

Handoff и отсутствие свободного сотрудника

Права: messaging_handoff; для чтения результата messaging_read. В operator mode обязателен approved authorUserId и прежние messaging_send_as_employee/read/reply/canAssign проверки; automation может вызвать без actor. Каналы: разрешённые диалоги с применимым handoff; это запрос человека, а не смена messenger group membership.

const url = "https://api.apx.chat/api/v1/messaging/dialogs/22222222-2222-4222-8222-222222222222/handoff";

const payload = {
  "authorUserId": "11111111-1111-4111-8111-111111111111",
  "reason": "Нужна помощь сотрудника"
};

const response = await fetch(url, {
  method: "POST",
  headers: {
    "X-Api-Token": "tok_xxxxx",
    "Content-Type": "application/json",
    "Idempotency-Key": "handoff-42",
  },
  body: JSON.stringify(payload),
});

const data = await response.json();
console.log(data);

Автоматизация останавливается. Без доступного кандидата handoffStatus=pending — успешный запрос человека, не подтверждённое назначение. Ошибки: 403 — actor/scope/capability;409 — конфигурация/назначение изменились; недопустимый явно выбранный operatorId — выбрать eligible сотрудника или не указывать кандидата. Дальше: перечитать диалог/ручную очередь, показать pending без обещания срока; доступный сотрудник выполняет claim. При потере ответа повторять тот же ключ/тело. Контракт метода.

Claim, assignment, close и reopen

Права: соответствующий messaging_claim/messaging_assign/messaging_close/messaging_reopen; read нужен для discovery. Все четыре требуют operator mode, approved authorUserId и актуальную собственную lifecycle capability. Дополнительный send scope для этих четырёх действий не требуется. Каналы: claim/assignment по действующим правилам отдела/очереди; close/reopen только поддерживаемые 1:1 диалоги. Групповые close/reopen отклоняются с group_action_not_supported.

automationControlVersion — непрозрачный маркер состояния, не счётчик обращений. Берите его из свежего диалога/discovery, передавайте как expectedControlVersion и не вычисляйте +1. Каждый пример ниже независим:3 надо заменить полученным маркером непосредственно перед своим действием.

const url = "https://api.apx.chat/api/v1/messaging/dialogs/22222222-2222-4222-8222-222222222222/claim";

const payload = {
  "authorUserId": "11111111-1111-4111-8111-111111111111",
  "expectedControlVersion": 3
};

const response = await fetch(url, {
  method: "POST",
  headers: {
    "X-Api-Token": "tok_xxxxx",
    "Content-Type": "application/json",
    "Idempotency-Key": "claim-42",
  },
  body: JSON.stringify(payload),
});

const data = await response.json();
console.log(data);
const url = "https://api.apx.chat/api/v1/messaging/dialogs/22222222-2222-4222-8222-222222222222/assignment";

const payload = {
  "authorUserId": "11111111-1111-4111-8111-111111111111",
  "expectedControlVersion": 3,
  "targetOperatorId": "66666666-6666-4666-8666-666666666666",
  "participation": "leave"
};

const response = await fetch(url, {
  method: "POST",
  headers: {
    "X-Api-Token": "tok_xxxxx",
    "Content-Type": "application/json",
    "Idempotency-Key": "assignment-42",
  },
  body: JSON.stringify(payload),
});

const data = await response.json();
console.log(data);

Assignment требует явного retain|leave. departmentId меняется по действующим правилам отдела; пауза, доступность и eligibility target проверяются повторно в transaction. После своей успешной leave автор может потерять object-read: replay прежнего запроса возвращает только сохранённый минимальный outcome, без текущей карточки/истории. Новый ключ без доступа запрещён; отзыв базовых прав запрещает и replay.

const url = "https://api.apx.chat/api/v1/messaging/dialogs/22222222-2222-4222-8222-222222222222/close";

const payload = {
  "authorUserId": "11111111-1111-4111-8111-111111111111",
  "expectedControlVersion": 3
};

const response = await fetch(url, {
  method: "POST",
  headers: {
    "X-Api-Token": "tok_xxxxx",
    "Content-Type": "application/json",
    "Idempotency-Key": "close-42",
  },
  body: JSON.stringify(payload),
});

const data = await response.json();
console.log(data);
const url = "https://api.apx.chat/api/v1/messaging/dialogs/22222222-2222-4222-8222-222222222222/reopen";

const payload = {
  "authorUserId": "11111111-1111-4111-8111-111111111111",
  "expectedControlVersion": 3
};

const response = await fetch(url, {
  method: "POST",
  headers: {
    "X-Api-Token": "tok_xxxxx",
    "Content-Type": "application/json",
    "Idempotency-Key": "reopen-42",
  },
  body: JSON.stringify(payload),
});

const data = await response.json();
console.log(data);

Close завершает сохранённый диалог; reopen начинает обращение по текущим правилам назначения. Они не выходят из messenger group и не возвращаются в неё. Ошибки: 409 control_version_conflict — перечитать и получить новое решение человека;409 idempotency_conflict — прежний ключ нельзя использовать с новым payload;403/operator_access_denied — проверить capability;claim_not_available/invalid_assignment_target — обновить discovery;group_action_not_supported — не предлагать действие для группы. Дальше: показать сохранённый outcome; при timeout повторить прежний ключ/тело, не вычислять следующую версию. Claim, assignment, close, reopen.

События, webhook и восстановление после 410

Права: messaging_read + messaging_events_read. Каналы: только разрешённые текущему app/tenant; transport receipt может отсутствовать. Сначала получите точку событий до полного snapshot:

const url = "https://api.apx.chat/api/v1/messaging/events/cursor";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Api-Token": "tok_xxxxx",
  },
});

const data = await response.json();
console.log(data);

Сохраните полученный cursor, перечислите все страницы /dialogs и /dialogs/:id/messages, затем применяйте события начиная с сохранённой точки. Так изменения во время snapshot не теряются. CURSOR_FROM_SERVER в следующем примере заменяется полученным подписанным cursor:

const url = "https://api.apx.chat/api/v1/messaging/events?cursor=CURSOR_FROM_SERVER&limit=100";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Api-Token": "tok_xxxxx",
  },
});

const data = await response.json();
console.log(data);

Применяйте событие идемпотентно по event ID; сохраняйте meta.nextCursor только после применения всей страницы. Webhook доставляется с повторами/без гарантии порядка: проверяйте HMAC на исходных bytes по контракту webhook, дедуплицируйте ID, перечитывайте актуальный объект. REST version и webhook payload version независимы.

Ошибки: 410 cursor_expired — отбросить старую точку, снова GET /events/cursor, выполнить полный snapshot диалогов/истории и затем replay от новой точки.400 invalid_cursor — проверить app/фильтр/режим и также начать новую синхронизацию;401/403 — восстановить права;429 — backoff с Retry-After. Дальше: продолжать с nextCursor, не со старым cursor; удалённые/private/чужие данные не восстанавливать из старого webhook snapshot. Точка событий, feed.

Operational события каналов: явная подписка

Права: messaging_read + messaging_events_read; webhook-настройку отдельного messaging_channel_event включает владелец приложения. Каналы: разрешённые app/tenant, включая channel-only событие без chatId. Старые messaging_event и обычный feed сохраняют прежний набор типов.

const url = "https://api.apx.chat/api/v1/messaging/events/cursor?includeChannelEvents=true";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Api-Token": "tok_xxxxx",
  },
});

const data = await response.json();
console.log(data);
const url = "https://api.apx.chat/api/v1/messaging/events?cursor=CURSOR_FROM_SERVER&includeChannelEvents=true&limit=100";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Api-Token": "tok_xxxxx",
  },
});

const data = await response.json();
console.log(data);

В каждой странице сохраняйте includeChannelEvents=true; cursor связан с режимом. Ошибки: 400 invalid_cursor при смене режима — новая точка и полный resync;410 — восстановление предыдущего сценария. Дальше: channel.operational_changed означает перечитать /channels: operational.state/reason/capabilities отражают актуальное безопасное состояние без provider error text/QR/ключей. После сбоя worker восстанавливает события из PostgreSQL без обращения к провайдеру. Контракт метода.