◆ Sapport для разработчиков Справочник API Каталог ошибок OpenAPI
Разделы

#Клиентский Chat API

Статус: Доступно. Маршруты /api/widget/* работают сегодня и используются самим виджетом. Они не оформлены как версионируемый контракт (нет префикса версии, нет OpenAPI): состав полей описан по коду на 11.10.2026. Заголовки CORS есть только у init; про остальные маршруты читайте раздел «Известные ограничения» до начала работы.

Назначение: построить собственный интерфейс чата («headless»): те же вызовы, что делает кадр виджета, но с вашим дизайном.

#Содержание

  1. Когда использовать
  2. Доступ и общие правила
  3. Схема работы
  4. Токен сессии
  5. POST /api/widget/init
  6. POST /api/widget/messages
  7. Согласие с офертой
  8. GET /api/widget/history
  9. GET /api/widget/poll
  10. GET /api/widget/events (SSE)
  11. POST /api/widget/upload
  12. Пределы
  13. Сводка ошибок
  14. Пример: чат на чистом JavaScript
  15. Пример: компонент React
  16. Известные ограничения
  17. Заметки
  18. Открытые вопросы

#Когда использовать

Если достаточно стандартного окна, используйте виджет. Если ответ нужен на вашем сервере или в вашем мессенджере — серверный API (Планируется).

#Доступ и общие правила

Базовый адресhttps://<домен ячейки> — RU https://formula-cream.pro, EN https://wfacademy.org, ID https://wfacademy.id
Аутентификацияключа нет. Канал определяется открытым channel_id, личность посетителя — подписанным токеном сессии
ФорматJSON, UTF-8, Content-Type: application/json (кроме upload)
Условиеканал типа web_chat, активный; арендатор активен
CORSтолько init (Access-Control-Allow-Origin: *). Остальные маршруты ответов для чужого origin не отдают (ограничения)
Идентификатор канала в примерах00000000-0000-4000-8000-000000000000
Деньгиответ ИИ списывается с кошелька арендатора (Обзор)
Версионированиенет. Состав полей может расшириться: игнорируйте неизвестные поля

Соглашения серверного API (Соглашения: problem+json, каталог кодов, Idempotency-Key, курсоры, operation) на эти маршруты не распространяются: ответы клиентского API — простые объекты {"error": "…"}, а не application/problem+json. Тот же случай исчерпанного суточного предела серверный API сообщает полем ai_reply: {status: "skipped", reason: "daily_limit"}.

#Схема работы

  Ваш интерфейс                                     Ячейка
      │  POST /init   {channel_id} ───────────────►  название, цвет, приветствие,
      │ ◄─────────────────────────────────────────  форма, согласие
      │
      │  POST /messages {channel_id, content,       первое сообщение: сессии ещё нет
      │                  [session_token]} ────────►
      │ ◄── {session_token, response, queued?} ───  сохраните session_token
      │
      │  GET /events?st=<token>  (SSE) ◄──────────  ответы оператора, набор текста
      │  либо GET /poll?st=<token> каждые 2,5 с
      │  GET /history?st=<token>  при открытии      история разговора
      │  POST /upload  (multipart)                  файл → url

#Токен сессии

Токен выдаётся в ответе на первое сообщение (POST /messages) и обновляется в каждом ответе. Сохраните последний.

СвойствоЗначение
Форматw1.<полезная нагрузка base64url>.<подпись HMAC>; не более 1024 знаков
Полезная нагрузкаидентификаторы разговора и канала, версия, срок; подписана, но не зашифрована — не помещайте в токен ничего, что вы не готовы показать пользователю
Срок30 суток от выдачи
Что даётдоступ к своему разговору: history, poll, events, upload и продолжение через messages
Где хранитьу кадра виджета — sessionStorage (sapport_session_v2_<channel_id>), то есть одна вкладка. Выбор за вами: localStorage сохранит разговор между вкладками
Передачав теле (session_token) для messages и upload; в query (st) для history, poll, events
Негодный токенmessages не считает его ошибкой: заводится новый разговор и новый токен. history, poll, events, upload отвечают 401

Токен — это ключ к разговору. Не пишите его в журналы и адреса, которые видят третьи лица. Токен в query (st) попадает в журнал ваших прокси и сервера.

Идентификатор разговора в ответах не возвращается: он нужен только внутри токена.

#POST /api/widget/init

Статус: Доступно.

Получить настройки канала для отрисовки интерфейса. Единственный маршрут с CORS.

#Запрос

POST /api/widget/init

ПолеТипОбязательныйОграниченияПример
channel_idстрока (UUID)даидентификатор канала web_chat00000000-0000-4000-8000-000000000000
curl -X POST https://formula-cream.pro/api/widget/init \
  -H 'Content-Type: application/json' \
  -d '{"channel_id":"00000000-0000-4000-8000-000000000000"}'

#Ответ 200

ПолеТипОписание
channel_idстрокаидентификатор канала
nameстроканазвание канала
greetingстрокаприветствие. Если не задано, подставляется приветствие на языке канала или рынка
colorстрокацвет акцента, #rrggbb; по умолчанию #6366f1
avatar_urlстрока или nullURL аватара
pre_chat_formобъект или nullнастройка формы перед чатом: поля name, email, phone и признак обязательности
localeстрока или nullязык канала
consentобъект, необязательноесть только если арендатор задал согласие: {text, url, version}
{
  "channel_id": "00000000-0000-4000-8000-000000000000",
  "name": "Формула крема",
  "greeting": "Здравствуйте! Чем можем помочь?",
  "color": "#6366f1",
  "avatar_url": null,
  "pre_chat_form": null,
  "locale": "ru",
  "consent": {
    "text": "Я согласен с условиями оферты",
    "url": "https://formula-cream.pro/offer",
    "version": "3a7bd3e2360a3d29"
  }
}

Приветствие greeting — локальное: вам нужно показать его самим. Оно не приходит из истории и не хранится как сообщение.

#Ошибки

СтатусТелоКогда
400{"error":"Missing channel_id"}нет channel_id
404{"error":"Channel not found or inactive"}канал не найден, выключен, не web_chat, либо арендатор неактивен
500{"error":"Internal server error"}сбой сервера

OPTIONS возвращает 204 с заголовками CORS.

init не проверяет, разрешён ли ваш домен для окна чата (frame-ancestors). Это касается только встроенного кадра; собственному интерфейсу не мешает.

#POST /api/widget/messages

Статус: Доступно.

Отправить сообщение посетителя и получить ответ ИИ. Платная операция.

#Запрос

POST /api/widget/messages, Content-Type: application/json

ПолеТипОбязательныйОграниченияПример
channel_idстрока (UUID)да00000000-0000-4000-8000-000000000000
contentстрокадане более 4000 знаковСколько стоит курс?
session_tokenстроканеттокен предыдущего ответа. Без него — новый посетитель и новый разговорw1.eyJ….Zm9v
sender_nameстроканетимя посетителяАнна
languageстроканетпринимается; по перечню расхождений проекта на ответ не влияетru
page_urlстроканетдо 300 знаков; адрес страницыhttps://shop.example.com/courses/hydrolat-basics
page_titleстроканетдо 200 знаков; в промпт идёт до 150Основы гидролатов
referrerстроканетдо 300 знаковhttps://yandex.ru/
first_touchобъектнетпервое касание, до 1200 знаков в JSON (поля){"source":"yandex","medium":"cpc"}
consent_versionстроканетверсия согласия с офертой; обязателен, если арендатор задал согласие3a7bd3e2360a3d29

Поля sender_email и sender_phone, которые отправляет кадр виджета, сервер не читает — не рассчитывайте на них (ограничения).

curl -X POST https://formula-cream.pro/api/widget/messages \
  -H 'Content-Type: application/json' \
  -d '{
    "channel_id": "00000000-0000-4000-8000-000000000000",
    "content": "Сколько стоит курс?",
    "sender_name": "Анна",
    "page_url": "https://shop.example.com/courses/hydrolat-basics",
    "page_title": "Основы гидролатов",
    "consent_version": "3a7bd3e2360a3d29"
  }'

#Ответ 200

ПолеТипОписание
session_tokenстрокатокен сессии; сохраните новый
responseстрока или nullответ ИИ. null, если ИИ не отвечал (диалог ведёт оператор и т. п.)
queuedboolean, необязательноtrue, если суточный предел ИИ исчерпан (daily_limit): сообщение сохранено, диалог получил состояние handoff_pending и передан оператору, ответа ИИ не будет
{
  "session_token": "w1.eyJjIjoiMDAwMDAwMDAifQ.c2lnbmF0dXJl",
  "response": "Курс «Основы гидролатов» стоит 9 900 ₽. Хотите, расскажу, что входит?"
}
{
  "session_token": "w1.eyJjIjoiMDAwMDAwMDAifQ.c2lnbmF0dXJl",
  "response": null,
  "queued": true
}

Форма поля response при отсутствии ответа ИИ (пустая строка или null) в коде не зафиксирована: проверяйте оба варианта. Точные причины отсутствия ответа в клиентском API не раскрываются; они есть только в серверном API (Планируется).

#Ошибки

СтатусТелоКогдаЧто делать
400{"error":"Missing required fields: channel_id, content"}нет channel_id или contentИсправить запрос
400{"error":"consent_required","consent":{"text","url","version"}}арендатор требует согласия, а версия не передана или устарелаПоказать текст, получить согласие, повторить с consent_version (ниже)
404{"error":"Channel not found or inactive"}канал не найден или выключенПроверить channel_id
413{"error":"message_too_long","max_length":4000}текст длиннее 4000 знаковСократить; вернуть текст в поле ввода
429{"error":"Too many requests"}превышен предел частотыПодождать и повторить
500{"error":"Internal server error"}сбой сервераПовторить с паузой
503{"error":"widget_not_configured"}на сервере не настроена подпись сессийОбратиться в поддержку ячейки

Повтор после 429, 500, 503 может привести к повторной записи сообщения: идемпотентности у этого маршрута нет. Если сомневаетесь, прочитайте history и повторите отправку, только если сообщения там нет.

#Что происходит на сервере

#Согласие с офертой

Если арендатор задал consent_text, сервер не принимает сообщения без согласия.

Порядок:

  1. init возвращает consent: {text, url, version}. Если ключа нет — согласие не требуется.
  2. Покажите text и (если есть) ссылку url с флажком. Не отправляйте сообщение, пока флажок не отмечен.
  3. Передавайте consent_version = consent.version в каждом POST /messages. Достаточно в первом: согласие записывается в разговор.
  4. Если арендатор изменил текст или ссылку, версия меняется. Сервер ответит 400 consent_required с актуальным consent: покажите его снова и повторите отправку с новой версией.

Версия — первые 16 шестнадцатеричных знаков sha256(text + "\n" + url). Не вычисляйте её сами: берите готовую из init или из ответа 400.

Запись согласия: kind: "offer", version, accepted_at, text_sha256, url, ip (исходный IP посетителя — ограничение).

{
  "error": "consent_required",
  "consent": {
    "text": "Я согласен с условиями оферты",
    "url": "https://formula-cream.pro/offer",
    "version": "3a7bd3e2360a3d29"
  }
}

#GET /api/widget/history

Статус: Доступно.

История разговора для восстановления окна при перезагрузке страницы.

#Запрос

GET /api/widget/history?st=<session_token>

ПараметрТипОбязательныйОписание
stстрокадатокен сессии

#Ответ 200

{
  "messages": [
    { "id": "11111111-1111-4111-8111-111111111111", "sender": "user", "content": "Сколько стоит курс?", "created_at": "2026-10-11T08:15:30Z" },
    { "id": "22222222-2222-4222-8222-222222222222", "sender": "ai", "content": "Курс стоит 9 900 ₽.", "created_at": "2026-10-11T08:15:33Z" }
  ]
}
ПолеТипОписание
messages[].idстрокаидентификатор сообщения в базе
messages[].senderстрокаuser — посетитель, ai, operator
messages[].contentстрокатекст
messages[].created_atстрокавремя создания

Возвращается не более 500 сообщений в порядке возрастания времени. Внутренние заметки сотрудников (is_internal) не включаются.

#Ошибки

СтатусТелоКогда
401{"error":"Session not available"}нет токена, негодный токен
404{"error":"Session not available"}канал или разговор не найден либо неактивен
500{"error":"Internal server error"}сбой сервера или базы

#GET /api/widget/poll

Статус: Доступно.

Запасной способ получать новые сообщения, если поток событий (SSE) недоступен. Кадр виджета опрашивает каждые 2,5 секунды. Без after каждый ответ содержит все сообщения ИИ и оператора разговора, поэтому на стороне клиента ведите учёт показанных id или передавайте after со временем последнего сообщения.

#Запрос

GET /api/widget/poll?st=<session_token>[&after=<время>]

ПараметрТипОбязательныйОписание
stстрокадатокен сессии
afterстрока (время ISO 8601)нетотфильтровывает сообщения с created_at не позже этого значения. Кадр виджета параметр не передаёт (ограничения)

#Ответ 200

{
  "new_messages": [
    {
      "id": "33333333-3333-4333-8333-333333333333",
      "sender": "operator",
      "content": "Здравствуйте! Меня зовут Анна, расскажу подробнее.",
      "created_at": "2026-10-11T08:17:02Z",
      "operator_name": "Анна",
      "operator_avatar": null
    }
  ],
  "operator": { "name": "Анна", "avatar": null },
  "typing": false,
  "typing_name": ""
}
ПолеТипОписание
new_messages[]массивсообщения ИИ и оператора
new_messages[].senderстрокаoperator или ai
new_messages[].operator_name, operator_avatarстрока, необязательноимя и аватар оператора
operatorобъект или nullназначенный оператор
typingbooleanпечатает ли оператор. Признак активен 5 секунд после последнего нажатия
typing_nameстрокаимя печатающего

Ошибки — как у history.

#GET /api/widget/events (SSE)

Статус: Доступно.

Поток серверных событий (Server-Sent Events): ответы оператора и ИИ приходят без опроса.

#Запрос

GET /api/widget/events?st=<session_token> — через EventSource.

Заголовки ответа: Content-Type: text/event-stream, Cache-Control: no-cache, no-transform, X-Accel-Buffering: no. Первая строка потока — : connected.

#События

СобытиеДанныеКогда
message{id, sender, content, created_at} (sender: operator или ai)в разговоре появилось сообщение не от посетителя (внутренние пропускаются)
conversation:update{status, assigned_operator_id}изменился статус разговора или назначенный оператор
typing{active, name}оператор печатает (active=true в течение 5 с)
status{status}состояние подписки (например, SUBSCRIBED)
heartbeat{ts}каждые 25 секунд
closed{reason: "session_unavailable"}доступ к разговору закрыт (канал выключен, арендатор отключён, сессия недействительна). Поток закрывается
: connected

event: status
data: {"status":"SUBSCRIBED"}

event: message
data: {"id":"33333333-3333-4333-8333-333333333333","sender":"operator","content":"Здравствуйте!","created_at":"2026-10-11T08:17:02Z"}

event: heartbeat
data: {"ts":1760170000000}

Свойства:

#POST /api/widget/upload

Статус: Доступно. Работает с ограничением: загруженный файл не создаёт сообщение (ограничения).

Загрузить файл и получить публичный адрес.

#Запрос

POST /api/widget/upload, Content-Type: multipart/form-data

Поле формыТипОбязательныйОграниченияПример
fileфайлдане более 10 МБscheme.pdf
channel_idстрокада00000000-0000-4000-8000-000000000000
session_tokenстрокадатокен действующей сессииw1.…

Допустимые типы: изображения JPEG, PNG, GIF, WebP; PDF; DOC, DOCX; XLS, XLSX; TXT, CSV, MD. Проверяются расширение, MIME-тип и сигнатура файла; опасные расширения блокируются.

curl -X POST https://formula-cream.pro/api/widget/upload \
  -F 'file=@scheme.pdf' \
  -F 'channel_id=00000000-0000-4000-8000-000000000000' \
  -F 'session_token=w1.…'

#Ответ 200

{
  "url": "https://formula-cream.pro/storage/…/scheme.pdf",
  "filename": "scheme.pdf",
  "size": 84213,
  "content_type": "application/pdf"
}

Адрес url публичный: любой, кто его знает, откроет файл.

#Ошибки

СтатусТелоКогда
400{"error":"file and channel_id required"}нет файла или канала
401{"error":"Session not available"}нет токена или он негоден (без пояснений)
400 / 413{"error":"<причина>"}файл не прошёл проверку типа, размера или сигнатуры: текст причины возвращает проверка файла
404{"error":"Session not available"}канал или разговор не найден
413{"error":"File too large (max 10MB)"}файл больше 10 МБ
429{"error":"Too many requests"}более 5 загрузок в минуту с IP или 30 в минуту на канал
500{"error":"Upload failed"}сбой загрузки

Проверка частоты по IP выполняется до разбора тела.

Чтобы оператор увидел файл, отправьте сообщение с адресом url в тексте (POST /messages, content: «Файл: https://…»).

#Пределы

ЧтоПределОбластьХранение счётчика
Длина сообщения4000 знаковсообщение—
Частота сообщений20 в минутуканал + IPпамять процесса
Частота сообщений240 в минутуканалпамять процесса
Суточный предел ИИ200 по умолчанию, максимум 10 000 (ai_daily_limit)канал арендаторабаза
Размер файла10 МБфайл—
Загрузка файлов5 в минутуIPпамять процесса
Загрузка файлов30 в минутуканалпамять процесса
История500 сообщенийзапрос history—
Срок токена30 сутоктокен—
Размер токена1024 знакатокен—

Счётчики в памяти процесса не общие для нескольких процессов и сбрасываются при перезапуске. Заголовков Retry-After и остатка предела у клиентских маршрутов нет: при 429 ждите минуту.

#Сводка ошибок

КодМаршрутыСмысл
400init, messages, uploadневерный запрос; consent_required
401history, poll, events, uploadнет или негодный токен
404всеканал, разговор или сессия не найдены или неактивны
413messages, uploadслишком длинное сообщение или большой файл
429messages, uploadпредел частоты
500всесбой сервера
503messagesподпись сессий не настроена

Тело ошибки — {"error": "<код или текст>"}; для events — текст. Ветвите логику по статусу и по строке error там, где она описана выше; тексты Session not available и Too many requests не предназначены для разбора.

#Пример: чат на чистом JavaScript

Рабочий файл — examples/custom-chat-vanilla.html, с настраиваемым API_BASE (см. CORS). Ниже — основные фрагменты.

const API_BASE = 'https://formula-cream.pro';          // ваша ячейка
const CHANNEL_ID = '00000000-0000-4000-8000-000000000000';
const TOKEN_KEY = 'my_chat_token';

let token = localStorage.getItem(TOKEN_KEY) || '';
let consentVersion = '';
const shown = new Set();                                // id уже показанных сообщений

async function api(path, options) {
  const res = await fetch(API_BASE + path, options);
  let body = null;
  try { body = await res.json(); } catch { /* не JSON */ }
  return { ok: res.ok, status: res.status, body };
}

