Stripe auto-invoicing
Auto-issue invoices from Stripe Connect charges — NIF capture in Checkout, the simplified-invoice threshold, requiring a NIF, and which charges are routed to manual review.
When you connect Stripe via Stripe Connect, Factuarea can auto-issue an
invoice for every successful charge: the charge is turned into an invoice with
status: sent, registered for VeriFactu, and a Payment is recorded against it.
The flow is idempotent end-to-end, so a redelivered webhook never produces a
duplicate invoice.
Two flows feed it:
- Flow A — a charge that pays an existing Factuarea invoice (a Checkout Session Factuarea created from a payment link). The invoice already exists; the charge marks it paid.
- Flow B — a spontaneous charge with no prior invoice (a Payment Link the merchant created in their own Stripe Dashboard, or any other Connect charge). Factuarea creates the invoice from the charge.
The configuration is read and written through the company-wide v1 endpoints:
- Retrieve the auto-invoicing config
(
stripe_autoinvoicing:read). - Update the auto-invoicing config
(
stripe_autoinvoicing:write). - List auto-invoiced charges
(
stripe_autoinvoicing:read). - List auto-invoiced correctives
(
stripe_autoinvoicing:read).
If you run several stores under separate Stripe accounts, each account has its own series and its own configuration — see Multiple stores.
Auto-invoicing is gated by the Stripe integration module of your plan and is
off by default — enable it explicitly with enabled: true.
What the Stripe API exposes. Company-wide auto-invoicing configuration (a convenience inherited from the single-store model — the real source of truth is the per-account config, see Multiple stores), connected accounts, auto-invoiced charges, correctives, and payouts for bank reconciliation.
Multiple stores
A business can run several "stores" or lines (a physical shop + an online course)
under different Stripe accounts (Stripe Connect) and want independent invoice
numbering for each one (TIENDA-2026-…, CURSOS-2026-…). Factuarea models each
Stripe account you connect as a connected account: every Account Link you
complete adds an account — it never overwrites the previous one — and charges
on each account are routed to that account's series and configuration.
Each connected account carries:
- a series (
series_id) used for the invoices auto-created from its charges —nullmeans the company default invoice series is used; - its own auto-invoicing configuration (
autoinvoicing_enabled,simplified_threshold_cents,require_nif,refunds_enabled,subscription_autoinvoicing_enabled) — every fiscal rule on this page applies per account.
When a webhook arrives, Factuarea resolves the Stripe account (acct_xxx) to its
connected account and issues the invoice in that account's series, with that
account's fiscal policy — so two stores produce invoices in two separate, correct
numbering series.
New accounts start safe
A newly connected account does not inherit another account's configuration: it starts with the same safe defaults as a fresh setup — auto-invoicing off, threshold 400 €, "require NIF" off, refunds on, subscriptions off — and no series (it falls back to the company default until you assign one). Configure it explicitly before it issues anything.
Per-account v1 endpoints
Manage accounts under the connected-accounts resource (same
stripe_autoinvoicing:read|write scopes; identity is the account id, a UUID v7):
- List connected accounts
(
stripe_autoinvoicing:read). - Retrieve a connected account
(
stripe_autoinvoicing:read). - Update a connected account
— name,
series_id(sendnullto clear it), and the per-account configuration (stripe_autoinvoicing:write). - Disconnect a connected account
(
stripe_autoinvoicing:write).
# Assign the CURSOS series and enable auto-invoicing on one store
curl -X PUT https://api.factuarea.com/v1/connected-accounts/0192f3a4-… \
-H "Authorization: Bearer fact_test_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0192f3a4-…" \
-d '{
"series_id": "0192aaaa-…",
"autoinvoicing_enabled": true
}'Disconnecting an account keeps its already-issued invoices and history; later
webhooks for it are recorded without processing. Referencing the id of an
account that belongs to another company returns 404
(connected_account_not_found) — multi-tenant isolation never leaks existence.
The company-wide endpoint while you have one store
The company-wide
config endpoints
above stay valid while you have exactly one connected account: the GET
returns that single account's effective config, and the PUT proxies to it
(it writes the account's configuration, never a duplicate company-level copy).
As soon as you connect a second account, the company-wide config can no longer
answer "which account?". Both the GET and the PUT then return 422
(per_account_config_required, message in Spanish) pointing you to the per-account
endpoints — Factuarea never writes to two places, so there is no divergence
between a company-level config and the accounts.
One source of truth. The per-account configuration is the only place
settings live. The company-wide endpoint is a convenience that proxies to the
single account; it never holds a separate copy, so reading and writing always
agree. With two or more accounts, use connected-accounts/{account} directly.
Ordinary or simplified invoice
A Spanish invoice needs the recipient's NIF to be issued as an ordinary invoice (F1). A B2C charge without a NIF is exactly the case the law settles with a simplified invoice (F2). Factuarea decides which one to issue from the data the charge carries plus your fiscal policy:
| Situation | Invoice issued |
|---|---|
| A valid NIF is captured in Checkout, or the resolved client already has a NIF on file | Ordinary (F1) |
| No NIF, charge total at or below the threshold, and "require NIF" is off | Simplified (F2) |
| No NIF and (total above the threshold or "require NIF" is on) | Manual review — no invoice is auto-issued |
A captured NIF is validated against the Spanish format (NIF/NIE/CIF). A NIF with an invalid format counts as no NIF, so an F1 is never issued with junk data.
Charges routed to manual review are not lost: they are recorded in the integration log so you can issue the invoice by hand. Auto-invoicing keeps going for the rest — a charge in review never fails the webhook.
The absolute legal ceiling for a simplified invoice is 3,000 €, enforced by the invoicing domain itself: a charge without a NIF above 3,000 € always goes to manual review, whatever the threshold is set to.
Capturing the NIF in Checkout
So a client who does have a NIF can provide it, the Checkout Sessions Factuarea
creates (Flow A) enable Stripe's tax-ID collection. The client can enter
their es_cif/eu_vat at payment time, and that NIF drives the F1 path.
NIFs are also read from any incoming checkout.session.completed:
- from
customer_details.tax_ids(the standard Stripe field), and - from the custom fields of Payment Links the merchant builds in their own
Stripe Dashboard — Factuarea looks for a field whose key looks like a tax ID
(
nif,dni,cif,vat,tax).
A charge that arrives only as payment_intent.succeeded (no Checkout) carries no
captured NIF, but it can still be an F1 if the client is resolved by email
and already has a NIF on file.
The threshold
simplified_threshold_cents is the amount in cents at or below which a charge
without a NIF is auto-issued as a simplified invoice. It defaults to 40000
(400 €) and accepts any value in the range [0, 300000] (0–3,000 €).
A conservative default of 400 € is deliberate: the 3,000 € ceiling is only legal in specific rated sectors, and Factuarea does not know your sector — raise the threshold only if your activity allows it.
curl -X PUT https://api.factuarea.com/v1/stripe-autoinvoicing/config \
-H "Authorization: Bearer fact_test_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0192f3a4-…" \
-d '{
"enabled": true,
"simplified_threshold_cents": 100000,
"require_nif": false
}'Both fiscal fields are optional: omit simplified_threshold_cents or
require_nif and their current values are kept. A threshold outside the range
returns 422 (validation_error).
Requiring a NIF
require_nif (default false) has two coherent effects:
- In the Checkout Sessions Factuarea creates, the tax ID is marked required
(
if_supported), so the client is prompted for it. - In the decision above, it vetoes the F2: a charge without a NIF goes to manual review instead of becoming a simplified invoice.
Turn it on when your company never wants automatic simplified invoices — every charge then either has a NIF (F1) or waits for you in manual review.
Auto-issued invoices are fiscally real and irreversible (the VeriFactu Alta
record is created). Validate your fiscal policy with a fact_test_ key first:
in the sandbox the VeriFactu record is created locally and
never transmitted to the AEAT, so you can exercise the F1/F2/review decision
safely before going live.
The outbound event
Every auto-created invoice — F1 or F2 — emits the
invoice.auto_created event and
a payment.received event. For a simplified invoice the payload carries
client_id: null (no recipient), so a webhook receiver can tell F1 and F2 apart.
Real VAT breakdown with Stripe Tax
If you use Stripe Tax, every charge already carries the real tax breakdown per line — the rate, the taxable base, and (where it applies) the reason a line is exempt or reverse-charged. Factuarea mirrors that breakdown onto the invoice instead of flattening everything to a single default rate.
The Checkout webhook doesn't include the line items, so Factuarea makes a second, read-only API call on your behalf to retrieve them with their taxes, then maps each line:
| Stripe Tax data | Invoice line |
|---|---|
rate.percentage | the line's VAT rate (vat_rate), used as-is — never recomputed |
taxable_amount | the line's taxable base (unit price = base ÷ quantity) |
taxability_reason zero_rated / product_exempt / customer_exempt | exempt line at 0 % |
taxability_reason reverse_charge | reverse charge (ISP) line at 0 % |
So a charge with mixed VAT (e.g. 21 % consulting + 10 % a book) becomes an invoice with two real lines, each at its own rate, and the multi-rate breakdown carries through to the VeriFactu record.
The VAT is never recalculated — Stripe already computed it, and recomputing would introduce cent-level drift. Factuarea takes the rate and taxable base straight from Stripe Tax.
When there's no Stripe Tax (you haven't enabled it in your Stripe account), nothing changes: every line falls back to your company's default VAT rate, exactly like before. A company with no default rate configured falls back to 0 % — never a phantom 21 %.
Real line items
When a charge carries several line items (several products or concepts), they appear as real, separate lines on the invoice — each with its own description, quantity and price — instead of being collapsed into one. The lines are free lines (not linked to your product catalogue).
This is orthogonal to the invoice type: the F1/F2 decision above chooses the type, the line mapping chooses the lines — both an ordinary and a simplified invoice get the same real lines.
A charge that arrives only as payment_intent.succeeded (no Checkout Session, so
no retrievable line items), or a charge whose line retrieval fails after retries,
still produces an invoice: it falls back to a single line at the default VAT
rate. The invoice is never lost over a non-essential detail.
Before issuing, Factuarea validates the total: the total derived from the
mirrored lines (sum of subtotal + VAT per line, in EUR) must match the amount
actually charged, within a small rounding tolerance (±1 cent per line, minimum
±0.05 €). If it doesn't, the charge is routed to manual review
(total_mismatch) instead of issuing an invoice whose total diverges from the
real charge — the webhook still succeeds.
Charges in another currency
A charge in a currency other than EUR is no longer skipped. Factuarea converts it to EUR using the European Central Bank (ECB) reference rate for the payment date and issues the invoice in euros — base, VAT and total, and the VeriFactu/AEAT record, all in EUR (Art. 12.1 RD 1619/2012: the VAT amount must be stated in euros).
- A EUR charge is passed through untouched.
- A non-EUR charge with an available rate is converted; the trace of the conversion — original amount and currency, ECB rate, and rate date — is written to the invoice notes and to the integration log for fiscal auditing.
- A charge in a currency with no ECB rate available is not auto-invoiced: it goes to manual review and the webhook still succeeds. Factuarea never invents a rate.
The ECB rates are cached daily (no extra database table), so several non-EUR charges on the same day share a single rate lookup.
Issuing the invoice in the original currency (option B) is intentionally out of scope — Factuarea always converts to EUR (option A).
Refunds and corrective invoices
An issued invoice is fiscally irreversible — it is never deleted or voided
once paid. The only legal way to undo it is a corrective invoice
(rectificativa). So when Stripe refunds a charge that Factuarea invoiced,
Factuarea closes the fiscal loop for you: the charge.refunded webhook generates
a linked corrective invoice automatically (with its own VeriFactu R record),
no manual rectification needed.
This is controlled by refunds_enabled (default true). It only acts while
auto-invoicing is enabled — the gate is enabled && refunds_enabled. A company
that never turned auto-invoicing on sees no change.
| Refund | Corrective issued |
|---|---|
Full (refunded: true) | A total corrective that mirrors the whole original invoice as negative lines |
Partial (amount_refunded < amount) | A partial corrective with a single negative line for the refunded amount, at the original line's tax rate |
The corrective is always issued by differences (AEAT TipoRectificativa: I),
because a refund is a credit with negative amounts. It carries the
devolucion correction reason; if the original is a simplified invoice (F2) the
corrective is issued as R5 automatically.
The original invoice is located by two paths — by the factuarea_invoice
metadata on the charge (Flow A) or by the deterministic UUID derived from the
payment_intent (Flow B) — so refunds of charges issued before this feature
existed are corrected too.
Idempotency is per individual refund. Stripe re-emits charge.refunded
with the cumulative amount_refunded, but Factuarea keys on each individual
refund id (re_xxx): each one produces at most one corrective. A
redelivered event, or a second partial refund, never double-credits. A refund
whose original invoice can't be located, isn't in a correctable state
(sent/paid), or is already corrected is recorded in the integration log for
manual review — it never fails the webhook.
Each automatic corrective emits the
invoice.corrective_auto_created
event (carrying the corrective, the original invoice, the originating refund_id
and the provider), and you can list them via
List auto-invoiced correctives.
To receive refunds you must enable the charge.refunded event on your
Connect webhook endpoint in the Stripe Dashboard. As with auto-invoicing,
validate the flow with a fact_test_ key first: in the
sandbox the VeriFactu R record is created locally and never
transmitted to the AEAT. To opt out, set refunds_enabled: false — refunds
then stay in the integration log for manual handling, the current behaviour.
Subscription cycles
If you charge with Stripe Billing on your connected account — recurring monthly or yearly subscriptions — Factuarea can auto-issue an invoice for each billed cycle. Every renewal Stripe collects produces its own VeriFactu-compliant invoice, mirroring the real breakdown (lines, period, taxes) Stripe already computed, exactly like one-shot charges.
This is a separate toggle, subscription_autoinvoicing_enabled (default
false), on top of the general enabled flag. Both must be on for a cycle to
be invoiced — turning on subscriptions alone does nothing while auto-invoicing is
globally off.
curl -X PUT https://api.factuarea.com/v1/stripe-autoinvoicing/config \
-H "Authorization: Bearer fact_test_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0192f3a4-…" \
-d '{
"enabled": true,
"subscription_autoinvoicing_enabled": true
}'Which cycles are invoiced
Factuarea invoices the standard billing cycle and routes everything else to
review or ignores it, keyed on Stripe's billing_reason:
billing_reason | What Factuarea does |
|---|---|
subscription_create (first cycle) | Invoiced |
subscription_cycle (each renewal) | Invoiced |
subscription_update, subscription_threshold (standalone prorations) | Manual review — recorded in the integration log, not auto-invoiced |
manual, upcoming, quote_accept, others | Ignored with an info log |
- Trials don't invoice. A cycle whose
invoice.totalis 0 € (a trial period, or a cycle fully covered by credit) emits no invoice; the first real charge after the trial is invoiced normally. - Prorations standalone (an upgrade/downgrade billed on its own, outside the regular cycle) are not auto-invoiced today — they go to manual review so you decide. The regular cycle that follows is invoiced as usual.
- The F1/F2 decision is the same. The recipient's NIF is read from the
invoice's
customer_tax_ids(plus the resolved client's file); with a NIF the cycle is an ordinary (F1) invoice, without one and at or below the threshold an simplified (F2), and without one above the threshold (or with "require NIF" on) it goes to manual review — identical to the rules in the decision section. The same Stripe Tax line/VAT mirroring, EUR conversion and total validation apply.
Each cycle is its own invoice
A subscription cycle becomes a standalone invoice — it does not create or
touch a Factuarea recurring invoice. The two are independent: Stripe drives the
cadence and each invoice.paid produces one invoice.
Avoid double-invoicing. If you already model the same client's subscription as a manual recurring invoice in Factuarea, enabling subscription auto-invoicing for that Stripe subscription will produce two invoices per period — one from your recurring template and one from the Stripe cycle. Pick one source per client: either stop the manual recurring invoice or leave this toggle off for those subscriptions.
The outbound event
A subscription-cycle invoice emits a distinct event,
invoice.subscription_auto_created
(plus payment.received), so a webhook receiver can tell subscription cycles apart
from one-shot charges. A one-shot charge keeps emitting invoice.auto_created as
before. The auto-invoiced charges listing
(List auto-invoiced charges)
exposes the subscription context (subscription_id, stripe_invoice_id,
period_start, period_end) for cycle charges (null for one-shot), and an
optional origin filter (subscription/oneshot).
Idempotency is per billing cycle. Factuarea keys on each Stripe invoice id
(in_xxx): a redelivered event, or a second event for the same cycle, produces
at most one invoice. Historical cycles are not back-invoiced — only
cycles billed after you enable the toggle are invoiced; replays of older
invoice.paid events are ignored.
To receive subscription cycles you must enable the invoice.paid event on
your Connect webhook endpoint in the Stripe Dashboard. As always, validate the
flow with a fact_test_ key first: in the sandbox the
VeriFactu Alta record is created locally and never transmitted to the AEAT.
Defaults at a glance
| Field | Default | Effect of the default |
|---|---|---|
enabled | false | Auto-invoicing off — nothing is issued until you enable it |
simplified_threshold_cents | 40000 (400 €) | Charges without a NIF up to 400 € become F2 |
require_nif | false | Simplified invoices are allowed; tax ID is offered but not required |
refunds_enabled | true | A Stripe refund generates an automatic corrective invoice (while auto-invoicing is on) |
subscription_autoinvoicing_enabled | false | Stripe subscription cycles are not auto-invoiced until you enable it (requires enabled too) |
With these defaults the only behaviour change once auto-invoicing is on is that a
charge without a NIF up to 400 € becomes a simplified invoice instead of waiting
in review, and a refund generates its corrective invoice automatically. Charges
with a NIF behave exactly as before. Subscription cycles stay off until you opt in
with subscription_autoinvoicing_enabled.
Glossary
Spanish fiscal and domain terms used across the Factuarea API — NIF, VeriFactu, AEAT, FacturaE, Modelo 303/347, series, rectificativa, huella, CSV and more.
Payouts & bank reconciliation
How Factuarea ingests Stripe payouts, links them to the charges they group, reconciles them against your Norma 43 bank statement, and emits the payout.reconciled event.