> ## 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 onboarding: shared test bot to your own bot

> Walk the Telegram channel end to end — send real messages today through Orbit's shared test bot, then move to your own BotFather bot via paste-token onboarding, with webhook security, edit/delete limits, and lifecycle handled.

# Telegram Onboarding: Shared Test Bot to Your Own Bot

This guide walks you from an empty Telegram channel to a working, production-ready setup. It complements the [Telegram channel page](/channels/telegram), which covers message shapes and capability detail — this guide is the ordered path you follow the first time, and re-run whenever you attach, rotate, or retire a bot.

Telegram has no approval cycle and no brand registration. The only real gate is that a production bot is a BotFather bot that you own, and Orbit can only connect it via the token BotFather hands you. Until you have a token, the built-in **test account** lets you send real messages through Orbit's shared bot so you can validate your integration today.

## 1. The test-account loop — send real messages today

The test account routes through Devotel's shared, already-connected bot. You never touch BotFather to use it. Three steps:

### Step 1: Start verification

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/telegram/test/start-verify \
  -H "X-API-Key: dv_live_sk_..."
```

The response carries a linking token, a deep link (`https://t.me/<shared_bot>?start=<token>`), and a QR-code value. Open the deep link (or scan the QR code) in Telegram on your device — the linked chat with the shared bot is what makes the channel `test_active`.

### Step 2: Send a test message through the shared bot

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/telegram/test/send \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "<linked_chat_id>",
    "body": "Test loop is live."
  }'
```

While the channel is `test_active`, the send goes out under the shared bot. Once you bring your own bot and the channel enters production mode, the same endpoint routes through your bot instead, so your integration code does not change between test and production.

Check where you are at any point with `GET /api/v1/telegram/channel-state` — it returns the channel mode plus the verified-chat list when test-linked.

### Step 3: Deliver demo flows while still test-scope

Test scope is enough to prove a group-keyword flow end to end: message a group that your shared-bot link is in, confirm the inbound `message.received` hits your webhook, then confirm your keyword router fires. The demo pack (`GET /api/v1/telegram/demo-content`) ships sample group-keyword and cost-rollup flows so you can validate rendering and routing before you commit a token.

To reset the loop — for example, to point a different tester device at it later — call `POST /api/v1/telegram/test/disconnect`. Verified-chat links clear; the daily test quota carries over.

The test account is for evaluation, not production: sends go out under the shared bot's branding, not yours, and the quota is deliberately small. Move to your own bot below for anything a customer touches.

## 2. Bring your own bot (paste-token onboarding)

The production path is a single call once BotFather hands you a token.

1. Talk to [@BotFather](https://t.me/BotFather) in Telegram and create a bot. Save the token it returns — treat it like a bearer credential (see [Token handling](/channels/telegram#token-handling) on the channel page).
2. Connect the token to Orbit:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/telegram/connect-bot \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "token": "<botfather_token>" }'
```

3. Orbit registers the webhook subscription immediately: the bot is pointed at `https://api.orbit.devotel.io/api/v1/webhooks/inbound/telegram`, and the header that Telegram must present (`secret_token`) is rotated and stored in Orbit's encrypted credential vault. Inbound delivery is live from that call.

