API Documentation

Quick start

Three steps to your first notification.

1

1. Create a project

Sign up with Yandex or VK, create a project in the dashboard.

2

2. Get an API key

Copy your API key (format: zn_aBcDeFgH...) from project settings.

3

3. Send your first request

Use curl or any HTTP client:

curl -X POST https://api.zapnoty.com/v1/send
-H "Authorization: Bearer zn_aBcDeFgH..."
-H "Content-Type: application/json"
-d '{
"subscriber_id": "550e8400-e29b-41d4-a716-446655440000",
"text": "Hello!"
}'

Authentication

All API requests require a Bearer token in the Authorization header.

Base URL: https://api.zapnoty.com
Content-Type: application/json required for all POST/PUT/PATCH requests

Header format:

Authorization: Bearer zn_aBcDeFgHiJkLmNoPqRsTuVwXyZ123456

API key format: zn_ + 32 characters. Keys are created in the project dashboard.

Never pass the key in URL or query parameters. Use only the Authorization header.

Bot Auth

Authenticate your website users via Telegram/Max bots. User clicks a link, confirms in the bot, automatically subscribes to notifications, and you receive their data.

How it works

1. User clicks the bot link

2. Bot shows authorization request with "Login" / "Cancel" buttons

3. User clicks "Login" → automatic subscription to notifications + code generation

4. Bot sends "Go to website" button → callback_url?code=CODE

5. Your server calls POST /v1/auth/verify → receives data

Setup

In the project dashboard go to Settings → Auth and set the Callback URL and Origin URL.

Static links

Simple option — a permanent link. Get it via API or in the dashboard:

GET /v1/auth/link
 
// Response:
{
"telegram_link": "https://t.me/YourBot?start=auth_my-app",
"max_link": "https://max.ru/YourBot?start=auth_my-app",
"configured": true
}

API sessions (with state)

To pass state (e.g., browser session ID), create a session via API:

POST /v1/auth/session
 
{
"state": "browser_session_abc"
}
 
// Response:
{
"session_id": "aBcDeFgH12345678",
"telegram_link": "https://t.me/YourBot?start=auths_aBcDeFgH12345678",
"expires_in": 300
}

Code verification

After confirmation, the user lands on callback_url?code=CODE. Verify the code:

POST /v1/auth/verify
 
{
"code": "aBcDeFgH..."
}
 
// Response:
{
"channel": "telegram",
"first_name": "John",
"username": "johndoe",
"avatar_url": null,
"subscriber_id": "uuid-...",
"lang": "ru",
"tags": [],
"state": "browser_session_abc"
}

QR/Polling (cross-device)

For desktop auth via a mobile bot: create a session, display a QR code with the link, and poll the status. When the user confirms on their phone, you receive a code. Poll interval: 2-3 seconds. The code is one-time — after receiving status=completed, a repeat request returns expired.

GET /v1/auth/session/{session_id}/status
 
// Pending:
{ "status": "pending" }
 
// Completed:
{ "status": "completed", "code": "aBcDeFgH..." }
 
// Expired:
{ "status": "expired" }

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

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

segment string

Filter by segment (tag)

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

{
"ok": true,
"message_id": "msg_abc123"
}

OTP (one-time passwords)

Send and verify confirmation codes via messenger.

POST /v1/otp/send

Generates a 6-digit code and sends it to the subscriber.

subscriber_id string (UUID) required

Subscriber UUID

POST /v1/otp/send
 
{
"subscriber_id": "550e8400-e29b-..."
}

Response

{
"sent": true,
"expires_in_seconds": 300
}

POST /v1/otp/verify

Verifies the entered code.

subscriber_id string (UUID) required

Subscriber UUID

code string required

6-digit code from the user

POST /v1/otp/verify
 
{
"subscriber_id": "550e8400-e29b-...",
"code": "482916"
}

Response

✓ Code valid

{
"verified": true
}

✗ Code invalid

{
"verified": false
}

OTP limits: max 5 verification attempts, 5-minute code TTL, 1 active code per subscriber.

Broadcast (mass delivery)

