Sending messages

Send a message. POST /messages/ — text, a file or a template. A file may carry text as its caption; a template does not combine with text or a file. Your own client_msg_id makes the call idempotent: retrying with the same client_msg_id and the same body returns the same message_id, no duplicate; the same client_msg_id with a different body returns 409 idempotency_conflict. chat_type is optional: the channel already decides the transport. If you do send it and it contradicts the channel, the request is refused rather than quietly delivered elsewhere. reply_to takes a message_id from THIS conversation.

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": "Order received, thanks!"
  }'

201 { "message_id": "msg_7f3c" }

Files. Upload the file first: POST /media/ in the file field (multipart/form-data), up to 100 MB. The response has media_url, file_name and file_mimetype; pass all three to POST /messages/. We do not download files from external links. If the storage quota is used up, you get 413 storage_quota_exceeded. note: true sends a video as a round video, voice: true sends audio as a voice note.

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": "Invoice attached",
    "media_url": "/media/17/2de7….pdf",
    "file_name": "invoice-42.pdf",
    "file_mimetype": "application/pdf"
  }'

Lost response. If the connection dropped and you do not know whether the send was accepted, repeat the POST with the same client_msg_id or ask GET /commands/<client_msg_id>/. outcome: accepted, sending, sent (handed to the messenger), rejected, unknown (the messenger outcome is unknown — watch statuses). delivery_status is the current delivery status; on failed an error with the reason sits next to it (as in statuses). 404 means this key made no such send.

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 ≠ delivered. A 201 means “accepted for sending”, not “arrived”. A real rejection (no WABA billing, a blocked number) arrives LATER on the statuses stream as failed — the most expensive integration mistake. Never judge delivery by the response code, only by the status.

Send a template outside the 24-hour window. When the window is closed, plain text is unavailable; an approved template is an available sending option. Use the same POST /messages/, with template: { name, language, params? } instead of text/media_url. These fields are mutually exclusive. params fills the {{1}}, {{2}} placeholders through the body and header arrays:

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": "en",
      "params": { "body": ["A-42", "DHL"] }
    }
  }'

201 { "message_id": "msg_7f3c" }

What can be sent right now. GET /allowed/?channel_id=&chat_id= returns a ready decision — do not compute the 24-hour window yourself, two implementations of one rule diverge exactly at the boundary. mode: free_text — plain typing is allowed, expires_in_seconds is how much longer (null when the channel has no window at all). mode: templates — free text is impossible, the templates field lists the approved templates that can go out this second. Only an INCOMING customer message extends the window — your own replies do not move it. Ask per number separately: windows differ per number. This is a read (GET); it does not create a conversation.

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

# window open
200 { "mode": "free_text", "expires_in_seconds": 6000, "templates": [] }

# window closed — free text is not allowed, only these templates
200 {
  "mode": "templates",
  "expires_in_seconds": 0,
  "templates": [
    { "template_id": "123", "name": "order_shipped",
      "language": "en", "status": "APPROVED", "category": "UTILITY" }
  ]
}

Conversation history. GET /messages/?channel_id=&chat_id= returns the past of a conversation — what the webhooks delivered before you connected, and what you missed while your service was down. Order is newest first, by internal message sequence rather than by timestamp: messages stored within the same millisecond are still strictly ordered, so a page can neither repeat nor skip one. Paging: take next_before_id from the response and pass it as before_id for the next (older) page; an empty next_before_id means the beginning of the conversation. Do not page by timestamp. limit is up to 200, 50 by default.

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": "Where is my order?", "content_type": "text",
      "media_url": "", "file_name": "", "file_mimetype": "",
      "reply_to": "", "sender_name": "Aigerim",
      "timestamp": "2026-08-08T09:12:44.512000+00:00",
      "status": "delivered" }
  ],
  "next_before_id": "8842"
}

A history item is exactly the object the messages stream delivers, plus timestamp (when Dialogs stored it) and status (the delivery status as of now). One parser serves both. Reactions and messages revoked by their sender are not returned. A number that has never written to this channel returns an empty list, not an error.