Billing API for European B2B: what the data model must cover
An Austrian GmbH buys 50 seats and gets a correct reverse-charge invoice in March. In May the customer is credited for 10 of those seats, and your system builds the credit note from today's customer record. Their VAT number stopped validating in April, so the credit note suddenly carries Austrian VAT although the original invoice had none. Nobody wrote a buggy function here. The bug is in the data model: the tax status lives on the customer instead of on the document.
This article covers the objects and fields a billing API has to model for European B2B before anyone discusses endpoints, SDKs or sandboxes. If you are evaluating a vendor and want to know what to test during a trial, read the companion piece on choosing an API-based billing platform.
Customer, bill-to and tax profile are three objects
Most billing APIs have a single customer object with name, email, address and an optional tax ID. That is fine for consumers. In B2B, that one object hides at least three things that change on different schedules:
- the legal entity you have the contract with (registered name, seat, company register details),
- the bill-to party: address, cost centre, purchase order number and delivery channel (email, portal, Peppol),
- the tax profile: VAT number, validation status, country of establishment, tax treatment.
The bill-to address changes when the customer's accounts team moves office. The tax profile changes when the company merges or loses its VAT registration. If both sit in one field set, an address edit in the customer portal can overwrite data that matters for invoices you've already issued.
So check whether the API keeps billing contacts and tax profiles as separate resources with history. A PATCH /customers/{id} must never change a finalised invoice, not even indirectly through a reference.
A VAT number needs a validation record
For cross-border B2B services the customer usually accounts for the VAT, and the EU VAT Directive requires the invoice to say so with a "Reverse charge" mention and to show the customer's VAT number. Whether reverse charge applies at all depends on the customer acting as a business, and a valid VAT number is your main evidence for that.
The number itself is the least interesting part of the record. What you need is what you know about the check:
- the result from the EU VIES service (valid, invalid, member state service unavailable),
- when the check ran and, where returned, the consultation number,
- the name and address VIES returned, if the member state provides them,
- the date from which this result is used for invoicing.
"Service unavailable" is not an edge case. When a member state's VIES endpoint is down, the API needs a rule: does the invoice wait as a draft, or is it finalised against the last valid result? That rule belongs in configuration, not in a retry buried in a worker. The article on automating VIES checks covers scheduling and re-validation.
A finalised invoice freezes its context
A finalised invoice is an accounting record. In Germany it must stay readable and unchanged for eight years from the end of the year of issue under § 14b UStG (German VAT Act), and other member states have their own retention periods. That means the invoice should hold copies of customer, price and tax data, not live references to them.
In practice: when an invoice moves from draft to finalized, the API copies both parties' names and addresses, the VAT numbers used, the tax treatment per line, the rate and the price version into the invoice. From then on it is read-only. PATCH /invoices/{id} should return an error after finalisation, not save quietly.
The invoice number follows the same logic. EU rules require a sequential number that identifies the invoice uniquely. For the API this means the number is assigned at finalisation, not when the draft is created. Otherwise discarded drafts leave gaps in the sequence that your accountant will be asked about.
Credit notes point back to the original
Back to the opening example, with made-up amounts:
| Document | Number | Lines | Net | Tax treatment |
|---|---|---|---|---|
| Invoice (March) | INV-2026-0412 | 50 seats at €40 | €2,000.00 | Reverse charge, VAT no. ATU… |
| Credit note (May) | INV-2026-0587 | −10 seats at €40 | −€400.00 | Reverse charge, copied from INV-2026-0412 |
The credit note gets its own number from the same series, references INV-2026-0412 and copies that invoice's tax treatment, rate and VAT numbers from the snapshot. It does not look up the customer's current tax profile. The new VIES status only applies to invoices for supplies made after the status changed. Because this is an intra-EU B2B supply, the correction also has to flow into the recapitulative statement (the EC Sales List), so the API should export credit notes with the original invoice reference and its service period.
A note on wording for German customers: in German VAT law a "Gutschrift" means self-billing by the customer. German teams often call a seller-issued correction "Rechnungskorrektur" or "Stornorechnung" to avoid confusion. A billing API should store the document type as a field (invoice, credit_note, cancellation) and make the printed label configurable per language and country.
A second legal entity changes the tax question
As soon as you run a Dutch B.V. or an Austrian subsidiary next to the German GmbH, the tax treatment no longer depends on the customer alone. A Dutch business customer of the German GmbH gets a reverse-charge invoice. The same customer served by the Dutch B.V. gets an invoice with Dutch VAT. Tax treatment is a function of the pair: selling entity and customer.
For the data model that means four requirements:
- Every invoice, credit note and subscription points to exactly one selling entity (
seller_entity_id). - Each entity has its own number series, its own VAT number and its own bank account, and for SEPA direct debit its own creditor identifier.
- A subscription can't switch entity silently. Switching ends the contract with one entity and starts one with the other.
- Accounting exports run per entity, because each one keeps its own books.
Without the entity as a dimension, the B.V.'s invoice ends up in the GmbH's number series. Teams usually notice when the subsidiary's first year-end close starts.
A data model to compare against
The sketch below isn't a schema to copy. Treat it as a checklist in code form: put it next to a vendor's API reference, or next to your own schema, and mark what's missing.
SellerEntity id, legal_name, country, vat_id, number_series[], bank_account, creditor_id
Customer id, legal_name, registered_address, seller_entity_id
BillingContact id, customer_id, address, po_number, delivery_channel, valid_from
TaxProfile id, customer_id, vat_id, vies_result, checked_at, request_id, valid_from
Invoice id, number (null until finalized), seller_entity_id, status,
parties_snapshot, tax_treatment, service_period, price_version_id
InvoiceLine invoice_id, description, quantity, unit_price, net, tax_rate, tax_category
CorrectionDoc id, number, type, corrects_invoice_id, lines[], reason, snapshot_from_original
Three fields are most often missing from generic APIs: seller_entity_id, service_period on the invoice, and corrects_invoice_id. Without a service period you can't assign a correction to the right reporting period. Without the reference, an auditor sees a negative invoice with no origin.
Five calls to run before you integrate
Before you wire up a billing API or build your own model, run these cases. Each can be run with test data in a test environment:
- Create a customer with a valid VAT number, finalise an invoice, then mark the VAT number invalid. Does the finalised invoice change? It must not.
- Issue a credit note after the tax status changed. Does it copy the original invoice's tax treatment?
- Create three drafts, discard two, finalise one. Is the number sequence gapless?
- Bill the same customer from two selling entities. Separate number series, correct tax treatment per entity?
- Change the billing address and regenerate an old invoice PDF. Does it still show the old address?
If any of these fail, the work moves to your accounting team. Kontier treats reverse charge, SEPA and e-invoicing as part of the core billing process rather than add-ons, and you can run the five cases against the Kontier API yourself.
Not tax or legal advice. This post describes the law and common practice in general terms. For your own case, ask your tax adviser. As of: