Getting started
In this tutorial you install Quittio, enter your company once and then create, render and cancel a first invoice. It takes about 15 minutes. If e-invoices, ZUGFeRD or XRechnung are new to you, read E-invoicing basics first (5 minutes); you can also start right away and look terms up there.
You need PHP 8.3 or newer and Laravel 13. Without Laravel, see Without Laravel.
Installation
composer require quittio/quittio # core, Laravel integration and country pack Germany
php artisan quittio:install # publishes config/quittio.php
Set up your company
Every invoice names the seller: your company. You enter it once in config/quittio.php; Quittio::invoice() then adds it to every invoice. The keys you need first:
// config/quittio.php (excerpt)
'seller' => [
'name' => env('QUITTIO_SELLER_NAME'), // e.g. Muster GmbH
'vat_id' => env('QUITTIO_SELLER_VAT_ID'), // e.g. DE123456789
'tax_number' => env('QUITTIO_SELLER_TAX_NUMBER'), // e.g. 12/345/67890
'address' => ['line1' => 'Hauptstraße 1', 'postcode' => '12345', 'city' => 'Musterstadt', 'country' => 'DE'],
'contact' => ['name' => 'Max Muster', 'phone' => '+49 123 456789', 'email' => 'rechnung@muster.de'],
'bank_accounts' => [
['iban' => 'DE02120300000000202051', 'bic' => 'BYLADEM1001', 'name' => 'Muster GmbH'],
],
// ...
],
| Key | Needed | Why |
|---|---|---|
name, address |
Always | German invoices must name the seller with the full address |
vat_id or tax_number |
Always | German invoices must show one of them; the VAT identification number (USt-IdNr.) is preferred |
contact, bank_accounts |
Recommended | Printed on the invoice; required for XRechnung (public buyers). The first account with an IBAN is used |
small_business |
Small businesses only | true (or QUITTIO_SELLER_SMALL_BUSINESS=true) marks every invoice as small business invoice without VAT (German Kleinunternehmer) |
A seller without VAT identification number (e.g. a small business with a tax number only) also needs a trade register number (legal_registration) or another identifier (identifiers, as [['value' => '…', 'scheme' => null]]), because EN 16931 requires one of them (rule BR-CO-26). Every other key, such as logo, colours, default VAT and payment target, is explained in Configuration and in the comments of config/quittio.php.
To see the PDF design with sample data:
php artisan quittio:preview --scenario=simple # storage/app/quittio/preview/simple.pdf
Building an invoice
Start an invoice with Quittio::invoice(), add what is specific to this invoice, and end 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::Reduced)
->dueInDays(14)->cashDiscount(days: 7, percent: '2.00')
->render();
return $result->download(); // or $result->save('invoices', '2026/')
What the lines do:
number()is your invoice number. The invoice date is today unless you callissueDate('2026-10-03'). Gapless numbering is your application’s job, or that of the Managed layer.deliveryDate()is the date the goods were delivered or the service was performed. German invoices must state it, even when it is the invoice date.buyer()is your customer.Party::make()takes the name; add the address and, for businesses, the VAT identification number. You can also pass your own customer model (Application models).line()adds a line with a name and named values.netPriceis the price per unit without VAT. Withoutquantitythe quantity is 1; withoutvatthe line gets the standard rate of your country (19 % in Germany). The third line uses the reduced rate of 7 %.dueInDays(14)sets the payment target,cashDiscount()an early payment discount (Skonto).render()calculates totals and VAT, checks the invoice, chooses the format and produces the PDF and the XML (Rendering).
Totals and VAT are always calculated by Quittio; you never pass them in. Amounts and quantities are strings ('95.00'), because floating point numbers cannot store every amount exactly; floats are rejected. Rounding follows EN 16931: half up, two decimals for amounts, VAT per rate.
One way to write it
Every part of an invoice has one usual form. The documentation and the examples use these forms; the other forms exist for special cases and are explained in Building invoices.
| You need | Write |
|---|---|
| A line with price, quantity, unit, VAT or description | ->line('Hosting', netPrice: '49.00', quantity: 12, unit: UnitCode::Month). Without quantity it is 1, without vat the default VAT applies (19 % for a German seller). |
| A line with more than that: item number, line discount, list price, period, sub lines, line group | ->line(Line::make('Pump', netPrice: '1450.00', quantity: 2)->sellerId('P-200')). Line::make() takes the same named values as line() and returns the line builder for the other methods. |
| Many lines from your data | ->lines($order->items) with models that implement ProvidesInvoiceLine, or ->lines($rows) with arrays such as ['name' => 'Hosting', 'netPrice' => '49.00'] |
| The customer | ->buyer($customer) with a model that implements ProvidesParty, otherwise ->buyer(Party::make('Kunde AG')->address(…)) |
| Payment terms | ->dueInDays(14)->cashDiscount(days: 7, percent: '2.00') |
| The result | ->render() for PDF and XML, ->build() for the data only, e.g. to store a draft |
The seller comes from the configuration; set it in code only for another company (see Starting invoices).
References, notes, further parties, allowances and charges, direct debit, attachments, gross prices and all other options are described in Building invoices; the German VAT presets (VatPreset::Standard, Reduced, ReverseChargeEu …) in Tax presets.
Application models
Your own models can provide invoice data through three contracts in Quittio\Core\Contracts: ProvidesParty (fillParty(PartyBuilder $party), e.g. a customer as buyer), ProvidesInvoiceLine (fillLine(LineBuilder $line), e.g. order items) and ProvidesInvoiceData (toInvoiceData(), e.g. an order for Quittio::make($order)).
use Quittio\Core\Builder\PartyBuilder;
use Quittio\Core\Contracts\ProvidesParty;
use Quittio\Laravel\Facades\Quittio;
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) // items implement ProvidesInvoiceLine
// ...
->build();
The invoice keeps a copy of these values; changing the model later does not change invoices that were already built. More in Application models; to store invoices and link them to the user (HasInvoices trait, $user->invoices()), see Customers and references in the Managed layer.
Rendering
render() without argument chooses the format for you: for a German seller ZUGFeRD for businesses (a PDF with the XML inside), XRechnung for public authorities and a plain PDF for consumers (Which format do I need?). Name a format when a customer asks for one:
$result = Quittio::invoice()->buyer($user)->line('Consulting', netPrice: '120.00')->render('xrechnung-cii');
The result holds everything that was produced:
$result->pdf(); // the PDF (PDF/A-3 with embedded XML for ZUGFeRD)
$result->xml(); // the XML
$result->filename(); // Rechnung_RE-2026-0042.pdf
$result->validation(); // findings of the validation, including warnings
$result->save('invoices', '2026/'); // to a filesystem disk and path
return $result->download(); // or ->stream(), or return $result
Mail::to($customer)->send(new YourInvoiceMail($result)); // your Mailable; the result is Attachable
Every render calculates, validates and renders. An invalid invoice throws a ValidationException that names the rule, the field and how to fix it (Troubleshooting).
For invoice data you built earlier (->build() instead of ->render()), or for more control, use Quittio::make():
use Quittio\Laravel\Facades\Quittio;
$result = Quittio::make($invoice)->render(); // format chosen automatically
$result = Quittio::make($invoice)->format('zugferd')->locale('en')->render(); // format and language
$result = Quittio::make($invoice)->formats(['zugferd', 'xrechnung-cii'])->render(); // several formats at once
$result->as('xrechnung-cii')->xml();
Quittio::make($invoice)->queue()->save('invoices', '2026/'); // render in a queue job
All formats and profiles are described in Formats and profiles; storage, downloads, queues, events and mail in Laravel integration.
Cancellation
An issued invoice is never changed or deleted. To cancel it, you issue a cancellation invoice (German Stornorechnung): a new invoice with its own number that refers to the original and reverses its amounts.
$cancellation = $result->cancel('ST-2026-0001', reason: 'Charged twice'); // rendered, in the formats of $result
For an invoice you stored as data, build the cancellation and render it:
$cancellation = Quittio::make(Quittio::cancellation($original, 'ST-2026-0001', reason: 'Charged twice'))->render();
$original is the issued, calculated invoice, e.g. $result->data() or InvoiceData::fromJson() of what you stored. In Germany the cancellation is a credit note with positive amounts by default (type code 381); strategy: 'negative_invoice' produces an invoice with negative amounts instead (type code 380, also corrections.cancellation_strategy). Both are permitted in Germany; the document is called “Stornorechnung” (cancellation invoice), never “Gutschrift” (which means a self-billed invoice in German law).
lines: cancels part of an invoice (whole lines, quantities or amounts):
$part = $result->cancel('ST-2026-0002', lines: [
['line' => '1', 'quantity' => '1'],
['line' => '2', 'amount' => '50.00'],
]);
When an invoice is cancelled in several parts, pass the earlier parts as history:, so that the VAT of all parts adds up exactly to the VAT of the original. Strategies, partial cancellations and the history are described in Cancellations and corrections; the Managed layer keeps the history itself (Managed layer).
Without Laravel
The core (quittio/core, plus a country pack such as quittio/country-de) works in any PHP application. Create an instance with Quittio\Core\Quittio::create() and build invoices with InvoiceBuilder::make(); there is no configuration file, so the seller is set in code and the default VAT is registered once:
use Quittio\Core\Builder\InvoiceBuilder;
use Quittio\Core\Builder\Party;
use Quittio\Core\Quittio;
use Quittio\Core\Tax\CountryDefaultVat;
use Quittio\Country\DE\CountryDE;
$quittio = Quittio::create([
'validation' => ['mode' => 'error', 'levels' => ['L1', 'L2', 'L3', 'L4', 'L7']],
'pdf' => ['b2c_embed_xml' => false],
], [new CountryDE]);
InvoiceBuilder::useDefaultVat(new CountryDefaultVat($quittio->countries())); // lines without vat(): 19 %
$invoice = InvoiceBuilder::make()
->seller(Party::make('Muster GmbH')->vatId('DE123456789')->address('Hauptstraße 1', '12345', 'Musterstadt', 'DE'))
// ... as above, without Quittio::invoice()
->build();
$result = $quittio->make($invoice)->format('zugferd')->render();
file_put_contents($result->filename(), $result->pdf()); // or $result->save('/path/to/dir')
$cancellation = $quittio->cancellation($result->data(), 'ST-2026-0001', reason: 'Charged twice');
Quittio::create(Config|array $config = [], iterable $countryPacks = [], ?PdfRenderer $renderer = null, ?TemplateEngine $templates = null, ?LoggerInterface $logger = null, ?FormatResolver $formatResolver = null) takes the configuration as array or as Quittio\Core\Config object (new Config(...), Config::fromArray([...]), $config->with([...])). The array keys are flatter than those of the Laravel config/quittio.php; the Laravel integration translates its configuration itself:
| Key | Default | Laravel key |
|---|---|---|
format |
null (automatic choice; also 'auto') |
defaults.format |
profile |
en16931 |
defaults.profile |
locale, country |
null (country pack default, seller country) |
defaults.locale, defaults.country |
due_days |
null |
defaults.due_days |
template, template_paths |
default, [] (directories with {name}.php) |
pdf.template (Blade view) |
filename |
{title}_{number}.{ext} |
pdf.filename |
pdf.pdfa, pdf.b2c_embed_xml, pdf.girocode_amount |
true, false, due |
pdf.pdfa, format_resolver.b2c_embed_xml, pdf.payment_qr.girocode_amount |
auto_upgrade_profile |
true |
format_resolver.auto_upgrade_profile |
validation.mode |
error (warn, off) |
validation.mode |
validation.levels |
['L1', 'L2', 'L3', 'L4', 'L7'] |
validation.levels (php, xsd) |
validation.block_on_warnings |
false |
validation.block_on_warnings |
validation.locale |
null (the language of the invoice; 'app' = the language of the translator; or a locale) |
validation.locale (default: the language of the application) |
validation.kosit, validation.verapdf |
disabled; ['enabled' => true, 'mode' => …, …] enables L5 and L6 (KoSIT scenario sets only in configurations) |
validation.levels (kosit, verapdf) and validation.drivers.* |
mandatory_fields |
exception (log, off) |
validation.mandatory_field_guard |
temp_dir |
system temp directory | pdf.cache_path |
options |
[] (e.g. paper, markdown, fold_marks, watermark) |
pdf.paper, pdf.markdown, pdf.fold_marks |
presentation |
null (a Presentation object with the design defaults) |
pdf.* design keys |
reader.max_xml_bytes, reader.max_pdf_bytes, reader.max_decompressed_bytes |
10, 25 and 50 MB | reader.* |
corrections.cancellation_strategy |
null (country pack default) |
corrections.cancellation_strategy |
The validation keys are described in Validation, the output keys in Output configuration, the Laravel keys in Configuration. The instance offers the methods of the facade: make(), edit() (continue existing invoice data), cancellation(), document(), read(), readFile(), validateFile() and validatePath(), plus registerCountry(), setRenderer(), setTemplateEngine(), setFormatResolver(), setPostProcessor() (PDF/A and XML embedding for renderers that cannot embed), addSerializer(), addValidator(), clearValidators(), configureRules() and ruleCodes() (switching rules off) and onMandatoryFieldsMissing(). A custom Quittio\Core\Calculation\TotalsCalculator, passed to Quittio::create(calculator: …) (or the constructor), calculates render runs, documents, read invoices, edits and cancellations; calculator() returns it. Without one, the EN 16931 calculator applies the calculation policy of calculation.declared_amounts (Declared amounts).
The core uses plain PHP templates (resources/templates/default.php); pass your own TemplateEngine to Quittio::create() for Twig or others.
JSON
InvoiceData::toArray(), toJson(), fromArray() and fromJson() convert the model losslessly. The JSON Schema is shipped in packages/core/resources/schema/invoice.schema.json; with opis/json-schema installed, (new JsonSchemaValidator)->validate($json) checks input before hydration. Details in Arrays and JSON; all fields are listed in the field reference.
Next steps
- Examples: complete invoices for common situations, from small businesses and XRechnung to customers abroad and reminders.
- Building invoices: every builder method.
- Templates and design: logo, colours, texts and your own template.
- Validation and Troubleshooting: what the checks do and how to fix errors.
- Laravel integration: storage, queues, events and mail.