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) orfailed. Arejectedorfaileddocument can be sent again.mail_status:not_sent,sentorfailed.
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_priceis never negative;quantitymay be. The document total must stay positive (422 invalid_amountotherwise) — a credit note also totals a positive amount, itstypecarries the direction.unit_code(unitby default —C62) is validated against the units list,currencyagainst the currency list — see Reference lists.- Amounts are computed as
subtotal = quantity × unit_price,total = subtotal + charges − discounts(excl. VAT), andtax_amount = total × vat_rate / 100. Document VAT is totalled per (code, rate) pair, rounding once per rate. item_propertiesis a flat key/value object;item_identificationis 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 requiredtype (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_amountotherwise). 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_termsis<days>-<base>with basedf(from document date),fm(from the 1st of next month) ormf(end of the month the delay lands in):30-df,0-df… Resolution: body value → the relation’s terms (companies only) →30-df.due_datealways derives from it. -
issue_dateis 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).
422 invalid_issue_date, the message gives the expected bound. -
delivery_dateis the date the delivery actually happened: today or earlier.
Journal and relation
journal_idis 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_idmust 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 asrelation_type.purchase_invoiceandpurchase_credit_noteare self-billing: they require the company’s self-billing setting (422 self_billing_disabledotherwise).
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
Requires the update permission on documents. Two ways:POST /documents/{id}/sendwith body{"peppol": true}— send an existing document.POST /documents?send_peppol=true— create and send in one call (also requires the create permission).
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_statusatfailedorrejected) 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_failedis returned. With?send_peppol=truethe document was created and numbered — the message gives its id, and the recovery isPOST /documents/{id}/send, never a second creation.
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_statusandpeppol_statusatnot_sent), - not transmitted to accounting.
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 acceptnull: 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
Changingissue_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.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.