tgpay cryptoAPI
crypto-payapiwebhookssignature

API reference: webhooks

4 min readUpdated Nov 2, 2026

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:00.123456Z",
  "payload": { … }
}

update_id is stable across redeliveries of the same event — key your deduplication on it. request_date (UTC, ISO 8601 with microseconds) 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 method pages: invoices, transfers and checks, subscriptions).

Event types

update_typeFires whenPayload
invoice_paidan invoice is paid — no opt-in requiredinvoice object
invoice_expiredan invoice passes its deadline unpaidinvoice object
check_activateda check or one recipient’s share is claimedcheck object; a named-list claim also has activated_slot
check_expireda named-list check expires and unclaimed shares return to the appcheck object with status: "expired", recipients and their claimed_at values
transfer_completeda transfer completes, or an on-chain payout is confirmedtransfer or withdrawal object
transfer_returnedan unclaimed pending transfer returns to the app, or an on-chain payout fails and its hold is returnedtransfer or withdrawal object
refund_completeda refund is executedinvoice object with the refunded_* fields
subscription_activateda user approves a plansubscription object
subscription_chargedpayment for a period is collected — charge.kind says which: initial, renewal, or resubscribesubscription object + charge: {period_no, kind, asset, amount, fee, paid_at}
subscription_cancelleda subscription is canceled by either sidesubscription object
subscription_expiredthe grace period runs out unpaidsubscription 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. Only this variant has kind: an ordinary transfer payload has no kind field at all, so treat a payload with kind: "withdrawal" as a payout and one without kind as a transfer. 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 within 10 seconds. Respond promptly and process long tasks separately. Redirects are not followed: a 3xx counts as a failure, so register the final URL.
  • An error status, timeout or connection failure triggers retries: up to 17 attempts, the delay starting at 10 seconds and doubling up to 8 hours (with some random spread), about 3 days in all. Delivery is not guaranteed; use getInvoices to reconcile payments after an outage.
  • Each attempt goes to the webhook URL saved at that moment: a changed URL receives the pending retries, and removing the URL drops them.
  • 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_id before 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.