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.
This inbox also covers WooCommerce and Shopify orders and refunds. Filter by provider, inspect discard_reason, and only replay a retained event when is_replayable permits it. See the store workflow.
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, woocommerce, shopify, prestashop | 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 documented discard reasons | 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
Typed reasons cover gateway and ecommerce processing. 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..
| 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 |
reversal_payment_not_found | No matching payment to reverse. Inspect the original payment and correct it manually if needed. | Yes | No | Follow the action described in this row. |
reversal_already_applied | The payment was already reversed; no further action. | No | No | No manual replay. |
dispute_in_progress | The dispute is still in progress; no monetary action yet. | No | No | No manual replay. |
dispute_resolved | If the money returned, register a new payment; the reversed payment does not reopen. | Yes | No | Follow the action described in this row. |
test_mode_event | The provider payload declares test mode; no real invoice is due. | No | No | No manual replay. |
order_event_not_covered | Unsupported order event type; replaying the same payload does not add support. | No | No | No manual replay. |
store_not_found | No store can be resolved for the event; check the provider connection and shop mapping. | No | No | No manual replay. |
store_environment_test | The store is configured as test; this result is expected. | No | No | No manual replay. |
refund_before_order | The refund arrived first. The ordering buffer handles it; do not create a competing manual replay. | No | No | No manual replay. |
refund_reason_unmapped | Choose the corrective reason and issue manually. | Yes | No | Follow the action described in this row. |
recurring_invoice_overlap | Choose one billing source. To invoice the order, pause the overlapping recurrence before replaying. | Yes | Yes | Follow the action described in this row. |
series_date_clamped | The invoice was issued with the date adjusted to the numbering series; nothing to replay. | No | No | No manual replay. |
store_autoinvoicing_disabled | Enable store auto-invoicing and replay, or invoice manually; choose one path. | Yes | Yes | Follow the action described in this row. |
vat_residual_out_of_tolerance | The order amounts do not reconcile with the payment; identical content would fail again. | No | No | No manual replay. |
simplified_absolute_limit_exceeded | Request the buyer tax ID and issue a full invoice manually. | Yes | No | Follow the action described in this row. |
simplified_threshold_exceeded_without_recipient | Review the store threshold within the permitted limit for your activity, then replay if appropriate. | Yes | Yes | Follow the action described in this row. |
store_requires_tax_id | Review the store’s tax-ID requirement, or obtain the ID for future orders. | Yes | Yes | Follow the action described in this row. |
order_line_amount_exceeds_column | The line amount exceeds supported storage limits; the same payload cannot be replayed successfully. | No | No | No manual replay. |
order_status_unknown | Inspect the extension producing this state and invoice manually when appropriate. | Yes | No | Follow the action described in this row. |
refund_not_settled | The refund is not settled; wait for the provider’s final state. | No | No | No manual replay. |
protected_customer_data_unavailable | Buyer fields were unavailable because Shopify has not granted the required permission to the Factuarea app. | Yes | No | Do not replay the stored empty fields. Once Shopify grants access, a new provider read recovers pending orders. If an order cannot wait, issue its invoice manually. |
Informational reasons return recommended_action: null; the API does not invent an action where none is needed.
"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
the flow that produces most of the events you will find here.
their toggle is behind subscription_autoinvoicing_disabled.
the
payout ingestion behind payout_missing_id and
payout_connected_account_missing.
validate your handling of the inbox with a
fact_test_ key before you wire a replay button to a production credential.
the envelope of the 400, 404 and 422 responses quoted above.