Webhook de suscripción


Qué te avisamos

Una suscripción cobra sola, mes tras mes, fuera de cualquier petición tuya. El webhook es cómo te contamos qué pasó: que se renovó, que el cobro falló, que el cliente la canceló, que la prueba está por terminar.

Configura la URL en el campo webhook_url al crear la suscripción, o en advanced_options.result_urls.webhook del plan.

{warning} Si el plan tiene result_urls.webhook, esa URL gana sobre el webhook_url que mandes al crear la suscripción.

Cómo se entrega y cómo se verifica

Igual que todos nuestros webhooks: POST con Content-Type: application/json, header Signature con el HMAC-SHA256 del cuerpo crudo, 3 segundos de espera y hasta 7 intentos repartidos en unas 17 horas.

El detalle completo, con ejemplos de verificación en PHP, Node y Python, está en Webhooks. Aquí solo va lo específico de suscripciones.

{danger} Verifica la firma antes de leer el cuerpo. Sin eso, cualquiera que conozca tu URL puede decirte que se renovó una suscripción que nunca se cobró.

Forma del cuerpo

{
    "meta":         { "event_id": "…", "event_type": "…", "timestamp": "…", "api_version": "v1" },
    "subscription": { "…": "estado completo de la suscripción" },
    "status":       { "key": "renew", "value": "Renovado", "description": "…" },
    "transaction":  { "…": "solo cuando hubo cobro" }
}
Bloque Siempre viene Qué es
meta Identificador de la entrega. Ver Identificar cada entrega
subscription El estado resultante de la suscripción, ya aplicado el evento
status El evento: qué acaba de pasar
transaction No Solo en los eventos que implican un cobro
previous_plan_id No Solo en plan_changed: el plan que tenía antes

{success} No necesitas una consulta extra. subscription ya trae status, ends_at, grace_ends_at, on_grace_period, is_active, paused_at, resumes_at y next_charge. Con eso decides sin volver a llamarnos.

{info} No confundas los dos status. El de primer nivel describe el evento (renew, canceled…). El de dentro de subscription es el estado de la suscripción (trial, activa, en_gracia, pausada, cancelada, finalizada); ver Estados.

Catálogo de eventos

status.key meta.event_type Trae transaction
new subscription.created
renew subscription.renewed
retry subscription.payment_retry
finished subscription.finished
finished time subscription.finished_by_limit No
canceled subscription.canceled No
paused subscription.paused No
resumed subscription.resumed No
plan_changed subscription.plan_changed Depende del flujo
trial_will_end subscription.trial_will_end No
renewed subscription.invitation_accepted Sí, si el cobro se ejecutó

{danger} finished time lleva un espacio literal. No es una errata: es el valor real y no lo vamos a cambiar, porque hay integraciones que ya lo manejan. Usa meta.event_type si prefieres una clave sin sorpresas.

Cuándo llega cada uno

Las dudas que más nos llegan, respondidas:

¿Qué evento marca el final por falta de pago? finished. La secuencia completa de un cobro que falla es: retry en cada intento mientras la suscripción siga viva, y finished cuando se agota la ventana de gracia sin haber cobrado. No hay un evento aparte para "se acabó la gracia".

¿retry llega una vez o en cada intento? En cada intento de cobro fallido. La cadencia de reintentos la define el plan (ventana de gracia) y el dunning del comercio.

¿En qué se diferencian renew, renewed y plan_changed?

Quién lo dispara Qué pasó
renew Nosotros, en el cobro automático Se cobró el ciclo y la suscripción sigue
renewed El suscriptor Aceptó una invitación de renovación o reactivación que le llegó por correo
plan_changed El comercio o el suscriptor Se cambió de plan, directo con prorrateo o aceptando la invitación

¿paused y resumed pueden originarse de nuestro lado? No. Solo los disparan POST /subscription/{id}/pause y /resume, que llamas tú o alguien desde el panel del comercio. Efipay no pausa suscripciones por su cuenta.

Después de cancelar, ¿puede llegarme un renew? No. El motor de cobro excluye toda suscripción con canceled_at, así que en cuanto la cancelación queda registrada no hay más cobros ni más renew. Lo que sí sigue vigente hasta ends_at es el servicio, salvo que canceles con at_period_end: false.

Sobre los identificadores

Es la confusión más común, porque conviven dos tipos:

Campo Tipo Ejemplo
subscription.id UUID 9ad70d0e-61e1-4c17-99c3-355b50d79954
subscription.plan.id UUID 9ae4dc2d-8b2e-4920-ac1f-80f2d648a2e6
subscription.subscriber.id UUID 9ae92bbb-2cab-4569-85b1-6173d0fb9d4b
transaction.transaction_id Entero 108
office.id, commerce_id Entero 1

