# Telegram transports

> Long polling and webhook mode compared, with Gateway routes and durable ingress behavior

- 网址：https://funcoding.ai/agents/openclaw/channels/telegram/transports/
- 来源：OpenClaw 官方文档原文（英文），MIT 许可，同步于 2026-10-11
- 官方原文：https://docs.openclaw.ai/zh-CN/channels/telegram/transports

---
Long polling is the default. Webhook mode is the alternative when an HTTPS ingress is available.

## Long polling and webhooks

<details>
<summary>Long polling vs webhook</summary>

Default is long polling. For webhook mode, set `channels.telegram.webhookUrl` and `channels.telegram.webhookSecret`; optional `webhookPath` (default `/telegram-webhook`) and `webhookCertPath` (self-signed cert PEM for direct-IP or no-domain setups).

The Gateway reserves `/health`, `/healthz`, `/ready`, `/readyz`, `/startup`, and `/startupz` for checks, including query variants. The `/api/channels` namespace also requires Gateway authentication, including encoded forms, and cannot receive direct Telegram callbacks. Choose `/telegram-webhook` or another route for Gateway ingress. The exact `/healthz` path remains reserved for the legacy listener's health check and cannot be a Telegram webhook path. Other Gateway-reserved paths can continue receiving callbacks through an explicit `legacyWebhook` endpoint while you update `webhookPath`, `webhookUrl`, and the reverse proxy mapping. Without an explicit endpoint, a reserved path produces an actionable startup error. Verify that [hot reload](https://funcoding.ai/agents/openclaw/gateway/configuration/hot-reload/) applied the route change with `openclaw channels status --probe`.

In long-polling mode, OpenClaw saves its restart position after an update is committed to the durable ingress queue. A failed handler remains retryable from that queue.

Webhook routes are available on the Gateway HTTP port (default `18789`). Point public HTTPS ingress at that port and `webhookPath`. OpenClaw registers the configured `webhookUrl` unchanged on every webhook startup, including after a restart. A configured `gateway.publicOrigin` does not replace an operator-managed callback or reverse-proxy mapping. Telegram's secret header remains the authentication boundary, so this route does not require a Gateway bearer token.

New installs open no legacy forwarding port. During an upgrade, Doctor preserves an existing implicit `127.0.0.1:8787` endpoint as an explicit `legacyWebhook` pin unless `webhookUrl` matches the configured `gateway.publicOrigin` plus that account's usable `webhookPath`. The comparison includes the path and query; OpenClaw cannot infer a Gateway destination from an arbitrary existing callback URL. To retire a pin, expose a usable Gateway webhook route, set `webhookUrl` to its public URL, and remove `legacyWebhook`; OpenClaw registers that URL before releasing the old listener. Alternatively, move the existing callback's reverse-proxy upstream to the Gateway port, verify incoming messages, and set `legacyWebhook: false` to preserve its public URL. Deleting the setting never restores an implicit listener.

During an in-process account reload, the Gateway keeps the previous listener retryable until Telegram accepts `setWebhook` for the replacement URL. Receiving an update does not complete that cutover. A full Gateway restart closes the old process's sockets; Telegram retries pending deliveries while the replacement starts and registers its route.

