Zapnoty — Testing your integration

API Documentation

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

Testing your integration

Before going live, test your integration. Zapnoty provides several tools: test keys, message preview and broadcast dry-run.

Two keys per project

Each project has two API keys: a live zn_live_ and a test zn_test_. It is the same project — shared subscribers, templates and webhook endpoints, the only difference is the key. When a project is created, the response contains both keys at once — the api_key and test_api_key fields.

What the test key is allowed to do

The test key zn_test_ is meant for verifying SENDING (runtime), not for managing the project. Hence its permissions are scoped:

Read — any GET request (templates, subscribers, tags, permissions, webhooks, logs, analytics).

Runtime operations (mocked in sandbox): /v1/send, /v1/send/batch, /v1/send/preview, /v1/otp/*, /v1/broadcast, /v1/auth/session, /v1/auth/verify, /v1/events/track, /v1/helpdesk/tickets/*, /v1/helpdesk/request, /v1/test/simulate-event.

Changing project settings — any mutating request (POST/PUT/PATCH/DELETE) to templates, permissions, tags, webhooks, sender, auto-messages, forms, helpdesk config (settings/SLA/ticket types/canned responses/routing rules), custom-bots, scheduler (scheduled/drip/recurring), key rotation, subscriber deletion → 403.

A settings-changing request with the test key returns 403 with the standard error envelope. This way the test key cannot accidentally be used to break the project: project setup is a one-time action done with the live key zn_live_ once during configuration.

POST /v1/templates
Authorization: Bearer zn_test_...
{ "key": "welcome", "text": "..." }
→ 403 {
"error": {
"code": "test_key_forbidden",
"message": "The test key (zn_test_) cannot change project settings — use the live key for that"
}
}

Sandbox: what the sandbox mode is

A request with the test key zn_test_ goes through the full API logic (validation, rate-limit, delivery log, webhooks) and responds exactly like a live request — the same responses and error codes — but no real message is sent to Telegram/Max (mock delivery). Sandbox covers /v1/send, /v1/send/batch, /v1/otp/send and /v1/broadcast. Delivery log rows are marked is_test=true and are excluded from the project live statistics.

Simulating the delivery outcome

In sandbox mode you can add _test_directives to the /v1/send body to reproduce a specific outcome. delivery_outcome: "failed" routes the delivery down the failure path: a delivery log row with status failed and a delivery.failed webhook with the reason from failure_reason. Allowed failure_reason values: user_blocked_bot, chat_not_found, bot_banned, invalid_message, rate_limited, platform_unavailable, unknown. For a live key, _test_directives is silently ignored.

POST /v1/send
Authorization: Bearer zn_test_...
{
"subscriber_id": "550e8400-...",
"text": "Test message",
"_test_directives": {
"delivery_outcome": "failed",
"failure_reason": "user_blocked_bot"
}
}
→ {
"sent": 0,
"failed": 1,
"details": [{
"success": false,
"error": "Simulated delivery failure (sandbox): user_blocked_bot",
"delivery_id": "..."
}]
}

OTP in the sandbox

When /v1/otp/send is called with the test key, the response includes an extra sandbox_code field — the actual generated code in plaintext. Your CI/test can pass it straight to /v1/otp/verify without messenger access. For a live key the sandbox_code field is absent.

POST /v1/otp/send
Authorization: Bearer zn_test_...
{ "subscriber_id": "550e8400-..." }
→ {
"sent": true,
"expires_in_seconds": 300,
"sandbox_code": "482915"
}

Message preview

POST /v1/send/preview renders the message (template, variables, signature) and returns the final text without sending. Useful to verify how vars are substituted and which format applies.

POST /v1/send/preview
{
"subscriber_id": "550e8400-...",
"template": "welcome",
"vars": { "name": "Anna" }
}
→ {
"rendered_text": "Hello, Anna!",
"format": null,
"text_length": 12,
"has_media": false,
"buttons_count": 0,
"entities_count": 0,
"template_key": "welcome"
}

Broadcast dry-run

The dry_run: true parameter in /v1/broadcast does not create a broadcast — it returns the audience size under the filters, an estimated credit charge and a sample rendered message. Use it to verify filters (tags, permissions) before a real send.

POST /v1/broadcast
{
"text": "...",
"tags_all": ["vip"],
"dry_run": true
}
→ { "would_send_to": 342, "estimated_credits": 342, "rendered_sample": "..." }

Testing webhooks

To test event delivery, temporarily point a webhook endpoint at a test receiver URL (webhook.site, ngrok on localhost). The X-Zapnoty-Signature is computed the same way — verify your HMAC validation. The webhook delivery log is available in the dashboard. Deliveries from sandbox requests are marked is_test=true.

In sandbox the delivery.success / delivery.failed webhooks are dispatched with a ~2 second delay — this imitates real asynchrony (live webhooks arrive after the API response).

Manually triggering events

POST /v1/test/simulate-event is available only with the test key zn_test_. The endpoint actually delivers an arbitrary event to the project webhook endpoints with an HMAC signature and the is_test=true mark — handy to debug a handler for any event without reproducing the scenario. Body: event (a name from the known events list) and data (an arbitrary JSON object). An unknown event or non-object data → 400. If the project has no webhook endpoints, the response is dispatched: true, endpoints: 0.

POST /v1/test/simulate-event
Authorization: Bearer zn_test_...
{
"event": "subscription.created",
"data": { "subscriber_id": "550e8400-...", "channel": "telegram" }
}
→ {
"dispatched": true,
"event": "subscription.created",
"endpoints": 1
}

Test key for an existing project

Projects created before sandbox have no test key. Generate one with the button in the dashboard: project settings → «Test key» block. The same is done by POST /api/projects/{id}/test-key — it creates the key or re-issues an existing one (calling again rotates the key). The response contains test_api_key — it is shown once, save it right away.

POST /api/projects/{id}/test-key
→ { "test_api_key": "zn_test_..." }

Related sections