Factuarea API

Safata d'esdeveniments d'integració

Per què un cobrament no va acabar en factura — els motius de descart tipats, quins t'avisen, quins aparquen l'esdeveniment perquè el puguis reprocessar, i com funciona la finestra de retenció de 30 dies.

Una passarel·la de pagament envia a Factuarea un esdeveniment per tot el que passa al teu compte: un cobrament amb èxit, una devolució emesa, un cicle de subscripció cobrat, un payout que arriba. La majoria d'aquests esdeveniments produeixen alguna cosa — una factura, una rectificativa, un registre de pagament. Alguns no produeixen res, i quan això passa la pregunta interessant sempre és la mateixa: per què aquest cobrament no va acabar en factura?

La safata d'esdeveniments d'integració la respon. Cada esdeveniment que Factuarea rep queda registrat amb allò que va produir i, quan no va produir res, amb un motiu de descart tipat sortit d'un catàleg tancat. Sense endevinar als logs, sense obrir un tiquet de suport: el motiu és un valor pel qual pots filtrar i, en els motius sobre els quals pots actuar, ve amb el pas següent i, de vegades, amb la possibilitat de reprocessar l'esdeveniment.

La safata és agnòstica de la passarel·la. Registra esdeveniments de qualsevol integració que escrigui historial — incloses les passarel·les que encara no estan alliberades i els esdeveniments històrics d'una que es retiri — perquè amagar aquestes files et deixaria sense explicació per a cobraments que no es van facturar mai.

L'exposen tres endpoints:

OperacióEndpointScope
Llistar esdevenimentsGET /v1/integrations/eventsintegration_events:read
Obtenir un esdevenimentGET /v1/integrations/events/{event}integration_events:read
Reprocessar un esdeveniment aparcatPOST /v1/integrations/events/{event}/replayintegration_events:write

La mateixa superfície existeix com a tools MCP — list_integrations_events, get_integrations_event i replay_integrations_event — amb els mateixos scopes.

Recórrer la safata

Del més recent al més antic, acotat a l'empresa autenticada. Paginació per cursor amb limit (d'1 a 100; 25 per defecte) i 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"
}

Tracta next_cursor com a opac: en aquest llistat és una cadena numèrica, no un UUID v7 com els cursors dels llistats de documents. Torna'l tal qual a starting_after.

discard_reason_label arriba sempre en castellà, l'idioma de la interfície del producte, sigui quin sigui l'idioma de la teva integració. Si construeixes un panell en un altre idioma, basa els teus propis textos en discard_reason — aquest valor és l'identificador estable i tancat.

Filtres

FiltreValorsNotes
providerstripe, gocardless, monei, slack, teams, a3, norma43, norma19, ublConjunt tancat
statussuccess, skipped, failureConjunt tancat
event_typetext lliure, coincidència exacta, fins a 100 caràctersNo és un enum — vegeu més avall
discard_reasonun dels vint motius del catàlegConjunt tancat
is_parkedtrue / falseVegeu la nota de més avall
created_at[gte], created_at[lte]ISO 8601Finestra inclusiva

discard_reason és l'eix tancat; event_type no és un enum. La columna event_type barreja a propòsit dues convencions: les branques instrumentades més tard guarden el tipus cru de la passarel·la (charge.refunded), mentre que les preexistents conserven el seu propi valor semàntic (autoinvoice.*). Busca'l per coincidència exacta quan sàpigues què persegueixes, però no el modelis mai com un conjunt tancat — estaries modelant una cosa que la columna no garanteix.

is_parked=false no és el mateix que ometre el paràmetre. El primer exclou els esdeveniments aparcats; el segon no exclou res.

Un valor fora del seu catàleg retorna 422, i un paràmetre de consulta desconegut retorna 400 parameter_unknown en comptes d'ignorar-se en silenci — un filtre que cau sense avisar et lliura una pàgina que creus acotada i no ho està.

El catàleg de motius de descart

Vint motius tipats, un per cada branca de descart del pipeline de webhooks de les passarel·les. Cadascun declara dues decisions de negoci que no són banderes decoratives:

  • Accionable — pot fer-hi alguna cosa el titular del compte? Només els motius accionables avisen. Avisar algú d'un descart que no pot resoldre li ensenya a ignorar la safata, i així és com es perd l'avís que sí que importava.
  • Aparcat — reprocessar el mateix contingut podria donar un altre resultat? Només els esdeveniments aparcats guarden el seu contingut xifrat i admeten un reprocessament.

