Zapnoty — OTP — confirmation codes

API Documentation

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

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

ttl_minutes integer

Code TTL in minutes (1–30, default 5)

max_attempts integer

Max verification attempts (1–10, default 3)

code_length integer

Code length: 4, 6 or 8 digits. Default — project setting (6)

purpose string

Code purpose (login/payment/signature etc., up to 32 chars). Multiple active OTPs of different purposes coexist

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

purpose string

Purpose — verifies the code of exactly this purpose (must match the one passed to /send)

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

Response

✓ Code valid

{
"verified": true
}

✗ Code invalid

{
"error": "Неверный код",
"code": "bad_request",
"retryable": false
}

Invalid code → HTTP 400 with body {error, code, retryable}. If no active code exists or attempts are exhausted → HTTP 404. There is no {verified: false} response — success is always HTTP 200 {verified: true}.

OTP limits: 3 verification attempts by default (range 1–10), 5-minute code TTL, 1 active code per (subscriber, purpose) pair.

Resending a code

Each new /v1/otp/send invalidates the subscriber's previous active code — only the latest one is valid. For a 'Resend' button on your side use a 30-second debounce to avoid spam. Limit: 3 codes per hour per subscriber (429 on exceed).

Invalidation is scoped per purpose: a purpose=login code does not cancel a purpose=payment code — the user can have both active at once.

OTP webhook events

Subscribe a webhook to these events for conversion analytics and security alerts.

otp.sent — code successfully delivered to the subscriber

otp.verified — code verified correctly

otp.failed_attempt — wrong code entered, attempts still remain

otp.max_attempts_reached — wrong code and all attempts exhausted — a possible brute-force signal

POST /v1/otp/request — OTP for non-subscribers

If the user isn't subscribed to the project yet, a plain /v1/otp/send returns 404. This endpoint creates a bot deeplink where the user consents to subscribe and get the code in one tap.

  1. User clicks «Get code» on your site → you call /v1/otp/request and get request_id + deeplinks.
  2. Open the right deeplink for the user (Telegram or Max). The bot shows project name + description and asks to subscribe and receive the code.
  3. Poll GET /v1/otp/request/{request_id}/status until status=sent — the response also contains subscriber_id.
  4. User enters the code on your site → you make a regular POST /v1/otp/verify with the obtained subscriber_id + code.

Create request

channel string

Preferred channel (telegram or max). If omitted, both deeplinks are returned.

ttl_minutes integer

Code TTL in minutes, 1-30 (default 5).

max_attempts integer

Max code entry attempts, 1-10 (default 3).

POST /v1/otp/request
 
{
"channel": "telegram" // optional
}

Response

{
"request_id": "Kq9xV3mN2pT7rL8s",
"telegram_link": "https://t.me/zapnotybot?start=otp_Kq9xV3mN2pT7rL8s",
"max_link": "https://max.ru/zapnotybot?start=otp_Kq9xV3mN2pT7rL8s",
"expires_in": 900
}

Check status

Poll every 1-2s. Statuses: pending (waiting for user), sent (subscribed + code delivered), cancelled (user tapped Cancel), expired (link expired after 15 min).

GET /v1/otp/request/{request_id}/status

Response

{
"status": "sent", // pending | sent | cancelled | expired
"subscriber_id": "550e8400-e29b-...",
"channel": "telegram"
}

request_id lives for 15 min. If the user is already subscribed, the bot skips the consent screen and sends the code immediately. The classic /v1/otp/send is unchanged and faster when subscriber_id is already known.

Related sections