Eventos de Cobros por API
Eventos de Cobros por API
Estos eventos corresponden a las suscripciones en modo Cobro por API, donde la organización decide cuándo y cuánto se cobra. No incluyen el objeto client: el evento gira alrededor de la solicitud de cobro que la propia organización creó.
| Evento | Descripción |
|---|---|
EXTERNAL_CHARGE_SUCCESS_EVENT | El banco confirmó el débito de una solicitud de cobro |
EXTERNAL_CHARGE_FAILED_EVENT | El banco rechazó el débito, o el cobro no pudo ejecutarse |
EXTERNAL_CHARGE_CANCELLED_EVENT | La solicitud se descartó sin llegar al banco |
Además, el evento de siempre SUBSCRIPTION_CREATE_EVENT llega con campos adicionales que te dicen si la suscripción nació en modo Cobro por API, si el primer ciclo ya se cobró, y de qué link salió. Ese evento sí incluye el objeto client.
Valores de estado
| Objeto | Campo | Valores Posibles |
|---|---|---|
charge | status | completed, failed, canceled (los estados scheduled y processing no llegan por webhook: no son un desenlace) |
EXTERNAL_CHARGE_SUCCESS_EVENT
Se envía cuando el banco confirma el débito de una solicitud de cobro creada con POST /v1/subscriptions/{id}/charges. La solicitud pasa a completed.
Puede llegar días después del cobro: los gateways liquidan de forma asíncrona y la confirmación puede venir por conciliación. Este evento es la única notificación de ese desenlace, y llega una sola vez por solicitud.
{
"eventType": "EXTERNAL_CHARGE_SUCCESS_EVENT",
"livemode": true,
"charge": {
"id": "8f14e45f-ceea-467a-9c2b-3a1b2c3d4e5f",
"status": "completed",
"amount": "47.50",
"currency": "USD",
"chargeDate": "2026-09-05",
"subscriptionId": "550e8400-e29b-41d4-a716-446655440001",
"invoiceId": "660e8400-e29b-41d4-a716-446655440002",
"transactionId": "770e8400-e29b-41d4-a716-446655440003",
"failureReason": null,
"metadata": { "invoice_number": "F-2026-0154" },
"retryPolicy": "none",
"totalCharged": "1972.75",
"exchangeRate": "35.8500"
}
}Campos del evento
| Campo | Tipo | Descripción |
|---|---|---|
eventType | string | El tipo de evento |
livemode | boolean | true cuando el cobro es real. Úsalo para separar los eventos de prueba de tu contabilidad |
charge | object | La solicitud de cobro, con los campos de abajo |
Campos de charge
| Campo | Tipo | Descripción |
|---|---|---|
id | string (UUID) | ID de la solicitud de cobro — el mismo que devolvió la llamada de creación |
status | string | completed, failed o canceled. Corresponde al tipo de evento |
amount | string | Monto solicitado, como texto y en la moneda que dice currency: "47.50" son 47,50 de esa moneda (nunca centavos) |
currency | string | USD o VES, tal como se creó la solicitud. Con USD el monto es neto, antes de IVA, y la factura puede totalizar más; con VES es el débito exacto en bolívares de ese día, impuestos incluidos |
chargeDate | string (YYYY-MM-DD) | Día en que correspondía ejecutar el cobro, en hora de Venezuela |
subscriptionId | string (UUID) | Suscripción cobrada |
invoiceId | string | null | Factura generada. null si el cobro nunca llegó a facturarse |
transactionId | string | null | Transacción que llevó el débito. null si no se intentó ningún débito |
failureReason | string | null | Motivo del rechazo o de la cancelación. Siempre null en este evento |
metadata | object | null | Los pares clave-valor que enviaste al crear la solicitud, devueltos tal cual |
retryPolicy | string | none o automatic, tal como se creó la solicitud. Te dice qué significa este evento: con none el cobro terminó y reintentar es decisión tuya (una solicitud nueva); con automatic Venflow ya agotó por su cuenta todos los reintentos que tu organización tiene configurados, y no va a intentar nada más sobre ESTA solicitud |
totalCharged | string | null | Lo que realmente se debitó, en bolívares (VES), leído de la factura. Es el número que vas a ver en tu banco. null si el cobro nunca llegó a facturarse |
exchangeRate | string | null | Tasa BCV (USD→VES) con la que se convirtió esa factura. null en los mismos casos que totalCharged |
amountes lo que pediste;totalChargedes lo que se movió. Concurrency: "USD"el monto es neto: si tu plan agrega IVA, pedir"47.50"debita más, y en bolívares. Para cuadrar contra tu banco usatotalCharged, que ya viene en el evento — no hace falta una llamada extra ni leer elPAYMENT_SUCCESS_EVENT. Concurrency: "VES",amountytotalChargedcoinciden: es el débito exacto que pediste.
EXTERNAL_CHARGE_FAILED_EVENT
Se envía cuando una solicitud de cobro termina en failed: el banco rechazó el débito (fondos insuficientes, cuenta inválida) o el cobro no pudo ejecutarse (gateway sin credenciales, organización deshabilitada, reintentos internos agotados).
{
"eventType": "EXTERNAL_CHARGE_FAILED_EVENT",
"livemode": true,
"charge": {
"id": "8f14e45f-ceea-467a-9c2b-3a1b2c3d4e5f",
"status": "failed",
"amount": "47.50",
"currency": "USD",
"chargeDate": "2026-09-05",
"subscriptionId": "550e8400-e29b-41d4-a716-446655440001",
"invoiceId": "660e8400-e29b-41d4-a716-446655440002",
"transactionId": "770e8400-e29b-41d4-a716-446655440003",
"failureReason": "Insufficient funds",
"metadata": { "invoice_number": "F-2026-0154" },
"retryPolicy": "none",
"totalCharged": "1972.75",
"exchangeRate": "35.8500"
}
}
failureReasonsiempre trae texto en este evento.invoiceIdytransactionIdvienen ennullcuando el cobro falló antes de llegar al banco — por ejemplo, si el gateway de la organización no está configurado.
Cuándo llega, según retry_policy
La solicitud se resuelve una sola vez, y el momento depende del retry_policy con el que la creaste:
retry_policy | Cuándo llega EXTERNAL_CHARGE_FAILED_EVENT |
|---|---|
none (predeterminado) | Apenas se conoce el rechazo: un solo intento, y la solicitud queda failed de una vez. Igual que el evento de éxito, puede llegar días después si el gateway liquida de forma asíncrona |
automatic | Solo si se agotan todos los reintentos de tu organización, varios días después del primer rechazo. Si algún reintento se cobra, en su lugar recibes EXTERNAL_CHARGE_SUCCESS_EVENT y este evento nunca llega |
Ningún reintento intermedio produce un evento
EXTERNAL_CHARGE_*. Conautomatic, mientras queden reintentos la solicitud sigue enprocessingy no se emite nada de esta página. Lo que sí llega por cada intento fallido es el evento de siemprePAYMENT_FAILED_EVENTde la factura, cuyo camponextPaymentAttempttrae la fecha del próximo intento. Connoneese campo llega ennull, porque no habrá otro intento.
Los reintentos cobran lo mismo que pediste. El monto y la moneda de la solicitud se mantienen intactos en cada intento:
automaticno vuelve a calcular el precio a partir del plan.
Venflow nunca reprograma un cobro por su cuenta. Con
none, y también cuandoautomaticse queda sin reintentos, la solicitud quedafailedpara siempre y la suscripción sigue activa: no se cancela ni cambia de estado. Volver a cobrar es crear una solicitud nueva, con una clave de idempotencia nueva, cuando tú lo decidas.
EXTERNAL_CHARGE_CANCELLED_EVENT
Se envía cuando el motor descarta una solicitud que estaba programada, porque la suscripción a la que apuntaba ya no puede recibir cobros: se canceló, se pausó, o dejó de estar en modo Cobro por API antes de la fecha.
{
"eventType": "EXTERNAL_CHARGE_CANCELLED_EVENT",
"livemode": true,
"charge": {
"id": "8f14e45f-ceea-467a-9c2b-3a1b2c3d4e5f",
"status": "canceled",
"amount": "3375.00",
"currency": "VES",
"chargeDate": "2026-10-01",
"subscriptionId": "550e8400-e29b-41d4-a716-446655440001",
"invoiceId": null,
"transactionId": null,
"failureReason": "Subscription is cancelled, not active — the charge was not attempted",
"metadata": { "invoice_number": "F-2026-0154" },
"retryPolicy": "none",
"totalCharged": null,
"exchangeRate": null
}
}Cancelar tú mismo un cobro con
DELETE /v1/charges/{id}NO emite este evento. Esa cancelación es síncrona: la respuesta ya te dice que quedócanceled. El evento existe solo para las cancelaciones que decide el motor, que son las que no puedes ver venir.
SUBSCRIPTION_CREATE_EVENT para Cobros por API
Este es el evento con el que te enteras de que la suscripción existe, y en modo Cobro por API es la única señal que produce un checkout sin cobro inicial: nada más se emite, porque no se pagó nada. De él sale el subscription.id que necesitas para pedir cobros con POST /v1/subscriptions/{id}/charges.
Al evento SUBSCRIPTION_CREATE_EVENT se le agregan cuatro campos. Llegan siempre, en cualquier suscripción y por cualquier flujo de checkout: una suscripción normal reporta collectionMode: "engine". No tienes que adivinar si el campo va a estar según el caso.
Campos nuevos
| Campo | Tipo | Descripción |
|---|---|---|
subscription.collectionMode | string | external (Cobro por API: tú pides cada cobro) o engine (Venflow cobra sola, según el plan) |
subscription.externalInitialCharge | boolean | true solo si el checkout ya cobró el primer ciclo. Si es true, no vuelvas a cobrar ese ciclo por el API. Siempre false en engine |
session | object | La sesión de checkout que dio origen a la suscripción: id (UUID) y metadata (object | null), mismo formato que en PAYMENT_SUCCESS_EVENT |
universalLinkId | string | null | El link universal por el que entró el cliente. null si la sesión se creó directamente, sin link |
Suscripción externa, sin cobro inicial
El caso normal de Cobro por API: el cliente afilió su cuenta, la suscripción quedó activa y no se cobró nada. El primer cobro lo pides tú.
{
"eventType": "SUBSCRIPTION_CREATE_EVENT",
"client": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "cliente@email.com",
"name": "Juan",
"lastName": "Pérez"
},
"subscription": {
"id": "550e8400-e29b-41d4-a716-446655440001",
"status": "active",
"externalId": "EXT123456",
"collectionMode": "external",
"externalInitialCharge": false
},
"session": {
"id": "660e8400-e29b-41d4-a716-446655440000",
"metadata": {
"order_id": "ORD-12345",
"campaign": "summer_promo"
}
},
"universalLinkId": "770e8400-e29b-41d4-a716-446655440009"
}Suscripción externa con el primer cobro en el checkout
El link tenía activada la opción "Cobrar el primer pago en el checkout" y el cobro salió bien. El primer ciclo ya está pagado, con su factura y su transacción; del segundo en adelante, los cobros los pides tú.
{
"eventType": "SUBSCRIPTION_CREATE_EVENT",
"client": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "cliente@email.com",
"name": "Juan",
"lastName": "Pérez"
},
"subscription": {
"id": "550e8400-e29b-41d4-a716-446655440001",
"status": "active",
"externalId": "EXT123456",
"collectionMode": "external",
"externalInitialCharge": true
},
"session": {
"id": "660e8400-e29b-41d4-a716-446655440000",
"metadata": {
"order_id": "ORD-12345"
}
},
"universalLinkId": "770e8400-e29b-41d4-a716-446655440009"
}Con cobro inicial recibes el evento dos veces, igual que en cualquier checkout con pago: primero con
status: "pending"al iniciarse el cobro, y después constatus: "active"cuando el banco lo confirma. Los dos traen los campos nuevos con los mismos valores. Espera elactiveantes de dar la suscripción por buena: si el cobro falla, elactivenunca llega y la suscripción no se activa.
El esquema es uno solo. El payload de un checkout externo con cobro inicial y el de uno sin cobro inicial tienen exactamente los mismos campos; lo único que cambia es el valor de
externalInitialCharge.
Suscripción normal (modo engine)
Los mismos campos viajan en las suscripciones que cobra Venflow. Aquí la sesión se creó directamente con POST /v1/checkout/sessions, sin link universal, y por eso universalLinkId es null.
{
"eventType": "SUBSCRIPTION_CREATE_EVENT",
"client": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "cliente@email.com",
"name": "Juan",
"lastName": "Pérez"
},
"subscription": {
"id": "550e8400-e29b-41d4-a716-446655440001",
"status": "active",
"externalId": "EXT123456",
"collectionMode": "engine",
"externalInitialCharge": false
},
"session": {
"id": "660e8400-e29b-41d4-a716-446655440000",
"metadata": null
},
"universalLinkId": null
}Cómo correlacionar el evento con tu propio registro
Tienes tres identificadores y cada uno sirve para algo distinto:
| Identificador | Para qué te sirve |
|---|---|
universalLinkId | Es el id que tú conoces, porque el link lo creaste tú. Si generas un link por cliente, este campo te dice de cuál de ellos salió la suscripción |
session.id | La sesión que se generó al abrir el link. Un link universal no tiene sesión propia: cada cliente que lo abre estrena una. Úsalo para cruzar con los eventos de pago (PAYMENT_SUCCESS_EVENT trae el mismo session.id) |
session.metadata | Los pares clave-valor que pusiste al crear la sesión, devueltos tal cual. Es la vía directa para pegar la suscripción a tu número de orden o de contrato. Las sesiones generadas por un link universal no llevan metadata tuya: para correlacionar un link, usa universalLinkId |
El payload es liviano a propósito. No trae el plan, el período, ni la factura del primer cobro: con
subscription.iden la mano,GET /v1/subscriptions/{id}te da el detalle completo cuando lo necesites. Ese endpoint también devuelvecollection_mode, así que si pierdes un webhook puedes confirmar por API si la suscripción es de Cobro por API.