Factuarea APIDevelopers

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_retention

If 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:

FieldTypeMeaning
retention_ratenumber (0–100)IRPF withholding applied to the line.
surcharge_ratenumberEquivalence surcharge, same legal pairs as sales.
vat_deductiblebooleanInformative — flags the VAT as deductible. It does not change the amount paid.
exemption_reasonenum or nullWhy 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_amount

surcharge_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:

ValueKind
E1, E2, E3, E4, E5, E6Exempt (LIVA exemption cause)
N1, N2Non-subject (out-of-scope cause)
nullInherit 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.

FieldContract
amountString 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).
SignEvery row has the sign of total_to_pay. A corrective invoice with a negative total has negative instalments; mixed signs are rejected.
SumThe exact sum of the amounts equals total_to_pay, disbursements included. Nothing is prorated, rounded or compensated for you.
due_dateA 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

ValueFormatExample
Document money and totalsEUR, two decimals — decimal string"1234.56"
Catalog unit price / costEUR, up to four decimals"8.7500"
Catalog quantity / stockUp to four decimals"12.3750"
Conversion factorUp to six decimals"12.000000"
CurrencyISO 4217, always EUR in v1"EUR"
Tax-report amountsInteger cents (minor units)25000 → 250.00 €
DateYYYY-MM-DD (ISO-8601 full-date)"2026-03-15"
Instalment amountSigned EUR decimal string with exactly two decimals; the sum equals total_to_pay"-20.00"
TimestampISO-8601 date-time, usually UTC Z, sometimes ±hh:mm"2026-05-15T10:34:21Z"
Quota / fiscal calendarEurope/Madrid civil timeday 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.

On this page

Need a hand?Contact support