API reference: webhooks
Webhooks push events to your server after they happen. Set the Webhook URL (optional) 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, the transfer object, or the subscription object (shapes on
the respective reference pages).
Event types
update_type | Fires when | Payload |
|---|---|---|
invoice_paid | an invoice is paid — no opt-in required | invoice object |
invoice_expired | an invoice passes its deadline unpaid | invoice object |
check_activated | a check or one recipient’s share is claimed | check object; a named-list claim also has activated_slot |
check_expired | a named-list check expires and unclaimed shares return to the app | check object with recipients and their claimed_at values |
transfer_completed | a transfer completes, or an on-chain payout is confirmed | transfer or withdrawal object |
transfer_returned | an unclaimed pending transfer returns to the app, or an on-chain payout fails and its hold is returned | transfer or withdrawal object |
refund_completed | a refund is executed | invoice object with the refunded_* fields |
subscription_activated | a user approves a plan | subscription object |
subscription_charged | payment for a period is collected — 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 |
On-chain payouts created in the app use these same transfer events. Their
payload has kind: "withdrawal", chain fields such as network, address
and txid, and status completed or failed. Their transfer_id belongs
to a separate ID sequence; distinguish payloads by kind before using it.
The downloadable OpenAPI reference describes both payload variants.
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 in the app’s Webhook section — the toggles use the identifiers from the
table above. Save a webhook URL to receive events.
Verifying the signature
Every delivery carries the TgPayCrypto-API-Signature header
(TgCryptoPay-API-Signature and Crypto-Pay-API-Signature are compatibility aliases 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["TgPayCrypto-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 a 2xx response received before the attempt times out. Respond promptly and process long tasks separately.
- An error status, timeout or connection failure triggers retries with
increasing delays, up to the platform’s attempt limit. Delivery is not
guaranteed; use
getInvoicesto reconcile payments after an outage. - 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.