Skip to content

For developers

Building on a European accounting API: VAT, e-invoices, GDPR, ledger

Last reviewed SUPPORTED WITH LIMITATIONS

An accounting API for Europe has to get eight things right that a single-country API can ignore: VAT that depends on both parties' countries and status, reverse charge and its invoice wording, VAT-ID verification against VIES, structured e-invoices in the national format, GDPR obligations on customer data, idempotent writes with signed webhooks, a real double-entry ledger, and multi-currency bookkeeping. This guide explains each requirement and shows how KRONENWERK's API maps onto it — including where the API stops: it creates customers, drafts and transactions and reads the books; it does not issue invoices, post entries or file returns.

Multi-country VAT is a function of facts, not a rate field

In the EU, the VAT treatment of a sale follows from the seller's country, the buyer's country, whether the buyer is a business or a consumer, and whether the supply is goods or services. Directive 2006/112/EC sets the framework: for B2B services the place of supply is where the customer is established (Article 44); when the supplier is not established there, the customer accounts for the VAT under the reverse charge (Article 196); intra-Community supplies of goods to a VAT-registered business in another member state are exempt when the conditions are met (Article 138); and Article 226 lists what an invoice must state, including the words "Reverse charge" where it applies. Rates, thresholds and exemptions are national.

The consequence for an API design is that a caller should never be asked for a VAT rate in isolation. A well-designed European API takes the facts and returns a verdict — standard, zero-rated, exempt, reverse charge, outside scope — with the rate and invoice wording that follow, and refuses to guess when a fact is missing.

KRONENWERK: every invoice carries a tax verdict derived from the transaction's facts (seller country, buyer country, business or consumer, kind of supply). The verdict is one of standard, zero-rated, exempt, reverse charge, outside scope, or "requires input" / "requires professional confirmation" when the facts do not decide it. It is stored with the invoice and never inferred later from master data. Over the API the verdict is not a field you set: a draft created with POST /invoices/drafts inherits the seller's jurisdiction, and the verdict is fixed when a person issues the invoice in the product. Whether a specific supply qualifies for an exemption is a question for a professional, and the product says so rather than deciding.

Reverse charge needs both VAT IDs and the right words

A reverse-charge invoice differs from a domestic one in three places: no VAT is charged, the buyer's VAT identifier must be present alongside the seller's, and the document must state that the buyer accounts for the tax. In EN 16931 terms the VAT category is AE, an exemption reason is mandatory, and the rules BR-AE-01 to BR-AE-10 enforce the breakdown and the identifiers. A structured e-invoice with the wrong category is rejected by the receiver's validator; a PDF with the wrong wording is a compliance problem for both parties.

KRONENWERK: when the verdict is reverse charge, the invoice is issued with 0 % VAT, category AE in the structured document, the buyer's VAT ID and the statutory wording in the document language. The API reads the outcome through GET /invoices/{number}, where tax is zero and net equals gross. There is no API parameter to force reverse charge; it follows from the customer's country and VAT ID, which you can supply with POST /customers:

POST https://kronenwerk.org/api/extern/v1/customers
Authorization: Bearer greif_test_XXXXXXXXXXXXXXXX
Idempotency-Key: 6f1c2f4e-3c0a-4b8f-9d61-2a7c1c2e9b10
Content-Type: application/json

{
  "name": "Atelier Dupont SARL",
  "email": "compta@example.fr",
  "street": "12 rue de la Paix",
  "postalCode": "75002",
  "city": "Paris",
  "country": "FR",
  "vatId": "FR12345678901",
  "currency": "EUR"
}

VIES verification, at the right moment

VIES is the Commission's VAT Information Exchange System. Its checkVat web service takes a country code and number and returns whether the number is valid on the request date, plus the name and address where the member state discloses them; checkVatApprox additionally matches trader details and returns a requestIdentifier — the consultation number that documents the check. The Commission also offers a REST interface with the same semantics. Member state back-ends are sometimes unavailable, in which case the service reports a status such as MS_UNAVAILABLE rather than a validity. The service exists for intra-Community transactions under Council Regulation (EC) No 904/2010.

Two design rules follow. Check at the moment the decision depends on it — issuance — not only at customer creation, because registrations lapse. And store the result with the document, because the question later is "was it valid when we invoiced", not "is it valid today".

