Venflow API

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

EventoDescripción
EXTERNAL_CHARGE_SUCCESS_EVENTEl banco confirmó el débito de una solicitud de cobro
EXTERNAL_CHARGE_FAILED_EVENTEl banco rechazó el débito, o el cobro no pudo ejecutarse
EXTERNAL_CHARGE_CANCELLED_EVENTLa 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

ObjetoCampoValores Posibles
chargestatuscompleted, 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.

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

CampoTipoDescripción
eventTypestringEl tipo de evento
livemodebooleantrue cuando el cobro es real. Úsalo para separar los eventos de prueba de tu contabilidad
chargeobjectLa solicitud de cobro, con los campos de abajo

Campos de charge

CampoTipoDescripción
idstring (UUID)ID de la solicitud de cobro — el mismo que devolvió la llamada de creación
statusstringcompleted, failed o canceled. Corresponde al tipo de evento
amountstringMonto solicitado, como texto y en la moneda que dice currency: "47.50" son 47,50 de esa moneda (nunca centavos)
currencystringUSD 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
chargeDatestring (YYYY-MM-DD)Día en que correspondía ejecutar el cobro, en hora de Venezuela
subscriptionIdstring (UUID)Suscripción cobrada
invoiceIdstring | nullFactura generada. null si el cobro nunca llegó a facturarse
transactionIdstring | nullTransacción que llevó el débito. null si no se intentó ningún débito
failureReasonstring | nullMotivo del rechazo o de la cancelación. Siempre null en este evento
metadataobject | nullLos pares clave-valor que enviaste al crear la solicitud, devueltos tal cual
retryPolicystringnone 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
totalChargedstring | nullLo 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
exchangeRatestring | nullTasa BCV (USD→VES) con la que se convirtió esa factura. null en los mismos casos que totalCharged

amount es lo que pediste; totalCharged es lo que se movió. Con currency: "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 usa totalCharged, que ya viene en el evento — no hace falta una llamada extra ni leer el PAYMENT_SUCCESS_EVENT. Con currency: "VES", amount y totalCharged coinciden: 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).

Payload
{
  "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"
  }
}

failureReason siempre trae texto en este evento. invoiceId y transactionId vienen en null cuando 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_policyCuá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
automaticSolo 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_*. Con automatic, mientras queden reintentos la solicitud sigue en processing y no se emite nada de esta página. Lo que sí llega por cada intento fallido es el evento de siempre PAYMENT_FAILED_EVENT de la factura, cuyo campo nextPaymentAttempt trae la fecha del próximo intento. Con none ese campo llega en null, 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: automatic no vuelve a calcular el precio a partir del plan.

Venflow nunca reprograma un cobro por su cuenta. Con none, y también cuando automatic se queda sin reintentos, la solicitud queda failed para 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.

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

CampoTipoDescripción
subscription.collectionModestringexternal (Cobro por API: tú pides cada cobro) o engine (Venflow cobra sola, según el plan)
subscription.externalInitialChargebooleantrue 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
sessionobjectLa sesión de checkout que dio origen a la suscripción: id (UUID) y metadata (object | null), mismo formato que en PAYMENT_SUCCESS_EVENT
universalLinkIdstring | nullEl 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ú.

Payload
{
  "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ú.

Payload
{
  "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 con status: "active" cuando el banco lo confirma. Los dos traen los campos nuevos con los mismos valores. Espera el active antes de dar la suscripción por buena: si el cobro falla, el active nunca 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.

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

IdentificadorPara qué te sirve
universalLinkIdEs 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.idLa 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.metadataLos 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.id en la mano, GET /v1/subscriptions/{id} te da el detalle completo cuando lo necesites. Ese endpoint también devuelve collection_mode, así que si pierdes un webhook puedes confirmar por API si la suscripción es de Cobro por API.