Docs

Integrations

Verify signatures and process incoming webhooks

Verify the raw body, prevent duplicate processing and put events in a durable queue before responding.

View Markdown

The receiver must verify requests before using event content. Separate HTTP receipt from slow work such as calling a CRM, replying to customers or updating spreadsheets.

Who can do this

The person managing the webhook receiver service handles this part. You need access to that service's secret configuration and deduplication store. The receiver does not need additional Telegram permissions just to verify signatures.

Before you start

Store the signing secret in the WEBHOOK_SECRET environment variable. The actual value has the form whsec_…. Do not put it in source code or logs.

Preserve the raw body. Do not parse JSON and stringify it again before verifying the signature: whitespace and byte order are part of the signed data.

Where to start

Open Settings → API documentation → Receive webhooks to view the receiver guide. The events section describes data structures; the receiver guide provides platform-specific examples.

The app has 9 recipes: Node.js Express, Hono for Node.js/Bun/Deno, Python FastAPI, Go, PHP, Cloudflare Worker, n8n, Make and Zapier.

Steps

  1. Read the raw body and the headers below.
  2. Check the timestamp.
  3. Verify the signature.
  4. Write the event to a durable queue with deduplication.
  5. Return a 2xx response.
  6. Process the business logic in the background.
Header Meaning
webhook-id Delivery ID; unchanged when retrying or redelivering that delivery
webhook-timestamp Signing time in Unix seconds
webhook-signature One or more signatures in the form v1,<base64>

Remove the whsec_ prefix, then base64-decode the secret. Calculate:

base64(HMAC-SHA256(secret_bytes, webhook-id + "." + webhook-timestamp + "." + raw_body))

Accept the request if one valid signature matches, using a constant-time comparison. The default tolerance is 300 seconds, or 5 minutes, in either direction. During key rotation, the receiver can accept a signature from any key that is still valid.

Node.js

This example uses Express and the standardwebhooks library. inbox is an adapter you provide that writes durably and enforces a unique constraint on the delivery ID.

import express from "express";
import { Webhook } from "standardwebhooks";

const app = express();
const verifier = new Webhook(process.env.WEBHOOK_SECRET);

app.post("/telegram-events", express.raw({ type: "*/*" }), async (req, res) => {
  let event;
  try {
    event = verifier.verify(req.body.toString("utf8"), req.headers);
  } catch {
    return res.sendStatus(401);
  }

  try {
    await inbox.acceptOnce({
      deliveryId: req.get("webhook-id"),
      eventId: event.id,
      event,
    });
    return res.sendStatus(204);
  } catch {
    return res.sendStatus(503);
  }
});

Python

This example uses FastAPI and the standardwebhooks library. The inbox adapter has the same responsibilities as in the example above.

import os
from fastapi import FastAPI, Request, Response
from standardwebhooks.webhooks import Webhook

app = FastAPI()
verifier = Webhook(os.environ["WEBHOOK_SECRET"])

@app.post("/telegram-events")
async def receive(request: Request):
    body = await request.body()
    try:
        event = verifier.verify(body, dict(request.headers))
    except Exception:
        return Response(status_code=401)

    try:
        await inbox.accept_once(
            delivery_id=request.headers["webhook-id"],
            event_id=event["id"],
            event=event,
        )
    except Exception:
        return Response(status_code=503)

    return Response(status_code=204)

Result

Requests with invalid signatures do not enter the queue. Valid requests are stored before you acknowledge successful receipt.

Check

Deduplicate by webhook-id for retries of the same delivery. Also prevent duplicate side effects by event.id: replay creates a new delivery but keeps the event ID.

Return 2xx promptly, within the sender's 10-second limit. Do not acknowledge success before writing durably.

If something goes wrong

Check the raw body, machine clock, secret and whether a proxy is changing the body. Do not promise processing in arrival order. If business logic requires ordering, compare event data with the current state.

Do it with the API or MCP

Read webhooks.receiver_guide through tools_run, then use webhooks.test or webhooks_test to test the receiving path.

See also webhook failures.