NewStripe: a buyer with a NIF always gets a standard invoice, and the simplified threshold is deprecated
BeeL
Get startedMulti-NIFVeriFactuRulesStripeAPI referenceChangelog

Data conversion — Stripe → BeeL

The Fiscal Mirror rule, how amounts and taxes are mapped, why IRPF is always 0, the silent sanitization BeeL. applies to inbound payloads, and how the forward and corrective emitters split responsibilities.


When BeeL. generates an invoice from a Stripe event, it does not recalculate the operation — it reflects the Stripe charge exactly. This is the Fiscal Mirror rule and it's the single most important thing to understand about the integration. This page covers it in detail and documents every conversion BeeL. applies — including the silent sanitization of inbound recipient fields and the split between the forward and rectificativa emitters that route Stripe events to the right kind of invoice.

The Fiscal Mirror rule

A Stripe charge is the fiscal truth.
BeeL's job is to translate it into a registro de facturación,
not to second-guess its tax breakdown.

In practice:

  • If Stripe charged 121 €, BeeL's invoice total is 121.00 € (never 120.99 €, never 121.05 €).
  • If Stripe Tax says the operation is 100 € base + 21 € IVA, BeeL's invoice carries those exact numbers.
  • If Stripe Tax doesn't expose a breakdown, BeeL. decomposes the charge using your configured default IVA rate treating the total as inclusive. The decomposition is exact (no residual rounding).

The only thing BeeL. never copies from Stripe is the VeriFactu metadata (classification, exemption, regime) — those depend on legal context Stripe doesn't model. BeeL. infers them from your settings + the customer's geography.

There is no "we recalculate taxes for you" mode. Fiscal Mirror is the only behaviour — the invoice total always equals the Stripe charge total.

Field-by-field mapping

Amounts and currency

Stripe fieldBeeL. invoice fieldConversion
payment_intent.amount_received (cents)total (EUR)Divide by 100
charge.amount (cents)Fallback when amount_received is absentDivide by 100
invoice.amount_paid (cents, Stripe Billing)Fallback for invoice.paid eventsDivide by 100
currencyValidated; must be eurOther currencies are rejected at processing time
metadata.external_payment_idmetadata.external_payment_idStripe payment_intent.id

Customer fiscal data

The resolved customer comes from Customer mappings. The conversion of fields:

