Онлайн-запись API

Онлайн-запись API — серверный доступ к записи клиентов APX Chat: филиалы, услуги, специалисты, свободное время и визиты. С ним можно записывать клиентов из своего сайта, приложения или бота и переносить визиты в учётную систему. API бесплатный: CRM, платная подписка, чат и каналы для него не нужны.

Базовый URL: https://api.apx.chat/api/v1/booking

Полный контракт — в OpenAPI JSON, раздел Booking. Интерактивная схема — в Swagger UI.

Авторизация

Токен выпускается в кабинете: Настройки → API-токены. Как выбрать scopes и срок действия — в описании API-токенов. Передавайте токен одним из двух заголовков:

PLAINTEXT
X-Api-Token: tok_xxxxxxxxxxxxx
Authorization: Bearer tok_xxxxxxxxxxxxx

Организация и приложение определяются по токену. Выдать токен с booking-scope может только сотрудник, у которого есть такие же права на записи: для booking_read и booking_events_read нужен просмотр всех визитов, для booking_write — создание, перенос, отмена и смена статуса всех визитов, для каталога, настроек и контактов — свои отдельные права. Права только на свои визиты не позволяют выдать доступ ко всей организации.

Scopes

ScopeДоступ
booking_readКаталог, специалисты, свободное время, список и карточка записи, история
booking_writeСоздание, перенос, отмена, смена статуса, изменение ответов
booking_catalog_writeСоздание и изменение филиалов и услуг
booking_settings_readЧтение настроек
booking_settings_writeИзменение настроек, формы и публикации
booking_contacts_readКонтакты, ответы, заметки и настройки уведомлений клиента; нужен и для PATCH записи и фильтра clientUuid
booking_events_readWebhook-события записи; вместе с ним нужен booking_read

Каталог, филиалы, услуги и специалисты доступны с любым одним из scopes booking_read, booking_write, booking_catalog_write, booking_settings_write. Свободное время — с booking_read или booking_write. Список, карточки и история визитов отдельно требуют booking_read. В OpenAPI «любой из» описан расширением x-any-scopes, «все сразу» — x-required-scopes.

booking_write не включает booking_contacts_read: отправить ответы клиента при создании можно, а получить их обратно — только с отдельным scope. Без него в ответах нет clientUuid, answers, formFields, formVersion, notes, notificationPreferences. Ссылку управления визитом и секрет клиента API не отдаёт никогда.

Специалисты и графики

Специалист записи — это сотрудник организации. Его ведут в кабинете: в карточке сотрудника (отметка «Принимает записи», услуги и филиалы), в составе сотрудников филиала и в «Кто оказывает» услуги. Рабочее время берётся из графика сотрудника. Поэтому GET /specialists доступен только на чтение.

Удалённые сотрудники не возвращаются в каталоге, справочниках и свободном времени, а их прошлые визиты остаются в списке записей. Сотрудника с будущими визитами удалить нельзя: сначала визиты переносят к другому специалисту или отменяют.

Формат ответов

Успешный ответ — конверт { "status": "OK", "data": ... }. Списки возвращают { "items": [], "nextCursor": null } внутри data, свободное время — { "slots": [], "nextCursor": null }. Создание отвечает 201, изменение — 200.

Методы

МетодПутьScope
GET/capabilitiesbooking_read
GET/settingsbooking_settings_read
PATCH/settingsbooking_settings_write
GET/catalog, /branches, /services, /specialistsлюбой booking-scope из списка выше
POST, PATCH/branches, /branches/{id}booking_catalog_write
POST, PATCH/services, /services/{id}booking_catalog_write
GET/availabilitybooking_read или booking_write
GET/appointments, /appointments/{id}, /appointments/{id}/historybooking_read
POST/appointmentsbooking_write
PATCH/appointments/{id}booking_write и booking_contacts_read
POST/appointments/{id}/reschedule, /cancel, /statusbooking_write

Филиалы

