Submit an invoice or credit note as canonical JSON
- In the sandbox
In plain words
The service checks the document against the canonical model and the pre-checks at once, and answers 422 if either fails. Everything after that runs asynchronously (build, official validation, route submission, statuses) and is reported through status events.
A corrected resubmission of a rejected invoice uses the same invoice_ref and a new
Idempotency-Key; the service links the attempts.
In production an ERP export is read only when the client's connector settings hold its own seller and payment,
not the mapping's example: otherwise 422 connector-settings-missing, naming what is missing. A sandbox reads it
with the example, as before.
apiKeyAuthorizationBearer <token>Send your key as a bearer token: Authorization: Bearer <your-api-key>. Health is the only call that needs no key.
Idempotency-Key*stringA unique key per logical request (a UUID is fine). Kept for as long as the client's data is kept. With the operator's key it may not begin with client: (400), the form client keys are stored under.
8 <= length <= 100application/json- body
Send either document (a canonical invoice) or connector with export (one ERP document as the ERP returns
it), not both (400). An export is mapped by the connector's live mapping version, or mapping_version, and
then handled exactly as the canonical invoice it maps to; the export itself is kept with the invoice
(erp-export). The connector's XRechnung profile applies on DE-XRECHNUNG only, and when the router chooses for
a German buyer. A mapping finding (an unknown unit, a missing ERP field) answers 422 naming the ERP field.
connector?connectorWhich ERP the export comes from. business-central: one Business Central API v2.0 sales invoice. Another SAP object, such as an order or a draft, answers 400.
"business-central""sap-b1"export?The ERP document as the ERP returns it. Fields the mapping does not read are ignored.
mapping_version?mapping_versionA mapping version of that connector. The live one when left out; an unknown one answers 400.
^v[0-9]+$client?stringThe client the invoice belongs to. The operator's key names it (left out, the invoice belongs to the operator's own local client). A client key may leave it out or name its own client; any other client answers 403.
^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$invoice_ref?stringThe ERP's own document ID. Echoed in every status event.
length <= 100route?|Left out or null, the router chooses it from the document: the seller's
country, the buyer's buyer.address.country, profile, the client's stored credentials and, when a rule
needs it, the buyer's Peppol registration. When no rule fits, the answer is 422 with EI-ROUTE-UNDECIDED or
EI-ROUTE-PEPPOL-UNKNOWN (source router). POST /validate still needs it.
environment?stringMust match the API key's environment; given explicitly as a guard.
"sandbox""production"document*One invoice in the canonical model. Field meanings follow the EN 16931 semantic model. Totals are not part of the model: the service computes them from the lines.
formats?array<>Which documents to produce where the route allows a choice (Germany: xrechnung-ubl,
xrechnung-cii or zugferd; France: ubl, cii or facturx). Defaults per route are set at onboarding.
On POST /invoices name at most one: it is the document that is built, checked and sent (the first in each
list above when left out). Peppol and Romania take ubl, Poland fa3. Another value, or more than one,
answers 400. Factur-X and ZUGFeRD are checked twice: the CII inside with the route's rules, then the PDF.
On the German route a document with no profile is built as an XRechnung.
erp_totals?The totals the ERP computed. The service computes its own from the lines and refuses the invoice (422, EI-TOTALS-MISMATCH, naming the ERP field) if they differ, instead of sending a document the ERP disagrees with. A connector reads these from the export itself; totals named here win over them.
Accepted for processing. Follow it with the returned links or wait for status events.
application/json- response
id*stringstate*InvoiceStateThe service's own state for an invoice. Status events report the partner-facing lifecycle;
queued and submitting are internal steps between validated and submitted. validation_failed means the official rules refused the document; since
0.18.4 a check that did not run (KOSIT-RUN, EI-PDF-CHECK) retries instead, then ends dead_letter with that
code. A dead_letter keeps its document held, so the same file answers with duplicate_of; since 0.18.6 the
operator can cancel one for which no call to the route was made, and then the file can go again.
"received""source_error""validated""validation_failed""queued""submitting""submitted""ready""accepted""rejected""delivered""cancelled""dead_letter"duplicate_of?|Set when the same document was already accepted for this client and route; nothing new is sent. A submission that ended rejected, validation_failed or cancelled does not count, so the file can be sent again. Since 0.18.5 this holds for two requests sent at the same moment too, under different keys; one makes the invoice and the other answers with duplicate_of.
links*route?RouteOnly when the router chose the route.
"PEPPOL""PL-KSEF""RO-EFACTURA""FR-PA""DE-XRECHNUNG"route_chosen_by?"router"Only when the request left out the route.
"router"route_rule?stringThe router's rule that chose the route; also written to the audit log as route_chosen.
"fr-domestic""fr-cross-border""pl-domestic""ro-domestic""be-domestic""de-domestic-peppol""de-domestic""cross-border-peppol"curl -X POST "https://example.com/invoices" \ -H "Authorization: Bearer <your-api-key>" \ -H "Idempotency-Key: order-2026-0001" \ -H "Content-Type: application/json" \ -d '{ "invoice_ref": "CAPTURE-JSON-1790961440", "route": "DE-XRECHNUNG", "environment": "sandbox", "document": { "lang": "de", "country": "DE", "invoice": { "number": "DOC-mux3dlqm", "issue_date": "2026-09-26", "due_date": "2026-10-10", "type_code": 380, "currency": "EUR", "buyer_reference": "PO-88731", "period": { "start": "2026-09-01", "end": "2026-09-30" }, "notes": [ "Vielen Dank für Ihren Auftrag." ] }, "seller": { "name": "Nordlicht Software GmbH", "address": { "street": "Hafenstraße 12", "city": "Hamburg", "postcode": "20457", "country": "DE" }, "vat_id": "DE938296582", "tax_number": "27/123/45678", "company_id": "HRB 123456", "register": "Amtsgericht Hamburg HRB 123456", "managing_directors": "Geschäftsführer: Jana Petersen", "legal_info": "GmbH", "endpoint": { "id": "DE938296582", "scheme": "9930" }, "contact": { "name": "Jana Petersen", "email": "rechnung@nordlicht.example", "phone": "+49 40 1234567" }, "brand_color": "#1160FF", "accent_color": "#FF9021" }, "buyer": { "name": "Brauhaus Weber AG", "address": { "street": "Marienplatz 4", "city": "München", "postcode": "80331", "country": "DE" }, "vat_id": "DE965003781", "endpoint": { "id": "DE965003781", "scheme": "9930" } }, "lines": [ { "name": "E-Rechnung Einführung (Festpreis)", "description": "Mapping Business Central → EN 16931, Validierung XRechnung, Test im Peppol-Testnetz", "quantity": 1, "unit": "LS", "unit_price": 5900, "vat_category": "S", "vat_rate": 19 }, { "name": "Betreuung abgelehnter Rechnungen", "description": "Care Plus, September 2026", "quantity": 1, "unit": "MON", "unit_price": 349, "vat_category": "S", "vat_rate": 19 }, { "name": "Zusätzliche Schulung", "description": "Remote, Buchhaltungsteam", "quantity": 3, "unit": "HUR", "unit_price": 120, "vat_category": "S", "vat_rate": 19 } ], "payment": { "means_code": 58, "iban": "DE89 3704 0044 0532 0130 00", "bic": "COBADEFFXXX", "reference": "RE-2026-0143", "terms": "Zahlbar innerhalb von 14 Tagen ohne Abzug." }, "profile": "xrechnung" } }'{ "links": { "self": "/invoices/inv_936a93e38de84e7b0a1d7681", "events": "/invoices/inv_936a93e38de84e7b0a1d7681/events" }, "id": "inv_936a93e38de84e7b0a1d7681", "state": "queued"}List and search invoices GET
A client key sees its own client's invoices; the operator sees every client's, or one client's with client. Rows carry what GET /invoices/{id} carries, without documents, and no invoice content: no buyer or amounts, and no issue date (for Romania and Poland the deadline_at follows from it, to within a few days). An unknown parameter answers 400.
Submit a finished UBL or CII document (pass-through) POST
For ERPs that already write UBL or CII, or FA(3) for the KSeF route. No mapping happens: the service runs the official validators for the route (for FA(3): the XSD plus KSeF's file and date rules) and sends the file unchanged. A Factur-X or ZUGFeRD PDF goes through the same endpoint as application/pdf. The XML is parsed with entities, DTDs and network access off, and a file with a DOCTYPE is refused (EI-XML-DTD) before any validator reads it, as is a file that is not well-formed (EI-XML-SYNTAX) or not an invoice the route knows (EI-XML-TYPE). Since 0.18.4 the same file sent again for the client and route is the invoice already held (duplicate_of), as on POST /invoices; nothing new is sent.