{info} Las suscripciones, planes, suscriptores y grupos han sido UUID desde el principio. No existen suscripciones antiguas con id numérico que haya que migrar. Lo numérico es el consecutivo de la transacción y los ids de sede y comercio.

{warning} Guárdalos todos como cadena, también los numéricos. Un entero de transacción puede crecer más allá de lo que tu lenguaje maneja con seguridad, y un UUID parseado como número se corrompe en silencio.

Detalle de los objetos

subscription

Campo Descripción
id UUID de la suscripción
status Estado actual: trial, activa, en_gracia, pausada, cancelada, finalizada
starts_at Inicio del período vigente
ends_at Fin del período vigente
grace_ends_at Hasta cuándo se puede reintentar el cobro sin cortar el servicio
on_grace_period true si está dentro de la ventana de gracia ahora mismo
trial_ends_at Fin del período de prueba, si lo hay
canceled_at Cuándo se canceló. null si sigue vigente
paused_at / resumes_at Pausa de cobros y cuándo se reanuda (null = indefinida)
is_active / is_inactive Si sigue dando servicio
is_ended / is_expired Si terminó el período / si terminó sin gracia disponible
on_trial / on_discount Si está en prueba / con descuento vigente
production false en pruebas
webhook_url La URL a la que te estamos escribiendo
plan Objeto del plan
subscriber Objeto del suscriptor
next_charge Próximo cobro programado
office Sede

plan

Campo Descripción
name Nombre del plan
description Descripción
price Precio del ciclo
currency_type Moneda
invoice_period / invoice_interval Cada cuánto se cobra
trial_period / trial_interval Duración de la prueba
grace_period / grace_interval Duración de la gracia

subscriber

Campo Descripción
identification_type Tipo de documento
id_number Número de documento
name / last_name Nombre y apellido
email Correo
phone_code / cellphone_number Indicativo y celular
billing_address / billing_city / billing_country Datos de facturación

next_charge

Campo Descripción
amount Monto del próximo cobro
currency_type Moneda
next_renew_at Fecha del próximo cobro

transaction

Es el mismo objeto que devuelve el checkout por API: incluye transaction_id, amount, status, status_key, response_code, error, card y transaction_details.

status

Campo Descripción
key Identificador heredado del evento (ver catálogo)
value Nombre corto para mostrar (Renovado, Reintento de Pago…)
description Frase explicativa

Ejemplos por evento

renew — el cobro recurrente salió bien

{
    "meta": {
        "event_id": "9b1f0e34-6b2a-4a7e-9d0a-3c5f2b7a1d80",
        "event_type": "subscription.renewed",
        "timestamp": "2026-09-07T10:32:16-05:00",
        "api_version": "v1"
    },
    "subscription": {
        "id": "9ad70d0e-61e1-4c17-99c3-355b50d79954",
        "status": "activa",
        "starts_at": "2026-09-07 00:00:00",
        "ends_at": "2026-10-07 00:00:00",
        "grace_ends_at": "2026-10-10 00:00:00",
        "on_grace_period": false,
        "trial_ends_at": null,
        "canceled_at": null,
        "paused_at": null,
        "resumes_at": null,
        "is_active": true,
        "is_inactive": false,
        "on_trial": false,
        "on_discount": false,
        "canceled": false,
        "is_ended": false,
        "is_expired": false,
        "production": true,
        "webhook_url": "https://tu-sistema.com/webhooks/efipay",
        "plan": {
            "id": "9ae4dc2d-8b2e-4920-ac1f-80f2d648a2e6",
            "name": "Plan mensual",
            "price": 49900,
            "currency_type": "COP",
            "invoice_period": 1,
            "invoice_interval": "month"
        },
        "subscriber": {
            "id": "9ae92bbb-2cab-4569-85b1-6173d0fb9d4b",
            "identification_type": "CC",
            "id_number": "1020304050",
            "name": "Ana",
            "last_name": "Rojas",
            "email": "ana@correo.com"
        },
        "next_charge": {
            "amount": 49900,
            "currency_type": "COP",
            "next_renew_at": "2026-10-07"
        },
        "office": { "id": 1, "name": "Principal" }
    },
    "transaction": {
        "transaction_id": 90210,
        "amount": "49900.00",
        "currency_type": "COP",
        "payment_method": "credit",
        "payment_method_source": "Visa",
        "status": "Aprobada",
        "status_key": "approved",
        "response_code": "00",
        "error": null,
        "card": { "franchise": "Visa", "bin": "453210", "last_four": "7890" },
        "approved_at": "2026-09-07 10:32:16"
    },
    "status": {
        "key": "renew",
        "value": "Renovado",
        "description": "El pago ha sido exitoso, la suscripción fue renovada y seguirá activa"
    }
}