KRONENWERK: a buyer's VAT ID is checked against VIES at issuance and the result is stored with the invoice together with the verdict. If VIES or the member state's back-end does not answer, the check is recorded on the invoice as not reachable and shown as a notice at issuance; an outage is never treated as a valid result and is not cached, while a real result is remembered for a day so the register is not queried on every keystroke. The free VAT ID checker runs the same check on a single number without storing it. The API does not expose a VIES endpoint of its own; it exposes the stored outcome on the issued invoice.

Structured e-invoices are national, not European

The EU directive on e-invoicing (2014/55/EU) obliges public bodies to receive EN 16931 invoices; B2B mandates are national and differ in format, network and timing. Germany requires businesses to receive structured invoices and phases in issuing (XRechnung or ZUGFeRD); Belgium mandates Peppol BIS Billing 3.0 over the Peppol network for B2B from 1 January 2026; Poland clears invoices through KSeF in FA(3); France runs on approved platforms with Factur-X as one accepted format. Canada and the United States have no structured mandate. See the timeline and the formats.

For an API this means the format is a property of the seller's country and the buyer's channel, decided at issuance, and that the document must be validated with the national artefacts (the CEN Schematron plus the profile rules) before it exists. An API that accepted arbitrary XML from callers would have to validate it anyway and would push the hardest part of the problem to every caller.

KRONENWERK: the structured file is generated and validated at issuance for the seller's country — XRechnung and ZUGFeRD (KoSIT Schematron, cross-checked with Mustang), Factur-X, Peppol BIS UBL, FA(3). The API creates the draft; the format, validation and dispatch are the product's. This is the largest honest gap for developers who expected to POST an invoice and receive XML: there is no "issue" endpoint and no XML in the API. The e-invoicing API page sets out exactly what is available, and the EN 16931 developer guide covers the rules if you generate documents yourself.

GDPR: customer data is personal data

A sole trader's name, address, e-mail and VAT number are personal data, so an accounting integration is a processing activity. The GDPR requires a contract between controller and processor setting out subject-matter, duration, nature, purpose and data categories (Article 28(3)); records of processing (Article 30); security appropriate to the risk, including encryption where appropriate (Article 32); and, for transfers outside the EU/EEA, a valid transfer mechanism (Chapter V, from Article 44). What a developer needs from an accounting provider is therefore concrete: a data-processing agreement, a statement of where data is stored and which sub-processors are used, and a way to delete or export a data subject's records.

KRONENWERK: the processing terms and hosting arrangements are set out on the privacy page and the security page; read those rather than this guide for the binding statement. KRONENWERK holds no security certification and makes no "GDPR certified" claim, because no such certification exists. Over the API, keys are scoped so that an integration that needs only reports:read never receives customer records at all, and every key is bound to one company.

Idempotency and webhooks

Network failures make every write ambiguous: a timed-out POST may or may not have created the record. The standard answer is an idempotency key — a client-chosen unique value per logical operation that the server stores with the outcome, so a retry with the same key returns the same result instead of a duplicate. On the outbound side, webhooks must be signed so the receiver can verify origin, carry an event identifier so duplicates can be discarded, and be retried on failure.

KRONENWERK: every POST requires an Idempotency-Key header; a repeat with the same key and body returns the original result, and a repeat with a different body answers 409 IDEMPOTENZ_KONFLIKT. Errors are JSON with a machine code and a sentence: UNAUTHENTICATED, ANFRAGE, PLAN_ERFORDERLICH, KEINE_BERECHTIGUNG, NICHT_GEFUNDEN, IDEMPOTENZ_KONFLIKT, FALSCHER_ZUSTAND, ABGELEHNT, ZU_VIELE_ANFRAGEN. The rate limit is a budget of 240 requests per key refilling continuously at about two per second, answered with 429 and Retry-After. Outbound webhooks are signed (KRONENWERK-Signature) and carry KRONENWERK-Event-Id, KRONENWERK-Event, KRONENWERK-Delivery, KRONENWERK-Attempt and an Idempotency-Key; events are invoice.issued, invoice.paid, invoice.cancelled, purchase.recorded and payment.recorded; delivery is HTTPS only, retried, and never follows redirects. Details: idempotency, webhooks, errors, limits.

The ledger is double entry, and the API reads it

An accounting system is not a list of invoices. Every invoice, payment and expense produces balanced journal entries — debit receivables, credit revenue and VAT payable; debit bank, credit receivables — and the reports are sums over accounts, not over documents. An API built on such a ledger can promise that "outstanding receivables" reconciles with the receivables control account, and that a profit-and-loss figure is only final when the periods behind it are closed. An API built on a document list cannot.

