Getting started

This tutorial creates, renders and cancels a first invoice. The references for each step are Building invoices (every builder method, VAT, calculation, cancellations, data model), Formats and profiles (outputs and the automatic format choice), Laravel integration (facade, storage, queue, mail) and Configuration (every key of config/quittio.php).

Installation

composer require quittio/quittio          # core, Laravel integration and country pack Germany
php artisan quittio:install               # publishes config/quittio.php

Without Laravel, require quittio/core (and a country pack such as quittio/country-de) and use Quittio\Core\Quittio::create() (see Without Laravel).

Set the seller in config/quittio.php (name, VAT identifier and tax number also via QUITTIO_SELLER_NAME, QUITTIO_SELLER_VAT_ID and QUITTIO_SELLER_TAX_NUMBER; small businesses set seller.small_business), then render the sample invoices to check the template.

Without a VAT identifier (e.g. a small business with a tax number only), EN 16931 requires a seller identifier (BT-29, seller.identifiers as [['value' => '…', 'scheme' => null]]) or the trade register number (BT-30, seller.legal_registration); otherwise rendering fails with BR-CO-26. Seller::apply() uses the first entry of seller.bank_accounts that has an IBAN.

php artisan quittio:preview --scenario=simple

Building an invoice

InvoiceBuilder creates the immutable InvoiceData (EN 16931 model). Totals and the VAT breakdown are calculated when rendering; never pass them in.

use Quittio\Core\Builder\InvoiceBuilder;
use Quittio\Core\Enums\UnitCode;
use Quittio\Country\DE\Tax\VatPreset;
use Quittio\Laravel\Seller;

$invoice = Seller::apply(InvoiceBuilder::make())       // seller and bank account from the config
    ->number('RE-2026-0042')
    ->issueDate('2026-10-03')
    ->deliveryDate('2026-09-30')                         // service date, mandatory in Germany
    ->buyerReference('PO-4711')                          // BT-10, required by XRechnung
    ->buyer(fn ($b) => $b
        ->name('Kunde AG')
        ->vatId('DE987654321')
        ->address('Bahnhofstraße 5', '54321', 'Beispielstadt', 'DE')
        ->electronicAddress('ap@kunde.de', 'EM'))
    ->line(fn ($l) => $l
        ->name('Web development')
        ->description('Customer portal, sprint 3')       // plain text (Markdown with pdf.markdown)
        ->quantity(24, UnitCode::Hour)
        ->netPrice('95.00')
        ->vat(VatPreset::Standard19))
    ->paymentTerms(fn ($t) => $t->dueInDays(14)->cashDiscount(days: 7, percent: '2.00'))
    ->build();

Amounts and quantities are strings (or Brick\Math values) to avoid floating point errors; floats are rejected with an InvalidArgumentException. Rounding follows EN 16931 (half up, two decimals for amounts, VAT per VAT group). References, notes, further parties, allowances and charges, payment means such as direct debit, attachments, gross prices and all other options are described in Building invoices; the German tax presets (VatPreset::Standard19, Reduced7, 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;

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 = Seller::apply(InvoiceBuilder::make())
    ->buyer(fn (PartyBuilder $b) => $b->fromModel($user))
    ->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

use Quittio\Laravel\Facades\Quittio;

$result = Quittio::make($invoice)->render();          // format chosen automatically
$result = Quittio::make($invoice)->format('zugferd')->profile('en16931')->locale('de')->render();

$result->pdf();            // PDF/A-3b with embedded factur-x.xml
$result->xml();            // CII XML
$result->filename();       // Rechnung_RE-2026-0042.pdf
$result->hashes();         // SHA-256 of PDF and XML
$result->validation();     // ValidationResult (warnings included)

$result->save('invoices', '2026/');                    // filesystem disk and path
return $result->download();                             // or ->stream(), or return $result
Mail::to($customer)->send(new YourInvoiceMail($result)); // your Mailable; RenderedInvoice is Attachable
Quittio::make($invoice)->queue()->save('invoices', '2026/');

Without format() (and without defaults.format), the format resolver chooses: in Germany XRechnung for public buyers with a Leitweg-ID, a PDF for consumers and small amount invoices, ZUGFeRD in profile EN 16931 for all other businesses (Automatic choice). Every run calculates, validates (errors throw a ValidationException, see Validation) and renders.

Several formats from one calculation (XRechnung requires the buyer reference BT-10, for public buyers the Leitweg-ID):

$result = Quittio::make($invoice)->formats(['zugferd', 'xrechnung-cii'])->render();
$result->as('xrechnung-cii')->xml();

All formats and profiles, the embedded files and own PDF renderers are described in Formats and profiles; storage, downloads, queues, events and mail in Laravel integration.

Cancellation

Cancel an issued invoice (a new invoice with its own number and a reference to the original):

$cancellation = Quittio::cancellation($original, 'ST-2026-0001', reason: 'Charged twice');
Quittio::make($cancellation)->render();

$original is the issued, calculated invoice, e.g. $result->data(). The sign strategy is credit_note (type code 381, positive amounts, default in Germany) or negative_invoice (type code 380, negative amounts); set it per call (strategy:) or in corrections.cancellation_strategy. Both are permitted in Germany; the document is called “Stornorechnung”, never “Gutschrift”.

lines: cancels part of an invoice (whole lines, quantities or amounts):

$part = Quittio::cancellation($original, 'ST-2026-0002', lines: [
    ['line' => '1', 'quantity' => '1'],
    ['line' => '2', 'amount' => '50.00'],
]);

Pass the earlier partial cancellations as history: (CancellationHistory, e.g. built with withCancellation() and the VAT groups the new part closes), so that the VAT is rounded cumulatively and all parts together reverse exactly the VAT of the original. Strategies, partial cancellations and the history are described in Cancellations and corrections; the Managed layer keeps the history itself (see Managed layer).

Without Laravel

use Quittio\Core\Builder\InvoiceBuilder;
use Quittio\Core\Quittio;
use Quittio\Country\DE\CountryDE;

$quittio = Quittio::create([
    'validation' => ['mode' => 'error', 'levels' => ['L1', 'L2', 'L3', 'L4', 'L7']],
    'pdf' => ['b2c_embed_xml' => false],
], [new CountryDE]);

$invoice = InvoiceBuilder::make()
    ->seller(fn ($s) => $s->name('Muster GmbH')->vatId('DE123456789')->address('Hauptstraße 1', '12345', 'Musterstadt', 'DE'))
    // ... as above, without Seller::apply()
    ->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.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(), cancellation(), document(), read(), readFile(), validateFile() and validatePath(), plus registerCountry(), withRenderer(), withTemplateEngine(), withFormatResolver(), withSerializer(), withValidator(), withoutValidators() and onMandatoryFieldsMissing().

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.