Factuarea APIDevelopers

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:

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, ubl, woocommerce, shopify, prestashopClosed set
statussuccess, skipped, failureClosed set
event_typefree-form text, exact match, up to 100 charactersNot an enum — see below
discard_reasonone of the documented discard reasonsClosed 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

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..

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
reversal_payment_not_foundNo matching payment to reverse. Inspect the original payment and correct it manually if needed.YesNoFollow the action described in this row.
reversal_already_appliedThe payment was already reversed; no further action.NoNoNo manual replay.
dispute_in_progressThe dispute is still in progress; no monetary action yet.NoNoNo manual replay.
dispute_resolvedIf the money returned, register a new payment; the reversed payment does not reopen.YesNoFollow the action described in this row.
test_mode_eventThe provider payload declares test mode; no real invoice is due.NoNoNo manual replay.
order_event_not_coveredUnsupported order event type; replaying the same payload does not add support.NoNoNo manual replay.
store_not_foundNo store can be resolved for the event; check the provider connection and shop mapping.NoNoNo manual replay.
store_environment_testThe store is configured as test; this result is expected.NoNoNo manual replay.
refund_before_orderThe refund arrived first. The ordering buffer handles it; do not create a competing manual replay.NoNoNo manual replay.
refund_reason_unmappedChoose the corrective reason and issue manually.YesNoFollow the action described in this row.
recurring_invoice_overlapChoose one billing source. To invoice the order, pause the overlapping recurrence before replaying.YesYesFollow the action described in this row.
series_date_clampedThe invoice was issued with the date adjusted to the numbering series; nothing to replay.NoNoNo manual replay.
store_autoinvoicing_disabledEnable store auto-invoicing and replay, or invoice manually; choose one path.YesYesFollow the action described in this row.
vat_residual_out_of_toleranceThe order amounts do not reconcile with the payment; identical content would fail again.NoNoNo manual replay.
simplified_absolute_limit_exceededRequest the buyer tax ID and issue a full invoice manually.YesNoFollow the action described in this row.
simplified_threshold_exceeded_without_recipientReview the store threshold within the permitted limit for your activity, then replay if appropriate.YesYesFollow the action described in this row.
store_requires_tax_idReview the store’s tax-ID requirement, or obtain the ID for future orders.YesYesFollow the action described in this row.
order_line_amount_exceeds_columnThe line amount exceeds supported storage limits; the same payload cannot be replayed successfully.NoNoNo manual replay.
order_status_unknownInspect the extension producing this state and invoice manually when appropriate.YesNoFollow the action described in this row.
refund_not_settledThe refund is not settled; wait for the provider’s final state.NoNoNo manual replay.
protected_customer_data_unavailableBuyer fields were unavailable because Shopify has not granted the required permission to the Factuarea app.YesNoDo 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. 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

On this page

Need a hand?Contact support