One bot, one tenant: Orbit identifies a bot by its `bot_id` (the numeric prefix before the colon) and enforces a single-tenant claim. Once an Orbit organization connects a bot, the same `bot_id` cannot be connected to a second organization until the first disconnects — a second attempt returns `409 TELEGRAM_BOT_ALREADY_CONNECTED`. If that trips on you, see the [ownership disputes](/channels/telegram#ownership-disputes) recovery paths on the channel page.

Access: `/test/*`, `/connect-bot`, and `/disconnect-bot` are write-class operations — they require the `owner`, `admin`, or `developer` role, not just a valid session.

## 3. Send with metadata — types, keyboards, and formatting

Sending goes through the unified Messaging surface (`POST /api/v1/messages/telegram`). The text body is the top-level `body` field; everything else — the send `type`, `parse_mode`, and keyboards — lives under `metadata`, whose values are all strings.

Supported `type` values: `text` (default), `photo`, `video`, `document`, `location`. A `photo`, `video`, or `document` send needs `metadata.mediaUrl`; a `location` send needs `metadata.latitude` and `metadata.longitude`.

Supply `inline_keyboard` and `reply_keyboard` as **JSON-stringified `button[][]` arrays** — one string, an outer array of rows, each row an array of button objects. `parse_mode` accepts `HTML` or `MarkdownV2`.

A full send with a two-row inline keyboard:

```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": "Order #4471 shipped. Track it or talk to a human:",
    "metadata": {
      "type": "text",
      "parse_mode": "HTML",
      "inline_keyboard": "[[{\"text\":\"Track shipment\",\"callback_data\":\"track_4471\"}],[{\"text\":\"Talk to an agent\",\"callback_data\":\"human_handoff\"}]]"
    }
  }'
```

`force_reply` and `remove_keyboard` use the same stringified-array convention when you want a forced text reply or a keyboard removed.

## 4. Callback ingestion — buttons land as normal inbound messages

An inline-keyboard press arrives in Telegram as a `callback_query` update. Orbit ingests each press as an inbound message: the button's `callback_data` becomes the message `body`, and the event 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, so your one webhook handler covers both plain text and button presses.

## 5. Edit and delete are Orbit-side only

`PATCH /messages/:id` and `DELETE /messages/:id` operate on the Orbit message record — they never call Telegram's `editMessageText` or `deleteMessage`:

* `PATCH`: 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` — a delivered message is not re-editable.
* `DELETE`: soft-deletes the Orbit message record and syncs the removal across your dashboards.

If you claim the message is edited or removed in Telegram itself, that is wrong — Orbit does not attempt it. For a delivered message the right recovery is a follow-up correction sent to the chat.

## 6. Lifecycle — retire the bot

`POST /api/v1/telegram/disconnect-bot` soft-deletes the connected-bot row and revokes the webhook registration with Telegram (`deleteWebhook`). The conversation history survives — Orbit keys history by `bot_id`, not by the token — so if you rotate the token via BotFather and reconnect with the new value, your history is preserved.

## 7. Error reference

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 — numeric for a user or group, `@channelname` for a channel.                                                        |
| `MISSING_REQUIRED_FIELD`         | 422                                           | A `photo` / `video` / `document` send was missing `mediaUrl`, or a `location` send was missing `latitude` / `longitude` in `metadata`. | Supply the field the send `type` requires.                                                                                                   |
| `TELEGRAM_BOT_ALREADY_CONNECTED` | 409                                           | Another Orbit organization already holds the `bot_id` claim for this bot.                                                              | Disconnect the other org first, or resolve it as an [ownership dispute](/channels/telegram#ownership-disputes).                              |
| `MESSAGE_NOT_EDITABLE`           | 409                                           | You attempted to `PATCH` a message that is already sent.                                                                               | Only scheduled messages are editable — send a follow-up correction instead.                                                                  |
| `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 — a revoked or invalid bot token, a malformed payload, or an upstream 5xx.                                  | For a 401, issue a new token via `/revoke` in BotFather and reconnect. For a 4xx, check the payload. A 502 is transient — retry.             |

Webhook security depends on the same `secret_token` per bot that Orbit rotates on connect — inbound updates that do not present it are rejected, so a spoofed sender cannot inject traffic into your callbacks.

## 8. Group events via the `telegram-group-events` module

Community bots added to groups or supergroups produce a different event shape — member joins and leaves, message-delete floods, and group keyword flows land on the dedicated `telegram-group-events` webhook module rather than the plain inbound path. Keep your primary handler on `message.received` for the customer chat surface, and subscribe the group module only if your bot operates in groups — the high-churn event classes (member-count churn, message-delete storms) are classified and dropped there so standard routing stays clean.