KRONENWERK: the ledger is double entry with periods that close. The REST API reads the open items with GET /reports/outstanding; the MCP server additionally exposes get_profit_and_loss, get_balance_sheet, list_receivables and list_payables from the same ledger, with an is_final flag on the P&L. Nothing in the API posts a journal entry; entries arise from acts a person performs in the product — issuing, recording a payment, approving a bill. A response from the outstanding report, abbreviated:

{
  "asOf": "2026-09-03",
  "currency": "EUR",
  "booksOpen": true,
  "receivables": { "outstanding": { "minor": 1284050, "currency": "EUR" },
                   "due":         { "minor": 412000,  "currency": "EUR" },
                   "overdue":     { "minor": 105910,  "currency": "EUR" },
                   "count": 9 },
  "payables":    { "outstanding": { "minor": 336000,  "currency": "EUR" },
                   "due":         { "minor": 0,       "currency": "EUR" },
                   "overdue":     { "minor": 0,       "currency": "EUR" },
                   "count": 3 }
}

Currency: minor units, one book currency, stored rates

Amounts as floating-point numbers lose cents; amounts as formatted strings are ambiguous across locales ("1.059,10" versus "1,059.10"). The robust representation is an integer count of minor units with an ISO 4217 code. A multi-currency business invoices in the customer's currency, keeps its books in one book currency, and must store the rate used on each document so that later reports do not drift with today's rate.

KRONENWERK: every amount on the API is {"minor": 105910, "currency": "EUR"}, never a decimal or a string. A company has one book currency; customers can have a preferred invoicing currency; the figures frozen on an issued invoice are the ones the API returns, not today's master data. Multi-company setups use one key per company. See several companies, several currencies and the multi-company product page.

How KRONENWERK handles this

SUPPORTED WITH LIMITATIONS The public API at https://kronenwerk.org/api/extern/v1 covers GET /me, customers (GET, GET /{id}, POST), invoices (GET, GET /{number}, POST /invoices/drafts), transactions (GET, POST) and GET /reports/outstanding, with scoped keys, mandatory idempotency on writes, a per-key rate budget, JSON errors with machine codes, signed webhooks and an MCP server on the same key. The limitations are deliberate and should be planned for: the API creates drafts and transactions only — it does not issue invoices, produce or accept structured XML, post ledger entries, record payments, run VIES checks on demand, or file or remit any tax. Those acts happen in the product, where the country modules validate them, and the API and webhooks report the results. All of this is part of the Enterprise plan; see pricing, the accounting API overview, the quick start and the SaaS connection guide.

Frequently asked questions

Can I set the VAT rate on an invoice through the API?

Not on the REST API: a draft inherits the seller's jurisdiction and the verdict is fixed at issuance from the facts. Through the MCP server, create_invoice_draft accepts a tax_rate_percent per line for the draft; a person still reviews and issues it. A line that bears several taxes at once — GST and QST in Québec, GST and PST in British Columbia, state, county and city in the United States — or a named tax that charges nothing, such as a zero-rated export, is given as taxes on the line instead: one entry per tax with its name, rate_percent and, where nothing is charged, the seller's own reason. Each tax is applied to the line's net and printed and totalled under its own name; KRONENWERK supplies no rate and decides no nexus. create_quote_draft takes the same list on a quote line, and the REST API carries it as steuern on the line of a quote as of an invoice; the invoice a quote becomes carries it unchanged. The tool's own schema, returned by the MCP server, is the reference for these fields.

Does the API issue legally valid invoices?

No. It creates drafts. Issuing — numbering, validation as a structured e-invoice, dispatch — is performed in the product, and the invoice.issued webhook reports it.

How do I verify a customer's VAT ID before invoicing?

KRONENWERK checks it against VIES at issuance and stores the outcome. For an ad-hoc check use the VAT ID checker; there is no VIES endpoint on the API.

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.

Where is my data stored, and is there a data-processing agreement?

The binding answers are on the privacy page and the security page. KRONENWERK holds no security certification and makes no "GDPR certified" claim.

Which countries does the API cover?

The same as the product: Germany, France, Belgium, Poland (limited — KSeF transmission is not yet production-proven), Canada and the United States.

Sources

  1. Council Directive 2006/112/EC on the common system of VAT (consolidated) read on
  2. European Commission — VIES checkVat web service (WSDL) read on
  3. Regulation (EU) 2016/679 (GDPR) read on
  4. ConnectingEurope — eInvoicing-EN16931 validation artefacts read on
  5. KRONENWERK developer documentation read on

Start integrating

Read the quickstart Reference

Read next