Update a draft invoice
PATCH/api/v1/invoices/:id
Update a draft invoice by ID. Only invoices in "draft" lifecycle state can be updated.
This is a complete replacement operation: all child records (parties, lines, tax subtotals, payment means, notes, allowance charges, invoice references) are cleared and rebuilt from the provided payload.
Before the invoice is rebuilt, the payload is completed with the same company-side
defaults as POST .../invoices - party_id resolution against the company directory, a
company-side party default when the payload omits it (never overwriting a party entry the
caller does supply), item_notes defaults on sales, and payment_means defaults (company
bank accounts, or the buyer's factor account for a confidentially factored customer) on
both directions. This is a behavior change for existing integrations: a PATCH that
omits item_notes or payment_means now refills them from the company instead of leaving
the invoice with none. The same guards run too, among them the ISO 4217 currency guard
documented on POST .../invoices: currency_code (BT-5), tax_currency_code (BT-6) and
each tax_subtotals[].currency_code are matched case-sensitively against the vendored
code lists, and an unlisted value returns 422 without writing anything. An omitted BT-5
leaves the invoice's existing currency in force and is never refused. The VAT
breakdown coherence guard documented there runs on this verb too, and here it is
judged against the EFFECTIVE totals rather than the payload's: on direction: "sales", the sum of tax_subtotals[].amount_without_taxes is reconciled with
total_amount_excluding_taxes and the sum of tax_subtotals[].vat_amount with
total_tax_amount, within the 0.01 tolerance of PPF rule G1.53, each total read
from the payload when its key is present and from the persisted invoice when it
is omitted, and a gap beyond the tolerance returns 422 without writing anything.
Reading the payload alone was wrong on this verb: the tax_subtotals sent
REPLACE the document's wholesale while a total whose key is absent keeps its
persisted value, so a PATCH carrying only a new ventilation left the totals
untouched, fell out of the rule's scope, and persisted the very incoherence the
guard exists to refuse. So does the SIREN conversion: a party filled from a
directory record whose SIRET is declared under scheme 0009 is written with the
SIREN of the legal unit under scheme 0002 (BR-FR-11 / BT-47), while a
legal_registration_id sent explicitly on the entry is never converted.
payment_means defaults resolve against the invoice's existing
currency_code when the payload omits it, not EUR. They are resolved from the payload's
buyer ONLY: because this endpoint replaces every party, omitting parties leaves the
invoice with no buyer, and no payment means are emitted in that case rather than carrying
over the destroyed buyer's bank account.
The already-paid coherence guard documented on POST .../invoices runs on this verb
too, judged against the EFFECTIVE values like the VAT breakdown guard: invoicing_process_id,
profile_id, the totals, due_date and issue_date are each read from the payload when
the key is present and from the persisted invoice when it is omitted. So a PATCH that
changes the total of a B2 / S2 / M2 invoice without restating prepaid_amount
returns 422, and a PATCH that moves the invoice back to the B1 / S1 / M1 family
leaves it out of the guard's scope.
Authorization is based on the OAuth application's tenant access to the invoice's workspace.
Request
Responses
- 200
- 401
- 403
- 404
- 422
The updated invoice, rebuilt from the payload with company-side defaults reapplied for any parties, item_notes, or payment_means the payload omitted.
The request carries no OAuth bearer token, or the token is invalid or expired.
The invoice is not in draft lifecycle state and therefore cannot be updated.
No invoice exists with the given ID, or the invoice belongs to a workspace the token's application cannot access - both cases return this same response so cross-workspace existence is never revealed.
The payload was refused. The error body reports error: "unprocessable_entity" with a code naming the failure class and a message describing it: operation_failed when a parties entry's party_id does not resolve in the company's directory, or when a payer party (EXT-FR-FE-BG-02) is sent while the invoice's effective customization_id - the payload's when the key is present, the persisted one when it is omitted - is not one of the two EXTENDED-CTC-FR forms, or when a role_code is sent on a role other than payer (EXT-FR-FE-44) or payee (EXT-FR-FE-26), with a value outside the UN/CEFACT PartyRoleCode D22A list, or on a payee while that effective customization_id is not one of the two EXTENDED-CTC-FR forms (EXT-FR-FE-26 is defined only in the EXTENDED profiles), or when a role_code is not a string at all (a JSON boolean or number is never absence, so on a payer or payee it is refused as a value outside that list and on any other role it is refused for the role), or when a currency_code (BT-5), tax_currency_code (BT-6) or tax_subtotals[].currency_code falls outside the vendored ISO 4217 code lists - matched case-sensitively, with the message naming which of the three fields was refused, and a value that is not a string at all refused the same way rather than read as absence, only an omitted key, an explicit null or an empty or whitespace-only string counting as sending none, or when a direction: "sales" payload carries a tax_subtotals breakdown that does not reconcile with the document's EFFECTIVE totals - the sum of amount_without_taxes against total_amount_excluding_taxes and the sum of vat_amount against total_tax_amount, each within the 0.01 tolerance of PPF rule G1.53, each total read from the payload when its key is present and from the persisted invoice when it is omitted, so a PATCH sending only a ventilation is judged against the totals the document will still carry, and each half out of scope when its effective total or its breakdown amounts are absent (api.invoices.errors.ventilation_not_reconciled), or when a direction: "sales" invoice whose EFFECTIVE invoicing_process_id or profile_id states the already-paid state (B2 / S2 / M2) does not carry an effective prepaid_amount equal to tax_inclusive_amount, a payable_amount of 0, no payable_rounding_amount other than 0 and a due_date on or before issue_date (flux 2 rule BR-FR-CO-09, EN 16931 BR-CO-16), or when such an invoice names a down-payment invoice by document_id in invoice_references (XP Z12-014 CU-21), api.invoices.errors.already_paid_* in either case; invalid_argument when the payload carries lifecycle_state - a state change on an existing invoice is what PATCH /api/v1/invoices/{id}/transition is for.