Amounts & dates
How the API represents money (EUR, two decimals), dates (YYYY-MM-DD), timestamps (ISO-8601) and the Europe/Madrid timezone used for quota resets.
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).
There is no multi-currency support yet.
Amounts carry two decimal places (cents precision). The canonical representation is a decimal string with exactly two decimals, Stripe-style:
{ "price": "1234.56" }Some resources currently emit amounts as JSON numbers (floats) rather
than decimal strings — for example a document's total, subtotal or
unit_price come back as 1802.9, 968, 100. Write your parser to accept
both a string and a number for any money field, and normalise to 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/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.
Purchase invoices — per-line tax
A purchase invoice 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). |
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 |
A purchase-invoice 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/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/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.
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.
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 |
|---|---|---|
| Money | EUR, two decimals — decimal string (some fields emit a number) | "1234.56" / 1802.9 |
| 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" |
| 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 |