Send a message to all subscribers or a segment.

POST /v1/broadcast

Creates a broadcast job. Messages are sent through a queue.

text string required

Message text

format string

Text format: plain (default), markdown, or html

media object

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

buttons array

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

permission string

Filter by permission

tags array

Filter by tags: ["vip", "beta"]

POST /v1/broadcast
 
{
"text": "Version 2.0 available!",
"permission": "updates",
"tags": ["beta"]
}

Response

{
"job_id": "a1b2c3d4-e5f6-...",
"status": "pending",
"total_subscribers": 1500
}

GET /v1/broadcast/:job_id

Get broadcast status.

Response fields: status (pending/processing/completed/failed), total, sent, failed.

{
"job_id": "b7f3...",
"status": "completed",
"total": 2847,
"sent": 2835,
"failed": 12
}

Subscribers

Manage subscriber list and their tags.

GET /v1/subscribers

Project subscriber list. Pagination via query parameters.

limit number

Number of records (default 50, max 200)

offset number

Offset from the beginning of the list (default 0)

channel string

Filter by channel: telegram or max

Response

{
"subscribers": [
{
"id": "550e8400-...",
"channel": "telegram",
"first_name": "John",
"username": "johndoe",
"lang": "ru",
"tags": ["vip"],
"created_at": "2026-03-05T12:00:00Z"
}
],
"total": 150
}

PUT /v1/subscribers/:id/tags

Update subscriber tags. Pass the full array of tags. Tags must be pre-created in project settings or via API. Unknown tags will be auto-created (if the 20-tag limit is not reached).

PUT /v1/subscribers/sub_abc/tags
 
{
"tags": ["vip", "beta"]
}

Response

{
"updated": true
}

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".

Example

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"}

Example with media and buttons

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"}
]]
}

Limitations

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.

Management API

API for managing templates, permissions, tags, and webhook. All endpoints require a Bearer token.

Templates

GET /v1/templates — list templates
POST /v1/templates — create template (key, text, format?)
PUT /v1/templates/{key} — update template (text?, format?)
DELETE /v1/templates/{key} — delete template

Permissions

GET /v1/permissions — list permissions
POST /v1/permissions — create (key, title, description?, required?)
PUT /v1/permissions/{key} — update (title?, description?, required?)

Deleting permissions is only available in the dashboard.

Tags

GET /v1/tags — list project tags
POST /v1/tags — create tag (name)
PUT /v1/tags/{name} — rename tag (new_name)
DELETE /v1/tags/{name} — delete tag (also removed from all subscribers)

Allowed characters: letters, numbers, - and _. Maximum 20 tags.

Sender signature

Each message can include a signature — sender name and description. Configured in the dashboard (Overview → Notifications or Settings → Notifications).

Signature format: text\n\n— Name\nDescription

Logo: uploaded via dashboard, resized to 256×256 PNG.

Auto-messages

Manage auto-messages on subscribe/unsubscribe via API. Each message is toggled independently and supports two languages (RU/EN). Project signature is appended automatically.

GET /v1/auto-messages — get settings
PUT /v1/auto-messages — update settings

Fields: subscribe_message_enabled, unsubscribe_message_enabled (boolean), subscribe_message, unsubscribe_message ({"ru":"...","en":"..."}). When disabled, default text is sent.

OTP template

Custom OTP message template. Must contain {{code}}. You can also use {{minutes}} for expiry time.

Example: Your verification code: {{code}}. Valid for {{minutes}} min.

Webhook

GET /v1/webhook — get current webhook
PUT /v1/webhook — set webhook (url, events?[])
DELETE /v1/webhook — delete webhook

If events is not specified — all events will be sent. Secret is generated automatically.

Webhooks

Zapnoty sends HTTP POST to your URL when events occur. One webhook per project. Configured in the dashboard or via API.

Events: subscription.created, subscription.deleted, delivery.success, delivery.failed, broadcast.completed, button.clicked, auth.completed, ticket.created, ticket.replied, ticket.status_changed, ticket.assigned, ticket.closed.

