For AI agents: a documentation index is available at the root level at /llms.txt and /llms-full.txt. Append /llms.txt to any URL for a page-level index, or .md for the markdown version of any page.
Guías (Español)Guides (English)Referencia API
  • Comenzar
    • Introducción
    • Autenticación
    • Paginación
    • Errores
    • Webhooks
  • Guías
    • Cuentas Conectadas
    • Embedded Checkouts
    • Terminal Sessions
    • Cambiar Tarjeta de Suscripción
    • Opciones de facturación para suscripciones
LogoLogo
On this page
  • Configurar Webhooks
  • Vía Dashboard
  • Vía API
  • Modo Test (Sandbox)
  • Tipos de Eventos Webhook
  • payment_intent.succeeded
  • payment_intent.failed
  • subscription.create
  • subscription.past_due
  • subscription.paused
  • subscription.cancel
  • bank_transfer_intent.pending
  • bank_transfer_intent.succeeded
  • bank_transfer_intent.failed
  • setup_intent.succeeded
  • setup_intent.cancelled
  • balance_intent.succeeded
  • balance_intent.paid
  • Payload del Webhook
  • Mejores Prácticas
  • Reintentos
  • Verificar Webhooks (Seguridad)
  • Signing Secret
  • Verificar con Librerías Oficiales (Recomendado)
  • Verificar Manualmente
Comenzar

Webhooks

Was this page helpful?
Previous

Cuentas Conectadas

Next
Built with

Los webhooks notifican a tu aplicación en tiempo real cuando ocurren eventos — pagos, cambios de suscripción, reembolsos y más.

Configurar Webhooks

Vía Dashboard

Para activar webhooks para tu cuenta, dentro de tu cuenta de Recurrente, ve a:

Configuración → Desarrolladores y API.

Ahí haz clic en “Webhooks”, y continúa a añadir el endpoint a donde quieres que sean enviados los requests.

Vía API

$curl -X POST https://app.recurrente.com/api/webhook_endpoints \
> -H "X-SECRET-KEY: ..." \
> -H "Content-Type: application/json" \
> -d '{
> "url": "https://tusitio.com/webhooks/recurrente",
> "description": "Webhook de producción"
> }'

Modo Test (Sandbox)

Los webhooks no se envían para checkouts creados con llaves de test (pk_test_* / sk_test_*). En modo test, los pagos con la tarjeta 4242 4242 4242 4242 se aprueban directamente sin pasar por el flujo completo de procesamiento, por lo que no se generan eventos de webhook.

Para probar tu integración de webhooks de punta a punta, necesitas usar llaves de producción (pk_live_* / sk_live_*) con una tarjeta real.

Tipos de Eventos Webhook

Recurrente envía webhooks para los siguientes eventos:

payment_intent.succeeded

Se emite con un cobro exitoso con tarjeta (crédito o débito). Los fondos ya están en tu balance de Recurrente.

Ejemplo de respuesta:

1{
2 "id": "pa_id123",
3 "event_type": "payment_intent.succeeded",
4 "api_version": "2024-04-24",
5 "checkout": {
6 "id": "ch_id123",
7 "status": "paid",
8 "payment": {
9 "id": "pa_laybj3zw",
10 "paymentable": {
11 "type": "OneTimePayment",
12 "id": "on_arognqni",
13 "tax_name": null,
14 "tax_id": null,
15 "address": null,
16 "phone_number": null
17 }
18 },
19 "payment_method": {
20 "id": "pay_m_7v5ie3pw",
21 "type": "card",
22 "card": {
23 "last4": "4242",
24 "network": "visa"
25 }
26 },
27 "transfer_setups": [],
28 "metadata": {}
29 },
30 "created_at": "2024-02-16T03:01:13.260Z",
31 "failure_reason": null,
32 "amount_in_cents": 10000,
33 "currency": "GTQ",
34 "fee": 450,
35 "vat_withheld": 160,
36 "vat_withheld_currency": "GTQ",
37 "customer": {
38 "email": "hello@example.com",
39 "full_name": "Max Rodriguez",
40 "id": "us_id123"
41 },
42 "product": {
43 "id": "prod_id123"
44 },
45 "invoice": {
46 "id": "inv_123",
47 "tax_invoice_url": null
48 }
49}

