telega/webhook_reply

Webhook reply optimization: answer an update by embedding a bot API call into the webhook HTTP response body instead of making a separate HTTP request to Telegram.

Telegram allows the webhook HTTP response to carry one API call as JSON ({"method": "sendMessage", ...}) — it is executed as if it were a normal request, saving one HTTP round-trip and not counting against the request quota. This module implements the claim protocol between a handler’s API call and the webhook HTTP process that is still holding the connection.

⚠️ Telegram does not return the result of an embedded call. A claimed call resolves to a synthetic stub: True for boolean methods, and for sendMessage a fake Message with message_id: -1 and date: 0. If your handler needs the real message_id, do not use webhook reply for that call.

This is opt-in: adapters expose it as handle_bot_with_reply, built on telega.handle_update_webhook. Regular handle_bot behavior is unchanged.

Usage

import telega_wisp

fn handle_request(bot, req) {
  use <- telega_wisp.handle_bot_with_reply(bot, req, timeout: 5000)
  // other routes...
}

telega_mist.handle_bot_with_reply works the same way. For a custom adapter, call telega.handle_update_webhook(telega:, update:, timeout:) and map the result yourself:

case telega.handle_update_webhook(telega:, update:, timeout: 5000) {
  // Answer 200 with this JSON body, Content-Type: application/json.
  telega.JsonResponse(body:) -> todo
  // Answer an empty 200.
  telega.EmptyResponse -> todo
}

With handle_bot_with_reply, a bot answering a callback button makes zero outgoing HTTP calls for that update.

Eligible methods

Only the first eligible POST call per update is claimed:

Everything else — GET requests included — always goes over HTTP. If a handler needs the real message_id (to edit or reply to the message later), that call must go over regular HTTP — do not use webhook reply for it.

Latency semantics

Unlike handle_bot, the request process waits — up to timeout ms — for the handler to either claim a call or finish. Telegram serializes updates per webhook connection slot, so pick a timeout well below Telegram’s own webhook timeout; 5000 ms is a sensible default. When the timeout fires the adapter answers an empty 200 OK, the handler keeps running in the background, and all of its API calls go over regular HTTP.

Claim protocol

The claimed call resolves inside your handler immediately on grant — the handler does not wait for Telegram to execute it.

Testing

Use a routed mock and assert on the response body — see test/webhook_reply_test.gleam for full examples:

let assert telega.JsonResponse(body:) =
  telega.handle_update_webhook(telega: bot, update: raw, timeout: 5000)
string.contains(body, "\"method\":\"answerCallbackQuery\"")

Types

Per-update channel owned by the webhook HTTP process.

pub type Envelope =
  process.Subject(EnvelopeMessage)

Message sent by a handler’s API call to the webhook HTTP process holding the connection. The sender waits on granted; only a still-waiting HTTP process replies True. If it already responded or timed out, the wait expires and the call goes over regular HTTP.

pub type EnvelopeMessage {
  Claim(reply: WebhookReply, granted: process.Subject(Bool))
}

Constructors

An API call claimed for delivery in the webhook HTTP response body.

pub type WebhookReply {
  WebhookReply(method: String, params_json: String)
}

Constructors

  • WebhookReply(method: String, params_json: String)

Values

pub fn to_response_body(reply reply: WebhookReply) -> String

Render a granted claim as the webhook HTTP response body: the request params with "method" spliced in front.

pub fn transformer(
  envelope envelope: process.Subject(EnvelopeMessage),
) -> fn(
  client.TelegramApiRequest,
  fn(client.TelegramApiRequest) -> Result(
    response.Response(String),
    error.TelegaError,
  ),
) -> Result(response.Response(String), error.TelegaError)

Build the API-call transformer that implements the handler side of the claim protocol for one update. Installed automatically by the chat instance when the update was dispatched with an envelope — not meant to be added manually.

pub fn without_claim(work work: fn() -> a) -> a

Run work with webhook-reply claiming disabled for the calling process. Use around API calls whose real result you need — a claimed call would resolve to a synthetic stub instead (see the module doc).

Search Document