Вид филиала — kind: place (по адресу, по умолчанию у нового филиала) или online (без адреса). У филиалов, созданных раньше, поля нет — это place; в ответах kind есть всегда. Без kind в PATCH вид филиала не меняется.

  • У place адрес обязателен: создание без адреса, стирание адреса и переход из online в place без адреса вернут 400 BOOKING_VALIDATION с details.fieldErrors.address. Старый филиал без адреса можно править (например, отправить в архив), не меняя адрес.
  • У online нет адреса, города и координат: сервер сохраняет address: "", а city, latitude и longitude — null; переданные значения не сохраняются. Без name филиал называется «Онлайн». Часовой пояс задаётся как у любого филиала. В записи клиент видит «Онлайн» вместо адреса и карты.

Город филиала (city) обязателен, если у активных филиалов организации больше одного города (в записи клиент сначала выбирает город). Без города создание филиала, снятие города и возвращение филиала из архива вернут 400 BOOKING_VALIDATION с details.fieldErrors.city — укажите город. Старый филиал без города можно править, не задавая город. Для онлайн-филиала эта проверка не действует.

Визит в ответах API v1 содержит branchKind: place | online — вид филиала визита на момент ответа. В снимке визита он не хранится; филиал без поля kind — place. В webhook-событиях поля нет.

Кто оказывает услугу

Услуга содержит staffMode: all — все сотрудники филиалов услуги, selected — выбранные специалисты. В режиме all связи специалист ↔ услуга (specialists[].services) сервер создаёт сам, без переопределений цены и длительности: когда сохраняют активную услугу, состав сотрудников филиала или профиль сотрудника с новым филиалом. Уже существующие связи и их цены не меняются. Специалистов для selected выбирают только в кабинете.

Без staffMode новая услуга получает режим all, а PATCH режим не меняет. Услуги, созданные раньше, работают в режиме selected. Каталог и свободное время, как и раньше, строятся по specialists[].services.

Изменение поведения API v1: POST /services без staffMode теперь создаёт услугу в режиме all, и сервер сам связывает её со специалистами её филиалов. Раньше связи вели только вручную, в карточке сотрудника; вручную их ведут и в режиме selected. Чтобы новая услуга вела себя как прежде, передавайте "staffMode": "selected". Услуги, созданные до этого изменения, остаются selected.

Свободное время

GET /availability принимает branchId, serviceId, необязательный specialistId и локальные даты филиала dateFrom, dateTo — не больше 31 дня включительно. Ответ — конкретные слоты в UTC с часовым поясом филиала (IANA).

const url = "https://api.apx.chat/api/v1/booking/availability?branchId=BRANCH_ID&serviceId=SERVICE_ID&dateFrom=2026-10-13&dateTo=2026-10-19";

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

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

Создание записи

Перед созданием прочитайте GET /catalog: в нём есть форма записи (form.version, поля и их id) и согласие на обработку данных (privacy.version). Ответы передаются по id поля формы.

const url = "https://api.apx.chat/api/v1/booking/appointments";

const payload = {
  "startsAt": "2026-10-13T07:00:00.000Z",
  "branchId": "BRANCH_ID",
  "serviceId": "SERVICE_ID",
  "specialistId": "SPECIALIST_ID",
  "formVersion": 3,
  "answers": {
    "FIELD_ID_NAME": "Анна",
    "FIELD_ID_PHONE": "+79990000000"
  },
  "notificationPreferences": {
    "email": false,
    "messenger": false
  },
  "privacyConsent": {
    "accepted": true,
    "version": 1
  }
};

const response = await fetch(url, {
  method: "POST",
  headers: {
    "X-Api-Token": "tok_xxxxxxxxxxxxx",
    "Idempotency-Key": "create-2026-10-13-0001",
    "Content-Type": "application/json",
  },
  body: JSON.stringify(payload),
});

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

Если слот уже заняли, придёт 409 BOOKING_SLOT_TAKEN — обновите свободное время и выберите другой. Если форму успели изменить — 409 BOOKING_FORM_CHANGED: перечитайте её и подтвердите ответы.

