Factuarea API

Bandeja de eventos de integración

Por qué un cobro no acabó en factura — los motivos de descarte tipados, cuáles te avisan, cuáles parquean el evento para que puedas reprocesarlo, y cómo funciona la ventana de retención de 30 días.

Una pasarela de pago envía a Factuarea un evento por todo lo que ocurre en tu cuenta: un cobro con éxito, una devolución emitida, un ciclo de suscripción cobrado, un payout que llega. La mayoría de esos eventos producen algo — una factura, una rectificativa, un registro de pago. Algunos no producen nada, y cuando eso pasa la pregunta interesante siempre es la misma: ¿por qué este cobro no acabó en factura?

La bandeja de eventos de integración la contesta. Cada evento que Factuarea recibe queda registrado con lo que produjo y, cuando no produjo nada, con un motivo de descarte tipado salido de un catálogo cerrado. Sin adivinar en los logs, sin abrir un ticket de soporte: el motivo es un valor por el que puedes filtrar y, en los motivos sobre los que puedes actuar, viene con el siguiente paso y, a veces, con la posibilidad de reprocesar el evento.

La bandeja es agnóstica de la pasarela. Registra eventos de cualquier integración que escriba historial — incluidas las pasarelas que todavía no están liberadas y los eventos históricos de una que se retire — porque ocultar esas filas te dejaría sin explicación para cobros que nunca se facturaron.

La exponen tres endpoints:

OperaciónEndpointScope
Listar eventosGET /v1/integrations/eventsintegration_events:read
Obtener un eventoGET /v1/integrations/events/{event}integration_events:read
Reprocesar un evento parqueadoPOST /v1/integrations/events/{event}/replayintegration_events:write

La misma superficie existe como tools MCP — list_integrations_events, get_integrations_event y replay_integrations_event — con los mismos scopes.

Recorrer la bandeja

Del más reciente al más antiguo, acotado a la empresa autenticada. Paginación por cursor con limit (de 1 a 100; 25 por defecto) y starting_after:

curl -G https://api.factuarea.com/v1/integrations/events \
  -H "Authorization: Bearer $FACTUAREA_API_KEY" \
  --data-urlencode "provider=stripe" \
  --data-urlencode "status=skipped" \
  --data-urlencode "limit=50"
{
  "data": [
    {
      "id": "0192f3a4-7b2c-7c1d-9e8f-1a2b3c4d5e6f",
      "object": "integration_event",
      "provider": "stripe",
      "event_type": "invoice.paid",
      "direction": "inbound",
      "status": "skipped",
      "discard_reason": "subscription_autoinvoicing_disabled",
      "discard_reason_label": "Auto-facturación de suscripciones desactivada",
      "is_actionable": true,
      "is_replayable": true,
      "error_message": null,
      "duration_ms": 412,
      "created_at": "2026-07-14T09:31:07Z"
    }
  ],
  "has_more": true,
  "next_cursor": "84120"
}

Trata next_cursor como opaco: en este listado es una cadena numérica, no un UUID v7 como los cursores de los listados de documentos. Devuélvelo tal cual en starting_after.

discard_reason_label llega siempre en español, el idioma de la interfaz del producto, sea cual sea el idioma de tu integración. Si construyes un panel en otro idioma, apoya tus propios textos en discard_reason — ese valor es el identificador estable y cerrado.

Filtros

FiltroValoresNotas
providerstripe, gocardless, monei, slack, teams, a3, norma43, norma19, ublConjunto cerrado
statussuccess, skipped, failureConjunto cerrado
event_typetexto libre, coincidencia exacta, hasta 100 caracteresNo es un enum — ver abajo
discard_reasonuno de los veinte motivos del catálogoConjunto cerrado
is_parkedtrue / falseVer la nota de abajo
created_at[gte], created_at[lte]ISO 8601Ventana inclusiva

discard_reason es el eje cerrado; event_type no es un enum. La columna event_type mezcla a propósito dos convenciones: las ramas instrumentadas más tarde guardan el tipo crudo de la pasarela (charge.refunded), mientras que las preexistentes conservan su propio valor semántico (autoinvoice.*). Búscalo por coincidencia exacta cuando sepas qué persigues, pero no lo modeles nunca como un conjunto cerrado — estarías modelando algo que la columna no garantiza.

is_parked=false no es lo mismo que omitir el parámetro. El primero excluye los eventos parqueados; el segundo no excluye nada.

Un valor fuera de su catálogo devuelve 422, y un parámetro de consulta desconocido devuelve 400 parameter_unknown en lugar de ignorarse en silencio — un filtro que se cae sin avisar te entrega una página que crees acotada y no lo está.

El catálogo de motivos de descarte

Veinte motivos tipados, uno por cada rama de descarte del pipeline de webhooks de las pasarelas. Cada uno declara dos decisiones de negocio que no son banderas decorativas:

  • Accionable — ¿puede hacer algo el titular de la cuenta? Solo los motivos accionables avisan. Avisar a alguien de un descarte que no puede resolver le enseña a ignorar la bandeja, y así es como se pierde el aviso que sí importaba.
  • Parqueado — ¿reprocesar el mismo contenido podría dar otro resultado? Solo los eventos parqueados guardan su contenido cifrado y admiten un reproceso.

