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:
| Operation | Endpoint | Scope |
|---|---|---|
| List events | GET /v1/integrations/events | integration_events:read |
| Retrieve one event | GET /v1/integrations/events/{event} | integration_events:read |
| Replay a parked event | POST /v1/integrations/events/{event}/replay | integration_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
| Filter | Values | Notes |
|---|---|---|
provider | stripe, gocardless, monei, slack, teams, a3, norma43, norma19, ubl | Closed set |
status | success, skipped, failure | Closed set |
event_type | free-form text, exact match, up to 100 characters | Not an enum — see below |
discard_reason | one of the twenty reasons of the catalogue | Closed set |
is_parked | true / false | See the note below |
created_at[gte], created_at[lte] | ISO 8601 | Inclusive 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.
| Reason | What causes it | Actionable | Parked | What to do |
|---|---|---|---|---|
event_not_normalizable | Malformed event, or of a type that cannot be interpreted | No | No | Nothing — you cannot fix the provider's payload |
duplicate_redelivery | The event was already processed; its effect exists | No | No | Nothing — reprocessing would be a no-op by deduplication |
connected_account_missing | The webhook is misconfigured at the gateway: the event does not say which account it belongs to | Yes | No | Check at the gateway that the webhook is sent from the account you linked in Factuarea |
connected_account_unknown | The account exists at the gateway but is not linked in Factuarea | Yes | Yes | Re-link that gateway account and replay the event |
spontaneous_payment_missing_id | The charge carries no id, so there is no idempotency key | No | No | Nothing — replaying would either duplicate or fail again |
autoinvoicing_disabled | Auto-invoicing is off for that integration | Yes | Yes | Either turn auto-invoicing on and replay, or create the invoice by hand — never both |
unsupported_currency | The European Central Bank rate for the day is not available yet | Yes | Yes | Replay the event later, once the official rate of the day is published |
refund_without_items | The refund carries no individual refunds to correct | No | No | Nothing — there is nothing to issue |
refund_autoinvoicing_disabled | Automatic corrective invoices are off for that integration | Yes | Yes | Either turn automatic correctives on and replay, or issue the corrective by hand — never both |
subscription_missing_invoice_id | The billed cycle has no invoice id | Yes | No | Create the invoice for this cycle by hand; replaying would give the same result |
subscription_proration_review | A standalone proration was charged and needs a human decision | Yes | No | Check the proration amount at the gateway and issue the invoice by hand |
subscription_not_a_cycle | The gateway invoice does not correspond to a billable subscription cycle | No | No | Nothing — the discard is correct |
subscription_trial_skipped | Zero or negative amount (trial or credit): no taxable base | No | No | Nothing — there is nothing to invoice |
subscription_autoinvoicing_disabled | Subscription auto-invoicing is off | Yes | Yes | Turn subscription auto-invoicing on and replay the event |
subscription_already_invoiced | The cycle already has its invoice | No | No | Nothing — reprocessing would be a no-op by idempotency |
payout_missing_id | The payout carries no identifier | No | No | Nothing — it can be neither reconciled nor safely replayed |
payout_connected_account_missing | The payout's connected account is not linked | Yes | Yes | Link the connected account and replay the event |
payment_failed | The charge failed at the gateway | No | No | Nothing — there is nothing to issue or retry |
event_type_not_covered | Event type outside the product's scope | No | No | Nothing — replaying would do nothing again |
checkout_lines_retrieve_failed | Degradation, not a discard: the invoice was issued, with a single line | No | No | Nothing 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.
202means accepted and queued, not completed. The body returns the event as it stands now — itsis_replayableis stilltrue— not the outcome of the retry. The outcome shows up as a new event in the inbox, so pollGET /v1/integrations/eventsto 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_unknowninstead 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:
subcode | What it means | Is there a way forward? |
|---|---|---|
integration_event_not_parked | The event was never parked — it either succeeded, or its reason does not keep the content | No, and there never will be |
integration_event_payload_purged | It was parked, but its content was deleted when the 30-day window elapsed | No — handle it by hand |
integration_event_reason_not_replayable | It is parked and still holds its content, but its reason would take exactly the same branch again | No — follow the recommended action instead |
Where this fits
- 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_idandpayout_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.