Referencia de la API: webhooks
Los webhooks envían eventos a tu servidor después de que ocurren. Pon la URL del webhook (opcional) en tu app, en Más → API para comerciantes; a partir de ahí la plataforma hace POST de un cuerpo JSON firmado por cada evento al que estés suscrito.
La estructura del mensaje
{
"update_id": 123,
"update_type": "invoice_paid",
"request_date": "2026-08-11T12:00:00.123456Z",
"payload": { … }
}
update_id se mantiene estable entre reenvíos del mismo evento: basa tu
deduplicación en él. request_date (UTC, ISO 8601 con microsegundos) se registra en cada intento de entrega.
payload es el objeto completo del tipo de evento: el objeto de factura para
los eventos de facturas, el objeto de cheque, de transferencia o de suscripción (las
estructuras están en las páginas de cada método: facturas,
transferencias y cheques,
suscripciones).
Tipos de eventos
update_type | Se dispara cuando | Payload |
|---|---|---|
invoice_paid | se paga una factura — no requiere activación adicional | objeto de factura |
invoice_expired | una factura pasa su fecha límite sin pagarse | objeto de factura |
check_activated | se canjea un cheque o la parte de un destinatario | objeto de cheque; el canje de una parte de un cheque con lista de destinatarios también incluye activated_slot |
check_expired | expira un cheque con lista de destinatarios y las partes sin canjear se devuelven a la app | objeto de cheque con status: "expired", recipients y sus valores claimed_at |
transfer_completed | se completa una transferencia o se confirma un pago on-chain | objeto de transferencia o de retiro |
transfer_returned | una transferencia pendiente sin reclamar se devuelve a la app, o un pago on-chain falla y se libera el monto retenido | objeto de transferencia o de retiro |
refund_completed | se ejecuta un reembolso | objeto de factura con los campos refunded_* |
subscription_activated | un usuario aprueba un plan | objeto de suscripción |
subscription_charged | se cobra un período — charge.kind dice cuál: initial, renewal o resubscribe | objeto de suscripción + charge: {period_no, kind, asset, amount, fee, paid_at} |
subscription_cancelled | una suscripción se cancela por cualquiera de los dos lados | objeto de suscripción |
subscription_expired | el período de gracia se agota sin pago | objeto de suscripción |
Los pagos on-chain creados en la app usan estos mismos eventos de transferencia.
Su payload incluye kind: "withdrawal", campos de la blockchain como network,
address y txid, y el estado completed o failed. Su transfer_id pertenece
a una secuencia de ID independiente. Solo esta variante tiene kind: el
payload de una transferencia normal no incluye el campo kind, así que trata
un payload con kind: "withdrawal" como un pago on-chain y uno sin kind
como una transferencia. La referencia OpenAPI descargable describe ambas variantes.
Todo salvo invoice_paid es opcional (una extensión sobre Crypto Bot: una
integración que solo admite los eventos de Crypto Bot no recibe un update_type
desconocido a menos que lo actives). Actívalos por app en Eventos de
webhook adicionales, dentro de la sección Webhook de la app. Los interruptores
usan los identificadores de la tabla anterior. Guarda una URL de webhook
para recibir los eventos.
Verificar la firma
Cada entrega lleva la cabecera TgPayCrypto-API-Signature
(TgCryptoPay-API-Signature y Crypto-Pay-API-Signature son alias de
compatibilidad con el mismo valor):
el HMAC-SHA256 en hexadecimal del cuerpo original de la petición, sin modificar, con clave el
resumen SHA-256 del token de API principal de tu app. Es el esquema de
Crypto Bot, así que el código de verificación que ya tengas funciona sin
cambios:
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"])
Verifica sobre los bytes originales recibidos: si analizas el JSON y vuelves a serializarlo, los bytes pueden cambiar y hacer fallar la comprobación. Solo firma el token principal; los tokens restringidos nunca firman. Renovar el token principal vuelve a fijar al instante la clave de firma de los webhooks, así que actualiza el secreto en tu servidor al mismo tiempo.
Entrega y reintentos
- Una entrega se considera exitosa si recibe una respuesta 2xx en menos de 10 segundos. Responde pronto y procesa las tareas largas por separado. Las redirecciones no se siguen: un 3xx cuenta como fallo, así que registra la URL final.
- Un estado de error, un tiempo de espera agotado o un fallo de conexión provoca
reintentos: hasta 17 intentos, con una espera que empieza en 10 segundos y se
duplica hasta 8 horas (con algo de variación aleatoria), unos 3 días en
total. La entrega no está garantizada; usa
getInvoicespara comprobar los pagos después de una interrupción del servicio. - Cada intento se envía a la URL del webhook guardada en ese momento: si la cambias, la nueva URL recibe los reintentos pendientes, y si la eliminas, esos reintentos se descartan.
- Después del último intento se descarta la entrega. La URL del webhook no se desactiva automáticamente: los fallos de un endpoint no cancelan la suscripción a los eventos.
- Como hay reintentos, tu manejador tiene que ser idempotente: deduplica por
update_idantes de actuar.
⚠️ Verifica antes de despachar
Cualquiera puede hacer POST a tu URL de webhook. Hasta que la comprobación de la
firma pase, trata el cuerpo como entrada no confiable: no despaches un pedido,
no le acredites nada a un usuario, no marques nada como pagado. El orden seguro
es: verificar la firma → deduplicar por update_id → actuar.
¿Te sirvió este artículo?
Gracias por tu comentario.