Skip to content

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

EventoCuándo
payment.completedVenta capturada exitosamente (estado final del flujo Sale)
payment.capturedAutorización movida a capturada (flujo Auth → Capture separado)
payment.capture_failedIntento de captura sobre un pago autorizado falló
payment.failedPago rechazado o fallido
payment.refundedReembolso procesado (total o parcial)
payment.refund_failedIntento de reembolso rechazado o con error — la captura original sigue liquidada
payment.voidedPago autorizado anulado antes de la captura
payment.void_failedIntento de anulación sobre un pago autorizado falló

Eventos de suscripción

EventoCuándo
subscription.createdSe creó una nueva suscripción (en trial o active)
subscription.activeUna suscripción entró al estado active (p. ej. tras fin de prueba)
subscription.trialingPeríodo de prueba iniciado — sin cobro aún
subscription.chargedCiclo de renovación exitoso
subscription.charge_failedRenovación rechazada — comienza el dunning según el calendario del comercio
subscription.cancelledSuscripción cancelada (por el comercio, el cliente o tras dunning)
subscription.updatedCambió el método de pago o el calendario

Eventos de sesión de checkout

EventoCuándo
checkout.session.expiredSesión abierta más allá de expires_at barrida (cron cada 5 min)
checkout.cancelledCliente pulsó Cancelar en la página de checkout hospedada

Payload

json
{
  "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

Campopayment.*payment.refundedsubscription.*checkout.*
transaction_id
receipt_number
amount, currency✅ (monto de reembolso)
subtotal, tax_amount, tax_name
payment_method_type
card_brand, card_last4con tarjetacon tarjetacon tarjeta
token_idsi tokenizadosi on-tokensiempre
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:

json
"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:

json
{
  "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:

EncabezadoPropósito
X-GC-SignatureLa firma HMAC-SHA256, codificada en hex
X-GC-TimestampTimestamp Unix del intento de entrega
X-GC-Event-IDID único del evento (evt_…) — también en el cuerpo, útil para búsqueda en logs
X-GC-Event-TypeEl 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:

  1. Panel → Webhooks → Endpoint → Rotar secret. Se genera un nuevo secret; el anterior se vuelve el "previo".
  2. 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_version por entrega).
  3. 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.
  4. 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 → 24h

Hasta 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.

Siguiente

Released under the proprietary Genius Checkout license.