Amounts and rounding
How BeeL. turns quantities, prices and rates into bases, taxes and totals: precision, the three ways to price a line, per-group tax rounding, IRPF, and the amounts that are rejected.
You never send a tax amount or an invoice total. You send lines — a quantity, a price, a tax rate — and BeeL. computes the rest. This page explains how, so the numbers you store match the ones on the invoice to the cent.
Precision
| Value | Decimals | Examples |
|---|---|---|
| Unit price | 4 | 0.0897 per label. More decimals are rounded: 0.08975 is stored as 0.0898. |
| Amounts (bases, taxes, totals) | 2 | 82.64 |
| Percentages | 3 | 5.2, 0.62. AEAT only takes two: on an invoice recorded with VeriFactu, a tax or surcharge rate with more decimals is rejected at issue with 422 INVOICE_TAX_RATE_TOO_MANY_DECIMALS. |
Every rounding is half up (0.005 → 0.01). All amounts are in euros: there is no currency field on an invoice.
Three ways to price a line
Each line carries exactly one of these fields; none or more than one is rejected with 422 LINE_UNIT_PRICE_XOR_DECLARED_TOTAL.
| Field | You state | BeeL. derives | Discount |
|---|---|---|---|
unit_price | Price per unit before taxes | Base = quantity × unit price × (1 − discount) | Allowed |
total_excluding_tax | The line's exact taxable base | The unit price, as total / quantity (4 decimals, informational) | Rejected |
total_including_tax | What the customer paid for the line (base + VAT + surcharge) | Base and taxes, worked backwards | Rejected |
- Discounts are per line.
discount_percentagegoes from 0 to 100 and applies only tounit_pricelines. A declared total already includes any discount, so sending both answers422LINE_DECLARED_TOTAL_FORBIDS_DISCOUNT. There is no invoice-level discount. - Declared totals win over the unit price. The unit price of a declared-total line is printed because article 6.1.f of the RD 1619/2012 lists it among the contents of an invoice, but the base is never recomputed from it: 1.00 for 300 units is a base of exactly
1.00, even though0.0033 × 300is0.99. The response tells you which mode a line used inpricing_mode.
Taxes are rounded per group, not per line
Adding up rounded line taxes does not give the same result as rounding the sum. BeeL. follows the second way, the one your tax return uses:
- Lines are grouped by tax: VAT (and IGIC, IPSI…) by type, rate and regime key; equivalence surcharge and IRPF by rate.
- The group's tax is computed once, on the sum of its unrounded bases, and rounded to the cent.
- That amount is spread back over the group's lines: each gets its exact share rounded down, and the leftover cents go to the lines with the largest remainders (ties go to the earlier line).
- The bases of
unit_pricelines are spread the same way, so the published base of a group always equals its rounded sum.
So a line's tax is not always round(base × rate) on its own — but the breakdowns (vat_breakdown, surcharge_breakdown, irpf_breakdown) match base × rate for their group, and every line still satisfies line_total = taxable_base + VAT + surcharge − IRPF. The one exception is a group that contains lines priced with taxes included: those lines keep the cent they absorbed, and the breakdown carries it.
A line in the response carries its taxable_base and its line_total, not its own VAT or IRPF amount: the per-line split below is what those two figures imply.
Worked example
Three identical lines, 21 % VAT and 15 % IRPF:
{
"type": "STANDARD",
"recipient": { "customer_id": "4f244735-980b-8d9c-80e8-6331fa0b1958" },
"lines": [
{ "description": "Support — week 1", "quantity": 1, "unit_price": 12.45, "main_tax": { "type": "IVA", "percentage": 21, "regime_key": "01" }, "irpf_rate": 15 },
{ "description": "Support — week 2", "quantity": 1, "unit_price": 12.45, "main_tax": { "type": "IVA", "percentage": 21, "regime_key": "01" }, "irpf_rate": 15 },
{ "description": "Support — week 3", "quantity": 1, "unit_price": 12.45, "main_tax": { "type": "IVA", "percentage": 21, "regime_key": "01" }, "irpf_rate": 15 }
]
}| Line 1 | Line 2 | Line 3 | Invoice | |
|---|---|---|---|---|
| Base | 12.45 | 12.45 | 12.45 | 37.35 |
| Exact VAT (21 %) | 2.6145 | 2.6145 | 2.6145 | 7.8435 → 7.84 |
| VAT per line | 2.62 | 2.61 | 2.61 | 7.84 |
| Exact IRPF (15 %) | 1.8675 | 1.8675 | 1.8675 | 5.6025 → 5.60 |
| IRPF per line | 1.87 | 1.87 | 1.86 | 5.60 |
line_total (returned) | 13.20 | 13.19 | 13.20 | 39.59 |
Rounding each line on its own would have given 3 × 2.61 = 7.83 of VAT (one cent short of 21 % of 37.35) and 3 × 1.87 = 5.61 of IRPF (one cent over). Per group, the VAT is 7.84: each line gets 2.61 and the leftover cent goes to line 1, since all three remainders tie. The IRPF is 5.60: two leftover cents go to lines 1 and 2. The invoice returns taxable_base: 37.35, total_vat: 7.84, total_irpf: 5.60 and invoice_total: 39.59.
Lines priced with taxes included
For total_including_tax, BeeL. works backwards from what the customer paid:
- Base =
total / (1 + VAT + surcharge), rounded to the cent. - VAT = the unrounded base × the VAT rate, rounded.
- The last tax — the surcharge if there is one, otherwise the VAT — absorbs the leftover cent, so that base + VAT + surcharge equals the declared total exactly.
10.00 including 21 % VAT and a 5.2 % surcharge:
{
"description": "Retail sale",
"quantity": 1,
"total_including_tax": 10.00,
"main_tax": { "type": "IVA", "percentage": 21, "regime_key": "18" },
"equivalence_surcharge_rate": 5.2,
"irpf_rate": 0
}| Computation | Result | |
|---|---|---|
| Base | 10.00 / 1.262 = 7.92393… | 7.92 |
| VAT | 7.92393… × 21 % = 1.66402… | 1.66 |
| Surcharge | 10.00 − 7.92 − 1.66 | 0.42 |
Computed on its own the surcharge would be 0.41, and the line would add up to 9.99. The surcharge takes the cent instead, so the total is the 10.00 you declared, and the base stays the plain rounding of the division. The derived unit_price is 7.92, and the invoice returns surcharge_breakdown: [{ "type": 5.2, "base": 7.92, "amount": 0.42 }] — 0.42, not the 0.41 that 5.2 % of 7.92 would give.
These lines keep their own VAT and surcharge; they are not redistributed with the rest of their group, so their declared total always holds. Their IRPF is, since the withholding is outside that total.
IRPF
- Rates:
0,1,2,2.8,6,7,7.6,9.5,15,19or24. - Base: the line's taxable base after discount. On a line priced by total, the final rounded base.
- Who can bear which rate: only individuals pay IRPF. A company whose NIF starts with
A,B,C,D,F,G,Q,R,UorWonly accepts0,19,24and9.5(rents in Ceuta and Melilla); a non-resident entity (N)0,19and24; the State, an Autonomous Community or a local entity (S,P) only0. Any other rate answers422IRPF_RATE_NOT_FOR_CORPORATE_ISSUER, and9.5from an individual answers422IRPF_RATE_ONLY_FOR_CORPORATE_ISSUER(the Ceuta and Melilla rates of individuals are6,2.8and7.6). It is checked on create, on edit and again on issue; a corrective invoice keeps what its original carried.withholding_optionsin Get the tax configuration of a company lists the rates the company can use (allowed_irpf_rates) and the one to preselect (suggested_irpf_rate,nullwhen it depends on facts the NIF does not show). - Default: a line of a
STANDARDinvoice (or a proforma) that omitsirpf_ratetakes the company's default IRPF rate (see Get the tax configuration of a company). With a default of 15 %, a 100.00 line sent withoutirpf_ratecomes back withirpf_rate: 15and aline_totalof 106.00. To invoice without withholding, sendirpf_rate: 0explicitly. A stored default the company cannot bear is not applied: the line gets0. - Simplified invoices cannot carry IRPF: any rate other than 0 is rejected with
422SIMPLIFICADA_FORBIDS_IRPF, never silently dropped. A simplified line that omitsirpf_rategets0, not the company default. - Recurring invoice templates do not inherit the default either: a template line without
irpf_rategenerates invoices with no withholding. Send the rate on the template line if you need it. - Accepted rates are the list above; any other value answers
422VALIDATION_ERRORnaming the line (lines[0].irpf_rate). The value counts, not how it is written:15.0is15and2.80is2.8. - Not part of the price. IRPF is withheld from what the customer pays: it lowers
line_totalandinvoice_total, and it is never part of atotal_including_tax.
Amounts that are rejected
| You send | Error | Status | Why |
|---|---|---|---|
| A negative total on anything but a corrective invoice | NEGATIVE_TOTAL_REQUIRES_RECTIFICATIVE | 422 | The lines add up to a negative total on an invoice that is not a corrective one. Individual negative lines (a discount, a deposit) are allowed; a negative invoice is a refund, and a refund has to reference the invoice it refunds. |
A line with quantity 0 | LINE_INVALID_QUANTITY | 422 | A line's quantity is zero or missing. A negative quantity is accepted, as long as the invoice total stays positive. |
A negative unit_price (lines[0].unit_price in error.details) | VALIDATION_ERROR | 422 | Part of the request is not acceptable. With 422, the body parsed but a value breaks a rule: a missing required property, a length or range, a value outside an enum. error.details maps each offending property (snake_case, nested paths joined with a dot) to what is wrong with it. With 400, the problem is in the URL: an unknown query parameter, a value that does not parse, such as a malformed UUID or an unknown enum value, an empty element in a list filter (status=ISSUED,; a parameter sent entirely empty, status=, is the same as omitting it) or a sort_by the list does not sort by. error.details names the parameter. |
A discount outside 0–100 (lines[0].discount_percentage in error.details) | VALIDATION_ERROR | 422 | Part of the request is not acceptable. With 422, the body parsed but a value breaks a rule: a missing required property, a length or range, a value outside an enum. error.details maps each offending property (snake_case, nested paths joined with a dot) to what is wrong with it. With 400, the problem is in the URL: an unknown query parameter, a value that does not parse, such as a malformed UUID or an unknown enum value, an empty element in a list filter (status=ISSUED,; a parameter sent entirely empty, status=, is the same as omitting it) or a sort_by the list does not sort by. error.details names the parameter. |
| A declared total together with a discount | LINE_DECLARED_TOTAL_FORBIDS_DISCOUNT | 422 | A line priced by a declared total (total_excluding_tax or total_including_tax) also sends a discount_percentage other than 0. A declared total already includes any discount; discount_percentage: 0 is accepted. |
| A declared total whose derived unit price does not fit | LINE_UNIT_PRICE_OUT_OF_RANGE | 422 | The unit price derived from a line's declared total does not fit the accepted range, typically a large total over a tiny quantity. error.details.field names the line, with max and derived_unit_price. |
An invoice whose lines are all SUPLIDO | INVOICE_REQUIRES_AT_LEAST_ONE_NORMAL_LINE | 422 | Every line of the invoice is a disbursement (SUPLIDO). An invoice of disbursements alone documents no operation, so it is not an invoice. |
A total of zero is accepted. An invoice whose total_to_pay is 0 has nothing to collect, so it is issued as PAID, with payment_date equal to issue_date, and registered with the AEAT like any other.
Negative amounts on a line are accepted — a negative quantity, or a negative declared total
(total_excluding_tax, total_including_tax) — as long as the invoice total stays positive.
You never send a line's result: taxable_base and line_total are output fields. If a line carries them, they are ignored and computed again from the quantity, price and rates.
Issued invoices are never recalculated
Once an invoice is issued its amounts are a fact: its JSON, its PDF and its VeriFactu record say the same thing, and keep saying it. If BeeL. improves the way it computes amounts, the change applies to new invoices only. Two identical lines issued months apart can therefore split a cent differently between base and tax, with the same total.
Related
Invoice lifecycle
Every invoice status, which operations each one allows, and the difference between the fiscal steps (issue, void, correct) and the commercial ones (sent, paid).
Series and numbering
When an invoice gets its number, how the format and counter resets work, default series per document type, and why a series locks once it has issued.