Отправка сообщений

Отправка сообщения. POST /messages/ — text, файл или template. К файлу можно добавить text как подпись; template с text и файлом не сочетается. Свой client_msg_id делает запрос идемпотентным: повтор с тем же client_msg_id и тем же телом вернёт тот же message_id, дубля не будет; тот же client_msg_id с другим телом вернёт 409 idempotency_conflict. Поле chat_type необязательно: транспорт определяет сам канал. Если всё же прислать его и оно противоречит каналу — запрос будет отклонён, а не отправлен молча в другой мессенджер. reply_to принимает message_id ИЗ ЭТОГО ЖЕ диалога.

curl -X POST https://dialogs.tehprof.kz/api/v1/gw/messages/ \
  -H "Authorization: Bearer gw_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "channel_id": "32",
    "chat_type": "whatsapp",
    "chat_id": "77010009988",
    "client_msg_id": "order-42-reply",
    "text": "Заказ принят, спасибо!"
  }'

201 { "message_id": "msg_7f3c" }

Файлы. Сначала загрузите файл: POST /media/ в поле file (multipart/form-data), до 100 МБ. Ответ — media_url, file_name и file_mimetype; передайте все три в POST /messages/. Ссылки на внешние файлы мы не скачиваем. Если исчерпана квота хранилища, придёт 413 storage_quota_exceeded. note: true отправит видео кружком, voice: true — аудио голосовым сообщением.

curl -X POST https://dialogs.tehprof.kz/api/v1/gw/media/ \
  -H "Authorization: Bearer gw_xxx" \
  -F "file=@invoice-42.pdf"

201 { "media_url": "/media/17/2de7….pdf",
      "file_name": "invoice-42.pdf",
      "file_mimetype": "application/pdf" }

curl -X POST https://dialogs.tehprof.kz/api/v1/gw/messages/ \
  -H "Authorization: Bearer gw_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "channel_id": "32",
    "chat_id": "77010009988",
    "client_msg_id": "invoice-42",
    "text": "Счёт во вложении",
    "media_url": "/media/17/2de7….pdf",
    "file_name": "invoice-42.pdf",
    "file_mimetype": "application/pdf"
  }'

Ответ потерялся. Если сеть оборвалась и вы не знаете, принята ли отправка, повторите POST с тем же client_msg_id или спросите GET /commands/<client_msg_id>/. outcome: accepted (принято), sending (отправляется), sent (передано мессенджеру), rejected (отклонено), unknown (исход у мессенджера неизвестен — смотрите statuses). delivery_status — текущий статус доставки; при failed рядом есть error с причиной (как в statuses). 404 — такой отправки этим ключом не было.

curl https://dialogs.tehprof.kz/api/v1/gw/commands/order-42-reply/ \
  -H "Authorization: Bearer gw_xxx"

200 { "client_msg_id": "order-42-reply", "message_id": "msg_7f3c",
      "outcome": "sent", "delivery_status": "delivered" }

200 ≠ доставлено. Ответ 201 значит «принято к отправке», а не «дошло». Реальный отказ (нет оплаты WABA, номер заблокирован) приходит ПОЗЖЕ в потоке statuses со статусом failed — это самая дорогая ошибка интеграции. Судить о доставке по коду ответа нельзя, только по статусу.

Отправка шаблоном (вне 24-часового окна). Окно закрыто — обычный текст не уйдёт, для отправки доступен одобренный шаблон. Тот же POST /messages/, только вместо text/media_url поле template: { name, language, params? } — взаимоисключимо с text и media_url. params заполняет плейсхолдеры {{1}}, {{2}} массивами body и header:

curl -X POST https://dialogs.tehprof.kz/api/v1/gw/messages/ \
  -H "Authorization: Bearer gw_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "channel_id": "32",
    "chat_type": "whatsapp",
    "chat_id": "77010009988",
    "client_msg_id": "order-42-tpl",
    "template": {
      "name": "order_shipped",
      "language": "ru",
      "params": { "body": ["A-42", "DHL"] }
    }
  }'

201 { "message_id": "msg_7f3c" }

Что можно отправить прямо сейчас. GET /allowed/?channel_id=&chat_id= возвращает готовое решение — не считайте 24-часовое окно сами, две реализации одного правила разойдутся ровно на границе. mode: free_text — можно писать обычным текстом, expires_in_seconds сколько ещё (null, если у канала окна нет вовсе). mode: templates — свободный текст невозможен, поле templates перечисляет одобренные шаблоны, что уйдут сейчас. Окно продлевает только ВХОДЯЩЕЕ сообщение клиента — ваши ответы его не двигают. Спрашивайте на каждый номер отдельно: окна у разных номеров разные. Это чтение (GET), диалог оно не создаёт.

curl "https://dialogs.tehprof.kz/api/v1/gw/allowed/?channel_id=32&chat_id=77010009988" \
  -H "Authorization: Bearer gw_xxx"

# окно открыто
200 { "mode": "free_text", "expires_in_seconds": 6000, "templates": [] }

# окно закрыто — свободный текст нельзя, только эти шаблоны
200 {
  "mode": "templates",
  "expires_in_seconds": 0,
  "templates": [
    { "template_id": "123", "name": "order_shipped",
      "language": "ru", "status": "APPROVED", "category": "UTILITY" }
  ]
}

История переписки. GET /messages/?channel_id=&chat_id= отдаёт прошлое диалога — то, что вебхуки принесли до вашего подключения, и то, что вы пропустили, пока сервис лежал. Порядок — от новых к старым, по внутренней последовательности сообщений, а не по времени: сообщения одной миллисекунды всё равно строго упорядочены, поэтому страница не повторит и не потеряет ни одного. Постранично: возьмите next_before_id из ответа и передайте его как before_id за следующей (более старой) страницей; пустой next_before_id значит «начало диалога». По времени не листайте. limit — до 200, по умолчанию 50.

curl "https://dialogs.tehprof.kz/api/v1/gw/messages/?channel_id=32&chat_id=77010009988&limit=50" \
  -H "Authorization: Bearer gw_xxx"

200 {
  "messages": [
    { "message_id": "8842", "channel_id": "32",
      "chat_type": "whatsapp", "chat_id": "77010009988",
      "direction": "incoming", "source": "customer",
      "text": "Где мой заказ?", "content_type": "text",
      "media_url": "", "file_name": "", "file_mimetype": "",
      "reply_to": "", "sender_name": "Айгерим",
      "sender_phone": "77010009988", "metadata": {}, "edited": false,
      "timestamp": "2026-08-08T09:12:44.512000+00:00",
      "status": "delivered" }
  ],
  "next_before_id": "8842"
}

Элемент истории — ровно тот же объект, что приходит в потоке messages, плюс timestamp (когда мы сохранили сообщение) и status (текущий статус доставки). Один парсер обслуживает и вебхук, и историю. Реакции и отозванные отправителем сообщения в историю не попадают. Номер, который никогда не писал в этот канал, вернёт пустой список, а не ошибку.