Signature: X-Zapnoty-Signature header contains HMAC-SHA256 of request body with your webhook secret.

Signature verification:

// Node.js
const crypto = require('crypto');
 
const signature = req.headers['x-zapnoty-signature'];
const expected = crypto
.createHmac('sha256', webhookSecret)
.update(JSON.stringify(req.body))
.digest('hex');
 
if (signature !== expected) // отклонить запрос

Payload format:

{
"event": "subscription.created",
"timestamp": "2026-03-05T12:00:00Z",
"data": {
"subscriber_id": "sub_abc",
"channel": "telegram"
}
}

Retry policy

On delivery failure, webhooks are automatically retried up to 3 times with exponential backoff (1s → 2s → 4s). Each attempt has a 10-second timeout. 4xx responses from your server are not retried. We recommend implementing idempotent processing on your side.

Scheduled Messages

Delayed messages, drip chains, and recurring broadcasts

Scheduled Send

Send a message at a specified time. Supports formatting (html/markdown), media attachments, and inline buttons.

subscriber_id string (UUID) required

Subscriber UUID

text string required

Message text

scheduled_at string (ISO 8601) required

Send date and time (ISO 8601 UTC)

name string

Name (for dashboard)

POST /v1/scheduled
 
{
"subscriber_id": "550e8400-...",
"text": "Meeting reminder",
"format": "html",
"scheduled_at": "2026-03-25T10:00:00Z",
"buttons": [[{"text": "Details", "url": "https://..."}]]
}

Drip Chains

Automatic sequence of messages on subscription or event. Each step supports formatting, media, and buttons.

name string required

Name (for dashboard)

trigger string required

Trigger: subscription, segment, permission

trigger_value string

Trigger value (tag or permission key)

steps[].text string required

Message text

steps[].delay_minutes number required

Delay from trigger moment (minutes)

POST /v1/drip-chains
 
{
"name": "Onboarding",
"trigger": "subscription",
"steps": [
{"text": "Welcome!", "format": "html", "delay_minutes": 0},
{"text": "Tip of the day", "delay_minutes": 1440,
"buttons": [[{"text": "Read", "url": "https://..."}]]}
]
}

Recurring

Regular broadcast at a set interval. Supports formatting, media, buttons, and filters (channel, segment, permission).

name string required

Name (for dashboard)

text string required

Message text

interval_hours number required

Repeat interval (hours)

POST /v1/recurring
 
{
"name": "Weekly digest",
"text": "<b>Weekly summary</b>",
"format": "html",
"interval_hours": 168,
"media": {"media_type": "image", "url": "https://..."}
}

Helpdesk API

Ticket support system. Create tickets from customers, reply to them and manage statuses. Users are automatically subscribed to notifications when contacting support. Enable Helpdesk in project settings.

POST /v1/helpdesk/tickets — create ticket

Creates a ticket on behalf of a customer. Ticket number is auto-generated.

text string required

First message text

subject string

Ticket subject (optional)

channel string

Channel: telegram, max or virtual. Defaults to virtual

chat_id number

Messenger chat ID. Required for telegram/max

first_name string

Customer first name

username string

Customer username

ticket_type_id string

Ticket type UUID (from GET /v1/helpdesk/ticket-types). Optional.

Response

{
"id": "550e8400-...",
"ticket_number": 42,
"status": "new",
"priority": "normal",
"channel": "virtual",
"ticket_type_id": "uuid-or-null",
"ticket_type_name": "Баг",
"created_at": "2026-03-05T12:00:00Z"
}

GET /v1/helpdesk/tickets — list tickets

Returns project tickets. Filter by status: ?status=new|in_progress|waiting|closed.

GET /v1/helpdesk/tickets/{id} — ticket details

Returns full ticket information by ID, including all messages.

POST /v1/helpdesk/tickets/{id}/reply — reply to ticket

Sends an agent reply to the ticket. Customer will receive a notification in messenger.

POST /v1/helpdesk/tickets/{id}/customer-reply — virtual customer reply

Sends a reply on behalf of a virtual customer (channel=virtual). Only for tickets with virtual channel. Agents will be notified and a ticket.replied webhook will be dispatched.

