Skip to main content
A document is either an invoice / credit note generated by the company, or a document received from outside (Peppol, mail, manual upload, Odoo, Billit…). Both come through the same resource; origin says where a document came from (fidly for documents generated in Fidly, api for files imported through this API, otherwise the import channel).
origin: api only applies to files imported through the API, not to documents created through it. A created document always carries origin: fidly, as if it had been issued in the interface.

Listing documents

GET /documents — paginated list, newest first. Filters: origin, type, accounting_status, payment_status, issue_date_from, issue_date_to. Enumeration filters accept comma-separated lists (origin=manual,peppol). Quotes are not documents and never appear; proforma invoices do.

Retrieving a document

GET /documents/{id} — the full document, including its lines, allowance_charges and attachments. GET /documents/{id}/file?format=pdf|xml — the file bytes, served as an attachment. format=pdf is the default; format=xml returns the UBL/Peppol version when it exists. 404 if the requested version does not exist. Each document carries its lines: sequence, item_name, item_description, quantity, unit_price, unit_code, vat_code, vat_rate, subtotal, total (both excl. VAT), tax_amount, section_name, item_properties, item_identification, product_code and per-line allowance_charges. Document-level allowance_charges (discounts and charges applied after the lines) sit at the root.

Send statuses

An issued document carries two send statuses, one per channel:
  • peppol_status: not_sent, sending, sent (delivered to the network), accepted (acknowledged by the recipient), rejected (refused by the recipient) or failed. A rejected or failed document can be sent again.
  • mail_status: not_sent, sent or failed.
Both are null on a received document: it was never sent by the company.

Creating a document

POST /documents — requires the create permission on documents. Creatable types: sale_invoice, sale_credit_note, purchase_invoice, purchase_credit_note, proforma_invoice. The response is the complete document (201), exactly as a later GET returns it. You provide what identifies the document and what is invoiced; everything else is computed. Sending a computed field is refused with a 422 naming it: Optional inputs: journal_id, delivery_location_id, issue_date, delivery_date, tax_point_date, period_start/period_end, payment_terms, order_number, despatch_reference, buyer_reference, legal_notice, currency (default EUR, validated against the currency list), paid_amount (default 0) and document-level allowance_charges.
Two exceptions, accepted without error but without effect: due_date, always recomputed from the payment terms, and, on each line, sequence, subtotal, total and tax_amount, always recomputed. Lines are ordered by their position in the lines array.

Lines

1 to 500 lines, at most 50 document-level discounts/charges. Per line, item_name, quantity, unit_price and vat_code are required.
  • unit_price is never negative; quantity may be. The document total must stay positive (422 invalid_amount otherwise) — a credit note also totals a positive amount, its type carries the direction.
  • unit_code (unit by default — C62) is validated against the units list, currency against the currency list — see Reference lists.
  • Amounts are computed as subtotal = quantity × unit_price, total = subtotal + charges − discounts (excl. VAT), and tax_amount = total × vat_rate / 100. Document VAT is totalled per (code, rate) pair, rounding once per rate.
  • item_properties is a flat key/value object; item_identification is a GTIN.

VAT codes

vat_code names the regime, vat_rate carries the percentage:
Combinable codes (✅) are line-level regimes: they mix freely on one domestic invoice. The other three (❌) are whole-invoice regimes — an intra-EU sale, an export, or a seller not subject to VAT — that decide the legal mentions of the document, its VAT category in the Peppol XML and how your accountant declares it. A domestic line has no place on such an invoice: a ❌ code must be carried by all lines, and never together with another ❌ code (422 otherwise). Issue one document per regime.
Any positive vat_rate is accepted with standard, but the legal Belgian rates are 6, 12 and 21 %.

Discounts and charges

Each entry carries a required type (allowance = discount, charge = extra charge) and an amount expressed either as a fixed amount or a rate (percentage). When both are present, rate wins and the amount is recomputed. On a line, the rate applies to the line’s subtotal; on the document, to the total of the lines. A free-text reason names the entry: optional on a line, required on the document. A document-level discount or charge is split across the document’s VAT rates, in proportion to what the lines of each rate total, and the VAT of each rate is computed on that adjusted base. Example: €1,000 of lines at 21 % and €500 at 6 %, with a €150 discount → €100 is deducted from the 21 % base and €50 from the 6 % base. Two conditions follow; if either is not met, 422 invalid_allowance_charge:
  • The document total, discount or charge included, must stay positive or zero (422 invalid_amount otherwise). In particular, a fixed amount cannot apply to lines totalling 0.
  • No VAT rate may total a negative amount (for instance a negative-quantity line that outweighs the other lines of the same rate): the discount would have no consistent base to split over. In that case, put the discount or charge on the lines rather than on the document.

Payment terms and dates

  • payment_terms is <days>-<base> with base df (from document date), fm (from the 1st of next month) or mf (end of the month the delay lands in): 30-df, 0-df… Resolution: body value → the relation’s terms (companies only) → 30-df. due_date always derives from it.
  • issue_date is never in the future and follows the numbering order of the journal:
    • on creation, the day of the last numbered document of the journal, or later;
    • on update, between the document numbered just before and the one numbered just after, their dates included (with no next document, the upper bound is today).
    For a journal whose counter resets every month or year, the date must also stay within the period the number was drawn from. Otherwise 422 invalid_issue_date, the message gives the expected bound.
  • delivery_date is the date the delivery actually happened: today or earlier.

