In short
Install quittio/quittio, enter your company once in config/quittio.php and build invoices with Quittio::invoice(). render() calculates totals and VAT itself, validates the invoice, picks the right format (ZUGFeRD for businesses, XRechnung for German authorities) and returns PDF and XML.
Requirements
- PHP 8.3 or newer with the extensions
ctype,dom,gd,libxml,mbstring,xmlwriterandzlib;intlis recommended. - Laravel 13. It also works without Laravel, see Create e-invoices with plain PHP.
- Optional for the official checks: Java for the KoSIT validator and Docker or a local installation for veraPDF. You do not need either to get started.
Step 1: Install
The quittio/quittio package brings everything a Laravel app needs: the core, the Laravel integration and the German country pack with the mandatory fields under German VAT law.
composer require quittio/quittio # core, Laravel integration and German country pack
php artisan quittio:install # publishes config/quittio.phpStep 2: Enter your company
Every invoice names the seller: your company. You enter it once in config/quittio.php, ideally through environment variables. Quittio::invoice() then adds it to every invoice.
// config/quittio.php (excerpt)
'seller' => [
'name' => env('QUITTIO_SELLER_NAME'), // Muster GmbH
'vat_id' => env('QUITTIO_SELLER_VAT_ID'), // DE123456789
'tax_number' => env('QUITTIO_SELLER_TAX_NUMBER'), // 12/345/67890
'address' => ['line1' => 'Hauptstraße 1', 'postcode' => '12345', 'city' => 'Musterstadt', 'country' => 'DE'],
'contact' => ['name' => 'Max Muster', 'phone' => '+49 123 456789', 'email' => '[email protected]'],
'bank_accounts' => [
['iban' => 'DE02120300000000202051', 'bic' => 'BYLADEM1001', 'name' => 'Muster GmbH'],
],
],| Key | Needed | Why |
|---|---|---|
name, address | always | German invoices name the seller with the full address |
vat_id or tax_number | always | One of them must be on the invoice; the VAT ID is preferred |
contact, bank_accounts | recommended | Printed on the invoice; mandatory for XRechnung to authorities |
small_business | small businesses only | true marks every invoice as a small business invoice without VAT |
No VAT ID?
If you only have a tax number, you also need a trade register number (legal_registration) or another identifier (identifiers). EN 16931 requires this with rule BR-CO-26.
To see the invoice with sample data, use the preview:
php artisan quittio:preview --scenario=simpleStep 3: Build your first invoice
An invoice starts with Quittio::invoice(). You add what belongs to this invoice and end the chain with render():
use Quittio\Core\Builder\Party;
use Quittio\Core\Enums\UnitCode;
use Quittio\Country\DE\Tax\VatPreset;
use Quittio\Laravel\Facades\Quittio;
$result = Quittio::invoice() // your company from config/quittio.php
->number('RE-2026-0042')
->deliveryDate('2026-09-30') // date of supply, mandatory in Germany
->buyer(Party::make('Kunde AG')
->vatId('DE987654321')
->address('Bahnhofstraße 5', '54321', 'Beispielstadt', 'DE'))
->line('Web development', netPrice: '95.00', quantity: 24, unit: UnitCode::Hour,
description: 'Customer portal, sprint 3')
->line('Hosting', netPrice: '49.00', unit: UnitCode::Month)
->line('Handbook of VAT law', netPrice: '39.00', vat: VatPreset::Reduced7) // 7 % instead of 19 %
->dueInDays(14)->cashDiscount(days: 7, percent: '2.00')
->render();
return $result->download(); // or $result->save('invoices', '2026/')number()is your invoice number. The invoice date is today unless you callissueDate(). Gapless numbering is your app’s job, or that of Quittio Managed.deliveryDate()is the date of supply. German invoices must state it, even when it equals the invoice date.buyer()is your customer: name, address and, for businesses, the VAT ID.line()adds a line.netPriceis the net price per unit. Withoutquantitythe quantity is 1; withoutvatthe standard rate of your country applies (19 % in Germany).dueInDays()sets the payment term,cashDiscount()an early payment discount.
Amounts as strings
Pass amounts and quantities as strings ('95.00'), never as floats. Floating point numbers cannot store every amount exactly; Quittio rejects them. Quittio always calculates totals and VAT itself, using the rounding rules of EN 16931.
Step 4: Output, save, send
The result of render() holds everything that was produced. Download it, save it or attach it straight to a mail:
$result->pdf(); // PDF/A-3 with embedded XML
$result->xml(); // XML
$result->filename(); // Rechnung_RE-2026-0042.pdf
$result->validation(); // validation findings, including warnings
$result->save('invoices', '2026/'); // to a storage disk
return $result->download(); // ->stream() / return $result
Mail::to($customer)->send(new YourInvoiceMail($result)); // the result is AttachableRender large volumes in the queue: Quittio::make($invoice)->queue()->save('invoices', '2026/').
Step 5: The right format
Usually you don’t need to do anything: without a format, Quittio chooses per invoice. For a German seller:
| Customer | Format | Why |
|---|---|---|
| Business in Germany | ZUGFeRD (profile EN 16931) | An e-invoice that is also a readable PDF |
| Public authority (Leitweg-ID as buyer reference) | XRechnung (CII) | Required by public buyers |
| Consumer | No e-invoicing mandate | |
Small invoice up to €250, marked with ->smallAmount() | Small invoices are exempt | |
| Business in another EU country | ZUGFeRD (EN 16931) | Readable PDF with standard XML |
| Customer outside the EU | No EU rules |
If a customer requires a specific format, name it. For German authorities you need the Leitweg-ID:
$result = Quittio::invoice()
->buyer($authority)
->buyerReference('04011000-12345-03') // Leitweg-ID
->line('Consulting', netPrice: '120.00')
->render('xrechnung-cii');More in the guide ZUGFeRD, XRechnung or Peppol.
Step 6: Connect your own models
Instead of repeating buyer data for every invoice, let your models provide it. There are three contracts: ProvidesParty (customer), ProvidesInvoiceLine (lines) and ProvidesInvoiceData (a whole invoice, e.g. from an order).
use Quittio\Core\Builder\PartyBuilder;
use Quittio\Core\Contracts\ProvidesParty;
class User extends Authenticatable implements ProvidesParty
{
public function fillParty(PartyBuilder $party): void
{
$party->name($this->company ?? $this->name)
->vatId($this->vat_id)
->address($this->street, $this->postcode, $this->city, $this->country)
->electronicAddress($this->email, 'EM');
}
}
$invoice = Quittio::invoice()
->buyer($user) // ProvidesParty
->lines($order->items) // ProvidesInvoiceLine
->build();The invoice keeps a copy of the values. If the model changes later, invoices already built stay unchanged.
Step 7: Understand error messages
Every invoice is validated before rendering. If it is invalid, render() throws a ValidationException with rule code, field and a hint, for example for an XRechnung without Leitweg-ID:
The invoice is not valid (2 errors):
[QUITTIO-PROFILE-REQUIRED] BT-10: The field buyerReference (BT-10) is mandatory in profile XRechnung 3.0.
Provide the field or choose a profile that does not require it.
[BR-DE-15] BT-10: The buyer reference (BT-10) must be provided.
Set the Leitweg-ID with ->buyerReference('04011000-12345-03').The prefix tells you where a rule comes from: BR- from EN 16931, BR-DE- from XRechnung, PEPPOL- from Peppol, DE-USTG- from the German VAT Act and QUITTIO- from Quittio itself. Use check() instead of render() to see all findings at once without anything being thrown.
Step 8: Test
The most common test checks that your code renders an invoice. Quittio::fake() replaces rendering with a fast fake that records every call:
use Quittio\Laravel\Facades\Quittio;
use Quittio\Laravel\Rendering\RenderedInvoice;
it('renders the invoice of an order', function () {
$fake = Quittio::fake();
$this->post('/orders/42/invoice')->assertOk();
Quittio::assertRenderedCount(1);
Quittio::assertRendered(fn (RenderedInvoice $r) => $r->format() === 'zugferd'
&& $r->data()->totals->amountDue->isEqualTo('1234.56'));
});To check that the invoice is also a valid e-invoice, use assertValidEInvoice() with the official validators. How to run that in CI is covered in Validating e-invoices.
Step 9: Cancel
An issued invoice is never changed or deleted. To reverse it, you issue a cancellation invoice: a new invoice with its own number that refers to the original.
$cancellation = $result->cancel('ST-2026-0001', reason: 'Charged twice');Partial cancellations, signs and corrections are covered in Cancellation, partial cancellation, correction.
Where to go next
- If your app issues invoices itself, you need gapless numbers, an archive and immutability. Quittio Managed handles that; background in Gapless invoice numbers.
- Adjust logo, colours and texts in the configuration; your own templates are possible.
- Read incoming e-invoices with
Quittio::read().