Skip to main content

Errors worth branching on

These six failures cover the message send (POST /api/v1/messages/telegram) and bot onboarding, hammered by every auto-response in the chat flow. Each block below is a full { error, meta } envelope as the API returns it, and the matrix at the bottom answers retry vs surface for the same six classes. For the platform-wide decision table these branches plug into, see the error handling guide.

401 — Unauthorized

A 401 on this page is the bearer key failing before the route ran — it never means the resource is wrong. Rotate the key or re-mint the scoped token; retrying the same request changes nothing.

403 — Forbidden

A 403 means the key authenticated but the operation is gated by scope — check the key’s scopes on the developer page; a 403 is never a data-not-found shape.

422 — Schema

A 422 means the payload did not match the request schema — branch on error.details.field and resend with the numeric chat id or the tenant-registered channel slug instead of blind-retrying the same body.

429 — Rate

The Retry-After header and error.details.retry_after are both set on every 429 — resend the SAME request after the lower of the two.

402 — Feature gate

Surface token renewal — resend only works after the re-auth webhook, not on replay.

409 — Bot already claimed

A 409 fires on POST /api/v1/telegram/connect-bot when the BotFather token you pasted resolves to a bot another organization already claimed. Disconnect the bot on the other organization first; for a token leak, revoke the token in BotFather and follow the ownership-disputes recovery flow. Retrying the same connect just re-raises the 409.

60-second retry matrix

Worked request and response samples

The scanner flagged this page because the worked sections below shipped as bare <CodeGroup> blocks — no <RequestExample> wrappers for the samples index to count. Each it registers — send and state-read — now carries RequestExample over a six-language CodeGroup (cURL, Node SDK escape hatch, Python, Go, Ruby, PHP), paired with a ResponseExample success envelope. The errors matrix above is unchanged; the envelope it branches on is rendered here.

Send a message — POST /api/v1/messages/telegram

The copy-paste pair for the production send path. POST /api/v1/messages/telegram delivers to any numeric chat id or @channel slug your connected bot can reach — the recipient goes in to, the message text in body (both required) — unlike the onboarding POST /api/v1/telegram/test/send operation the generated block below documents (onboarding-only, no recipient parameter; see “Test send is onboarding-only” under this section).
A 202 means accepted, not delivered — the 200-response walkthrough of the onboarding test/send sample below explains WHEN the mode-aware test operation matters; production traffic uses the pair above.

Read the bot state — GET /api/v1/telegram/channel-state

The 404 evicted-id and 402 revoked-bot envelopes the errors section carries above map to this same read/send pair — TELEGRAM_BOT_TOKEN_REVOKED (402) is the state-read branch, TELEGRAM_CHANNEL_NOT_CONNECTED (409) is what the send raises when channel-state reports not_connected. Keep the lifecycle samples below ordered the same way.

Worked Telegram samples

The generated operations below carry the raw request/response schemas. This section walks the exact calls the dashboard and your integration make when they run the Telegram send/link flow: check channel state, send a message on the right mode, connect or disconnect your own bot, and watch the rollout cost. Each sample shows a curl request, a TypeScript fetch, and the 200 envelope the API returns. Four-hundred-series failures on these calls map to the branch list in the errors section above — resending is application-level when your handler replays a message (e.g. a chabot auto-response), so keep retries limited to 429 here.

1. Check channel state

GET /api/v1/telegram/channel-state is the call you make first, every time — the response tells you which mode the next four samples run in (not_connected → verifying → test_active → production). Bot details (username, status, connected_at) are only present in production mode; test counters only on test states.
A production envelope (the BYO bot block is the whole state):
200
A test_active envelope drops the bot block and surfaces the verified chat + counters that gate /test/send:
200
A verifying mid-flow state carries the single-use linking token (verify_token) the FE renders as a deep link / QR — the same token POST /test/start-verify mints below.

2. Send a test message

POST /api/v1/telegram/test/send fires one message on the right delivery path for the mode: in production it goes out your own bot to the chat you verified in step 1; in test_active it goes out the shared platform bot and bumps the daily cap. The 409 TELEGRAM_CHANNEL_NOT_CONNECTED from sample 1 (before a bot is connected) is the guard the operation itself raises.
The successful envelope mirrors the shared sent / message_id / mode shape the send-schemas give:
200

3. Connect and disconnect your own bot