payment_intent.failed

Cobro fallido con tarjeta.

Ejemplo de respuesta:

1{
2 "id": "pa_id123",
3 "event_type": "payment_intent.failed",
4 "api_version": "2024-03-13",
5 "checkout": {
6 "id": "ch_id123"
7 },
8 "created_at": "2024-02-16T03:01:13.260Z",
9 "failure_reason": "Tu banco ha rechazado la transacción. Llama a tu banco y pide que autoricen esta transacción.",
10 "amount_in_cents": 10000,
11 "currency": "GTQ",
12 "fee": 0,
13 "vat_withheld": 0,
14 "vat_withheld_currency": "GTQ",
15 "customer": {
16 "email": "hello@example.com",
17 "full_name": "Max Rodriguez",
18 "id": "us_id123"
19 },
20 "product": {
21 "id": "prod_id123"
22 }
23}

subscription.create

Si el producto es recurrente, se emite además del payment.succeeded este evento con la información de la suscripción.

Ejemplo de respuesta:

1{
2 "api_version": "2024-04-24",
3 "created_at": "2025-10-13T13:59:27.931Z",
4 "customer_email": "example@example.com",
5 "customer_id": "us_1234",
6 "customer_name": "Pedro Pérez",
7 "event_type": "subscription.create",
8 "id": "su_123",
9 "payment": {
10 "id": "pa_123",
11 "paymentable": {
12 "address": null,
13 "id": "su_123",
14 "phone_number": "+50255555555",
15 "tax_id": "",
16 "tax_name": null,
17 "type": "Subscription"
18 }
19 },
20 "product": {
21 "address_requirement": "none",
22 "billing_info_requirement": "optional",
23 "cancel_url": "",
24 "custom_terms_and_conditions": "Términos y condiciones",
25 "description": "Suscripción de prueba",
26 "has_dynamic_pricing": false,
27 "id": "prod_123",
28 "metadata": {},
29 "name": "Plan de Prueba",
30 "phone_requirement": "required",
31 "prices": [
32 {
33 "amount_in_cents": 999,
34 "billing_interval": "month",
35 "billing_interval_count": 1,
36 "charge_type": "recurring",
37 "currency": "GTQ",
38 "free_trial_interval": "month",
39 "free_trial_interval_count": 0,
40 "id": "price_123",
41 "periods_before_automatic_cancellation": null
42 }
43 ],
44 "status": "active",
45 "storefront_link": "https://app.recurrente.com/s/recurrente/plan-de-prueba",
46 "success_url": ""
47 }
48}

subscription.past_due

Se emite cuando el cobro automático de una suscripción falla por primera vez.

Nota: En una suscripción, cuando un pago falla, Recurrente intenta cobrarlo de nuevo 3 y 5 días después. Si ambos re-intentos son fallidos, en ese momento se cancela la suscripción.

subscription.paused

Se emite cuando se pausa la suscripción. Una suscripción pausada no se volverá a cobrar hasta que sea reactivada.

subscription.cancel

Se emite cuando el cobro automático de una suscripción falla por tercera vez.

Nota: En una suscripción, cuando un pago falla, Recurrente intenta cobrarlo de nuevo 3 y 5 días después. Si ambos re-intentos son fallidos, en ese momento se cancela la suscripción.

bank_transfer_intent.pending

Se emite cuando se inicia un cobro con transferencia bancaria. En cuanto se reciba el dinero en la cuenta, se emitirá bank_transfer_intent.succeeded. De lo contrario, se emitirá bank_transfer_intent.failed.

bank_transfer_intent.succeeded

Se emite con un cobro exitoso con transferencia bancaria. Los fondos ya están en tu balance de Recurrente.

bank_transfer_intent.failed

Se emite con un cobro fallido con transferencia bancaria. Esto sucede cuando no se reciben los fondos en la cuenta de banco, o se recibe el monto incorrecto.

setup_intent.succeeded

Se emite cuando se inicia exitosamente una suscripción con un período de prueba. También se emite cuando se tokeniza una tarjeta sin cobrarla.