specialistId при создании необязателен. Без него специалиста назначает сервер: из специалистов, у кого выбранное время свободно, — тот, у кого меньше записей в этот день (по локальной дате филиала записи; считаются визиты во всех филиалах специалиста и «Пришёл без записи», отменённые не считаются), при равенстве — тот, кому дольше всего не назначали. Назначенный специалист приходит в specialistId ответа. Перенос (/reschedule) всегда требует specialistId.

При записи к любому специалисту (specialistId не передан), если слот не подошёл никому, ответ — по самой сильной причине: занят хотя бы у одного — 409 BOOKING_SLOT_TAKEN; иначе пересекающиеся смены — 409 BOOKING_SCHEDULE_CONFLICT; 422 BOOKING_OUTSIDE_AVAILABILITY — только если время вне доступности у всех. В каждом случае обновите свободное время и выберите другой слот.

Идемпотентность и версии

Все изменяющие запросы требуют заголовок Idempotency-Key: 8–128 печатных ASCII-символов без пробелов. При повторе той же операции отправляйте тот же ключ. Тот же ключ с другим содержимым вернёт 409 BOOKING_IDEMPOTENCY_CONFLICT. Ключ действует в пределах организации, приложения, операции и объекта; ротация токена его не сбрасывает. Повтор не обходит отзыв scope, токена или прав сотрудника и заново применяет текущие правила скрытия контактов.

Изменение существующего объекта требует expectedVersion. При расхождении придёт 409 BOOKING_VERSION_CONFLICT, а details.currentVersion подскажет, что объект нужно перечитать.

  • PATCH /appointments/{id} меняет ответы, заметки и настройки уведомлений.
  • POST /appointments/{id}/reschedule меняет время, филиал и специалиста; specialistId обязателен. Услугу визита изменить нельзя.
  • POST /appointments/{id}/cancel отменяет визит. Значение cancelled в /status не принимается.

Список записей

GET /appointments принимает время в UTC: from и until — полуоткрытый интервал не длиннее 31 суток. Фильтры: branchId, specialistId, status, clientUuid; страницы — cursor и limit (1–200, по умолчанию 100). Курсор привязан к организации и фильтрам. clientUuid — только действующий идентификатор клиента этой организации; введённые клиентом email или телефон клиента не связывают.

Уведомления по номеру и ответственные филиала

Настройки (GET/PATCH /settings) содержат notifications.phoneMessenger: { enabled, maxChannelUuid, telegramChannelUuid, templates: { confirmation, reminder, rescheduled, cancelled } }. Каналы — подключённые личные аккаунты MAX и Telegram организации; включить настройку без каналов нельзя. Шаблоны — до 1000 символов с подстановками {услуга}, {дата}, {время}, {специалист}, {филиал}, {адрес}, {ссылка}; null в PATCH возвращает текст по умолчанию. У онлайн-филиала уведомления клиенту (письмо, MAX, Telegram, подстановка {адрес}) и заметки команде пишут «Онлайн» вместо адреса; пустые подстановки не оставляют висящих «, ».

Сообщения уходят, только если в записи передано согласие notificationPreferences.phoneMessenger: true и в ответах формы есть телефон. Сначала MAX, затем Telegram, затем письмо (если email указан и не выбран отдельно); следующие сообщения идут туда же, куда дошло первое.

Ответ клиента «1» отмечает визит подтверждённым — событие истории customer_confirmed, версия визита не меняется; «2» отменяет визит, если ещё не прошёл customerCutoffMinutes. Ответом считается только первое входящее сразу после уведомления: последнее исходящее диалога — уведомление, диалог закрыт и у клиента в канале нет другого активного диалога, визит закончился не раньше чем сутки назад. Иначе это обычное входящее. Ответ цитатой (reply-to) на уведомление относится к визиту этого уведомления, без цитаты — к визиту последнего уведомления. «2» без цитаты при нескольких действующих визитах ничего не отменяет: диалог уходит операторам с заметкой обо всех визитах. Отмена ответом — как отмена клиентом по ссылке управления (история cancelled, источник public).

