Skip to content

For developers

Accounting API for developers: endpoints, auth, webhooks, MCP

Last reviewed SUPPORTED

The KRONENWERK accounting API is a small, documented HTTPS interface at https://kronenwerk.org/api/extern/v1. A Bearer API key bound to exactly one company reads customers, issued invoices, transactions and open items, and creates customers, transactions and invoice drafts. Every write requires an Idempotency-Key. Irreversible acts — issuing an invoice, recording a payment, cancelling — are not exposed; they happen in the product and are reported back through signed webhooks. The API, webhooks and the MCP server are part of the Enterprise plan.

What the accounting API is for

It is for software that needs to put records into a company's books, or read what is owed, without a person copying figures between two systems. Typical callers are a SaaS back end that opens a transaction and an invoice draft when a customer signs a contract, an internal dashboard that reads receivables, or an AI agent that answers "which invoices are overdue" through the MCP server.

The design principle is that the API reaches the same services the screens use. There is nothing an external caller can read that the browser cannot, and nothing it can do by a shorter route. The tenant boundary, the plan check, the numbering sequence and each country module's rules are enforced once, in the product, and the API inherits them. That is why the API deliberately accepts fewer fields than you might expect: a draft's number, due date and payment sentence come from the seller's jurisdiction and the customer's agreed terms, not from the request.

Authentication: one key, one company

You authenticate with an API key in the Authorization header as a Bearer token. Live keys begin with greif_live_, test keys with greif_test_. A test key works against the same host in test mode for the company that created it; there is no separate sandbox host.

A key belongs to exactly one company, decided when the key is issued and never changed afterwards. The organisation column on the key is not updatable, the acting context is built from that column, and nothing on a request can influence it. A person who is a member of two companies and issues a key while viewing company A holds a key that can never read company B. GET /me reports this as a named binding object so that you find it out before you build on the opposite assumption.

Keys carry scopes chosen when the key is created:

ScopeWhat it allows
customers:readList and read customers
customers:writeCreate customers
invoices:readList and read issued invoices
invoices:writeCreate invoice drafts
transactions:readList transactions (jobs with a stage and a due date)
transactions:writeCreate transactions
reports:readRead the outstanding report
companies:readRead the issuing company's settings (needed to build a draft)

A scope is a documented name for a set of capabilities the product already enforces; it is not a second permission model. Details are on the authentication page.

Endpoints: the complete list

These ten addresses are the whole interface. The reference table on the developer portal is rendered from the same constant the server enforces, so the scope printed next to an address is the scope checked at it.

Method and pathScopes requiredReturns
GET /menoneThe key, its company binding, environment and scopes
GET /customerscustomers:readA page of customers
GET /customers/{id}customers:readOne customer
POST /customerscustomers:write201 with the created customer, including its number
GET /invoicesinvoices:readA page of issued invoices, newest first; filters status (OPEN or PAID), from, to
GET /invoices/{number}invoices:readOne issued invoice by its number
POST /invoices/draftsinvoices:write customers:read companies:read201 with a draft in state DRAFT
GET /transactionstransactions:readA page of transactions; filters stage, search, includeArchived
POST /transactionstransactions:write transactions:read201 with the created transaction and its number
GET /reports/outstandingreports:readReceivables and payables open today

Lists are paged with page (from 0) and size (default 25, maximum 100). A page has the shape {"data": [...], "page": 0, "size": 25, "total": 137, "more": true}. Money is always a count of minor units with a currency code — {"minor": 105910, "currency": "EUR"} — never a formatted string. Calendar dates such as issuedOn are ISO dates without a time; timestamps such as createdAt are instants.

Example: GET /me

The first call to write, because it answers the questions a misconfigured integration actually has: am I talking to the right company, is this the test key, which scopes did I get.