setup_intent.cancelled

Se emite cuando no se logra tokenizar una tarjeta sin cobrarla. Esto sucede cuando el primer pago de una suscripción con período de prueba, falla.

balance_intent.succeeded

Se emite con un cobro exitoso pagado con el balance de Recurrente del cliente. Los fondos ya están en tu balance. Este evento llega a la cuenta del comercio (la que cobró).

balance_intent.paid

Se emite a la cuenta del pagador (la cuenta cuyo balance fue debitado) cuando paga exitosamente un checkout usando su balance de Recurrente. Solo lo recibes si configuraste un endpoint de webhook en la cuenta pagadora.

Nota: un mismo cobro con balance dispara dos webhooks independientes: balance_intent.succeeded al comercio y balance_intent.paid al pagador. El payload sigue la misma estructura que bank_transfer_intent.succeeded (incluye checkout, payment, customer, amount_in_cents, currency, product, tax_invoice_url).

Payload del Webhook

Cada POST del webhook incluye un cuerpo JSON con los datos del evento. La estructura varía según el tipo de evento.

Mejores Prácticas

  • Retorna 2xx rápidamente — Confirma la recepción antes de hacer procesamiento pesado
  • Maneja duplicados — Los webhooks pueden ser entregados más de una vez; usa el ID del evento para idempotencia
  • Verifica la fuente — Valida las firmas de los webhooks como se describe en Verificar Webhooks
  • Procesa de forma asíncrona — Usa una cola para procesamiento pesado después de confirmar recepción

Reintentos

Si tu endpoint no responde con un código 2xx, Recurrente reintentará enviar el webhook:

  • Inmediatamente después del primer fallo
  • 1 minuto después
  • 5 minutos después
  • 30 minutos después
  • 2 horas después
  • 6 horas después

Verificar Webhooks (Seguridad)

Cada webhook enviado por Recurrente incluye una firma para que puedas verificar que es auténtico y no ha sido alterado. Recomendamos fuertemente verificar las firmas en producción.

Signing Secret

Cada endpoint de webhook tiene un signing secret único. Lo puedes encontrar en el dashboard de Svix — accesible desde Configuración → Desarrolladores y API → Webhooks en tu cuenta de Recurrente. El secret tiene el formato whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw.

Verificar con Librerías Oficiales (Recomendado)

La forma más fácil de verificar firmas es usando las librerías de verificación de webhooks de Svix, disponibles para muchos lenguajes:

1// npm install svix
2const { Webhook } = require("svix");
3
4const wh = new Webhook(secret);
5// Lanza error si falla, retorna el contenido verificado si es exitoso
6const payload = wh.verify(rawBody, headers);

Debes usar el body crudo del request al verificar webhooks. Si parseas el JSON y lo re-serializas, la verificación de firma fallará.

Verificar Manualmente

Cada request de webhook incluye tres headers:

HeaderDescripción
svix-idIdentificador único del mensaje (se mantiene igual en reintentos)
svix-timestampTimestamp Unix (segundos) de cuando se envió el webhook
svix-signatureFirma(s) codificada(s) en Base64, con prefijo v1,

Paso 1. Construye el contenido firmado concatenando el svix-id, svix-timestamp, y el body crudo del request, separados por puntos:

signed_content = "${svix_id}.${svix_timestamp}.${body}"

Paso 2. Decodifica la porción en base64 de tu signing secret (todo después de whsec_) y úsalo como llave para un hash HMAC-SHA256 del contenido firmado:

1const crypto = require("crypto");
2
3const secretBytes = Buffer.from(secret.split("_")[1], "base64");
4const signature = crypto
5 .createHmac("sha256", secretBytes)
6 .update(signedContent)
7 .digest("base64");

Paso 3. Compara tu firma calculada con la del header svix-signature. El valor del header tiene el prefijo v1, — quita ese prefijo antes de comparar. Usa una comparación de tiempo constante para prevenir ataques de timing.

Paso 4. Opcionalmente, verifica que el svix-timestamp esté dentro de unos minutos del tiempo actual de tu servidor para prevenir ataques de replay.