// 1. Настройки канала и согласие
async function init() {
  const { ok, body } = await api('/api/widget/init', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ channel_id: CHANNEL_ID })
  });
  if (!ok) throw new Error('Канал недоступен');
  addMessage('ai', body.greeting);                      // приветствие показываем сами
  if (body.consent) consentVersion = body.consent.version;
  return body;
}

// 2. Отправка сообщения
async function send(text) {
  const payload = {
    channel_id: CHANNEL_ID,
    content: text,
    session_token: token || undefined,
    page_url: location.href,
    page_title: document.title,
    consent_version: consentVersion || undefined
  };
  const { ok, status, body } = await api('/api/widget/messages', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload)
  });

  if (status === 400 && body && body.error === 'consent_required') {
    consentVersion = body.consent.version;              // показать согласие и повторить
    throw new Error('consent_required');
  }
  if (status === 413) throw new Error('Сообщение слишком длинное');
  if (status === 429) throw new Error('Слишком часто, подождите минуту');
  if (!ok) throw new Error('Не удалось отправить');

  token = body.session_token;
  localStorage.setItem(TOKEN_KEY, token);
  if (body.queued) addMessage('system', 'Передано оператору, он ответит позже');
  else if (body.response) addMessage('ai', body.response);
}

// 3. Подписка на ответы оператора: SSE с запасным опросом
function listen() {
  if (!token) return;
  const es = new EventSource(API_BASE + '/api/widget/events?st=' + encodeURIComponent(token));
  es.addEventListener('message', (e) => {
    const m = JSON.parse(e.data);
    addMessage(m.sender, m.content, m.id);
  });
  es.addEventListener('closed', () => es.close());
  es.onerror = () => { es.close(); setTimeout(poll, 2500); };
}