La regla detrás de la columna de parqueo: un evento se parquea cuando el descarte lo causó un estado externo que puedes cambiar (un ajuste apagado, una cuenta conectada que se desvinculó, una moneda todavía sin tipo de cambio). No se parquea cuando la causa es el contenido del propio evento (malformado, duplicado, de tipo no cubierto, importe cero, ciclo ya facturado) — reprocesarlo tomaría exactamente la misma rama y solo escribiría una segunda fila. De ahí el invariante: todo motivo parqueado es accionable, y seis de los nueve accionables se parquean.

MotivoQué lo provocaAccionableParqueadoQué hacer
event_not_normalizableEvento malformado, o de un tipo que no se puede interpretarNoNoNada — no puedes arreglar el payload de la pasarela
duplicate_redeliveryEl evento ya se procesó; su efecto existeNoNoNada — reprocesarlo sería un no-op por deduplicación
connected_account_missingEl webhook está mal configurado en la pasarela: el evento no dice a qué cuenta perteneceNoRevisa en la pasarela que el webhook se envía desde la cuenta que tienes vinculada en Factuarea
connected_account_unknownLa cuenta existe en la pasarela pero no está vinculada en FactuareaVuelve a vincular esa cuenta de la pasarela y reprocesa el evento
spontaneous_payment_missing_idEl cobro no trae id, así que no hay clave de idempotenciaNoNoNada — reprocesarlo duplicaría o volvería a fallar
autoinvoicing_disabledLa auto-facturación está desactivada para esa integraciónActiva la auto-facturación y reprocesa, o crea la factura a mano — nunca las dos cosas
unsupported_currencyEl tipo de cambio del Banco Central Europeo del día todavía no está disponibleReprocesa el evento más tarde, cuando el tipo oficial del día esté publicado
refund_without_itemsLa devolución no trae reembolsos individuales que rectificarNoNoNada — no hay nada que emitir
refund_autoinvoicing_disabledLa rectificativa automática está desactivada para esa integraciónActiva la rectificativa automática y reprocesa, o emite la rectificativa a mano — nunca las dos cosas
subscription_missing_invoice_idEl ciclo cobrado no tiene identificador de facturaNoCrea a mano la factura de este ciclo; reprocesar daría el mismo resultado
subscription_proration_reviewSe cobró un prorrateo suelto y exige una decisión humanaNoComprueba el importe del prorrateo en la pasarela y emite la factura a mano
subscription_not_a_cycleLa factura de la pasarela no corresponde a un ciclo de suscripción facturableNoNoNada — el descarte es correcto
subscription_trial_skippedImporte cero o negativo (prueba o crédito): no hay base imponibleNoNoNada — no hay nada que facturar
subscription_autoinvoicing_disabledLa auto-facturación de suscripciones está desactivadaActiva la auto-facturación de suscripciones y reprocesa el evento
subscription_already_invoicedEl ciclo ya tiene su facturaNoNoNada — reprocesarlo sería un no-op por idempotencia
payout_missing_idEl payout no trae identificadorNoNoNada — no se puede conciliar ni reprocesar con seguridad
payout_connected_account_missingLa cuenta conectada del payout no está vinculadaVincula la cuenta conectada y reprocesa el evento
payment_failedEl cobro falló en la pasarelaNoNoNada — no hay nada que emitir ni que reintentar
event_type_not_coveredTipo de evento fuera del alcance del productoNoNoNada — reprocesarlo volvería a no hacer nada
checkout_lines_retrieve_failedDegradación, no descarte: la factura se emitió, con una línea únicaNoNoNada que reprocesar; revisa las líneas de la factura si te importa el desglose

Un motivo sin nada que hacer lo dice explícitamente. Once de los veinte son informativos, y el contrato no se inventa una instrucción para ellos: el endpoint de detalle devuelve recommended_action: null en lugar de una frase fabricada para rellenar el campo.

«O una, o la otra» significa una, no las dos. Dos motivos te ofrecen dos salidas — activar el ajuste y reprocesar, o emitir el documento a mano. Son excluyentes. La idempotencia del reproceso va por la identidad del cobro y solo reconoce los documentos emitidos por esa misma vía automática, así que una factura que hayas creado a mano no lo frena. Hacer las dos cosas deja el mismo cobro con dos facturas, cada una numerada en su serie y dada de alta en VeriFactu — un daño fiscal que solo se deshace con una rectificativa.

Avisos: solo lo que puedes arreglar

Un descarte accionable avisa a los administradores de la cuenta. Uno informativo no avisa nunca.

