Factuarea API

Integration event inbox

Why a charge did not become an invoice — the typed discard reasons, which ones notify you, which ones park the event so you can replay it, and how the 30-day retention window works.

A payment gateway sends Factuarea an event for everything that happens on your account: a charge succeeded, a refund was issued, a subscription cycle was billed, a payout landed. Most of those events produce something — an invoice, a corrective invoice, a payment record. Some produce nothing, and when that happens the interesting question is always the same: why didn't this charge become an invoice?

The integration event inbox answers it. Every event Factuarea receives is recorded with what it produced and, when it produced nothing, a typed discard reason drawn from a closed catalogue. No guessing from logs, no support ticket: the reason is a value you can filter on, and for the reasons you can act upon it comes with the next step and, sometimes, with the ability to reprocess the event.

The inbox is gateway-agnostic. It records events from every integration that writes history — including gateways that are not released yet and historical events of one that gets retired — because hiding those rows would leave you without an explanation for charges that never got invoiced.

Three endpoints expose it:

OperationEndpointScope
List eventsGET /v1/integrations/eventsintegration_events:read
Retrieve one eventGET /v1/integrations/events/{event}integration_events:read
Replay a parked eventPOST /v1/integrations/events/{event}/replayintegration_events:write

The same surface exists as MCP tools — list_integrations_events, get_integrations_event and replay_integrations_event — with the same scopes.

Browsing the inbox

Newest first, scoped to the authenticated company. Cursor-based pagination with limit (1 to 100, defaults to 25) and 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"
}

Treat next_cursor as opaque: for this listing it is a numeric string, not a UUID v7 like the cursors of the document listings. Feed it back verbatim in starting_after.

discard_reason_label is returned in Spanish, the language of the product's own interface. If you build a panel in another language, key your own copy on discard_reason — that value is the stable, closed identifier.

Filters

FilterValuesNotes
providerstripe, gocardless, monei, slack, teams, a3, norma43, norma19, ublClosed set
statussuccess, skipped, failureClosed set
event_typefree-form text, exact match, up to 100 charactersNot an enum — see below
discard_reasonone of the twenty reasons of the catalogueClosed set
is_parkedtrue / falseSee the note below
created_at[gte], created_at[lte]ISO 8601Inclusive window

discard_reason is the closed axis; event_type is not an enum. The event_type column deliberately mixes two conventions: branches instrumented later store the raw provider type (charge.refunded), while pre-existing ones keep their own semantic value (autoinvoice.*). Match it exactly when you know what you are after, but never model it as a closed set — you would be modelling something the column does not guarantee.

is_parked=false is not the same as omitting the parameter. The first one excludes parked events; the second one excludes nothing.

A value outside its catalogue returns 422, and an unknown query parameter returns 400 parameter_unknown instead of being silently ignored — a filter that gets dropped in silence hands you a page you believe is narrowed when it is not.

The catalogue of discard reasons

Twenty typed reasons, one per discard branch of the gateway webhook pipeline. Each one declares two business decisions that are not decorative flags:

  • Actionable — can the account owner do something about it? Only actionable reasons notify. Telling someone about a discard they cannot resolve teaches them to ignore the inbox, and that is how the notification that mattered gets missed.
  • Parked — could reprocessing the same content produce a different outcome? Only parked events keep their content encrypted and accept a replay.

The rule behind the parked column: an event is parked when the discard was caused by an external state you can change (a toggle that is off, a connected account that came unlinked, a currency with no exchange rate yet). It is not parked when the cause is the content of the event itself (malformed, duplicate, uncovered type, zero amount, cycle already invoiced) — reprocessing it would take exactly the same branch and only write a second row. Hence the invariant: every parked reason is actionable, and six of the nine actionable ones are parked.

ReasonWhat causes itActionableParkedWhat to do
event_not_normalizableMalformed event, or of a type that cannot be interpretedNoNoNothing — you cannot fix the provider's payload
duplicate_redeliveryThe event was already processed; its effect existsNoNoNothing — reprocessing would be a no-op by deduplication
connected_account_missingThe webhook is misconfigured at the gateway: the event does not say which account it belongs toYesNoCheck at the gateway that the webhook is sent from the account you linked in Factuarea
connected_account_unknownThe account exists at the gateway but is not linked in FactuareaYesYesRe-link that gateway account and replay the event
spontaneous_payment_missing_idThe charge carries no id, so there is no idempotency keyNoNoNothing — replaying would either duplicate or fail again
autoinvoicing_disabledAuto-invoicing is off for that integrationYesYesEither turn auto-invoicing on and replay, or create the invoice by hand — never both
unsupported_currencyThe European Central Bank rate for the day is not available yetYesYesReplay the event later, once the official rate of the day is published
refund_without_itemsThe refund carries no individual refunds to correctNoNoNothing — there is nothing to issue
refund_autoinvoicing_disabledAutomatic corrective invoices are off for that integrationYesYesEither turn automatic correctives on and replay, or issue the corrective by hand — never both
subscription_missing_invoice_idThe billed cycle has no invoice idYesNoCreate the invoice for this cycle by hand; replaying would give the same result
subscription_proration_reviewA standalone proration was charged and needs a human decisionYesNoCheck the proration amount at the gateway and issue the invoice by hand
subscription_not_a_cycleThe gateway invoice does not correspond to a billable subscription cycleNoNoNothing — the discard is correct
subscription_trial_skippedZero or negative amount (trial or credit): no taxable baseNoNoNothing — there is nothing to invoice
subscription_autoinvoicing_disabledSubscription auto-invoicing is offYesYesTurn subscription auto-invoicing on and replay the event
subscription_already_invoicedThe cycle already has its invoiceNoNoNothing — reprocessing would be a no-op by idempotency
payout_missing_idThe payout carries no identifierNoNoNothing — it can be neither reconciled nor safely replayed
payout_connected_account_missingThe payout's connected account is not linkedYesYesLink the connected account and replay the event
payment_failedThe charge failed at the gatewayNoNoNothing — there is nothing to issue or retry
event_type_not_coveredEvent type outside the product's scopeNoNoNothing — replaying would do nothing again
checkout_lines_retrieve_failedDegradation, not a discard: the invoice was issued, with a single lineNoNoNothing to replay; review the invoice's lines if the breakdown matters to you

