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 field | BeeL. invoice field | Conversion |
|---|---|---|
payment_intent.amount_received (cents) | total (EUR) | Divide by 100 |
charge.amount (cents) | Fallback when amount_received is absent | Divide by 100 |
invoice.amount_paid (cents, Stripe Billing) | Fallback for invoice.paid events | Divide by 100 |
currency | Validated; must be eur | Other currencies are rejected at processing time |
metadata.external_payment_id | metadata.external_payment_id | Stripe payment_intent.id |
Customer fiscal data
The resolved customer comes from Customer mappings. The conversion of fields:
| Stripe source | BeeL. invoice field |
|---|---|
customer.email → fallback charge.billing_details.email | recipient.email |
customer.name → fallback charge.billing_details.name | recipient.legal_name |
customer.tax_ids[0].value | recipient.nif (if Spanish) or recipient.alternative_id.number |
customer.address.line1 / line2 | recipient.address.street |
customer.address.postal_code | recipient.address.postal_code |
customer.address.city | recipient.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:
- Read the connection's tax-inclusive pricing setting (per integration)
- Read your default tax configuration (Settings > Taxes): main tax (IVA / IGIC / IPSI) and standard rate
- Treat the Stripe total as inclusive of that rate
- 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 country | Resulting exemption_reason | main_tax.type |
|---|---|---|
| Spain (peninsula or Balearic) | omitted (S1) | IVA |
| Canary Islands (resolved from address) | omitted | IGIC if configured, else IVA |
| Ceuta / Melilla | omitted | IPSI if configured, else IVA |
EU B2B (VIES VAT-ID on tax_ids) | EXENTA_ART_25 (goods) or NO_SUJETA_LOCALIZACION (services) | IVA |
| EU B2C | omitted — origin IVA, or the destination rate with regime_key: "17" (if OSS enabled) | IVA |
| Non-EU | EXENTA_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:
- A refund happened between charge and invoice generation — check for a sibling corrective
- The event was retried after a configuration change — the retry applied the new configuration
- 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.
| Field | Limit / rule | What happens when violated |
|---|---|---|
recipient.legal_name | Max 120 UTF-16 code units, as VeriFactu counts them (an emoji takes two) | Truncated to fit |
recipient.email | Max 255 chars | Truncated 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 check | The 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 code | An 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 event | Channel | Route | Result |
|---|---|---|---|
payment_intent.succeeded | Connect | Forward | New F1 / F2 |
checkout.session.completed | Connect & Subscription | Forward | New F1 / F2 |
invoice.payment_succeeded / invoice.paid | Subscription / Connect | Forward | New F1 / F2 |
refund.updated | Connect | Corrective | R1 / R4 / R5 against the original |
credit_note.created | Connect | Corrective | R1 / 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.succeededagain 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_...orcn_...again always resolves to the same corrective.
The practical guarantee is: retrying any Stripe event always yields the same BeeL. invoice, never a duplicate.