Connecting a SaaS product to accounting with KRONENWERK means four things: create one customer per paying account, open a transaction and an invoice draft for each billable event with an Idempotency-Key derived from your own record, receive invoice.issued, invoice.paid and payment.recorded on a webhook endpoint, and reconcile with GET /reports/outstanding. The API does not issue invoices or record payments; a person does that in the product, and the events tell your system what happened. This is the honest shape of the integration, and this page shows it step by step.
What the integration can and cannot automate
The KRONENWERK API creates recoverable records and reads the books. It does not perform the irreversible acts — issuing an invoice, recording a payment, cancelling — because each of them spends something that cannot be un-spent or moves money in the ledger. For a SaaS back end that is a constraint worth knowing before you design.
| Your event | What your app does | What happens in the product | What comes back |
|---|---|---|---|
| Account signs up or upgrades to a paid plan | POST /customers | The customer appears in master data with a partner number | 201 with the customer's id and number |
| Contract or order is agreed | POST /transactions | A transaction (a job with a stage and a due date) appears on the company's board | 201 with number such as V2026-0481 |
| Billing period is due for a B2B account | POST /invoices/drafts | A draft awaits lines and issuance by a person | 201 with the draft; later invoice.issued |
| Customer pays (card, transfer) | Nothing on the API | The payment is recorded in the product: manually, or matched from the connected bank account | payment.recorded and, when the receivable clears, invoice.paid |
| Refund or dispute reverses an invoice | Nothing on the API | A person issues a full correction | invoice.cancelled naming the credit note |
| Month-end check | GET /reports/outstanding, GET /invoices?status=OPEN | — | Receivables and payables open today; the open invoices |
The API is part of the Enterprise plan; the whole surface is on the accounting API page. The business side of running books for a subscription company — deferred revenue, VAT on digital services, multi-currency — is discussed in accounting for SaaS and connecting an app to accounting.
Pattern 1: one customer per paying account, created idempotently
Create the KRONENWERK customer when an account becomes a paying customer, not at sign-up, and use your own account identifier in the Idempotency-Key. A retry after a lost response then returns the customer that was created rather than a duplicate, and a duplicate customer is the mistake nobody notices until an invoice goes to the wrong record.
curl -X POST https://kronenwerk.org/api/extern/v1/customers \
-H "Authorization: Bearer greif_live_…" \
-H "Idempotency-Key: account-88213" \
-H "Content-Type: application/json" \
-d '{
"name": "Beispiel GmbH",
"email": "billing@example.de",
"street": "Musterstraße 1",
"postalCode": "10115",
"city": "Berlin",
"country": "DE",
"vatId": "DE123456789",
"currency": "EUR"
}'
Name, street, postcode and city are required — EN 16931 carries an address in parts, and a customer without a street is one no invoice can be addressed to. Country, VAT ID and currency are optional; a missing country defaults to the company's own. Store the returned id against your account. The VAT ID matters more than it looks: at issuance KRONENWERK checks it against VIES and derives the tax verdict (reverse charge for a business customer in another EU country, for example) from it. Collect it at checkout.
Pattern 2: a transaction per order, with your identifiers in it
A transaction in KRONENWERK is a unit of work with a title, a customer, a stage from the company's own workflow and a due date — an order, a project, a subscription period. It carries no money, which is exactly why it is the right record for "something billable happened" before anyone has decided what the invoice will say. Put your provider identifiers in the description so that the person who later issues the invoice can find the Stripe subscription or the order in one search.
curl -X POST https://kronenwerk.org/api/extern/v1/transactions \
-H "Authorization: Bearer greif_live_…" \
-H "Idempotency-Key: sub_1Q…-2026-09" \
-H "Content-Type: application/json" \
-d '{
"title": "Team plan, September 2026 — Beispiel GmbH",
"customerId": "3f2b…",
"description": "Stripe subscription sub_1Q…, invoice in_1Q…, 12 seats",
"dueOn": "2026-09-30"
}'
Only title is required. stage may name a stage key of the company's workflow; omitted, the transaction starts where the workflow begins. The response carries id, number, title, stage, customer, dueOn, archived and createdAt. GET /transactions?search=sub_1Q… finds it again. Note that this is not a ledger posting: nothing is booked until an invoice is issued or a payment is recorded in the product.
Pattern 3: an invoice draft for each B2B billing event
For business customers who need a proper invoice — an XRechnung for a German public buyer, a Peppol document for a Belgian one, a Factur-X for a French one — start a draft when the billing period is due. The draft carries the seller's numbering suggestion and the customer's payment terms; a person adds the lines and issues it, and the structured file is generated and validated at that moment. Why the line is drawn at the draft is explained on the invoice API page.
curl -X POST https://kronenwerk.org/api/extern/v1/invoices/drafts \
-H "Authorization: Bearer greif_live_…" \
-H "Idempotency-Key: in_1Q…" \
-H "Content-Type: application/json" \
-d '{ "customerId": "3f2b…" }'
Use the provider's invoice identifier as the key: one Stripe invoice, one KRONENWERK draft, however many times the webhook that triggered it is delivered. If your product bills consumers rather than businesses, you may not need a draft per payment at all; a periodic summary handled in the product is often the better fit, and whether a consumer sale requires an individual invoice in a given country requires professional confirmation.
Pattern 4: listen to the webhooks, verify, deduplicate, answer fast
Register an HTTPS endpoint on port 443 in the product and subscribe to invoice.issued, invoice.paid, invoice.cancelled and payment.recorded. Each delivery carries KRONENWERK-Signature: t=<seconds>,v1=<hex> — an HMAC-SHA256 over the timestamp, a dot and the raw body — and a stable event id in KRONENWERK-Event-Id and Idempotency-Key. The body is a flat JSON object whose values are strings, keys sorted, with event and id always present.
POST /hooks/kronenwerk HTTP/1.1
Content-Type: application/json; charset=utf-8
KRONENWERK-Event: invoice.paid
KRONENWERK-Event-Id: 0d3c…
KRONENWERK-Delivery: 61af…
KRONENWERK-Attempt: 1
Idempotency-Key: 0d3c…
KRONENWERK-Signature: t=1788768000,v1=9f2c…
User-Agent: KRONENWERK-Webhooks/1
{"amountDueMinor":"0","buyerName":"Beispiel GmbH","currency":"EUR","documentType":"INVOICE","dueDate":"2026-09-17","event":"invoice.paid","grossMinor":"105910","id":"0d3c…","invoiceId":"7a90…","issueDate":"2026-09-03","netMinor":"89000","number":"2026-0042","paidOn":"2026-09-10","paymentState":"PAID","taxMinor":"16910"}
The handling rules are the same ones Stripe documents for its own webhooks, and for the same reasons: verify the signature against the raw body before parsing, reject a timestamp outside your tolerance (KRONENWERK's own verifier uses five minutes), record the event id and skip anything you have seen, do not rely on order, and return a 2xx quickly and do the work on a queue. Delivery is at-least-once with retries; redirects are treated as failures. The full description is on webhooks.
Pattern 5: reconcile with the outstanding report
GET /reports/outstanding answers "what is owed in each direction today" from the same overview the owner's dashboard renders, so your figure and theirs cannot disagree.
{
"asOf": "2026-09-03",
"currency": "EUR",
"booksOpen": true,
"receivables": {
"outstanding": { "minor": 4237650, "currency": "EUR" },
"due": { "minor": 1105910, "currency": "EUR" },
"overdue": { "minor": 318400, "currency": "EUR" },
"count": 23
},
"payables": {
"outstanding": { "minor": 812000, "currency": "EUR" },
"due": { "minor": 0, "currency": "EUR" },
"overdue": { "minor": 0, "currency": "EUR" },
"count": 4
}
}
The two sides are reported separately and never netted: money owed to a business and money owed by it fall due on different days, and a single figure would be reassuring at exactly the moment it should not be. For a per-invoice view, page through GET /invoices?status=OPEN and compare outstanding and overdue with your own billing state. A discrepancy usually means a payment that reached the bank but has not been matched in the product yet, or an invoice your webhook handler dropped — the event id log tells you which.
Sample sequence for a Stripe-billed subscription
- Stripe sends
customer.subscription.created. Your handler verifies it, deduplicates on the Stripe event id, and callsPOST /customerswithIdempotency-Key: account-<id>if no KRONENWERK customer exists for the account yet. Store the returnedid. - Your handler calls
POST /transactionswithIdempotency-Key: sub_<id>-<period>, putting the subscription and Stripe invoice identifiers indescriptionand the period end indueOn. - For B2B accounts, your handler calls
POST /invoices/draftswithIdempotency-Key: in_<id>. - A person in finance opens the draft, adds the line for the period from the transaction's description, and issues it. KRONENWERK computes the tax verdict, checks the VAT ID, validates the structured file, spends the number and sends
invoice.issuedto your endpoint. StorenumberandinvoiceIdagainst the Stripe invoice. - Stripe collects the card payment and pays out to the company's bank account. The bank connection (Enable Banking for European banks, Plaid for Canadian and US banks) shows the payout; a person matches it to the invoice, or records the payment manually.
payment.recordedandinvoice.paidreach your endpoint. - If Stripe refunds the charge, a person issues a correction in the product;
invoice.cancelledarrives withcancelledByNumber, and you attach the credit note number to the refund. - At month end, compare
GET /reports/outstandingwith your own open receivables.
Pitfalls
- Deriving idempotency keys from time or randomness
- A key that changes on retry protects nothing. Derive it from the record you are mirroring — account id, subscription id plus period, provider invoice id — so that a retry means the same operation. A different body under the same key answers
409 IDEMPOTENZ_KONFLIKT, which is a defect in your code, not a transient error. - Creating drafts before the customer exists
- Stripe does not guarantee event order;
invoice.paidcan arrive beforecustomer.subscription.created. Resolve the KRONENWERK customer first, creating it idempotently if missing, then create the draft. - One key, one company
- A KRONENWERK API key is bound to exactly one company for its whole life. A SaaS with several legal entities (a German GmbH and a French SAS, say) needs one key per entity and must route each account to the right one; see several companies, several currencies.
- Treating
invoice.issuedas "sent" - It means a validated document exists and a number was spent. Delivery — email, Peppol through the connected access point provider, or a national system — is a separate step in the product and is not reported on the API.
- Ignoring the rate limit
- The budget is 240 requests per key, refilling at about two per second. A month-end job that creates a thousand drafts in a burst will meet
429withRetry-After; honour the header and spread the work. Details on limits. - Testing against the live key
- Use a
greif_test_key during development; it works against the same host in test mode for the company that created it.GET /mereportsenvironmentasSANDBOXorPRODUCTION, so a start-up check can refuse to run a test build with a live key. - Storing formatted amounts
- Amounts are integer minor units with a currency code on the API and strings of minor units in webhook bodies. Do not parse "1.059,10 €" from anywhere; there is nothing to parse.
How KRONENWERK handles this
SUPPORTED WITH LIMITATIONS Everything shown here runs on the API as it exists today, on the Enterprise plan (pricing): idempotent creation of customers, transactions and invoice drafts; signed webhooks for issuance, payment, cancellation, purchases and payments; the outstanding report and paged invoice reads. The limitations are the ones stated throughout: no issuing, no payment recording, no cancellation and no file download through the API, and no automatic Stripe payment import. If your integration needs a person-free billing loop, KRONENWERK's API is not that. If it needs a company's books to reflect what your product sold, with the legal steps taken by a person under the country's rules, this is the intended path. The related product pages are accounting, invoices and automation; the comparison of accounting tools with an API is at accounting software with an API.
Frequently asked questions
Can I record a Stripe payment through the API?
No. The API does not record payments. Payouts reach the books through the bank connection, or a person records the payment; payment.recorded and invoice.paid then reach your endpoint.
What is a "transaction" on the API?
A unit of work with a title, an optional customer, a stage from the company's workflow and a due date — an order or a subscription period. It is not a ledger entry.
Do I need a draft for every consumer payment?
Often not. Drafts are intended for customers who need an individual, structured invoice. Whether consumer sales in a given country require one is a question for professional confirmation.
How should I derive idempotency keys?
From your own stable identifiers: account id for customers, subscription id plus period for transactions, provider invoice id for drafts. Keys may be up to 200 characters.
How do I test without touching the live books?
Create a greif_test_ key. It works against the same API in test mode for the company that created it, and GET /me reports "environment" so your build can check it.