La regla que hi ha darrere de la columna d'aparcament: un esdeveniment s'aparca quan el descart el va causar un estat extern que pots canviar (un ajust apagat, un compte connectat que es va desvincular, una moneda encara sense tipus de canvi). No s'aparca quan la causa és el contingut del mateix esdeveniment (mal format, duplicat, de tipus no cobert, import zero, cicle ja facturat) — reprocessar-lo prendria exactament la mateixa branca i només escriuria una segona fila. D'aquí l'invariant: tot motiu aparcat és accionable, i sis dels nou accionables s'aparquen.

MotiuQuè el provocaAccionableAparcatQuè fer
event_not_normalizableEsdeveniment mal format, o d'un tipus que no es pot interpretarNoNoRes — no pots arreglar el payload de la passarel·la
duplicate_redeliveryL'esdeveniment ja es va processar; el seu efecte existeixNoNoRes — reprocessar-lo seria un no-op per deduplicació
connected_account_missingEl webhook està mal configurat a la passarel·la: l'esdeveniment no diu a quin compte pertanyNoRevisa a la passarel·la que el webhook s'envia des del compte que tens vinculat a Factuarea
connected_account_unknownEl compte existeix a la passarel·la però no està vinculat a FactuareaTorna a vincular aquest compte de la passarel·la i reprocessa l'esdeveniment
spontaneous_payment_missing_idEl cobrament no porta id, així que no hi ha clau d'idempotènciaNoNoRes — reprocessar-lo duplicaria o tornaria a fallar
autoinvoicing_disabledL'auto-facturació està desactivada per a aquesta integracióActiva l'auto-facturació i reprocessa, o crea la factura a mà — mai les dues coses
unsupported_currencyEl tipus de canvi del Banc Central Europeu del dia encara no està disponibleReprocessa l'esdeveniment més tard, quan el tipus oficial del dia estigui publicat
refund_without_itemsLa devolució no porta reemborsaments individuals que rectificarNoNoRes — no hi ha res a emetre
refund_autoinvoicing_disabledLa rectificativa automàtica està desactivada per a aquesta integracióActiva la rectificativa automàtica i reprocessa, o emet la rectificativa a mà — mai les dues coses
subscription_missing_invoice_idEl cicle cobrat no té identificador de facturaNoCrea a mà la factura d'aquest cicle; reprocessar donaria el mateix resultat
subscription_proration_reviewS'ha cobrat un prorrateig solt i exigeix una decisió humanaNoComprova l'import del prorrateig a la passarel·la i emet la factura a mà
subscription_not_a_cycleLa factura de la passarel·la no correspon a un cicle de subscripció facturableNoNoRes — el descart és correcte
subscription_trial_skippedImport zero o negatiu (prova o crèdit): no hi ha base imposableNoNoRes — no hi ha res a facturar
subscription_autoinvoicing_disabledL'auto-facturació de subscripcions està desactivadaActiva l'auto-facturació de subscripcions i reprocessa l'esdeveniment
subscription_already_invoicedEl cicle ja té la seva facturaNoNoRes — reprocessar-lo seria un no-op per idempotència
payout_missing_idEl payout no porta identificadorNoNoRes — no es pot conciliar ni reprocessar amb seguretat
payout_connected_account_missingEl compte connectat del payout no està vinculatVincula el compte connectat i reprocessa l'esdeveniment
payment_failedEl cobrament ha fallat a la passarel·laNoNoRes — no hi ha res a emetre ni a reintentar
event_type_not_coveredTipus d'esdeveniment fora de l'abast del producteNoNoRes — reprocessar-lo tornaria a no fer res
checkout_lines_retrieve_failedDegradació, no descart: la factura que es va emetre, amb una única líniaNoNoRes a reprocessar; revisa les línies de la factura si t'importa el desglossament

Un motiu sense res a fer ho diu explícitament. Onze dels vint són informatius, i el contracte no s'inventa una instrucció per a ells: l'endpoint de detall retorna recommended_action: null en comptes d'una frase fabricada per omplir el camp.

«O l'una, o l'altra» vol dir una, no les dues. Dos motius t'ofereixen dues sortides — activar l'ajust i reprocessar, o emetre el document a mà. Són excloents. La idempotència del reprocessament va per la identitat del cobrament i només reconeix els documents emesos per aquesta mateixa via automàtica, així que una factura que hagis creat a mà no el frena. Fer les dues coses deixa el mateix cobrament amb dues factures, cadascuna numerada a la seva sèrie i donada d'alta a VeriFactu — un dany fiscal que només es desfà amb una factura rectificativa.

Avisos: només el que pots arreglar

Un descart accionable avisa els administradors del compte. Un d'informatiu no avisa mai.

L'avís porta throttling: si ja existeix un avís sense llegir de la mateixa empresa, la mateixa passarel·la i el mateix motiu dins de les últimes 24 hores, no se'n crea un segon — un webhook mal configurat dispara centenars d'esdeveniments idèntics. La condició és sense llegir a propòsit: un cop l'has llegit, si continuen arribant descarts, el següent que avisa. Això no és soroll, vol dir que la incidència segueix viva.

