Factuarea APIDevelopers

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 422 business_rule_violation and subcode invoice_not_editable_in_current_state. The only thing you can still change is the administrative routing, by appending an administrative revision.
BlockRequest fieldsFrozen at issue
Payment schedulepayment_terms, expected_payment_terms_versionYes. Validated and frozen once.
Referencesbusiness_references, lines[].business_references, lines[].delivery_notes_references, lines[].sequence_numberYes
Invoicing period and literalsinvoicing_period, legal_literalsYes
DIR3 centresadministrative_centresThe 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 sendOn createOn update (PATCH)
The key is absentNothing is stored (the schedule stays unknown). For DIR3, the recipient's centres are inherited.Keeps the current value.
nullSame 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 listStored as sent, order and duplicates included.Replaces the whole list.
An objectStored 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_on must be that date. A missing or different due_on answers 422 invoice_due_date_not_last_instalment (param: due_on) before a number is assigned, and nothing is issued. It applies to issue, to a create that issues at once through options and to every other way of issuing.
  • A draft keeps the due_on you send, even if it differs; the check runs when it is issued.
  • An update (PATCH) that replaces payment_terms and leaves due_on out moves due_on to the last instalment. If you send both and they differ, the draft keeps them and issuing it answers the same 422.
  • Scheduling or rescheduling the issue (schedule, reschedule) checks the same rule: the invoice keeps its status and date and the request answers the same 422.
  • Duplicating an invoice issues the draft today: each instalment keeps its amount and moves by the days between both issue dates, and due_on is the last moved instalment.
  • Without a schedule, due_on is the date you send.
FieldTypeNotes
idstring (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_datestring (YYYY-MM-DD)A real date. Dates never decrease along the list; equal dates are allowed.
amountstringSigned decimal with exactly two decimals, not zero: "40.00", "-20.00". Never a JSON number.
payment_meansstring"01" to "19", the Facturae 3.2.2 payment means.
accountobject 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:

FieldMeaning
payment_terms_versionVersion of the schedule. 0 when there is none, 1 after the first write.
payment_terms_sourcenone, explicit (you sent it) or legacy (captured by Factuarea from an older issued invoice).
payment_terms_currency, payment_terms_captured_at, payment_terms_frozen_atCurrency (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 02 or 04 may lack an account while 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}/invoices with an external_id that 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):

ReferenceDate
issuer_contract_referenceissuer_contract_date
issuer_transaction_referenceissuer_transaction_date
receiver_contract_referencereceiver_contract_date
receiver_transaction_referencereceiver_transaction_date
file_referencefile_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_codeRole
01Oficina contable
02Órgano gestor
03Unidad tramitadora
04Proponente (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:

  1. The override of the invoice (what you send in administrative_centres).
  2. 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).
  3. 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:

ValueMeans
administrative_snapshot: nullUnknown (legacy). A fallback to the customer is allowed.
administrative_snapshot: []Known, with no centres. They are never filled from the live customer.
administrative_override: nullNo 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_version is a body field (an integer, 1 or more) and the only guard: an If-Match of the invoice does not apply. Without it, 428 invoice_administrative_precondition_required; stale, 409 invoice_administrative_version_conflict with details.current_version. Read the history again and retry on that version.
  • reason is required, 1 to 500 characters. administrative_centres is required and may be [] to record "no routing".
  • Only issued and overdue invoices accept a revision; a draft, scheduled, cancelled or annulled one answers 422 business_rule_violation with subcode invoice_not_editable_in_current_state (edit a draft through PATCH instead).
  • 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

HTTPcodeWhen
422invalid_invoice_business_dataA 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.
422invalid_invoice_payment_termsAn instalment is invalid (payment_terms.N.amount, .id, .account…), the schedule does not add up, or a read-only field was sent.
422invoice_due_date_not_last_instalmentThe 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.
428invoice_payment_terms_precondition_requiredYou change the schedule of an existing invoice without expected_payment_terms_version.
409invoice_payment_terms_version_conflictexpected_payment_terms_version is stale (details.current_version).
428invoice_administrative_precondition_requiredAn administrative revision without expected_version.
409invoice_administrative_version_conflictexpected_version is stale (details.current_version).
422business_rule_violation / invoice_not_editable_in_current_stateYou edit the data of an issued invoice, or revise the routing of a draft.
404invoice_not_foundThe invoice does not exist or belongs to another company.
403insufficient_scopeThe 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.

On this page

Need a hand?Contact support