curl https://kronenwerk.org/api/extern/v1/me \
  -H "Authorization: Bearer greif_test_…"
{
  "organisationId": "5b1e…",
  "organisation": "Beispiel GmbH",
  "binding": { "organisationId": "5b1e…", "organisation": "Beispiel GmbH", "immutable": true },
  "key": "greif_test_a1b2",
  "name": "Billing service",
  "environment": "SANDBOX",
  "scopes": ["customers:read", "invoices:read", "transactions:write", "transactions:read"],
  "permissions": ["EDIT_TRANSACTIONS", "VIEW_CUSTOMERS", "VIEW_INVOICES", "VIEW_TRANSACTIONS"],
  "lastUsedAt": "2026-09-03T08:14:02Z",
  "expiresAt": null
}

environment is SANDBOX for a greif_test_ key and PRODUCTION for a greif_live_ key. scopes is the published vocabulary; permissions is the internal capability list that actually decides. Both are reported so that a capability without a scope over it is visible rather than hidden.

Example: POST /transactions with an Idempotency-Key

A transaction in KRONENWERK is a unit of work with a title, an optional customer, a stage from the company's own workflow and a due date — the thing a business calls a job or an order. It carries no money and no legal consequence, which is why it is the write with the least ceremony. It still goes through the same service as the screen: the number is drawn from the company's sequence for the year, the stage is resolved against the stages this business uses, and a customer belonging to somebody else is refused.

curl -X POST https://kronenwerk.org/api/extern/v1/transactions \
  -H "Authorization: Bearer greif_live_…" \
  -H "Idempotency-Key: order-2026-000481" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Annual licence 2026/27 — Beispiel GmbH",
    "customerId": "3f2b…",
    "description": "Stripe subscription sub_1Q…",
    "dueOn": "2026-09-30"
  }'
{
  "id": "9c7d…",
  "number": "V2026-0481",
  "title": "Annual licence 2026/27 — Beispiel GmbH",
  "stage": "NEU",
  "customer": "Beispiel GmbH",
  "dueOn": "2026-09-30",
  "archived": false,
  "createdAt": "2026-09-03T08:15:41Z"
}

The Idempotency-Key header is required on every POST, not optional. The first request under a key does the work and the key remembers the identifier of what it created; a repeat under the same key re-reads that record through the same tenant check and returns it without doing the work again. A repeat with a different body under the same key is refused with 409 IDEMPOTENZ_KONFLIKT, because a key names one intended operation. Keys may be up to 200 characters, so an order number with a prefix is fine. Two copies of the same retry arriving together are settled by a unique index, not by a look-then-write. See idempotency.

Errors and rate limits

Every refusal is JSON with a sentence for a person and a code for a program: {"fehler": "…", "code": "…"}. The sentence may be reworded in any release; the code is part of the contract.

StatusCodeMeaning
400ANFRAGEThe request could not be read: a missing field (the sentence names it), an unparseable body
401UNAUTHENTICATEDNo key, an unreadable, revoked or expired one — one answer for all four
403PLAN_ERFORDERLICHThe company's plan does not include the API; only the person who pays can fix this
403KEINE_BERECHTIGUNGThe key lacks the scope this address requires
404NICHT_GEFUNDENNo such record — including a real record belonging to another company
409IDEMPOTENZ_KONFLIKTThe idempotency key was already used for a different request, or the same request is still in flight
409FALSCHER_ZUSTANDThe record's state does not allow this
409ABGELEHNTA domain rule refused the change; the sentence says which
429ZU_VIELE_ANFRAGENToo many requests for this key; carries Retry-After

The two 403s look identical at the status line and need completely different action, which is the reason the code exists. A 404 is returned for "exists but is not yours" on purpose: a 403 there would let a caller confirm which identifiers are real in somebody else's company.

The rate limit is per key, not per IP address: a budget of 240 requests that refills continuously at one request every 500 milliseconds, about two per second sustained with room for bursts. When the budget is empty the answer is 429 with a Retry-After header in seconds and a body of the usual shape plus "wartesekunden". An integration that needs more throughput is split into two keys, and the logs then say which half is loud. See limits and errors.