L'aparcament i la finestra de 30 dies

Quan un motiu és aparcable, Factuarea guarda l'esdeveniment cru xifrat en repòs, per poder reprocessar-lo més tard. Aquest contingut no es retorna mai per l'API — ni al llistat, ni al detall. Conté dades personals dels teus clients finals i dades de pagament, i existeix per a exactament un propòsit: fer possible el reprocessament.

El contingut es purga als 30 dies d'aparcar-se l'esdeveniment. La fila sobreviu: el seu motiu, el seu estat, la seva data i la seva marca is_parked segueixen a la teva safata indefinidament, perquè el registre que un cobrament no va produir factura és historial que pots necessitar molt després que el contingut caduqui.

Un esdeveniment que segueix amb is_parked: true però ja no és is_replayable vol dir exactament una cosa: la finestra de retenció s'ha esgotat. La marca es deriva de si el contingut encara hi és, així que canvia sola el dia que corre la purga. Un reprocessament intentat després retorna 422 amb el subcodi integration_event_payload_purged.

El detall: què fer a continuació

L'endpoint de detall retorna tot el del llistat, més recommended_action: una frase en imperatiu amb el pas següent per a aquell motiu concret, o null quan el motiu és informatiu.

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

La frase distingeix a propòsit els motius reproduïbles («… i reprocessa l'esdeveniment») dels que no ho són («… emet-la a mà»), de manera que mai no t'apunta a una operació que respondria 422.

Un esdeveniment d'una altra empresa i un esdeveniment que no existeix retornen el mateix 404 resource_not_found. L'endpoint no revela mai si un id existeix en un altre lloc.

Reprocessar un esdeveniment aparcat

Torna a processar un esdeveniment de la passarel·la que va quedar aparcat, un cop ja no hi és la causa que li va impedir produir el seu efecte — has tornat a activar l'auto-facturació, has tornat a vincular el compte connectat, ja hi ha disponible el tipus de canvi oficial del dia.

Aquesta acció pot tenir conseqüències fiscals reals. Si la causa del descart ja està resolta, el reprocessament pot emetre una factura real, amb el seu número de sèrie i la seva alta a VeriFactu. No és un reintent innocu: confirma-ho amb el titular del compte abans de cridar-lo. Per això porta el seu propi scope d'escriptura, integration_events:write, en comptes del scope de lectura de la safata — una credencial de només lectura no ha de poder facturar mai.

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

Quatre propietats d'aquesta operació importen més que la seva firma:

  • No duplica factures. El reprocessament passa pel mateixíssim control d'idempotència que l'intent original, així que si aquell cobrament ja va produir una factura, el job s'atura sol i no crea res.
  • És asíncron. 202 vol dir acceptat i encuat, no completat. El cos retorna l'esdeveniment tal com està ara — el seu is_replayable segueix sent true —, no el resultat del reintent. El resultat apareix com un esdeveniment nou a la safata, així que consulta GET /v1/integrations/events per veure com ha acabat.
  • Si la causa segueix present, l'esdeveniment es descarta un altre cop i es registra de nou. És correcte, i és observable.
  • No admet entrada. Qualsevol paràmetre de consulta o clau del cos retorna 400 parameter_unknown en comptes d'ignorar-se. Enviar-ne un vol dir que et penses que estàs configurant alguna cosa del reintent — un mode, una sèrie, una data — que aquesta operació no suporta, i acceptar-ho en silenci confirmaria aquella expectativa falsa sobre una acció que pot emetre una factura. Un cos buit, o directament cap cos, és el cas normal.

Quan es rebutja un reprocessament

is_replayable: true és el contracte: quan val true, el reprocessament no respon 422. És la conjunció de tres condicions — l'esdeveniment està aparcat, encara conserva el seu contingut i el seu motiu admet reprocessament —, avaluades en aquest mateix ordre pel mateix handler que guarda el reprocessament. Això és el que et permet oferir un botó de reintent sense endevinar.

Els tres rebutjos retornen 422 business_rule_violation i et diuen quin és a través del subcode:

subcodeQuè significaHi ha sortida?
integration_event_not_parkedL'esdeveniment no es va aparcar mai — o va tenir èxit, o el seu motiu no guarda el contingutNo, i mai no n'hi haurà
integration_event_payload_purgedEs va aparcar, però el seu contingut es va esborrar en esgotar-se la finestra de 30 diesNo — resol-ho a mà
integration_event_reason_not_replayableEstà aparcat i conserva el seu contingut, però el seu motiu tornaria a prendre exactament la mateixa brancaNo — segueix en el seu lloc l'acció recomanada

En aquesta pàgina