Bring-your-own-bot onboarding is two owner/admin/developer-gated calls: POST /api/v1/telegram/connect-bot accepts the BotFather token and mints the webhook; POST /api/v1/telegram/disconnect-bot revokes the webhook upstream and soft-deletes the row. The connected mode locks this page to production.
Successful connect envelope (the webhook URL is returned once — store it):
200
Disconnect:

4. Watch the rollout cost

GET /api/v1/telegram/analytics/costs?days=30 (default window, clamp 1–90) returns the per-tenant Telegram cost rollup for the dashboard donut, so test-mode/street-traffic reads stay inside the same envelope discipline.
For webhook-delivered events (inbound MO replies, DLR), register your endpoint on the webhooks page and verify X-Orbit-Signature before trusting the body — the full per-language verification handler is at Verify webhook signatures.

Lifecycle send-receive-analytics samples

The four samples above exercise the onboarding flow. These five cover the day-two lifecycle — the calls your integration makes once the bot is in production mode: send, receive, measure, and monitor.

1. Send a text message

POST /api/v1/messages/telegram accepts a message for delivery through the connected bot. The recipient to is the chat id you verified during onboarding (a numeric id, or the @channel slug for channels). The API enqueues the send and returns the Devotel message id, so your handler can track the DLR without holding the HTTP window open.
202
A 202 here means the message is accepted, not yet delivered — the lifecycle flips to delivered (or failed with an error code) on the DLR webhook. If the connection is not in production mode the same call raises the 409 TELEGRAM_CHANNEL_NOT_CONNECTED above, so keep the launch gate on sample 4 (bot profile reads).

2. Send rich content with inline buttons

The same endpoint carries one media attachment per message via media_url. Inline-keyboard buttons and captions travel in metadata — keep button capacity to two rows and let the captions come through the text surface, which is how Telegram renders rich messages in DM chats:
The media URL must be publicly fetchable by the Bot API — signed CDN URLs need to stay valid for at least the delivery window (status stays queued until the Bot API acknowledges the attachment). The same shape works for video/… and document/… attachments — swap the media type in the URL, keep caption under 1024 characters.

3. Inbound webhook handler — /start makes it a conversation

Customer replies arrive on the HTTPS endpoint you register on the webhooks page. The handler below verifies the X-Orbit-Signature HMAC before answering a /start — the pattern every inbound surface repeats:
Return 200 promptly — the webhook surface re-delivers with backoff on non-2xx, so the reply send belongs behind an async queue the moment the handler grows past a trivial auto-answer. The same signature pattern is written out per SDK language at Verify webhook signatures.

4. Read channel analytics

GET /api/v1/analytics/telegram is the Telegram-only KPI surface the generic per-channel rollup can’t express — the Bot API has no carrier DLR, so the funnel counters you care about are outbound volume, the bot-blocked share (users who blocked the bot or closed the chat), custom-emoji usage, and the group-vs-DM split:
200
Branch on bot_blocked_rate — when it climbs past your baseline the bot is being blocked by users, and the fix is content-side (fewer messages, deeper opt-out), not transport-side. The response is cached for 60s, so dashboards can poll it per page-load without per-user cost.

5. Bot profile reads — health before the first send

The connect call returns the bot profile once; GET /api/v1/telegram/channel-state re-reads it on demand, which is the right pre-send check for any automated sender (an “is the bot still connected and active?” gate that catches a revoked token before the first send attempt of the run).
200
Chat-level reads are analytics-history reads, not bot-state reads — after a bot connects, the contacts your messages create sync into the Contacts surface via the inbound webhook; browse recent chats from the Contacts API rather than polling Telegram directly. The production-only bot block above is the single source of truth for connection health on this page.

Test send is onboarding-only

POST /api/v1/telegram/test/send (the “Send Telegram test message” operation generated below) is an onboarding-only endpoint: it delivers exactly one message to the chat you verified during onboarding and takes no recipient or chat_id parameter — the only required body field is text (1–1024 characters). For production delivery to any chat id or @channel slug your bot can reach, use POST /api/v1/messages/telegram — worked sample in “Send a message” at the top of this page. The success envelope the send returns (the same shape the worked send section shows):
200
A successful POST /api/v1/telegram/connect-bot (required body bot_token, the BotFather token) returns the connected-bot profile along with the webhook URL — returned once at connect time, and re-readable any time from GET /api/v1/telegram/channel-state:
200
And the GET /api/v1/telegram/analytics/costs rollup that feeds the dashboard donut (per-tenant cost estimate; Telegram currently bills at $0):
200