Webhooks: how irreversible acts reach you

Issuing an invoice spends a number and releases a legal document; recording a payment moves the books; cancelling produces a correction. None of these has an address on the API and none has a scope that could reach one. They are reported outward instead, as signed webhook deliveries for the events invoice.issued, invoice.paid, invoice.cancelled, purchase.recorded and payment.recorded.

Each delivery is an HTTPS POST to the endpoint you register, with the headers KRONENWERK-Signature (t=<unix seconds>,v1=<hex>, an HMAC-SHA256 over the timestamp, a dot and the raw body, with a five-minute tolerance), KRONENWERK-Event-Id, KRONENWERK-Event, KRONENWERK-Delivery, KRONENWERK-Attempt and Idempotency-Key (equal to the event id). The body is a flat JSON object whose values are strings, with keys in sorted order, and always includes "event" and "id". Delivery is at-least-once with retries; deduplicate on the event id. Only HTTPS on port 443 is accepted, and redirects are not followed automatically — each redirect target is checked against the same rules as the original address, and a chain longer than three hops fails. Details and a verification example are on the webhooks page.

MCP: the same data for AI agents

The MCP server at https://kronenwerk.org/api/extern/mcp speaks Streamable HTTP (POST only, protocol revision 2026-07-28 with the three previous revisions still accepted) and authenticates with the same Bearer API key. There is no session and no organisation parameter anywhere in the protocol: the company an agent reaches is the key's company, decided before the request is routed.

Read tools: get_customer, search_customers, get_invoice, list_invoices, get_transaction, list_transactions, list_receivables, list_payables, get_profit_and_loss, get_balance_sheet, get_business_attention. Draft tools: create_invoice_draft, create_transaction, add_transaction_note. No tool issues an invoice, sends mail, moves money or changes settings. See MCP.

How KRONENWERK handles this

SUPPORTED The API described here is the API that runs today, at https://kronenwerk.org/api/extern/v1. It is available on the Enterprise plan together with webhooks, the MCP server and the integration directory; see pricing. Keys are created in the product's developer settings with the scopes and environment you choose, and GET /me tells you what you got.

What it does not do, stated plainly: it does not issue invoices, does not record payments, does not cancel, does not send e-invoices, and does not change settings. Those are acts a person performs in the product with the approvals their country and their business put around them — the tax verdict, the VIES check, the validation of the structured file and the gap-free number all happen there. The invoice API page explains why the line is drawn exactly at the draft, and connecting a SaaS product shows the whole loop end to end. Start with the quick start and the reference.

Frequently asked questions

Is there a sandbox?

A greif_test_ key works against the same host in test mode for the company that created it. There is no separate sandbox host or base URL.

Can one API key access several companies?

No. A key is bound to one company when it is issued and the binding cannot change. Create one key per company; GET /me shows the binding.

Why can I not issue an invoice through the API?

Issuing spends a number from a gap-free sequence, freezes the archive row, produces the structured file and, in some countries, hands it to a state system. That is not a step a loop that ran twice should be able to take, so it stays in the product; the API creates drafts and the invoice.issued webhook tells you when a person issued one.

What happens if I forget the Idempotency-Key header?

The POST is refused with 400. The header is required rather than offered because an optional guarantee protects only the callers who did not need protecting.

How are amounts represented?

As an integer count of minor units plus a currency code, for example {"minor": 105910, "currency": "EUR"}. Never as a formatted string.

Which plan includes the API?

The API, webhooks, MCP and the integration directory are part of the Enterprise plan. Current prices are on the pricing page.

Sources

  1. KRONENWERK developer documentation read on
  2. RFC 6750 — The OAuth 2.0 Authorization Framework: Bearer Token Usage read on
  3. Model Context Protocol specification read on

Start integrating

Read the quickstart Reference

Read next