Webhooks
Los webhooks son payloads de eventos firmados con HMAC-SHA256 entregados a tu endpoint cuando algo sucede con un pago. Reintentan en caso de fallo y son la confirmación autoritativa de que un cobro se capturó.
Eventos
Eventos de pago
| Evento | Cuándo |
|---|---|
payment.completed | Venta capturada exitosamente (estado final del flujo Sale) |
payment.captured | Autorización movida a capturada (flujo Auth → Capture separado) |
payment.capture_failed | Intento de captura sobre un pago autorizado falló |
payment.failed | Pago rechazado o fallido |
payment.refunded | Reembolso procesado (total o parcial) |
payment.refund_failed | Intento de reembolso rechazado o con error — la captura original sigue liquidada |
payment.voided | Pago autorizado anulado antes de la captura |
payment.void_failed | Intento de anulación sobre un pago autorizado falló |
Eventos de suscripción
| Evento | Cuándo |
|---|---|
subscription.created | Se creó una nueva suscripción (en trial o active) |
subscription.active | Una suscripción entró al estado active (p. ej. tras fin de prueba) |
subscription.trialing | Período de prueba iniciado — sin cobro aún |
subscription.charged | Ciclo de renovación exitoso |
subscription.charge_failed | Renovación rechazada — comienza el dunning según el calendario del comercio |
subscription.cancelled | Suscripción cancelada (por el comercio, el cliente o tras dunning) |
subscription.updated | Cambió el método de pago o el calendario |
Eventos de sesión de checkout
| Evento | Cuándo |
|---|---|
checkout.session.expired | Sesión abierta más allá de expires_at barrida (cron cada 5 min) |
checkout.cancelled | Cliente pulsó Cancelar en la página de checkout hospedada |
Payload
{
"event_id": "evt_abc123",
"event_type": "payment.completed",
"payload_redacted": {
"transaction_id": "txn_789xyz",
"receipt_number": "RCT-000042",
"amount": 2500,
"currency": "USD",
"status": "captured",
"subtotal": 2300,
"tax_amount": 200,
"tax_name": "VAT",
"payment_method_type": "card",
"card_brand": "Visa",
"card_last4": "0071",
"token_id": "tok_abc123",
"description": "Pedido #1042",
"line_items": [
{ "name": "T-shirt", "quantity": 1, "unit_amount": 2300 }
],
"customer": { "name": "Jane Doe", "email": "[email protected]" },
"metadata": { "order_id": "1042", "source": "woocommerce" }
}
}payload_redacted es la copia segura para registro — sin PAN completo, sin CVV, solo last4.
Disponibilidad de campos por tipo de evento
| Campo | payment.* | payment.refunded | subscription.* | checkout.* |
|---|---|---|---|---|
transaction_id | ✅ | ✅ | ✅ | — |
receipt_number | ✅ | ✅ | ✅ | — |
amount, currency | ✅ | ✅ (monto de reembolso) | ✅ | ✅ |
subtotal, tax_amount, tax_name | ✅ | ✅ | ✅ | ✅ |
payment_method_type | ✅ | ✅ | ✅ | — |
card_brand, card_last4 | con tarjeta | con tarjeta | con tarjeta | — |
token_id | si tokenizado | si on-token | siempre | — |
line_items, description | ✅ | ✅ | ✅ | ✅ |
customer | ✅ | ✅ | ✅ | ✅ |
metadata | ✅ — metadata del caller en la sesión original | ✅ — la misma metadata que la sesión original (NO la metadata.merchant_order_id del reembolso) | ✅ | ✅ |
checkout_session_external_id | — | — | — | ✅ |
merchant_order_id (top-level en reembolsos) | — | ✅ | — | — |
metadata se reenvía sin cambios desde la llamada POST /api/v1/checkout-sessions que hiciste — así los plugins de tienda (WC / Give / Magento / Shopify) correlacionan webhooks con el pedido original. Configura lo que necesites (un order id, un SKU, un tag de campaña) al crear la sesión y lo recibirás de vuelta en cada evento.
Eventos de ciclo de suscripción
subscription.charged y subscription.cancelled llevan la misma forma que payment.* más un bloque subscription:
"subscription": {
"external_id": "sub_abc123",
"plan_id": "plan_monthly_pro",
"cycle": 7,
"next_billing_date": "2026-06-12"
}Eventos de sesión de checkout
checkout.session.expired y checkout.cancelled omiten el campo transaction_id — no existe porque no se intentó cobro. Úsalos para limpiar filas huérfanas pending en tu tienda:
{
"event_id": "evt_xyz",
"event_type": "checkout.session.expired",
"payload_redacted": {
"checkout_session_external_id": "cs_abc123",
"amount": 2500,
"currency": "USD",
"metadata": { "order_id": "1042", "source": "give" }
}
}Verificación de firma
Cada entrega de webhook incluye cuatro encabezados:
| Encabezado | Propósito |
|---|---|
X-GC-Signature | La firma HMAC-SHA256, codificada en hex |
X-GC-Timestamp | Timestamp Unix del intento de entrega |
X-GC-Event-ID | ID único del evento (evt_…) — también en el cuerpo, útil para búsqueda en logs |
X-GC-Event-Type | El tipo de evento (p. ej. payment.completed) |
Calcula la firma esperada en el servidor y compara en tiempo constante:
expected = HMAC-SHA256("{timestamp}.{payload}", webhook_secret)Hay un ejemplo PHP completo en Ejemplos de código → Webhook handler.
Rechaza eventos antiguos
Rechaza eventos con un X-GC-Timestamp desfasado más de 5 minutos del reloj. Esto bloquea ataques de repetición incluso si una firma se filtra.
Rotación del secret
Cada endpoint de webhook tiene un secret actual y uno previo. Para rotar:
- Panel → Webhooks → Endpoint → Rotar secret. Se genera un nuevo secret; el anterior se vuelve el "previo".
- Las nuevas entregas se firman con el secret actual. Las reentregas de eventos previos a la rotación se firman con el secret previo (la plataforma rastrea
secret_versionpor entrega). - Tu receptor debería aceptar cualquiera de las dos firmas durante la ventana de transición — prueba primero el actual, cae al previo en caso de mismatch.
- Tras drenar todas las entregas en vuelo que te importan (24h es un techo seguro), revoca el secret previo desde el panel.
Calendario de reintentos
Las entregas fallidas se reintentan con backoff exponencial:
1m → 5m → 30m → 2h → 12h → 24hHasta 6 intentos por evento. Tras agotarse, el evento se marca como failed en el panel y puedes reentregarlo manualmente.
Una entrega se considera fallida si tu endpoint devuelve cualquier respuesta no-2xx o si supera el timeout (límite de 10 segundos).
Idempotencia en el receptor
Haz tu handler idempotente sobre event_id (o transaction_id para eventos de pago). El mismo evento puede entregarse más de una vez — por ejemplo, cuando nuestro reintento y tu servidor responden a la vez.
