> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Telegram

> Send and receive Telegram bot messages via the Orbit Messaging API

# Telegram

Orbit's Telegram channel wraps the Telegram Bot API so you can send and receive messages, photos, documents, inline keyboards, and callbacks from the same unified Messaging surface used for every other channel. Each Orbit organization can attach one bot; webhook delivery is automatic.

## Send a message

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/telegram \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "<chat_id>",
    "from": "<your_bot_username>",
    "body": "Welcome! Tap a button below to continue.",
    "metadata": {
      "type": "text",
      "inline_keyboard": "[[{\"text\":\"Order status\",\"callback_data\":\"order_status\"},{\"text\":\"Talk to an agent\",\"callback_data\":\"human_handoff\"}]]"
    }
  }'
```

The message text goes in the top-level `body` field. Per-message options — the
send `type` (`text` (default), `photo`, `video`, `document`, `location`),
`parse_mode`, and keyboards — are passed under `metadata`, whose values are all
strings. Supply `inline_keyboard` (and `reply_keyboard`) as a JSON-stringified
`button[][]` array, exactly as shown above.

### Response

```json theme={null}
{
  "data": {
    "id": "msg_tg_abc123",
    "status": "sent",
    "channel": "telegram"
  },
  "meta": {
    "request_id": "req_xyz789",
    "timestamp": "2026-05-09T00:00:00Z"
  }
}
```

## Capabilities

* **Text + media** — text (with HTML / MarkdownV2 formatting), photo, video, document, and location. Set the send `type` in `metadata`; a photo, video, or document send needs `mediaUrl`, and a location send needs `latitude` / `longitude`.
* **Inline + reply keyboards** — `inline_keyboard`, `reply_keyboard`, plus `force_reply` and `remove_keyboard`.
* **Callbacks** — inline-keyboard button presses arrive as Telegram `callback_query` updates. Orbit ingests each press as an inbound message (the button's `callback_data` becomes the message `body`), so it lands in the conversation timeline and fans out on the standard `message.received` webhook — the same event as any other inbound Telegram message. There is no separate `telegram.callback` event type to subscribe to.
* **Chat actions** — `typing`, `upload_photo`, etc., either via the API or auto-emitted by Orbit when a long-running send is in flight.
* **Edit + delete (Orbit-side only)** — `PATCH /messages/:id` edits the `body` and/or `scheduled_at` of a message that is still **scheduled** (not yet dispatched); once the message has been sent the edit returns `409 MESSAGE_NOT_EDITABLE`. `DELETE /messages/:id` soft-deletes the Orbit message record and syncs that removal across your dashboards. Neither endpoint calls Telegram's `editMessageText` / `deleteMessage`, so a message already delivered to the chat is not edited or removed in Telegram itself.
* **Bot commands + deep links** — registered via the bot setup wizard; `/start` deep-links land on your conversation routing rules.
* **Webhook security** — Telegram's `secret_token` header is validated against the per-bot secret stored in Orbit's encrypted credential vault.

## Onboarding flow

1. Talk to [@BotFather](https://t.me/BotFather) and create a bot. Save the token it returns.
2. From the dashboard, navigate to **Channels → Telegram → Add bot** and paste the token.
3. Orbit auto-registers the webhook URL `https://api.orbit.devotel.io/api/v1/webhooks/inbound/telegram` and rotates the `secret_token` header value.
4. Optional: configure the bot command list directly from the channel detail page.

### Token handling — your responsibility

Telegram bot tokens are bearer credentials. Anyone who has the token can read every message sent to your bot, send messages on its behalf, and — until the token is revoked via BotFather — pose as your bot to your users. Orbit encrypts the token at rest with an `enc:v1:` envelope and never echoes it back through any API response, but the token's safety upstream of Orbit (in your password manager, your laptop, your CI/CD variables, your screenshots) is yours to protect:

* **Never commit a bot token to git**, public or private. GitHub and GitLab both surface tokens to search; bots have scraped them in seconds.
* **Never paste a token into a chat, ticket, screenshot, or screen-share**. Use the dashboard paste field, which masks the value after submit.
* **Rotate immediately** via `/revoke` in BotFather if you believe a token has been exposed. Old tokens stop working the moment a new one is issued; reconnect in Orbit with the new value and the existing conversation history is preserved (we key history by `bot_id`, not by the token).
* **Scope each environment to its own bot.** Use distinct BotFather bots for dev / staging / production so a leaked dev token doesn't compromise customer conversations.