Journal and relation

  • journal_id is optional: by default the document takes the default journal matching its type (sales invoice, sales credit note, purchase invoice…). A journal of another category than the type’s is refused (422 invalid_journal).
  • relation_id must designate a relation of the company, and that relation must have an address (422 invalid_relation). Whether it is a company or a contact is read from the relation itself and returned as relation_type.
  • purchase_invoice and purchase_credit_note are self-billing: they require the company’s self-billing setting (422 self_billing_disabled otherwise).

After creation

The PDF (and, when the type allows, the Peppol XML) is rendered asynchronously: pdf_available / xml_available are false in the 201 response and flip to true shortly after — the files are usually available within 5 to 7 seconds. Poll GET /documents/{id}. Nothing is sent to the customer at creation — see Sending below.

Sending a document

Sending by e-mail is not yet available through the API: only sending over Peppol is.
Requires the update permission on documents. Two ways:
  • POST /documents/{id}/send with body {"peppol": true} — send an existing document.
  • POST /documents?send_peppol=true — create and send in one call (also requires the create permission).
The response is 202: sending is asynchronous, the API acknowledges the hand-off, not the delivery. The document is re-rendered before leaving, so attachments added since creation are embedded in the XML.
  • Transportable types: sale_invoice, sale_credit_note, purchase_invoice, purchase_credit_note. A proforma is refused (422 not_sendable), and a received document too (409).
  • A document already sent returns 409 already_sent; a failed send (peppol_status at failed or rejected) can be retried with the same call.
  • A body that requests no channel (empty or all-false) is refused (422).
  • If the send workflow cannot be started, 422 send_failed is returned. With ?send_peppol=true the document was created and numbered — the message gives its id, and the recovery is POST /documents/{id}/send, never a second creation.
Network-level requirements (company registered on Peppol, customer being a company with a VAT number or identifier) are checked by the send workflow itself; the outcome is read afterwards in the document’s peppol_status, not in the 202 response: sending, then sent once delivered to the network, then accepted or rejected depending on the recipient’s answer — or failed if the send did not go through.

Updating a document

PATCH /documents/{id} — requires the update permission on documents. Only the fields present in the body are applied; the response is the full document. Allowed as long as the document has left nowhere:
  • not sent to the customer (mail_status and peppol_status at not_sent),
  • not transmitted to accounting.
Otherwise 409 document_not_editable, naming the blocking condition. Only documents with origin: fidly can be modified.

Read-only fields

type and journal_id no longer change after creation (422 field_not_editable on change). To change the type or the journal, delete and recreate the document.

Clearing a field

Only optional fields accept null: it clears them (or detaches the delivery address for delivery_location_id). On a required field, null is refused (422 validation_error) — omit it to leave it untouched.

Changing the date or the payment terms

Changing issue_date obeys the same numbering rules as creation; changing it or payment_terms recomputes the due_date.

Recompute and re-render

Every update recomputes the document (VAT, totals, payment status) and re-renders its PDF/XML.

Editing lines, discounts and charges

lines and allowance_charges replace the stored lists entirely; omitting them leaves the stored lists untouched.
For technical reasons, lines and allowance_charges are always sent back whole: a single line or a single discount cannot be modified on its own. Take the array of the document you read, edit it and send it back complete — its computed fields (sequence, subtotal, total, tax_amount) can stay in: they are recomputed, the value you send is ignored.
There is no version token: the last writer wins. A lines array built from a stale read overwrites changes made in the meantime.

Recording a payment

paid_amount records a payment Fidly cannot see by itself (cash, an untracked bank account). A PATCH carrying only paid_amount is accepted whatever the state of the document — already sent, already exported — and regenerates nothing. It drives payment_status and remaining_amount. Declaring more than the total is refused (422 invalid_paid_amount). Adding any other editable field makes the request an ordinary modification, subject to the lock above.

Attachments

POST /documents/{id}/attachments — requires the update permission on documents. Add files to a document the company issued: multipart/form-data, the files field repeated, 10 files max per call, 10 MB for the whole call. The response is the full document with its attachments list.
  • All-or-nothing: one refused file fails the whole call and nothing is attached.
  • A filename already attached is refused (409 duplicate_attachment).
  • Strictly additive: attached files can never be removed or replaced through the API.
  • Attaching does not regenerate the stored PDF/XML. Attachments enter the Peppol XML at send time, which re-renders the document. Attaching stays possible on a document already sent or exported.
  • A received document does not take attachments (409).

Importing a file

POST /documents/import — requires the create permission on documents. The API equivalent of dropping a file in the interface: multipart/form-data with a single file field.

Allowed files

Accepted extensions: .pdf, .xml, .jpg, .jpeg, .png. Any other extension is refused (415).

File size

10 MB maximum per file; above that, 413.

Processing

?type= (query) drives the processing: purchase (default), sale or other. Returns 201 with only the id of the created document: at creation the document is deliberately empty (type: unknown, null amounts) until the asynchronous chain has parsed it (Peppol parsing for XML, AI analysis for PDF/images). Follow the enrichment with GET /documents/{id}. Only type=other is final at creation (other_document, archived without analysis). Errors: 415 refused extension, 413 file too big, 400 unknown type, 409 file already imported.

Deleting a document

DELETE /documents/{id} — requires the delete permission on documents. Returns 204. Deletion is definitive, and only possible when:
  • the document holds the highest number of its sequence (journal + type) in the current numbering window — otherwise 409 document_not_deletable, naming both numbers. Deleting the last returns its number to the sequence. A proforma has no number, so this rule does not apply to it;
  • it has left nowhere (same conditions as PATCH): not sent, not transmitted to accounting, not a received document;
  • nothing is attached to it (409 document_in_use): no bank reconciliation, no invoiced timesheet line, no payment intent.