Invoice business data
Payment schedule, header and line references, invoicing period, legal literals and DIR3 administrative centres on an invoice — omit versus null, versions, and how to change them after issuing.
Besides its fiscal content, an invoice can carry structured business data: the agreed payment schedule, the references the customer asks to see (contract, transaction, file, delivery notes), the invoicing period, commercial legal literals and the DIR3 administrative centres that route it in the public sector. This data is additive: an invoice without it behaves exactly as before.
All of it lives in the same bodies you already use — POST /v1/companies/{company}/invoices,
PATCH /v1/companies/{company}/invoices/{invoice} and the rows of bulk-create — and comes back in the
invoice resource. All the routes in this guide hang off /v1/companies/{company}/…:
{company} is the id (UUID v7) of the tax ID you operate on, and in the curl
examples it is $COMPANY_ID. Reach a tax ID explains how
to resolve it.
The life cycle has two phases:
- While the invoice is a draft you can create, replace and clear every field.
- When it is issued the data is frozen with it. Editing it is rejected with
422business_rule_violationand subcodeinvoice_not_editable_in_current_state. The only thing you can still change is the administrative routing, by appending an administrative revision.
| Block | Request fields | Frozen at issue |
|---|---|---|
| Payment schedule | payment_terms, expected_payment_terms_version | Yes. Validated and frozen once. |
| References | business_references, lines[].business_references, lines[].delivery_notes_references, lines[].sequence_number | Yes |
| Invoicing period and literals | invoicing_period, legal_literals | Yes |
| DIR3 centres | administrative_centres | The captured routing, plus revisions |
Omit, null and []
The API distinguishes a key that is absent from one that is null or empty,
and the difference matters. The same rule applies to every field of this guide:
| You send | On create | On update (PATCH) |
|---|---|---|
| The key is absent | Nothing is stored (the schedule stays unknown). For DIR3, the recipient's centres are inherited. | Keeps the current value. |
null | Same as absent. | Clears it (for administrative_centres, removes your override and goes back to the inherited centres). Only on a draft. |
[] (lists) | Explicit "none". For payment_terms: a known empty schedule. | Replaces the whole list with an empty one. |
| A list | Stored as sent, order and duplicates included. | Replaces the whole list. |
| An object | Stored as sent. | Merges by key: only the keys you send change (business_references, line business_references). {} changes nothing. |
Nothing is trimmed, deduplicated or reordered. Text limits count Unicode characters.
Payment schedule
payment_terms is the agreed calendar of instalments. It is contractual
information and it does not register payments.
With a schedule, the invoice falls due on the day of its last instalment.
- To issue it,
due_onmust be that date. A missing or differentdue_onanswers422 invoice_due_date_not_last_instalment(param: due_on) before a number is assigned, and nothing is issued. It applies toissue, to a create that issues at once throughoptionsand to every other way of issuing. - A draft keeps the
due_onyou send, even if it differs; the check runs when it is issued. - An update (
PATCH) that replacespayment_termsand leavesdue_onout movesdue_onto the last instalment. If you send both and they differ, the draft keeps them and issuing it answers the same422. - Scheduling or rescheduling the issue (
schedule,reschedule) checks the same rule: the invoice keeps its status and date and the request answers the same422. - Duplicating an invoice issues the draft today: each instalment keeps its amount and
moves by the days between both issue dates, and
due_onis the last moved instalment. - Without a schedule,
due_onis the date you send.
| Field | Type | Notes |
|---|---|---|
id | string (UUID v7) | Optional. Omitted: the backend assigns one on create. On update, send it to keep the identity of an existing instalment. null and empty are invalid. |
due_date | string (YYYY-MM-DD) | A real date. Dates never decrease along the list; equal dates are allowed. |
amount | string | Signed decimal with exactly two decimals, not zero: "40.00", "-20.00". Never a JSON number. |
payment_means | string | "01" to "19", the Facturae 3.2.2 payment means. |
account | object or null | { "iban": "...", "bic": "..." }. Only for 02 (direct debit) and 04 (credit transfer). bic is optional. |
The amounts must add up exactly to total_to_pay (the total plus
disbursements), with the sign of that amount, so a negative total takes negative
instalments. Factuarea never converts, splits or rounds amounts for you. A
payable of zero only admits null or [].
curl -X POST https://api.factuarea.com/v1/companies/$COMPANY_ID/invoices \
-H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"client_id": "0197c5a0-4d2b-7c18-9f63-1a8e5b3d7c24",
"issued_on": "2026-10-14",
"due_on": "2026-12-13",
"lines": [
{ "description": "Mantenimiento mensual", "quantity": 1, "unit_price": 100, "tax_rate": 21 }
],
"payment_terms": [
{ "due_date": "2026-11-13", "amount": "60.50", "payment_means": "01" },
{
"due_date": "2026-12-13",
"amount": "60.50",
"payment_means": "04",
"account": { "iban": "ES9121000418450200051332", "bic": "CAIXESBBXXX" }
}
]
}'The response returns payment_terms with the identities, plus:
| Field | Meaning |
|---|---|
payment_terms_version | Version of the schedule. 0 when there is none, 1 after the first write. |
payment_terms_source | none, explicit (you sent it) or legacy (captured by Factuarea from an older issued invoice). |
payment_terms_currency, payment_terms_captured_at, payment_terms_frozen_at | Currency (EUR) and the moments it was captured and frozen. |
Those five fields are read-only: sending them answers 422.
Changing the schedule of an existing invoice requires
expected_payment_terms_version, an integer JSON value (0 or more), even
when you send null, [] or the same list. It is the compare-and-set of the
schedule, independent of anything else. Without it you get
428 invoice_payment_terms_precondition_required; with a stale one,
409 invoice_payment_terms_version_conflict with details.current_version.
Recover by reading the invoice again and reapplying your change on its
payment_terms_version.
curl -X PATCH https://api.factuarea.com/v1/companies/$COMPANY_ID/invoices/0197c5b1-7e90-7a42-b5d8-3c6f2e1a9b07 \
-H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"payment_terms": [],
"expected_payment_terms_version": 1
}'Rules worth knowing:
- Editing only descriptions, or reordering lines while the payable stays the same, does not ask for a new schedule. Changing the payable while a schedule exists does: you must send the complete, balanced replacement and its version.
- An instalment with
02or04may lack anaccountwhile the invoice is a draft. Issuing (direct, scheduled or by status change) checks every account and that the schedule adds up, then freezes it. A failure rolls the whole issue back: no number is consumed. - IBANs are validated (format and checksum, any country) and spaces are normalized; the BIC, if sent, keeps its 8 or 11 characters.
- Duplicating an invoice copies the schedule with new identities. A corrective starts with no schedule unless you send one (negative instalments are valid there).
- In
POST /v1/companies/{company}/invoiceswith anexternal_idthat already exists, the request is an update and follows the update rules.
References, period and literals
Header references go in business_references, a closed object with three
keys, each up to 20 characters: receiver_contract_reference,
receiver_transaction_reference and file_reference.
Line references go in lines[].business_references: five pairs of reference
(up to 20 characters) and date (YYYY-MM-DD):
| Reference | Date |
|---|---|
issuer_contract_reference | issuer_contract_date |
issuer_transaction_reference | issuer_transaction_date |
receiver_contract_reference | receiver_contract_date |
receiver_transaction_reference | receiver_transaction_date |
file_reference | file_date |
A reference without a date is valid; a date without its reference is not, once a partial edit has been merged. Header objects do not accept dates.
Each line also takes delivery_notes_references, a list of
{ "number": "ALB-2026-014", "date": "2026-10-02" } (number up to 30
characters, date optional), and sequence_number: null or an integer from 0
to 2147483647 (0 is valid; a string, a float or a boolean is not).
invoicing_period is an object with both dates, start_date and end_date, real
dates with start on or before end. legal_literals is an ordered list of
commercial texts of up to 250 characters, kept with duplicates. They are
independent of the fiscal legal_mentions the invoice already computes and never
replace them.
{
"business_references": { "file_reference": "EXP-2026-0042" },
"invoicing_period": { "start_date": "2026-10-01", "end_date": "2026-10-31" },
"legal_literals": ["Operación sujeta al convenio marco 2026."],
"lines": [
{
"description": "Mantenimiento mensual",
"quantity": 1,
"unit_price": 100,
"tax_rate": 21,
"business_references": {
"issuer_contract_reference": "C-1",
"issuer_contract_date": "2026-01-15"
},
"delivery_notes_references": [{ "number": "ALB-2026-014", "date": "2026-10-02" }],
"sequence_number": 0
}
]
}Line identity. In the response each line has an id (UUID v7). On PATCH, send
that id to say which line you mean: you can reorder the lines and each keeps
its references. A line without id is new. On create, lines[].id is rejected,
and so is an id that belongs to another invoice, a removed line or a duplicate.
Corrective invoices inherit line references only from the line you name with
lines[].source_line_id, never by position; header-level data
(business_references, invoicing_period, legal_literals and
administrative_centres) is not accepted on a corrective.
DIR3 administrative centres
Invoices to the public administration carry up to four DIR3 centres, each with a role. They are what FACe invoicing uses to route:
role_code | Role |
|---|---|
01 | Oficina contable |
02 | Órgano gestor |
03 | Unidad tramitadora |
04 | Proponente (optional) |
administrative_centres is a list of { "role_code", "code", "name" }. Roles are
unique. code is trimmed and upper-cased and must match ^[A-Z][A-Z0-9]{8,9}$
(for example L01280796); name is optional text of up to 40 characters, kept as
sent. The response sorts the centres by role.
Where the centres come from, in order:
- The override of the invoice (what you send in
administrative_centres). - The snapshot the invoice captured from the customer's profile when it was created (and refreshed while it is a draft and you change the customer).
- Only for an older invoice whose captured routing is unknown, the customer's current codes.
The response exposes all three: administrative_centres (effective),
administrative_snapshot and administrative_override, with
administrative_version and administrative_revision_id. Mind the two kinds of
empty:
| Value | Means |
|---|---|
administrative_snapshot: null | Unknown (legacy). A fallback to the customer is allowed. |
administrative_snapshot: [] | Known, with no centres. They are never filled from the live customer. |
administrative_override: null | No override: the snapshot applies. |
administrative_override: [] | Explicit override with no centres at all. |
On update, null removes your override and [] replaces it with an empty one.
The role 04 survives even when 01 to 03 are empty. The captured routing is
not rewritten when the customer changes later.
The customer's codes (decision DEC-06)
The codes come from the customer profile:
PATCH /v1/companies/{company}/contacts/{contact}/customer-profile
takes dir3_accounting_office, dir3_managing_body, dir3_processing_unit and
dir3_proponent, each with the same code format. The four codes follow one
omission rule: the operation replaces the customer profile, so a DIR3 code you
leave out is cleared, dir3_proponent included. Send the four you want to keep
every time. Changing them never touches invoices or FACe submissions already
created.
curl -X PATCH https://api.factuarea.com/v1/companies/$COMPANY_ID/contacts/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42/customer-profile \
-H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"dir3_accounting_office": "L01280796",
"dir3_managing_body": "L01280796",
"dir3_processing_unit": "L01280796",
"dir3_proponent": "L01280797"
}'Revise the routing after issuing
An invoice in issued or overdue status cannot change its business data, but
its administrative routing can still be corrected. Every correction is an
append: a new revision with its own identity, version, reason and author. It
does not reissue, send or alter amounts, dates, legal identities, the payment
schedule or the PDF.
curl https://api.factuarea.com/v1/companies/$COMPANY_ID/invoices/0197c5b1-7e90-7a42-b5d8-3c6f2e1a9b07/administrative-revisions \
-H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW"{
"data": {
"invoice_id": "0197c5b1-7e90-7a42-b5d8-3c6f2e1a9b07",
"version": 1,
"snapshot": [
{ "role_code": "01", "code": "L01280796", "name": null },
{ "role_code": "02", "code": "L01280796", "name": null },
{ "role_code": "03", "code": "L01280796", "name": null }
],
"override": null,
"revisions": []
}
}To correct the routing, send the complete list and the version you read:
curl -X POST https://api.factuarea.com/v1/companies/$COMPANY_ID/invoices/0197c5b1-7e90-7a42-b5d8-3c6f2e1a9b07/administrative-revisions \
-H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"administrative_centres": [
{ "role_code": "01", "code": "L01280796", "name": null },
{ "role_code": "02", "code": "L01280796", "name": null },
{ "role_code": "03", "code": "L01280798", "name": "Unidad corregida" },
{ "role_code": "04", "code": "L01280797", "name": null }
],
"reason": "La unidad tramitadora indicada por el cliente era otra.",
"expected_version": 1
}'It answers 201 with the revision just created and its version, even if another
revision is confirmed right after:
{
"data": {
"id": "0197c5c2-0f31-7b74-8d29-6e4a1c7b3d58",
"version": 2,
"administrative_centres": [
{ "role_code": "01", "code": "L01280796", "name": null },
{ "role_code": "02", "code": "L01280796", "name": null },
{ "role_code": "03", "code": "L01280798", "name": "Unidad corregida" },
{ "role_code": "04", "code": "L01280797", "name": null }
],
"reason": "La unidad tramitadora indicada por el cliente era otra.",
"created_at": "2026-10-14T10:05:41.000000Z",
"actor_id": "0197a001-3c5d-7e12-8a47-5b9c2d1f6e80",
"correlation_id": null
},
"administrative_version": 2
}expected_versionis a body field (an integer,1or more) and the only guard: anIf-Matchof the invoice does not apply. Without it,428invoice_administrative_precondition_required; stale,409invoice_administrative_version_conflictwithdetails.current_version. Read the history again and retry on that version.reasonis required, 1 to 500 characters.administrative_centresis required and may be[]to record "no routing".- Only
issuedandoverdueinvoices accept a revision; a draft, scheduled, cancelled or annulled one answers422business_rule_violationwith subcodeinvoice_not_editable_in_current_state(edit a draft throughPATCHinstead). - The revision is part of the history of the invoice; a FACe submission that was already prepared keeps the routing it froze, and the next attempt uses the new one.
Scopes: invoices:read to list, invoices:write to append. The MCP server offers
list_invoice_administrative_revisions and revise_invoice_administrative_routing.
Errors
| HTTP | code | When |
|---|---|---|
| 422 | invalid_invoice_business_data | A reference, period, literal, centre, line id or sequence_number is invalid. param is the exact path, for example lines.0.business_references.issuer_contract_date. |
| 422 | invalid_invoice_payment_terms | An instalment is invalid (payment_terms.N.amount, .id, .account…), the schedule does not add up, or a read-only field was sent. |
| 422 | invoice_due_date_not_last_instalment | The invoice has a payment schedule and is issued with a due_on that is missing or is not the date of the last instalment (param: due_on). Set it to that date, or adjust the instalments, and issue again. See Payment schedule. |
| 428 | invoice_payment_terms_precondition_required | You change the schedule of an existing invoice without expected_payment_terms_version. |
| 409 | invoice_payment_terms_version_conflict | expected_payment_terms_version is stale (details.current_version). |
| 428 | invoice_administrative_precondition_required | An administrative revision without expected_version. |
| 409 | invoice_administrative_version_conflict | expected_version is stale (details.current_version). |
| 422 | business_rule_violation / invoice_not_editable_in_current_state | You edit the data of an issued invoice, or revise the routing of a draft. |
| 404 | invoice_not_found | The invoice does not exist or belongs to another company. |
| 403 | insufficient_scope | The key lacks invoices:read or invoices:write. |
{
"error": {
"type": "invalid_request_error",
"code": "invalid_invoice_business_data",
"message": "Los datos comerciales de la factura no son válidos.",
"param": "lines.0.business_references.issuer_contract_date",
"request_id": "req_01JBVH7K9Y4N3CDQ2EHJB1AGSV",
"doc_url": "https://docs.factuarea.com/guides/errors#invalid_invoice_business_data"
}
}In bulk-create each row is validated on its own and a bad row is reported in
results[] without failing the others; see Bulk operations.
Related
- FACe invoicing: how the effective centres and the schedule travel in the FacturaE XML.
- Corrective invoices: what a corrective inherits and what it cannot carry.
- Document presentations: the PDF that prints this data.
- Amounts and dates: decimal strings and date formats.