text string required

First message text

PATCH /v1/helpdesk/tickets/{id}/status — change status

Changes ticket status. Allowed values: open, in_progress, closed.

PATCH /v1/helpdesk/tickets/{id}/assign — assign agent

Assigns the ticket to a support agent.

user_id string required

Agent user ID

PATCH /v1/helpdesk/tickets/{id}/priority — ticket priority

Changes ticket priority. Allowed values: low, normal, high, urgent.

priority string required

Priority: low, normal, high, urgent

GET /v1/helpdesk/ticket-types — ticket types

Returns list of active ticket types for the project. Each type contains id, name, description. Use ticket_type_id when creating a ticket to classify requests. Types are configured in the dashboard (Settings → Support).

Webhook events

ticket.created — ticket created, ticket.replied — ticket replied, ticket.status_changed — status changed, ticket.assigned — agent assigned, ticket.closed — ticket closed.

Forms

Public endpoint for receiving form submissions from websites. No API key required — safe for client-side JavaScript.

POST /f/{form_id}

body object required

Arbitrary JSON — any form fields

Request example

POST /f/{form_id}
 
{
"name": "John",
"email": "john@example.com",
"message": "I want to learn more"
}

Response

{
"ok": true
}

Form Fields

Any fields are accepted. Standard fields are highlighted in notifications and will be used in future integrations (CRM, Google Sheets).

name string

Sender name

email string

Sender email

phone string

Phone number

subject string

Subject

company string

Company

city string

City

url string

Website URL

message string

Message text

* any

Any other fields — passed as-is

allowed_origins format

Allowed origins are specified as domains. Scheme (http/https), www and slashes are normalized automatically — just write example.com and all variants (https://example.com, www.example.com, example.com/) will be accepted.

example.com — accepts submissions from https://example.com, http://example.com, https://www.example.com

*.example.com — any subdomain and example.com itself (requires at least second level, *.com is forbidden)

Local hosts (localhost, 127.0.0.1), private IPs and reserved TLDs (.local, .test, .example) are forbidden — the server returns an error on save

Security

Origin check — only allowed domains

Rate limit — 30 submissions/min per form

Honeypot — hidden _honey field to protect from bots

Sanitization — automatic HTML tag and dangerous content removal

Time-to-submit — submissions faster than 3s are rejected (configurable)

Blocklist of throwaway email services (mailinator, tempmail, …) — flagged as spam

Spam folder & filters

Spam submissions are NOT deleted — they go to a separate list for review. Webhooks and email notifications skip spam.

Spam tab in dashboard — separate list of flagged submissions

Honeypot — hidden field with a unique per-form name. If a bot fills it, the submission is silently rejected

Keyword blocklist — customizable word list. Matches are flagged as spam

Timestamp check — `_submit_time` set on form load, verified on submit

Manually mark submissions as spam/not-spam in the dashboard

Submission Routes (Form Routes)

Unified model: route = trigger + action. Trigger — «always» (default recipient) or «conditional». Actions: deliver to recipient / mark as spam / block. Up to 40 routes per form.

Condition operators: `contains` (substring), `equals` (exact match). Condition field — field name or `*` for any string field

`mark_spam` — save, but flag as spam (no delivery, no webhook)

`block` — don't save at all (silent 200 OK)

`deliver` — deliver to recipient (email/messenger/subscriber/team). Conditional route adds to always-routes by default

Flag `replace_defaults` on a conditional deliver-route replaces all always-routes when matched — use for re-routing (e.g. «only sales submissions → sales@»)

Post-submit redirect

If `redirect_url` is set in form settings and the request came as `application/x-www-form-urlencoded` (HTML form, no JS), the server responds with `303 See Other` and `Location: <redirect_url>`. JSON requests (fetch/AJAX) still get `{ok:true}`.

Recipients: messengers (Telegram/Max), email, project subscribers

Integration example

fetch('https://api.zapnoty.com/f/form_abc123', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: form.name,
email: form.email,
message: form.message
})
})

Limits