retry — el cobro falló y se reintentará

{
    "meta": { "event_id": "…", "event_type": "subscription.payment_retry", "timestamp": "…", "api_version": "v1" },
    "subscription": {
        "id": "9ad70d0e-61e1-4c17-99c3-355b50d79954",
        "status": "en_gracia",
        "ends_at": "2026-09-07 00:00:00",
        "grace_ends_at": "2026-09-10 00:00:00",
        "on_grace_period": true,
        "is_active": true
    },
    "transaction": {
        "transaction_id": 90211,
        "status": "Rechazada",
        "status_key": "rejected",
        "response_code": "51",
        "error": {
            "code": "51",
            "message": "Fondos insuficientes en la cuenta del tarjetahabiente.",
            "retryable": false,
            "action": "contact_issuer"
        }
    },
    "status": {
        "key": "retry",
        "value": "Reintento de Pago",
        "description": "El pago no fue exitoso, se volverá a intentar el pago, la suscripción sigue activa"
    }
}

{warning} on_grace_period: true con is_active: true significa sigue dando servicio, aunque el cobro haya fallado. No cortes el acceso todavía: espera a finished.

finished — se agotó la gracia sin cobrar

{
    "meta": { "event_id": "…", "event_type": "subscription.finished", "timestamp": "…", "api_version": "v1" },
    "subscription": {
        "id": "9ad70d0e-61e1-4c17-99c3-355b50d79954",
        "status": "finalizada",
        "on_grace_period": false,
        "is_active": false,
        "is_expired": true
    },
    "transaction": { "transaction_id": 90215, "status": "Rechazada", "status_key": "rejected" },
    "status": {
        "key": "finished",
        "value": "Suscripción Finalizada",
        "description": "La suscripción no pudo ser renovada, la suscripción esta inactiva, no se seguirán haciendo cobros"
    }
}

canceled — sin cobro, sin objeto transaction

{
    "meta": { "event_id": "…", "event_type": "subscription.canceled", "timestamp": "…", "api_version": "v1" },
    "subscription": {
        "id": "9ad70d0e-61e1-4c17-99c3-355b50d79954",
        "status": "cancelada",
        "canceled_at": "2026-09-07 11:04:00",
        "ends_at": "2026-10-07 00:00:00",
        "is_active": true
    },
    "status": {
        "key": "canceled",
        "value": "Suscripción Cancelada",
        "description": "La suscripción ha sido cancelada por el comercio, permanecerá activa hasta el fin del período actual"
    }
}

{info} Fíjate en is_active: true con canceled_at ya puesto: está cancelada pero el cliente conserva el servicio hasta ends_at. Con at_period_end: false, ends_at sería igual a canceled_at y is_active vendría en false.

paused y resumed

{
    "meta": { "event_id": "…", "event_type": "subscription.paused", "timestamp": "…", "api_version": "v1" },
    "subscription": {
        "id": "9ad70d0e-61e1-4c17-99c3-355b50d79954",
        "status": "pausada",
        "paused_at": "2026-09-07 11:20:00",
        "resumes_at": "2026-12-01 00:00:00"
    },
    "status": {
        "key": "paused",
        "value": "Suscripción Pausada",
        "description": "Los cobros de la suscripción han sido pausados."
    }
}

plan_changed — con el plan anterior

{
    "meta": { "event_id": "…", "event_type": "subscription.plan_changed", "timestamp": "…", "api_version": "v1" },
    "subscription": {
        "id": "9ad70d0e-61e1-4c17-99c3-355b50d79954",
        "status": "activa",
        "plan": { "id": "9b0100aa-1111-2222-3333-444455556666", "name": "Plan anual", "price": 499000 }
    },
    "previous_plan_id": "9ae4dc2d-8b2e-4920-ac1f-80f2d648a2e6",
    "status": {
        "key": "plan_changed",
        "value": "Plan Cambiado",
        "description": "El plan de la suscripción fue cambiado con prorrateo."
    }
}

trial_will_end — aviso previo al primer cobro

{
    "meta": { "event_id": "…", "event_type": "subscription.trial_will_end", "timestamp": "…", "api_version": "v1" },
    "subscription": {
        "id": "9ad70d0e-61e1-4c17-99c3-355b50d79954",
        "status": "trial",
        "on_trial": true,
        "trial_ends_at": "2026-09-10 00:00:00",
        "next_charge": { "amount": 49900, "currency_type": "COP", "next_renew_at": "2026-09-10" }
    },
    "status": {
        "key": "trial_will_end",
        "value": "Prueba por finalizar",
        "description": "El período de prueba de la suscripción está por finalizar."
    }
}