跳到正文
FunCoding

搜索

搜索文档、文章、Skill 和 MCP

Telegram transports

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

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

Long polling and webhooks

Long polling vs webhook

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 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 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.

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.

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, at-least-once side effects, and 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 and the hook catalog.