async function poll() {
  if (!token) return;
  const { ok, body } = await api('/api/widget/poll?st=' + encodeURIComponent(token));
  if (ok) body.new_messages.forEach((m) => addMessage(m.sender, m.content, m.id));
  setTimeout(poll, 2500);
}

// 4. Показ без дублей
function addMessage(sender, content, id) {
  if (id) {
    if (shown.has(id)) return;
    shown.add(id);
  }
  // ответ ИИ из POST и тот же ответ из SSE: сравнить по sender+content с последним сообщением
  const last = document.querySelector('#log > :last-child');
  if (last && last.dataset.sender === sender && last.textContent === content) return;
  const div = document.createElement('div');
  div.dataset.sender = sender;
  div.textContent = content;                            // не innerHTML: текст чужой
  document.querySelector('#log').appendChild(div);
}

Важное:

#Пример: компонент React

Рабочий файл — examples/custom-chat-react.jsx.

import { useEffect, useRef, useState } from 'react';

const API_BASE = 'https://formula-cream.pro';
const CHANNEL_ID = '00000000-0000-4000-8000-000000000000';

export function ChatBox() {
  const [messages, setMessages] = useState([]);
  const [text, setText] = useState('');
  const [busy, setBusy] = useState(false);
  const token = useRef(localStorage.getItem('my_chat_token') || '');
  const consent = useRef('');

  const add = (m) =>
    setMessages((list) => {
      if (m.id && list.some((x) => x.id === m.id)) return list;
      const last = list[list.length - 1];
      if (last && last.sender === m.sender && last.content === m.content) return list;
      return [...list, m];
    });

  useEffect(() => {
    let es;
    (async () => {
      const init = await fetch(`${API_BASE}/api/widget/init`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ channel_id: CHANNEL_ID })
      }).then((r) => r.json());
      if (init.consent) consent.current = init.consent.version;
      add({ sender: 'ai', content: init.greeting });

      if (token.current) {
        es = new EventSource(`${API_BASE}/api/widget/events?st=${encodeURIComponent(token.current)}`);
        es.addEventListener('message', (e) => add(JSON.parse(e.data)));
      }
    })();
    return () => es && es.close();
  }, []);

  async function submit(e) {
    e.preventDefault();
    if (!text.trim() || busy) return;
    setBusy(true);
    add({ sender: 'user', content: text });
    const res = await fetch(`${API_BASE}/api/widget/messages`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        channel_id: CHANNEL_ID,
        content: text,
        session_token: token.current || undefined,
        consent_version: consent.current || undefined
      })
    });
    const body = await res.json().catch(() => ({}));
    setBusy(false);
    if (!res.ok) return add({ sender: 'system', content: `Ошибка ${res.status}` });
    token.current = body.session_token;
    localStorage.setItem('my_chat_token', token.current);
    if (body.response) add({ sender: 'ai', content: body.response });
    setText('');
  }

  return (
    <form onSubmit={submit}>
      <ul>{messages.map((m, i) => <li key={m.id || i}>{m.content}</li>)}</ul>
      <input value={text} onChange={(e) => setText(e.target.value)} maxLength={4000} />
      <button disabled={busy}>Отправить</button>
    </form>
  );
}