Stripe sourceBeeL. invoice field
customer.email → fallback charge.billing_details.emailrecipient.email
customer.name → fallback charge.billing_details.namerecipient.legal_name
customer.tax_ids[0].valuerecipient.nif (if Spanish) or recipient.alternative_id.number
customer.address.line1 / line2recipient.address.street
customer.address.postal_coderecipient.address.postal_code
customer.address.cityrecipient.address.city
customer.address.country (ISO-2)recipient.address.country (full name — the API's address block carries the country name, not the ISO code)

When the payment is a Stripe invoice (invoice.paid) and neither the Stripe invoice nor the Stripe customer carries an address country, BeeL. reads it from the billing address of the charge that paid the invoice. It is used last: any address the invoice or customer carries wins, and the charge only supplies the country when the chosen address has none. This way invoice.paid and the payment event of the same charge see the same buyer, and the country decides how the sale is taxed.

Country no longer defaults to "ES". If Stripe sends no country, BeeL. leaves it empty — and may fall back to a simplified invoice (up to 3 000 €) or a draft (above it) if F1 requirements aren't met. (Prior behaviour silently set ES, which masked malformed payloads.)

Tax breakdown — Stripe Tax enabled

When you have Stripe Tax active, every charge carries tax_amounts[] per line with the rate and inclusivity flag. BeeL. reads these and pre-populates each invoice line:

Stripe says:
  line { amount: 12100, tax_amounts: [{ amount: 2100, inclusive: true, rate: 21.00 }] }

BeeL emits (BeeL invoice line):
  {
    "unit_price": 100,
    "taxable_base": 100,
    "main_tax": { "type": "IVA", "percentage": 21, "regime_key": "01" },
    "line_total": 121
  }

The user's default tax configuration is not consulted when Stripe exposes a breakdown — your Stripe Tax setup wins.

Tax breakdown — Stripe Tax NOT enabled

When tax_amounts[] is empty (simple PaymentIntents, manual Stripe Invoices without Tax), BeeL. falls back to your account configuration:

  1. Read the connection's tax-inclusive pricing setting (per integration)
  2. Read your default tax configuration (Settings > Taxes): main tax (IVA / IGIC / IPSI) and standard rate
  3. Treat the Stripe total as inclusive of that rate
  4. Decompose: base = total / (1 + rate/100), tax = total - base

Example: Stripe charges 121.00 €, your default is IVA 21 %, inclusive → BeeL. emits base: 100.00, IVA: 21.00.

This decomposition is exact; we don't round to "round" base values. If your IVA rate is 10 % and Stripe charged 121.00 €, the base is 110.00 and the IVA is 11.00.

Shipping charged without VAT

Shipping is part of the sale it delivers, so Spanish law taxes it at the sale's rate (art. 78.Dos.1º LIVA). When Stripe charged shipping with no tax rate and no taxability reason, on a sale that does carry VAT, BeeL. invoices the shipping as an ancillary cost:

  • The shipping amount Stripe charged is read as tax-inclusive at the sale's VAT rate, so the invoice total is still exactly what Stripe charged.
  • If the sale mixes several rates, the shipping is split between them in proportion to each rate's base.
  • The share that belongs to an exempt or non-taxable part of the sale goes out exempt, with that part's own exemption reason and no tax.
  • Tips and discounts are not part of that split.

Coupons and discounts (Stripe Billing)

A Stripe Invoice paid with a coupon is invoiced for what was charged, not for the gross line. The sale line keeps its full price and the discount goes on a separate line with a negative quantity, so sale minus discount equals the amount Stripe collected. The discount line carries its share of the tax too, so the invoice's total tax is the tax Stripe calculated on the discounted amount.

A credit note on a discounted invoice credits the discounted amount as well — see Generated invoices.

IRPF — always 0 on Stripe lines

Every line BeeL. emits from a Stripe webhook has irpf_rate: 0 (the API field) regardless of your default IRPF configuration.

Why: this is a BeeL. rule, not a statement about who must withhold. If BeeL. applied your default professional IRPF (e.g. 15 %), the invoice total would diverge from the Stripe charge — violating Fiscal Mirror.

If a customer withholds IRPF from what they pay you, don't issue that invoice through Stripe — issue it through the Invoicing API with your usual settings.

VeriFactu classification

BeeL. infers each line's exemption_reason (or leaves it unset for the default S1) based on the customer's geography. The full mapping lives in Tax classification; a Stripe-specific summary:

Customer countryResulting exemption_reasonmain_tax.type
Spain (peninsula or Balearic)omitted (S1)IVA
Canary Islands (resolved from address)omittedIGIC if configured, else IVA
Ceuta / MelillaomittedIPSI if configured, else IVA
EU B2B (VIES VAT-ID on tax_ids)EXENTA_ART_25 (goods) or NO_SUJETA_LOCALIZACION (services)IVA
EU B2Comitted — origin IVA, or the destination rate with regime_key: "17" (if OSS enabled)IVA
Non-EUEXENTA_ART_21 + regime_key: "02" (goods) or NO_SUJETA_LOCALIZACION (services)IVA

BeeL. doesn't auto-detect whether you sell goods or services — set the operation type per Stripe product (in Stripe metadata) or as a connection default.

What BeeL. never modifies

  • The Stripe charge total — invoice total = Stripe total, always
  • The currency — must be EUR; we don't convert
  • The Stripe payment intent ID — kept as metadata.external_payment_id
  • The Stripe customer ID — kept on the customer mapping for future matching

Reconciling discrepancies

If you spot a Stripe charge whose generated invoice has a different total, it's almost always one of:

  1. A refund happened between charge and invoice generation — check for a sibling corrective
  2. The event was retried after a configuration change — the retry applied the new configuration
  3. Tax inclusivity flipped between Stripe Tax and the connection override — check the connection's tax-inclusive pricing toggle

For genuine mismatches that none of these explain, write to it@beel.es with the Stripe event ID and the BeeL. invoice number.

Silent sanitization

Before building the invoice, BeeL. checks a few fields of the Stripe payload and trims or annotates the ones that would otherwise make the invoice fail. Every modification is recorded with the resulting invoice, so the change can be audited.

FieldLimit / ruleWhat happens when violated
recipient.legal_nameMax 120 UTF-16 code units, as VeriFactu counts them (an emoji takes two)Truncated to fit
recipient.emailMax 255 charsTruncated to fit. If the email is so long that the address can't be preserved, BeeL. prefers to issue the invoice without an email rather than persist a malformed one
recipient.nif (Spanish context only)Must match Spanish DNI/NIE/CIF shape and pass the control-digit checkThe NIF itself is not modified. The invoice is rejected when it is issued, and the event waits in Necesita atención
recipient.country_code (ISO 3166-1 alpha-2)Must be a recognised codeAn unrecognised code falls back to the non-EU branch for tax classification; the invoice is not aborted

The email-drop case is surprising. When the supplied address is so long that even @domain exceeds 255 chars, BeeL. prefers to issue the invoice without an email rather than persist a malformed one (whose automatic send would then fail without telling anyone). The dropped value's original length is preserved in the warning so you can correlate the event.

The Spanish-context check for the NIF treats a payload with no country at all as Spain (BeeL's default) — so a missing country_code still triggers NIF validation, but a foreign country_code skips it.

Where the warnings end up

Every modification is recorded with the resulting invoice. Those records are not exposed through the public API today — write to it@beel.es with the invoice number if you need to trace a specific transformation.

A NIF warning means the invoice was created as a draft (or left in Needs attention, Necesita atención in the dashboard, depending on the flow) and will be rejected at issuance with NIF_INVALID_CONTROL_DIGIT / NIF_INVALID_FORMAT. See Customer mappings for how the recipient is resolved and the Necesita atención section in Edge cases and limits for what to do next.

Forward vs corrective routing

Stripe events fan out into two distinct routes, picked by the type of event:

  • Forward route — issues a brand-new F1 / F2 (forward invoice) with a fresh number against the resolved series.
  • Corrective route — looks up the original BeeL. invoice via the Stripe payment identifier and issues an R1 / R4 / R5 corrective invoice (factura rectificativa) that mirrors the original's fiscal shape.
Stripe eventChannelRouteResult
payment_intent.succeededConnectForwardNew F1 / F2
checkout.session.completedConnect & SubscriptionForwardNew F1 / F2
invoice.payment_succeeded / invoice.paidSubscription / ConnectForwardNew F1 / F2
refund.updatedConnectCorrectiveR1 / R4 / R5 against the original
credit_note.createdConnectCorrectiveR1 / R4 / R5 against the original

The full list of supported events lives in Supported events; this page only documents how each is routed.

What each route takes from where

  • A forward invoice resolves its recipient from the Stripe payment (optionally creating the customer mapping), picks F1 or F2 as described in Generated invoices, and takes the next number of the series.
  • A corrective never resolves a recipient of its own: it copies the recipient of the original invoice. The Stripe reason is mapped to a rectification code, and the tax-inclusive mode and IVA rate are taken from the original, so the corrective nets against it exactly.

What happens when the corrective arrives first

Stripe does not guarantee event ordering. A refund.updated can land before BeeL. has seen the payment_intent.succeeded that produced the original invoice. In that case the event waits with reason REFUND_ORIGINAL_NOT_FOUND and BeeL. retries it automatically — up to 8 times, about a day in all; once the payment arrives and creates the original, the next retry of the refund produces its rectificativa. See the Necesita atención section in Edge cases and limits.

Idempotency on both sides

Both routes deduplicate by the Stripe identifier of what they invoice:

  • A forward invoice is deduplicated by the Stripe payment: processing the same payment_intent.succeeded again always resolves to the same invoice, even when two deliveries race each other.
  • A corrective is deduplicated by the Stripe refund or credit-note ID: processing the same re_... or cn_... again always resolves to the same corrective.

The practical guarantee is: retrying any Stripe event always yields the same BeeL. invoice, never a duplicate.