## Ownership disputes

Orbit identifies a bot by its `bot_id` (the numeric prefix of the token) and enforces a single-tenant claim: once an Orbit organization connects a bot, the same bot cannot be connected to a second Orbit organization until the first one disconnects. The dashboard surfaces `TELEGRAM_BOT_ALREADY_CONNECTED` (HTTP 409) when a second tenant attempts the same bot.

Connecting a bot to Orbit proves you have the bot's token. It does NOT prove you are the BotFather owner of the bot. If a token leaks (see "Token handling" above) and a hostile actor claims it on their own Orbit tenant before you do, you will see a 409 on your own connect attempt — your bot is locked to someone else's Orbit organization. Two recovery paths, in order of preference:

1. **Revoke the token via BotFather (you, as the bot's BotFather owner).** Open [@BotFather](https://t.me/BotFather), select the bot, and run `/revoke`. BotFather issues a new token; the leaked token immediately stops working. The hostile tenant's webhook registration is now dead — Telegram refuses every send / receive against the old token. Once the hostile claim is functionally inert, contact Orbit support (see step 2) to release the `bot_id` claim so you can connect with the new token.

2. **Open an ownership-dispute ticket with Orbit support.** Email [support@devotel.io](mailto:support@devotel.io) with subject `Telegram bot ownership dispute — <bot_username>` and include:

   * The bot's `@username` and numeric `bot_id` (the digits before the colon in any token you've held).
   * Proof of BotFather ownership — a screenshot of the `/mybots` list in BotFather showing the bot under your Telegram account, OR a fresh token rotation timestamp from BotFather that we can correlate against Telegram's `getMe` response on the live token.
   * The Orbit organization you want the bot claim transferred to.

   Support will verify your BotFather ownership against Telegram's API, force-disconnect the hostile tenant's row (revoking their webhook registration in the process), and unblock the `bot_id` so you can connect from your own Orbit organization. Median response time is one business day; security-sensitive disputes are escalated to on-call.

**To prevent this class of incident**: keep bot tokens out of every code path that touches the public internet, rotate on any whiff of exposure, and connect the bot to Orbit yourself the moment BotFather hands you a fresh token — the small window between issuance and Orbit-claim is the entire attack surface.

## Common errors

A failed send returns a JSON error envelope whose `error.code` is one of the
values below.

| Code                     | HTTP                                         | Cause                                                                                                                                                                                                                             | Fix                                                                                                                                                        |
| ------------------------ | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INVALID_RECIPIENT`      | 422                                          | The `to` chat id is missing or empty.                                                                                                                                                                                             | Send a non-empty chat id — a numeric id for a user or group, or `@channelname` for a channel.                                                              |
| `MISSING_REQUIRED_FIELD` | 422                                          | A `photo`, `video`, or `document` send was missing `mediaUrl`, or a `location` send was missing `latitude` / `longitude` in `metadata`.                                                                                           | Supply the field the send `type` requires.                                                                                                                 |
| `RATE_LIMIT_EXCEEDED`    | 429                                          | Telegram's flood-control rejected the send (too many requests in a short window).                                                                                                                                                 | Orbit already waits the `retry_after` Telegram returns before retrying. Once the budget is exhausted, back off and re-send after the window.               |
| `MESSAGE_SEND_FAILED`    | 502, or the Telegram status when it is a 4xx | Telegram rejected the send for any other reason — a revoked or invalid bot token, a malformed payload, or an upstream 5xx. The original Telegram status (for example 401 for a revoked token) is passed through; 5xx maps to 502. | For a 401, issue a new token via `/revoke` in BotFather and update credentials in the dashboard. For a 4xx, check the payload. A 502 is transient — retry. |

### Blocked recipients

When a user blocks the bot, leaves a chat, deactivates their account, or the
chat id is gone, Telegram returns a 403. Orbit does **not** raise an error for
this — the message record is marked `failed` with `error_code` set to
`TELEGRAM_USER_BLOCKED`. Treat that recipient as undeliverable: mark the contact
opted-out and skip future sends rather than retrying, because the condition is
permanent until the user re-engages the bot.

## Pricing

The Telegram Bot API itself is free. Orbit charges a flat per-1M platform fee for delivery and webhook fan-in. See the [pricing page](https://orbit.devotel.io/pricing).
