Skip to content

For developers

Connect your SaaS to accounting: customers, drafts, webhooks

Last reviewed SUPPORTED WITH LIMITATIONS

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 eventWhat your app doesWhat happens in the productWhat comes back
Account signs up or upgrades to a paid planPOST /customersThe customer appears in master data with a partner number201 with the customer's id and number
Contract or order is agreedPOST /transactionsA transaction (a job with a stage and a due date) appears on the company's board201 with number such as V2026-0481
Billing period is due for a B2B accountPOST /invoices/draftsA draft awaits lines and issuance by a person201 with the draft; later invoice.issued
Customer pays (card, transfer)Nothing on the APIThe payment is recorded in the product: manually, or matched from the connected bank accountpayment.recorded and, when the receivable clears, invoice.paid
Refund or dispute reverses an invoiceNothing on the APIA person issues a full correctioninvoice.cancelled naming the credit note
Month-end checkGET /reports/outstanding, GET /invoices?status=OPENReceivables 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

  1. Stripe sends customer.subscription.created. Your handler verifies it, deduplicates on the Stripe event id, and calls POST /customers with Idempotency-Key: account-<id> if no KRONENWERK customer exists for the account yet. Store the returned id.
  2. Your handler calls POST /transactions with Idempotency-Key: sub_<id>-<period>, putting the subscription and Stripe invoice identifiers in description and the period end in dueOn.
  3. For B2B accounts, your handler calls POST /invoices/drafts with Idempotency-Key: in_<id>.
  4. 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.issued to your endpoint. Store number and invoiceId against the Stripe invoice.
  5. 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.recorded and invoice.paid reach your endpoint.
  6. If Stripe refunds the charge, a person issues a correction in the product; invoice.cancelled arrives with cancelledByNumber, and you attach the credit note number to the refund.
  7. At month end, compare GET /reports/outstanding with 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.paid can arrive before customer.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.issued as "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 429 with Retry-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 /me reports environment as SANDBOX or PRODUCTION, 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.

Sources

  1. KRONENWERK developer documentation read on
  2. Stripe — Receive Stripe events in your webhook endpoint (duplicates, ordering, signatures) read on
  3. Stripe — Idempotent requests read on

Start integrating

Read the quickstart Reference

Read next