#Известные ограничения

Формулировки нейтральны: по коду на 11.10.2026.

№ОграничениеПоследствия и обходы
—CORS: заголовков нет у messages, history, poll, events, upload. CORS (*) и OPTIONS есть только у initЗапрос с другого origin не пройдёт (для application/json браузер сначала отправит предварительный запрос OPTIONS, который этот маршрут не обслужит), а ответ не будет доступен коду страницы. Работает: (а) страница на домене самой ячейки; (б) ваш серверный прокси (запросы браузер→ваш сервер→ячейка), с пробросом реального IP посетителя и отключённой буферизацией для SSE; (в) мобильное приложение (CORS браузера нет). Разрешение CORS на стороне платформы — Планируется
1Форма перед чатом: sender_email и sender_phone отправляются кадром, но сервер их не читаетПоле sender_name работает. Собирайте контакты текстом сообщения или своей формой
2Загрузка файла не создаёт сообщение; оператор файла не видит. До первого сообщения (нет токена) — 401 без пояснений; атрибут accept поля в кадре шире списка сервераЗагрузите файл, затем отправьте сообщение с url. Загружайте только после первого сообщения
13Кадр виджета не передаёт after в poll (сервер фильтр поддерживает); language в messages на ответ не влияет; атрибут data-consent в widget.js не читаетсяВ своём клиенте передавайте after или дедуплицируйте по id. Язык ответа определяется настройками канала

Дополнительно, не из перечня:

#Заметки

#Открытые вопросы