Филиал (/branches) может содержать responsible — кто отвечает клиентам, которые ответили на уведомление свободным текстом: { "mode": "employees", "userUuids": [] } (1–50 сотрудников организации) или { "mode": "department", "departmentUuid": "…" }; null — общая очередь канала. Архив филиала очищает поле.

Ошибки

Ошибка — { status, code, message, details?, timestamp, path }. Сообщения безопасные и на русском; неизвестные поля в запросе запрещены. details.fieldErrors подсказывает, какие поля формы исправить.

КодHTTPЧто делать
BOOKING_VALIDATION400Исправить поля
BOOKING_FORBIDDEN403Проверить scopes токена и права сотрудника
BOOKING_NOT_FOUND404Объект отсутствует или относится к другой организации
BOOKING_SLOT_TAKEN409Обновить свободное время и выбрать слот
BOOKING_VERSION_CONFLICT409Перечитать объект
BOOKING_FORM_CHANGED409Перечитать форму и подтвердить ответы
BOOKING_IDEMPOTENCY_CONFLICT409Не использовать ключ повторно для другого содержимого
BOOKING_INVALID_STATUS409Перечитать текущий статус
BOOKING_SCHEDULE_CONFLICT409Время визита попадает на пересекающиеся смены специалиста — выберите другое время; при архиве филиала (details.reason: future_appointment) — сначала перенесите или отмените его будущие визиты
BOOKING_OUTSIDE_AVAILABILITY422Выбрать допустимое время
BOOKING_CUTOFF422Перенос или отмена клиентом закрыты правилами записи
BOOKING_RATE_LIMITED429Подождать Retry-After секунд

Webhooks

Пять событий: booking.appointment.created, booking.appointment.updated, booking.appointment.rescheduled, booking.appointment.cancelled, booking.appointment.status_changed. Подписка настраивается в кабинете в разделе Настройки → Вебхуки: у выбранного API-приложения должны быть одновременно booking_read и booking_events_read.

Тело события — { id, type, apiVersion, occurredAt, data }, где data содержит только appointmentId, version, branchId, serviceId, specialistId, startsAt, endsAt, status. Контактов, ответов, ссылок и токенов в событиях нет. Подпись — заголовок X-APX-Signature: v1= и hex HMAC-SHA256 от timestamp + "." + тело запроса с секретом подписки; время — X-APX-Event-Timestamp, ID события — X-APX-Event-Id (при повторной доставке не меняется). Подробнее о проверке подписи — в разделе Webhook.

Квитанции посетителя сайта

Клиент, который записался через виджет сайта, видит свои визиты в разделе «Ваши записи» и получает ссылку управления визитом. Эти данные виджет восстанавливает сам по подписанному токену посетителя (X-Client-Token) через отдельные маршруты посетителя: /api/public/booking/visitor-receipts и /api/public/booking/appointments/{id}/visitor-receipt. Они не входят в API v1, не принимают API-токен или токен сотрудника и не выдают секрет управления ни персоналу, ни интеграциям. Вызывать их из своей интеграции не нужно.

Не копируйте ссылку управления визитом в переписку, уведомления операторам, экспортируемые переменные и логи: по ней клиент переносит и отменяет визит без входа.

Публичная страница и виджет

Запись без интеграции доступна клиентам по ссылке /book/{ключ записи} и во вкладке «Запись» виджета сайта. Название и описание страницы меняются через PATCH /settings (publicPage.title — до 80 символов, publicPage.description — до 500 символов простого текста), логотип загружается только в кабинете. Открыть запись своей кнопкой на сайте с виджетом можно методом booking.open().

Настройка specialistSelection (client — по умолчанию, или auto; меняется через PATCH /settings с expectedVersion) определяет, показывают ли клиенту выбор специалиста виджет, страница /book и бот. Публичный каталог отдаёт её в policy.specialistSelection. При auto эти интерфейсы показывают свободное время всех подходящих специалистов и создают запись без specialistId — специалиста назначает сервер по правилу из раздела «Создание записи». На API v1 настройка не влияет: передавать specialistId или нет, решает ваше приложение.