Rate limits and field size restrictions.

Rate limit: 300 requests/min per project. Exceeding it returns 429 Too Many Requests.

Message text: up to 4,096 characters.

Buttons: up to 3 rows, up to 3 buttons per row.

Media: up to 20 MB (photo), 50 MB (video/document).

Broadcast: up to 100,000 subscribers per broadcast.

OTP: 5-min TTL, max 5 attempts, 1 active code per subscriber_id.

Tags: up to 20 tags per project, tag length up to 64 characters.

Permissions: up to 20 per project.

Templates: up to 100 per project.

X-RateLimit-* headers are not returned. When the limit is exceeded, the API only returns HTTP 429 with an error body. We recommend implementing exponential backoff on your side.

Error codes

API returns standard HTTP codes with JSON error body. The retryable field indicates whether the request can be retried.

Error format:

{
"error": "text is required",
"retryable": false
}

400 — Invalid request (missing required fields, wrong format)

401 — Invalid or missing API key

403 — No access to resource

404 — Subscriber or resource not found

409 — Conflict (e.g., OTP already sent)

422 — Validation error (text too long, invalid URL)

429 — Rate limit exceeded (retryable: true, Retry-After header)

500 — Internal server error (retryable: true)

Error handling & retry

Recommendations for handling API errors and retry strategies.

retryable: true — the error is temporary (429, 500). Use exponential backoff: 1s → 2s → 4s, max 3 attempts.

retryable: false — retrying won't help (400, 401, 403, 404, 409). Fix the request before resending.

Retry-After — on 429, this header specifies how many seconds to wait before the next request. Rate limit: 300 requests per minute per project.

Exponential backoff — increase delay between attempts: 1s, 2s, 4s. Do not automatically retry POST requests (send, broadcast) — use the retryable field to make decisions.

Playground

Build an API request and copy the ready curl command.

Request body
{
  "subscriber_id": "550e8400-e29b-41d4-a716-446655440000",
  "text": "Hello from Zapnoty!"
}
curl command
curl -X POST https://api.zapnoty.com/v1/send \
  -H "Authorization: Bearer zn_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "subscriber_id": "550e8400-e29b-41d4-a716-446655440000",
  "text": "Hello from Zapnoty!"
}'

API vs Dashboard

What you can automate via REST API and what is only available in the dashboard.

Available via API

Send notifications — POST /v1/send (personal, by subscriber_id, with media, buttons, templates)

OTP codes — POST /v1/otp/send and /v1/otp/verify

Mass broadcasts — POST /v1/broadcast, GET /v1/broadcast/:job_id

Subscribers — GET /v1/subscribers, PUT /v1/subscribers/:id/tags

Templates — full CRUD: GET/POST /v1/templates, PUT/DELETE /v1/templates/{key}

Permissions — GET/POST /v1/permissions, PUT /v1/permissions/{key}

Tags — full CRUD: GET/POST /v1/tags, PUT/DELETE /v1/tags/{name}

Webhook — GET/PUT/DELETE /v1/webhook

Bot Auth — GET /v1/auth/link, POST /v1/auth/session, POST /v1/auth/verify, GET session status

Helpdesk — create tickets, reply, change status, assign, set priority, ticket types

Auto-messages — GET/PUT /v1/auto-messages (enable/disable, texts in two languages)

Analytics — GET /api/projects/:id/analytics (via JWT dashboard session)

Dashboard only

Create project — registration and project creation only via UI

API key — generate and regenerate key (requires OTP confirmation)

Delete project — with OTP confirmation via messenger

Sender signature — configure name and description (Settings → Notifications)

Logo — upload and delete project logo

OTP template — multilingual template with HTML formatting and preview

Auth settings — callback URL, origin URL, authorization button text

Enable Helpdesk — activate ticket module in project settings

SLA timers — configure first response and resolution deadlines

Delete permissions — DELETE permissions is only available in the dashboard

QR codes — generate subscription QR codes with styles and logo

API key (Bearer zn_...) is used for all /v1/* endpoints. Dashboard works via JWT session after OAuth (Yandex/VK).