Doctor saves the pin and its `meta.migrations.webhookListeners` completion record together. Keep that record when removing the pin so later Doctor runs and updates leave it removed. See [webhook migrations](https://funcoding.ai/agents/openclaw/gateway/doctor/config-migrations/#channel-webhook-listeners) for included and read-only config sources.

The legacy port preserves its unauthenticated `/healthz` response (`200`, plain `ok`) and account-local failed-secret rate limit. Health matching is exact: query strings, trailing slashes, case changes, and encoded variants are not health checks. HEAD returns the same status without a body. The canonical Gateway port keeps its own check and response-header behavior.

After upgrading, run `openclaw doctor --fix`. Doctor backs up the config and migrates old `webhookPort` and `webhookHost` settings to `legacyWebhook: { port, host }`, preserving a host-only setting with port `8787`. An explicit endpoint object overrides the default; an omitted object host uses `127.0.0.1`. Existing `legacyWebhook: false` settings remain disabled during migration.

Named accounts inherit the channel's `legacyWebhook` setting. An account-level `false` disables that account's legacy endpoint even when the channel config specifies an endpoint. An explicit account endpoint overrides an inherited `false`. A shared legacy socket stays open while another account still uses that endpoint. Both ports use the same Gateway route handler and Telegram secret verification.

Separately installed Telegram plugins require OpenClaw 2026.9.8 or newer, whose Doctor can preserve omitted endpoints before the new runtime starts. Upgrade OpenClaw before manually replacing the plugin. Older hosts reject the incompatible package and retain their current plugin. Gateway owns every explicitly configured forwarding listener; Telegram no longer opens a separate account-owned server.

Accounts may share a Gateway route when their webhook secrets differ. Requests matching more than one account are rejected; assign distinct secrets or paths before moving traffic to the Gateway port. Migrated legacy endpoints preserve account selection for accounts that previously shared a secret and path on separate explicit ports.

Webhook mode validates request guards, the Telegram secret token, and the JSON body, then commits the update to its durable ingress queue before returning an empty `200`. Successful durable adoption includes `x-openclaw-delivery-accepted: durable`; health, routing, authentication, validation, and storage-error responses omit this header. Reverse proxies and host controllers can require the header to distinguish OpenClaw adoption from a generic empty `200` without inferring acceptance from response timing.

After the durable write, OpenClaw claims and processes updates through the core channel-ingress drain (per-chat/per-topic lanes, complete at turn adoption, pre-adoption stall timeout). Slow agent turns do not hold Telegram's delivery ACK.

</details>

## Ingress acknowledgment boundary

Telegram acknowledgment is gated by durable queue admission, not by plugin
hooks or completion of an agent turn. Webhook mode uses the boundary described
above; long polling uses this sequence:

1. The polling worker receives one Telegram update and waits.
2. OpenClaw transactionally enqueues the raw update in the account-scoped
   `channel_ingress_events` queue in `state/openclaw.sqlite`.
3. After enqueue succeeds, OpenClaw schedules persistence of the restart offset
   and acknowledges the worker, allowing polling to continue.
4. The shared drain processes the queued update separately. If admission fails,
   OpenClaw rejects the worker acknowledgment instead of silently advancing.

`offset queued` means the restart-offset write was scheduled, not committed. A
crash before that write finishes can make Telegram redeliver an update that is
already queued. The queue rejects the same transport event ID only while its
pending row, completed tombstone, or failed row remains. This is bounded replay
deduplication, not exactly-once processing. See
[durable ingress and replay dedupe](https://funcoding.ai/agents/openclaw/plugins/sdk-channel-plugins/durable-ingress/#durable-ingress-and-replay-dedupe).

### Replay limits

Both transports record the account's bot identity before accepting updates, even
without a polling offset. Replacing a known bot clears its old ingress rows before
the replacement starts. Same-bot restarts and token rotations retain queued work
and replay protection, as do legacy queues without a known previous identity.
If identity preparation fails, account startup stops with an error asking you to
restart the account; an interrupted reset retains the previous identity for retry.

For each Telegram account queue, completed tombstones and failed rows are
retained for up to 30 days and capped at 1,000 entries per class. Whichever
limit is reached first ends retention for that class. Completion scrubs the
inbound payload and metadata while retaining the event identity.

A crash after a side effect but before queue completion can repeat that side
effect. See [transport retention](https://funcoding.ai/agents/openclaw/plugins/sdk-channel-plugins/durable-ingress/#transport-classes-and-retention),
[at-least-once side effects](https://funcoding.ai/agents/openclaw/plugins/sdk-channel-plugins/durable-ingress/#at-least-once-side-effects),
and [inbound dead letters](https://funcoding.ai/agents/openclaw/cli/channels/#inbound-dead-letters).

The documented durability boundary is successful completion of the SQLite
transaction, not a separate per-event fsync guarantee. Use a persistent state
directory; deleting it loses both queued updates and the saved polling offset.

### Shutdown

Long polling and webhook accounts wait for already-admitted replay commits or
rollbacks even after their 15-second ingress shutdown grace expires. Accepted
group introductions remain tracked through their existing 60-second agent-turn
budget and the following dedupe commit before account shutdown completes.
Cancellation before an introduction is accepted prevents it from starting.
Ordinary handler and bot shutdown grace periods remain unchanged.

### Plugin hooks

No plugin hook can defer Telegram's transport acknowledgment until plugin-owned
persistence completes:

- `message_received` is a fire-and-forget observation of an accepted inbound turn.
- `before_dispatch` is a conditional claim before normal model dispatch.
- `before_agent_run` is a gate immediately before model submission and runs only
  when a model turn reaches that stage.

None receives the raw update as a persistence boundary. See
[message and delivery hooks](https://funcoding.ai/agents/openclaw/plugins/hooks/messages/) and the
[hook catalog](https://funcoding.ai/agents/openclaw/plugins/hooks/reference/#hook-catalog).
