API reference: webhooks
Webhooks push events to your server the moment they happen. Set the Webhook URL on your app in More → Merchant API; the platform then POSTs a signed JSON body for every event you’re subscribed to.
The envelope
{
"update_id": 123,
"update_type": "invoice_paid",
"request_date": "2026-08-11T12:00:00Z",
"payload": { … }
}
update_id is stable across redeliveries of the same event — key your
deduplication on it. request_date is stamped per delivery attempt.
payload is the full object for the event type: the invoice object for
invoice events, the check object, or the subscription object (shapes on
the respective reference pages).
Event types
update_type | Fires when | Payload |
|---|---|---|
invoice_paid | an invoice is paid — always delivered | invoice object |
invoice_expired | an invoice passes its deadline unpaid | invoice object |
check_activated | one of your checks is claimed | check object |
refund_completed | a refund is executed | invoice object with the refunded_* fields |
subscription_activated | a user approves a plan | subscription object |
subscription_charged | a period is billed — charge.kind says which: initial, renewal, or resubscribe | subscription object + charge: {period_no, kind, asset, amount, fee, paid_at} |
subscription_cancelled | a subscription is canceled by either side | subscription object |
subscription_expired | the grace period runs out unpaid | subscription object |
Everything except invoice_paid is opt-in (an extension over Crypto
Bot — a strict Crypto Bot-shaped consumer never meets an unknown
update_type unless you asked for it). Opt in per app under Extra
webhook events on the Merchant API screen — the toggles appear once a
webhook URL is set, and they’re labeled with the raw identifiers from the
table above.
Verifying the signature
Every delivery carries the TgCryptoPay-API-Signature header
(Crypto-Pay-API-Signature is a compatibility alias with the same
value): the hex HMAC-SHA256 of the raw request body, keyed by the SHA-256
digest of your app’s main API token. It’s the Crypto Bot scheme, so
existing verification code works unchanged:
import hashlib, hmac
secret = hashlib.sha256(API_TOKEN.encode()).digest()
expected = hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(expected, headers["TgCryptoPay-API-Signature"])
Verify over the raw received bytes — a re-serialized parse can differ byte-for-byte and fail the check. Only the main token signs; restricted tokens never do. Rotating the main token re-keys webhook signing instantly, so update the secret on your server in the same move.
Delivery and retries
- A delivery counts as successful on any 2xx response within 10 seconds.
- Anything else — an error status, a timeout, a connection failure — is retried with exponential backoff: the first retry after about 10 seconds, the gap doubling up to 8 hours, for up to 17 attempts spread over roughly 3 days.
- After the last attempt the delivery is dropped. The webhook URL itself is never disabled automatically — a flaky endpoint doesn’t silently unsubscribe your app.
- Because retries happen, your handler must be idempotent: dedupe on
update_idbefore acting.
⚠️ Verify before you fulfill
Anyone can POST to your webhook URL. Until the signature check passes,
treat the body as untrusted input: don’t ship an order, don’t credit a
user, don’t mark anything paid. The safe order is: verify the signature →
dedupe on update_id → act.
Was this article helpful?
Thanks for the feedback.