Amounts & dates
How the API represents document money, precise catalog prices and quantities, dates, timestamps and Europe/Madrid boundaries.
Every monetary, date and time value in the public API follows a small set of fixed conventions. They are the same across every resource, so once you handle them in one place your client works everywhere.
Money
Amounts are always in euros (EUR) — the currency field is present on
every document and is "EUR" in v1 (ISO 4217).
Public document writes remain EUR-only; store connectors can convert incoming orders to EUR.
Amounts carry two decimal places (cents precision). The canonical representation is a decimal string with exactly two decimals, Stripe-style:
{ "price": "1234.56" }Document and line money always travels as a decimal string: a document's
total, subtotal and unit_price come back as "1210.00", "1000.00" and
"100.00", never as bare JSON numbers. The JSON numbers you still see are
not document money — quantities and percentages (quantity, tax_rate,
discount_percent), the preview totals of
POST /v1/companies/{company}/taxes/calculate-totals, the aggregates of the
statistics endpoints, and tax-report amounts in integer cents. Parse every
money value into a fixed decimal type on your side (e.g. Decimal in Python,
a big-decimal / minor-units integer in JS). Never store money as a raw binary
float.
Let the API compute totals
Do not pre-round and do not pre-compute. Send the raw inputs of each line
(quantity, unit_price, discount, the tax *_id) and let the API derive
the subtotal, VAT, surcharge, retention and the grand total. The server is the
single source of truth for every total — if you round line amounts yourself
before sending them, your figures can drift from what the API stores.
The total of a document follows one formula across the whole API:
total = subtotal + total_vat + total_surcharge − total_retentionIf you need to preview the breakdown before creating a document — for an
order summary, a cart, or to reconcile your own figures — call
POST /v1/companies/{company}/taxes/calculate-totals with the lines and read back the computed
subtotal, total_vat, total_surcharge, total_retention and total
(amounts in EUR), plus a per-line breakdown in the same order:
{
"subtotal": 250,
"total_vat": 52.5,
"total_surcharge": 0,
"total_retention": 15,
"total": 287.5,
"lines": [
{ "subtotal": 100, "vat_amount": 21, "surcharge_amount": 0, "retention_amount": 0, "total": 121 }
]
}The same per-line tax breakdown applies to every sales document:
invoices, quotes, proformas and delivery notes all accept a per-line
retention_rate and surcharge_rate (IRPF withholding and equivalence
surcharge, 0–100), and their header carries the aggregated total_vat,
total_surcharge and total_retention. The same formula holds everywhere.
Each line also echoes retention_rate and surcharge_rate back in the
response, so you can reconcile the breakdown line by line.
Equivalence surcharge follows fixed legal pairs. When a line declares a
surcharge_rate, it must match the VAT rate of that line according to the
Spanish regime: 21% → 5.2%, 10% → 1.4%, 4% → 0.5%, 0% → 0%.
An illegal pair (e.g. tax_rate: 21 with surcharge_rate: 1.4) is rejected
with 422 and the allowed pairs are returned in error.allowed_values. Send
only the surcharge that the line's VAT rate admits.
Expenses — per-line tax
An expense records what a supplier charged you, so each of its
lines accepts a few extra fiscal qualifiers that the sales side covers in its
own way. On CreatePurchaseInvoiceRequest.lines[] you can set:
| Field | Type | Meaning |
|---|---|---|
retention_rate | number (0–100) | IRPF withholding applied to the line. |
surcharge_rate | number | Equivalence surcharge, same legal pairs as sales. |
vat_deductible | boolean | Informative — flags the VAT as deductible. It does not change the amount paid. |
exemption_reason | enum or null | Why the line is exempt or non-subject (see below). |
Catalog lines also accept discount_percent (0–100, applied before tax and
already deducted from subtotal) and the purchase_measurement block that
separates packages, billed quantity and unit cost. On a measured line the API
returns quantity and unit_price as 4-decimal strings; a legacy line keeps
the number it has always published, so parse both. See
Measured purchase lines.
The per-line total follows the same shape as the sales side, with retention subtracted and surcharge added:
line total = subtotal + taxes − retention_amount + surcharge_amountsurcharge_rate on a purchase line follows the same legal VAT↔surcharge
pairs as a sales line, validated server-side: 21 → 5.2, 10 → 1.4,
4 → 0.5, 0 → 0. The 0 → 0 pair is valid on the purchase side too — an
exempt or zero-rated line carries a zero surcharge. An illegal pair is rejected
with 422.
exemption_reason qualifies why a line is outside ordinary VAT. It is an enum
or null (or absent), which means the line inherits the qualification from the
invoice header, or declares none:
| Value | Kind |
|---|---|
E1, E2, E3, E4, E5, E6 | Exempt (LIVA exemption cause) |
N1, N2 | Non-subject (out-of-scope cause) |
null | Inherit from header / none declared |
An expense line carrying both a surcharge and an exemption reason:
{
"description": "Wholesale goods",
"quantity": 10,
"unit_price": "50.00",
"tax_rate": 21,
"surcharge_rate": 5.2,
"retention_rate": 0,
"vat_deductible": true,
"exemption_reason": null
}curl -s -X POST https://api.factuarea.com/v1/companies/$COMPANY_ID/purchase-invoices \
-H "Authorization: Bearer $FACTUAREA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"supplier_id": "01931b3e-...s01",
"received_on": "2026-03-15",
"lines": [
{ "description": "Exempt service", "quantity": 1, "unit_price": "200.00", "tax_rate": 0, "surcharge_rate": 0, "exemption_reason": "E1" }
]
}' | jq '.data | {subtotal, total_vat, total_surcharge, total_retention, total}'Cents in tax reports. Aggregated fiscal endpoints (Modelo 303 / 347 via
/v1/companies/{company}/tax-reports/*) return their amounts as integer cents, not EUR
decimals — e.g. an accumulated taxable base of 25000 means 250.00 €. This
is documented per field in the spec; treat tax-report figures as minor units
and divide by 100 only for display.
Payment instalments
The payment_terms of an invoice is its contractual schedule of instalments. Unlike
the totals above, its amounts are never JSON numbers and are never inferred.
| Field | Contract |
|---|---|
amount | String with exactly two decimals and an optional leading minus: "40.00", "-20.00". "0.00", 40, 40.5 and "40.5" are rejected. Always EUR (payment_terms_currency is "EUR" and read-only). |
| Sign | Every row has the sign of total_to_pay. A corrective invoice with a negative total has negative instalments; mixed signs are rejected. |
| Sum | The exact sum of the amounts equals total_to_pay, disbursements included. Nothing is prorated, rounded or compensated for you. |
due_date | A real YYYY-MM-DD date (years 0001 to 9999). Rows keep the order you send and their dates must not decrease; equal dates are allowed. |
An invoice of 121.00 split in three instalments:
{
"payment_terms": [
{ "due_date": "2026-02-15", "amount": "40.00", "payment_means": "01" },
{ "due_date": "2026-03-15", "amount": "30.00", "payment_means": "04",
"account": { "iban": "ES9121000418450200051332", "bic": "CAIXESBBXXX" } },
{ "due_date": "2026-04-15", "amount": "51.00", "payment_means": "04",
"account": { "iban": "ES9121000418450200051332", "bic": "CAIXESBBXXX" } }
]
}40.00 + 30.00 + 51.00 is 121.00. A corrective invoice of -50.00 takes
"-20.00" and "-30.00". A schedule that does not add up, or whose amount is not a
two-decimal string, answers 422 invalid_invoice_payment_terms and writes nothing:
param names the row (payment_terms.1.amount) or payment_terms for order, sign and
total. Omission, null and [] mean different things (unknown, remove, explicitly no
schedule); see Invoice business data.
With a schedule, the invoice falls due on the day of its last instalment: in the
example above, due_on must be 2026-04-15 to issue it. A draft keeps another date,
but issuing it answers 422 invoice_due_date_not_last_instalment; see
the payment schedule.
Dates
Calendar dates (no time component) use YYYY-MM-DD — the
ISO-8601 / RFC 3339 full-date form.
This covers fields such as issued_on, due_on, paid_on, valid_until,
delivery_date, received_on, start_on and end_on:
{
"issued_on": "2026-03-15",
"due_on": "2026-04-14",
"paid_on": "2026-03-20"
}Send dates in the same format. There is no time and no timezone on a date — it is the calendar day as recorded for the document.
Invoicing period and generation date
invoicing_period is { "start_date", "end_date" }, two real dates with
start_date <= end_date (equal dates are valid). A period you send replaces the whole
period and null clears it. Neither date carries a time or a zone.
The generation date of a recurring invoice is not a field you send: the server fixes it
as the business day of the request that generates the invoice, and a replay of the same
request returns the invoice already created. The relative calendar of a recurrence
(payment_terms_template) gives every new invoice its own instalment dates, counted from
that day.
Timestamps
Instant-in-time fields (audit and lifecycle metadata such as created_at,
updated_at, signed_at, last_delivery_at) use full ISO-8601 / RFC 3339
date-time strings. Most are emitted in UTC with a Z suffix:
{ "created_at": "2026-05-15T10:34:21Z" }Some timestamps carry an explicit Europe/Madrid offset instead
(+01:00 in winter, +02:00 in summer):
{ "created_at": "2026-04-15T10:31:05+02:00" }Both forms are valid ISO-8601 and denote the same kind of value: an exact
instant. Parse the offset — do not assume the string is always UTC. A
proper ISO-8601 parser (Instant.parse, datetime.fromisoformat,
new Date(...), Carbon::parse) handles Z and ±hh:mm identically and
normalises to the absolute instant.
Timezone for quotas
The monthly rate-limit quota resets on day 1 of each calendar month at
00:00 Europe/Madrid (CET/CEST), not UTC. The per-minute quota is a sliding
window and the X-RateLimit-Reset header is a UNIX timestamp (seconds since
the epoch, timezone-independent). See Rate limits for the
full window semantics.
Whenever the API needs a single civil-calendar reference for a business boundary — fiscal periods, the monthly quota reset — that reference is Europe/Madrid.
Quick reference
| Value | Format | Example |
|---|---|---|
| Document money and totals | EUR, two decimals — decimal string | "1234.56" |
| Catalog unit price / cost | EUR, up to four decimals | "8.7500" |
| Catalog quantity / stock | Up to four decimals | "12.3750" |
| Conversion factor | Up to six decimals | "12.000000" |
| Currency | ISO 4217, always EUR in v1 | "EUR" |
| Tax-report amounts | Integer cents (minor units) | 25000 → 250.00 € |
| Date | YYYY-MM-DD (ISO-8601 full-date) | "2026-03-15" |
| Instalment amount | Signed EUR decimal string with exactly two decimals; the sum equals total_to_pay | "-20.00" |
| Timestamp | ISO-8601 date-time, usually UTC Z, sometimes ±hh:mm | "2026-05-15T10:34:21Z" |
| Quota / fiscal calendar | Europe/Madrid civil time | day 1, 00:00 CET/CEST |
Additional line descriptions
Document lines support nullable additional_description up to 5000 characters alongside the main description. It is available on invoices and correctives, quotes, proformas, delivery notes, expenses and recurring templates. Keep it separate from quantities, prices, tax fields and catalog identifiers; adding detail does not change the amount calculation.