The KRONENWERK invoice API lets your software start an invoice draft for a customer with POST /invoices/drafts, read issued invoices with GET /invoices and GET /invoices/{number}, and learn about issuing, payment and cancellation through the invoice.issued, invoice.paid and invoice.cancelled webhooks. Issuing itself is not exposed: the number, the tax verdict, the VIES check and the validated structured file are produced when a person issues the draft in the product.
What the invoice API does and does not do
It creates drafts and reads the archive. A draft is a working document: it can be corrected, re-priced or thrown away, and nobody outside the company has seen it. An issued invoice is the opposite: a number was spent from a gap-free sequence, an archive row was frozen, a file was produced that satisfies a national standard, and in Poland the document may have been handed to a state system that decides whether it legally exists. The API gives you the first and only reads the second.
| Operation | Through the API | Where it happens |
|---|---|---|
| Create a customer | POST /customers | API or product |
| Start an invoice draft | POST /invoices/drafts | API or product |
| Add lines, set prices, choose the delivery date | not exposed | Product |
| Issue: number, tax verdict, VIES check, structured file, validation | not exposed | Product; reported by invoice.issued |
| Record a payment | not exposed | Product (manual, bank import); reported by invoice.paid and payment.recorded |
| Cancel by full correction | not exposed | Product; reported by invoice.cancelled |
| Read issued invoices | GET /invoices, GET /invoices/{number} | API |
| Read what is owed today | GET /reports/outstanding | API |
The complete endpoint list, authentication and error shape are on the accounting API page and in the reference.
Creating a draft: POST /invoices/drafts
The request names a customer and nothing else. What comes back is a draft prepared the way the product prepares one: the seller's numbering suggestion, the due date from the customer's agreed payment days, and the payment sentence of the seller's jurisdiction in the language that jurisdiction writes documents in. Each of those is a decision a country module owns; a request field for it would let a caller quietly overrule the rules of a country it has not read.
Three scopes are required — invoices:write, customers:read and companies:read — because the draft is built from a customer and for a legal entity whose numbering, payment days and jurisdiction decide what the document says. The Idempotency-Key header is required.
curl -X POST https://kronenwerk.org/api/extern/v1/invoices/drafts \
-H "Authorization: Bearer greif_live_…" \
-H "Idempotency-Key: contract-7731-invoice-1" \
-H "Content-Type: application/json" \
-d '{ "customerId": "3f2b…" }'
{
"id": "e41a…",
"number": "2026-0042",
"documentType": "INVOICE",
"customerId": "3f2b…",
"customer": "Beispiel GmbH",
"currency": "EUR",
"issuedOn": "2026-09-03",
"dueOn": "2026-09-17",
"state": "DRAFT",
"createdAt": "2026-09-03T08:16:05Z"
}
number is a suggestion in the format the seller's country module considers lawful, with the company's prefix if one is configured; it becomes final only at issuance. state is always DRAFT on this surface. A customerId that does not exist, or that belongs to another company, answers 404 NICHT_GEFUNDEN — one answer for both, so that status codes cannot be used to enumerate somebody else's identifiers. A repeat with the same Idempotency-Key returns the same draft; a repeat with a different body answers 409 IDEMPOTENZ_KONFLIKT. See idempotency.
After the call, the draft appears in the product's invoice list, where a person adds lines, checks the customer's VAT ID and issues it. Your integration learns about the outcome through the webhook, not by polling.
Why issuing stays in the product
Because issuing is the moment three decisions become permanent, and each of them is checked against facts the API caller does not hold.
Validation of the structured file
At issuance KRONENWERK generates the structured e-invoice that the seller's country expects — XRechnung or ZUGFeRD in Germany, Factur-X in France, Peppol BIS Billing 3.0 UBL in Belgium, FA(3) XML in Poland — and validates it before the number is spent. A German document is checked against the KoSIT Schematron rules and cross-checked with the Mustang library. A draft that fails validation is not issued; the person sees which rule failed. An API that issued directly would have to either skip that check or invent an error channel for a document its caller never saw. See the e-invoicing API page.
The tax verdict
Each invoice receives a tax verdict from the transaction's facts — seller and buyer country, business or consumer, kind of supply: standard, zero-rated, exempt, reverse charge, outside scope, or "requires input" / "requires professional confirmation". The buyer's VAT ID is checked against VIES at issuance and the verdict is stored with the invoice. Where the facts do not decide, the product says so and asks a person; it does not guess, and neither should a script.
Numbering
Invoice numbers come from a gap-free sequence, and a spent number cannot be un-spent; the remedy for a wrong invoice is a correction document, not a deletion. A loop that ran twice must not be able to consume two numbers, which is why the API stops at the draft and why every write it does offer is idempotent.
Reading issued invoices: GET /invoices
GET /invoices returns issued invoices newest first, one page at a time, with the figures frozen at issuance — a customer renamed last week does not change the name on an invoice issued last year. Filters are status (OPEN or PAID; an unreadable value means no filter), from and to (ISO issue dates), page and size (default 25, maximum 100). GET /invoices/{number} reads one invoice by the number the business and its customer both know it by.
curl "https://kronenwerk.org/api/extern/v1/invoices?status=OPEN&from=2026-01-01&size=50" \
-H "Authorization: Bearer greif_live_…"
{
"data": [
{
"id": "7a90…",
"number": "2026-0041",
"documentType": "INVOICE",
"buyer": "Beispiel GmbH",
"currency": "EUR",
"issuedOn": "2026-08-28",
"dueOn": "2026-09-11",
"deliveredOn": "2026-08-28",
"net": { "minor": 89000, "currency": "EUR" },
"tax": { "minor": 16910, "currency": "EUR" },
"gross": { "minor": 105910, "currency": "EUR" },
"outstanding": { "minor": 105910, "currency": "EUR" },
"paymentState": "OPEN",
"overdue": false,
"cancelled": false,
"creditNote": false,
"paidOn": null,
"recordedAt": "2026-08-28T14:02:11Z"
}
],
"page": 0,
"size": 50,
"total": 1,
"more": false
}
paymentState is one of OPEN, OVERDUE, PAID, CANCELLED or CORRECTION; overdue is computed against today. cancelled means a correction document exists that reverses this invoice in full; creditNote means this document is a correction. Nothing is ever deleted or edited in the archive.
Webhooks: invoice.issued, invoice.paid, invoice.cancelled
Each event is delivered as an HTTPS POST with a signature header (KRONENWERK-Signature: t=…,v1=…, HMAC-SHA256 over the timestamp, a dot and the raw body) and a stable event id in KRONENWERK-Event-Id and Idempotency-Key. Delivery is at-least-once, so store the event id and skip repeats. The body is a flat JSON object; every value is a string, and the keys are sorted.
{
"amountDueMinor": "105910",
"buyerName": "Beispiel GmbH",
"currency": "EUR",
"documentType": "INVOICE",
"dueDate": "2026-09-17",
"event": "invoice.issued",
"grossMinor": "105910",
"id": "0d3c…",
"invoiceId": "7a90…",
"issueDate": "2026-09-03",
"netMinor": "89000",
"number": "2026-0042",
"paymentState": "OPEN",
"taxMinor": "16910"
}
invoice.paid carries the same fields plus paidOn; it is raised where the payment actually clears the receivable, whichever road led there — a payment screen, a bank import, the phone. invoice.cancelled adds cancelledByInvoiceId, cancelledByNumber and reason, naming the correction document so that a receiver can void the right record and reconcile the credit note. A separate payment.recorded event reports money moving in either direction, with direction, amountMinor, method, reference, targetKind and targetId. Verification steps are on the webhooks page.
A typical sequence
- Your app creates the customer once with
POST /customers(name, street, postcode, city are required; country, VAT ID and currency are optional) and stores the returnedid. - When a billable event happens, your app calls
POST /invoices/draftswith thatcustomerIdand anIdempotency-Keyderived from your own record, for example the contract or order number. - A person in the company completes and issues the draft. KRONENWERK computes the tax verdict, checks the VAT ID against VIES, validates the structured file and spends the number.
- Your webhook endpoint receives
invoice.issued, verifies the signature, deduplicates onidand storesnumberandinvoiceIdagainst your record. - When the invoice is settled,
invoice.paidarrives. If the person cancels it,invoice.cancelledarrives with the correction's number. - For reconciliation,
GET /reports/outstandingreturns receivables and payables open today in the books' currency, from the same figures the owner's dashboard shows.
The pattern is worked through for a subscription business in connecting a SaaS product to accounting.
How KRONENWERK handles this
SUPPORTED WITH LIMITATIONS Draft creation, invoice reading and the three invoice events work as described, on the Enterprise plan (pricing). The limitation is deliberate and permanent in the current design: the API does not add lines to a draft, does not issue, does not record payments and does not cancel. If your integration needs a fully automated issue-without-a-person flow, KRONENWERK's API is not that today, and this page says so rather than implying otherwise.
What the product does at issuance is described on invoices and e-invoicing: XRechnung and ZUGFeRD for Germany, Factur-X for France, Peppol BIS UBL for Belgium, FA(3) for Poland, and PDF invoices with national tax rules for Canada and the United States. Sending over Peppol goes through an accredited access point provider (Storecove) once the company is connected in Settings → Delivery; Polish KSeF transmission is not yet production-ready. Both are covered on Peppol integration and KSeF integration.
Frequently asked questions
Can I set the invoice lines or the total through the API?
No. POST /invoices/drafts accepts only customerId. Lines, prices and the delivery date are entered in the product before a person issues the draft.
Can the API issue an invoice?
No, at any scope. Issuing spends a number and releases a legal document; it stays in the product, and the invoice.issued webhook reports it.
How do I find out an invoice was paid?
Subscribe to invoice.paid (and payment.recorded for the payment itself), or poll GET /invoices?status=PAID with a from date. The webhook is the intended route.
Why does the draft endpoint need companies:read?
The draft is built for the issuing legal entity: its numbering, payment days and jurisdiction decide what the document says. Reading those settings is part of creating the draft, so the scope is declared rather than silently required.
Is the invoice number in the draft final?
No. It is the country module's suggestion for the next lawful number and is confirmed only when the invoice is issued.