In short
German VAT law requires a sequential number that is assigned only once; several number ranges are allowed. According to the VAT application decree, a gapless sequence is not mandatory, but gaps must be explainable, because the GoBD require completeness and traceability. Gapless numbers are the simplest proof. Technically: assign the number only when issuing, inside a locked transaction, and never change it afterwards.
What the German VAT Act requires
Under sec. 14 (4) sentence 1 no. 4 UStG, every invoice must carry a sequential number, with one or more series of numbers, that the issuer assigns only once to identify the invoice.
- Unique: the same number may exist only once in the company, across all brands, branches and systems.
- Sequential: the numbers follow a recognisable order. Letters and prefixes such as
RE-2026-are allowed. - Several number ranges: allowed, e.g. one per year, location or document type, as long as every number stays unique.
The VAT application decree (UStAE) states that a gapless sequence is not mandatory. That does not mean gaps don’t matter.
What the GoBD say
The GoBD, the German principles for keeping books and records electronically, require among other things:
- Completeness: all transactions are recorded. A missing invoice number raises the question whether an invoice has disappeared.
- Traceability: a knowledgeable third party must be able to follow what happened within a reasonable time.
- Immutability: a record must not be changed in a way that the original content can no longer be determined (sec. 146 (4) AO).
- Retention: invoices are accounting records and are kept for 8 years (sec. 147 AO, sec. 14b UStG).
In practice
In a tax audit, gaps in the number range are a classic reason for questions. Without gaps, there is nothing to explain. With gaps, you need documentation of why.
Typical mistakes when building it yourself
Most problems are not caused by bad intent but by technology built for something else:
| Approach | Problem |
|---|---|
MAX(number) + 1 | Two concurrent requests read the same value and assign the same number |
| Auto-increment ID as invoice number | Databases consume IDs in rolled-back transactions too; gaps appear |
| Assigning the number to drafts | Deleted or discarded drafts leave gaps |
| Deleting instead of cancelling | A gap and a missing document at once |
| Separate numbers per brand or system | Duplicate numbers within the same company |
| Yearly reset without the year in the number | 0001 exists every year; the number is no longer unique |
| Changing the number after issuing | Violates immutability |
The most common mistake looks harmless:
// race condition: two processes read the same MAX()
$last = Invoice::max('sequence');
$invoice->sequence = $last + 1;
$invoice->number = sprintf('RE-2026-%04d', $invoice->sequence);
$invoice->save();How to build it properly
Four rules are enough for safe numbers:
- Assign the number only when issuing. Drafts have no number, so they can be deleted without leaving a gap.
- Inside a transaction with a lock. The counter is locked (
SELECT … FOR UPDATE), increased and saved together with the invoice. If anything fails, everything rolls back and the number is free again. - Enforce uniqueness in the database. A unique index on the number is the last line of defence.
- Never change or delete. Fix mistakes with a cancellation or correction, not by editing.
A sketch with Laravel tools:
use Illuminate\Support\Facades\DB;
// create the counter row if it is missing (outside the transaction)
DB::table('invoice_sequences')->insertOrIgnore(['key' => 'invoice:2026', 'value' => 0]);
DB::transaction(function () use ($invoice) {
// lock the counter: concurrent processes wait here
$sequence = DB::table('invoice_sequences')->where('key', 'invoice:2026')->lockForUpdate()->first();
$next = $sequence->value + 1;
DB::table('invoice_sequences')->where('key', 'invoice:2026')->update(['value' => $next]);
$invoice->number = sprintf('RE-2026-%04d', $next);
$invoice->save(); // if this fails, the counter rolls back too
}, 3);The details make it hard
Deadlocks and serialisation failures must be retried, SQLite needs a different transaction mode, backdated invoices belong to the counter of their year, and several brands of one company must share a number space. That is exactly what Quittio Managed takes care of.
With Quittio Managed
Quittio Managed assigns numbers from a pattern and a counter. With the German defaults, the invoices of 2026 are numbered RE-2026-0001, RE-2026-0002 and so on; the counter starts again in 2027.
// config/quittio-managed.php
'numbering' => [
'pattern' => 'RE-{Y}-{seq:4}', // null = country pack (DE: RE-{Y}-{seq:4})
'reset' => 'yearly', // never | yearly | monthly
'start' => 1,
'per_kind' => [
'cancellation' => 'ST-{Y}-{seq:4}', // own range for cancellations, null = shared
'correction' => null,
],
],| Placeholder | Replaced by |
|---|---|
{Y}, {y} | Year of the issue date, four or two digits |
{m} | Month, two digits |
{seq}, {seq:N} | Running number, padded to N digits |
{issuer} | Key of the issuer profile |
{customer} | Buyer identifier (BT-46) |
The number is created only by issue(), inside the issuing transaction:
$draft = Invoice::draft(Quittio::invoice()->buyer($customer)->line('Consulting', netPrice: '120.00'));
$draft->number; // null: drafts have no number
$invoice = $draft->issue();
$invoice->number; // RE-2026-0001- Drafts, validation errors and failed cancellations use no number; a rollback gives it back.
- Concurrent issues are serialised with a row lock; deadlocks and lock conflicts are retried up to three times.
- Unique indexes on number space and number add another safeguard.
- Patterns are checked: a yearly reset without
{Y}, or two ranges that could produce the same number, are rejected. - Backdated invoices use the counter of their period: an invoice dated 30 Dec 2026 and issued in January 2027 gets the next number of 2026.
Several brands, one company
Numbers must be unique per company. If a company has several issuer profiles, e.g. one per brand, they share a number space in Quittio Managed. By default it follows from the VAT ID or tax number, so profiles of the same company belong together automatically. A number one brand used is blocked for the others, so give the brands different patterns, for example with {issuer}.
Moving from another system
Coming from an existing system, set the counter once to the next value. Counters can only go up, never down, and every change is written to the audit log:
php artisan quittio:sequence:show
php artisan quittio:sequence:set default:invoice:2026 1043