El aviso lleva throttling: si ya existe un aviso sin leer de la misma empresa, la misma pasarela y el mismo motivo dentro de las últimas 24 horas, no se crea un segundo — un webhook mal configurado dispara cientos de eventos idénticos. La condición es sin leer a propósito: una vez lo has leído, si siguen llegando descartes, el siguiente avisa. Eso no es ruido, significa que la incidencia sigue viva.

El parqueo y la ventana de 30 días

Cuando un motivo es parqueable, Factuarea guarda el evento crudo cifrado en reposo, para poder reprocesarlo más tarde. Ese contenido nunca se devuelve por la API — ni en el listado, ni en el detalle. Contiene datos personales de tus clientes finales y datos de pago, y existe para exactamente un propósito: hacer posible el reproceso.

El contenido se purga a los 30 días de parquearse el evento. La fila sobrevive: su motivo, su estado, su fecha y su marca is_parked siguen en tu bandeja indefinidamente, porque el registro de que un cobro no produjo factura es historial que puedes necesitar mucho después de que el contenido caduque.

Un evento que sigue con is_parked: true pero ya no es is_replayable significa exactamente una cosa: la ventana de retención se agotó. La marca se deriva de si el contenido sigue ahí, así que cambia sola el día que corre la purga. Un reproceso intentado después devuelve 422 con el subcódigo integration_event_payload_purged.

El detalle: qué hacer a continuación

El endpoint de detalle devuelve todo lo del listado, más recommended_action: una frase en imperativo con el siguiente paso para ese motivo concreto, o null cuando el motivo es informativo.

curl https://api.factuarea.com/v1/integrations/events/0192f3a4-7b2c-7c1d-9e8f-1a2b3c4d5e6f \
  -H "Authorization: Bearer $FACTUAREA_API_KEY"

La frase distingue a propósito los motivos reproducibles («… y reprocesa el evento») de los que no lo son («… emítela a mano»), de modo que nunca te apunta a una operación que respondería 422.

Un evento de otra empresa y un evento que no existe devuelven el mismo 404 resource_not_found. El endpoint nunca revela si un id existe en otro sitio.

Reprocesar un evento parqueado

Vuelve a procesar un evento de la pasarela que quedó parqueado, una vez que ya no está la causa que le impidió producir su efecto — has reactivado la auto-facturación, has vuelto a vincular la cuenta conectada, ya está disponible el tipo de cambio oficial del día.

Esta acción puede tener consecuencias fiscales reales. Si la causa del descarte ya está resuelta, el reproceso puede emitir una factura real, con su número de serie y su alta en VeriFactu. No es un reintento inocuo: confírmalo con el titular de la cuenta antes de llamarlo. Por eso lleva su propio scope de escritura, integration_events:write, en lugar del scope de lectura de la bandeja — una credencial de solo lectura no debe poder facturar jamás.

curl -X POST https://api.factuarea.com/v1/integrations/events/0192f3a4-7b2c-7c1d-9e8f-1a2b3c4d5e6f/replay \
  -H "Authorization: Bearer $FACTUAREA_API_KEY"

Cuatro propiedades de esta operación importan más que su firma:

  • No duplica facturas. El reproceso pasa por el mismísimo control de idempotencia que el intento original, así que si ese cobro ya produjo una factura, el job se detiene solo y no crea nada.
  • Es asíncrono. 202 significa aceptado y encolado, no completado. El cuerpo devuelve el evento tal como está ahora — su is_replayable sigue siendo true —, no el resultado del reintento. El resultado aparece como un evento nuevo en la bandeja, así que consulta GET /v1/integrations/events para ver cómo acabó.
  • Si la causa sigue presente, el evento se descarta otra vez y se registra de nuevo. Es correcto, y es observable.
  • No admite entrada. Cualquier parámetro de consulta o clave del cuerpo devuelve 400 parameter_unknown en lugar de ignorarse. Enviar uno significa que crees estar configurando algo del reintento — un modo, una serie, una fecha — que esta operación no soporta, y aceptarlo en silencio confirmaría esa expectativa falsa sobre una acción que puede emitir una factura. Un cuerpo vacío, o directamente ningún cuerpo, es el caso normal.

Cuando se rechaza un reproceso

is_replayable: true es el contrato: cuando vale true, el reproceso no responde 422. Es la conjunción de tres condiciones — el evento está parqueado, todavía conserva su contenido y su motivo admite reproceso —, evaluadas en ese mismo orden por el propio handler que guarda el reproceso. Eso es lo que te permite ofrecer un botón de reintento sin adivinar.

Los tres rechazos devuelven 422 business_rule_violation y te dicen cuál es a través del subcode:

subcodeQué significa¿Hay salida?
integration_event_not_parkedEl evento nunca se parqueó — o tuvo éxito, o su motivo no guarda el contenidoNo, y nunca la habrá
integration_event_payload_purgedSe parqueó, pero su contenido se borró al agotarse la ventana de 30 díasNo — resuélvelo a mano
integration_event_reason_not_replayableEstá parqueado y conserva su contenido, pero su motivo volvería a tomar exactamente la misma ramaNo — sigue en su lugar la acción recomendada

En esta página