Zapnoty — POST /v1/send — delivery

API Documentation

REST API for notifications via Telegram and Max. Subscribers, OTP, broadcasts, forms, helpdesk.

POST /v1/send

Send a personal notification to a specific subscriber.

Parameters

subscriber_id string (UUID)

Subscriber UUID (from subscriber list)

text string

Message text (up to 4000 characters). Required if template is not specified

format string

Text format: plain (default), markdown, or html. Mutually exclusive with entities — if entities are passed, format must be empty.

entities array

Structured formatting (recommended). Array of [{type, offset, length, ...}] with offset/length in UTF-16 units. Types: bold, italic, underline, strikethrough, code, pre, blockquote, spoiler, mention, text_link (with url). Immune to HTML/Markdown injection.

external_id string

Alternative to subscriber_id: your own ID for the customer in your system. Resolves all active subscriptions of this customer (multi-channel one-shot send). Mutually exclusive with subscriber_id.

channels array

Channel filter when resolving by external_id, array of strings (e.g. ["telegram","max"]). If empty — all active subscriptions.

channel string

Single delivery channel (telegram/max) — legacy alternative to the channels array, behaves as channels: [channel].

media object

Media object: {type, url}. Types: photo, video, document

buttons array

Array of button rows: [[{text, url}]] or [[{text, callback_data}]]

template string

Template slug instead of text

vars object

Template variables: {key: value}

permission string

Filter by permission: send only to subscribers with this key

Request example

POST /v1/send
 
{
"subscriber_id": "550e8400-e29b-41d4-a716-446655440000",
"text": "Order #1042 shipped!",
"format": "markdown",
"buttons": [[{
"text": "Track",
"url": "https://example.com/track/1042"
}]]
}

Response

The response is aggregated: sent / failed are counters, details is an array per recipient. The delivery_id field is the id of the record in the project delivery log (for support reference / audit, visible in the dashboard → Deliveries); present for both successful and failed sends. The status is always 200, even if some sends failed — check details.

HTTP 200
{
"sent": 1,
"failed": 0,
"details": [
{
"subscriber_id": "550e8400-e29b-41d4-a716-446655440000",
"channel": "telegram",
"success": true,
"error": null,
"delivery_id": "7c9e6a1b-..."
}
]
}

Idempotency

To prevent a retried request (on timeout) from creating a duplicate, pass the Idempotency-Key header — a string of 8–128 chars [A-Za-z0-9_-], unique per operation.

  • Same key + same body → the same response is returned (status 200) with the X-Zapnoty-Idempotent-Replay: true header. No re-send happens.
  • Same key + different body → 409 idempotency_conflict.
  • A request with the same key is still being processed → 425 idempotency_in_progress, retry shortly.
POST /v1/send
Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890
{ "subscriber_id": "...", "text": "..." }
# Повтор с тем же ключом и тем же телом → тот же ответ (200):
HTTP 200
X-Zapnoty-Idempotent-Replay: true

Entities — structured formatting

Recommended replacement for format=html/markdown. Instead of parsing markup, the client sends an array of ranges: offset, length and format type. Immune to HTML/Markdown injection — text is never interpreted, it's sent as-is.

offset and length are measured in UTF-16 code units (like Telegram). For most languages 1 character = 1 unit; an emoji (👋) takes 2 units. Entities must be sorted by offset and not overlap.

Types: bold, italic, underline, strikethrough, code, pre, blockquote, spoiler, mention, text_link (with url field, passes private-IP SSRF check). Spoiler and mention are ignored in Max (no native counterpart).

POST /v1/send
 
{
"subscriber_id": "550e8400-e29b-41d4-a716-446655440000",
"text": "Order #1042 ready for pickup",
"entities": [
{"type": "bold", "offset": 0, "length": 11},
{"type": "text_link", "offset": 16, "length": 11, "url": "https://shop.ru/orders/1042"}
]
}

Templates

Templates let you reuse text with variables. Created in the dashboard or via API.

Usage: pass template and vars instead of text in /v1/send.

Variables in templates use {{name}} syntax. Example: "Order {{order_id}} delivered".

POST /v1/send
 
{
"subscriber_id": "550e8400-e29b-41d4-a716-446655440000",
"template": "order_delivered",
"vars": {
"order_id": "1042",
"customer": "John"
}
}

Media & buttons

Attach media files and inline buttons to notifications.

Media types: photo, video, document. Pass the file URL.

Buttons are a 2D array: outer array = rows, inner array = buttons in a row.

  • URL button: {"text": "Open", "url": "https://..."}
  • Callback button: {"text": "Yes", "callback_data": "confirm_123"}
POST /v1/send
 
{
"subscriber_id": "550e8400-e29b-41d4-a716-446655440000",
"text": "Your order is ready",
"media": {
"type": "photo",
"url": "https://example.com/photo.jpg"
},
"buttons": [[
{"text": "Details", "url": "https://..."},
{"text": "Cancel", "callback_data": "cancel_123"}
]]
}

1 media + buttons — supported. Multiple media + buttons — not supported (Telegram limitation). Caption with media: up to 1024 characters (Telegram) / up to 4000 (Max). Text without media — up to 4000 characters.

Related sections