A reason with nothing to do says so explicitly. Eleven of the twenty are informational, and the contract does not invent an instruction for them: the detail endpoint returns recommended_action: null rather than a sentence manufactured to fill the field.

"Either … or" means either, not both. Two reasons offer you two ways out — turn the toggle on and replay, or issue the document by hand. They are mutually exclusive. Replay idempotency keys on the identity of the charge and only recognises documents issued through that same automatic path, so an invoice you created by hand does not stop it. Doing both leaves the same charge with two invoices, each numbered in its series and registered in VeriFactu — fiscal damage that can only be undone with a corrective invoice.

Notifications: only what you can fix

An actionable discard notifies the account's administrators. An informational one never does.

The notification is throttled: if an unread notice already exists for the same company, gateway and reason within the last 24 hours, no second one is created — a misconfigured webhook fires hundreds of identical events. The condition is unread on purpose: once you have read it and discards keep arriving, the next one does notify. That is not noise, it means the incident is still live.

Parking and the 30-day window

When a reason is parkable, Factuarea stores the raw event encrypted at rest, so that it can be reprocessed later. That content is never returned by the API — not in the listing, not in the detail. It holds personal data of your end customers and payment details, and it exists for exactly one purpose: making the replay possible.

The content is purged 30 days after the event was parked. The row survives: its reason, its status, its date and its is_parked flag stay in your inbox indefinitely, because the record that a charge did not produce an invoice is history you may need long after the content expired.

An event that is still is_parked: true but no longer is_replayable means exactly one thing: the retention window elapsed. The flag is derived from whether the content is still there, so it flips on its own the day the purge runs. A replay attempted after that returns 422 with the subcode integration_event_payload_purged.

The detail: what to do next

The detail endpoint returns everything the listing does, plus recommended_action: one imperative sentence with the next step for that specific reason, or null when the reason is informational.

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

The sentence deliberately tells reproducible reasons ("… and replay the event") apart from the ones that are not ("… issue it by hand"), so it never points you at an operation that would answer 422.

An event of another company and an event that does not exist return the same 404 resource_not_found. The endpoint never reveals whether an id exists elsewhere.

Replaying a parked event

Reprocess a gateway event that was parked, once the cause that prevented it from producing its effect is gone — you turned automatic invoicing back on, you re-linked the connected account, the official exchange rate of the day became available.

This action can have real fiscal consequences. If the cause of the discard is already resolved, the replay can issue a real invoice, with its series number and its registration in VeriFactu. It is not an innocuous retry: confirm with the account owner before calling it. That is why it takes its own write scope, integration_events:write, instead of the read scope of the inbox — a read-only credential must never be able to invoice.

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

Four properties of this operation matter more than its signature:

  • It does not duplicate invoices. The replay goes through the very same idempotency check as the original attempt, so if that charge already produced an invoice, the job stops on its own and creates nothing.
  • It is asynchronous. 202 means accepted and queued, not completed. The body returns the event as it stands now — its is_replayable is still true — not the outcome of the retry. The outcome shows up as a new event in the inbox, so poll GET /v1/integrations/events to see how it ended.
  • If the cause is still present, the event is discarded again and recorded once more. That is correct, and it is observable.
  • It takes no input. Any query parameter or body key returns 400 parameter_unknown instead of being ignored. Sending one means you believe you are configuring something about the retry — a mode, a series, a date — that this operation does not support, and silently accepting it would confirm that false expectation about an action that can issue an invoice. An empty body or no body at all is the normal case.

When a replay is refused

is_replayable: true is the contract: when it is true, the replay does not answer 422. It is the conjunction of three conditions — the event is parked, it still holds its content, and its reason admits reprocessing — evaluated in that same order by the very handler that guards the replay. That is what lets you offer a retry button without guessing.

The three refusals all return 422 business_rule_violation and tell you which one it is through the subcode:

subcodeWhat it meansIs there a way forward?
integration_event_not_parkedThe event was never parked — it either succeeded, or its reason does not keep the contentNo, and there never will be
integration_event_payload_purgedIt was parked, but its content was deleted when the 30-day window elapsedNo — handle it by hand
integration_event_reason_not_replayableIt is parked and still holds its content, but its reason would take exactly the same branch againNo — follow the recommended action instead
  • Stripe auto-invoicing — the flow that produces most of the events you will find here, including the subscription cycles whose toggle is behind subscription_autoinvoicing_disabled.
  • Payouts and bank reconciliation — the payout ingestion behind payout_missing_id and payout_connected_account_missing.
  • Test mode — validate your handling of the inbox with a fact_test_ key before you wire a replay button to a production credential.
  • Error codes — the envelope of the 400, 404 and 422 responses quoted above.

On this page