Skip to content

Create an e-invoice in Laravel, step by step

From an empty Laravel app to your first valid e-invoice in about 15 minutes, with real code examples from the documentation.

How-to15 Oct 2026 · about 12 minutes

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, xmlwriter and zlib; intl is 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.

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

Step 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
// 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'],
    ],
],
KeyNeededWhy
name, addressalwaysGerman invoices name the seller with the full address
vat_id or tax_numberalwaysOne of them must be on the invoice; the VAT ID is preferred
contact, bank_accountsrecommendedPrinted on the invoice; mandatory for XRechnung to authorities
small_businesssmall businesses onlytrue 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=simple

Step 3: Build your first invoice

An invoice starts with Quittio::invoice(). You add what belongs to this invoice and end the chain with render():

InvoiceController.php
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 call issueDate(). 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. netPrice is the net price per unit. Without quantity the quantity is 1; without vat the 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:

InvoiceController.php
$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 Attachable

Render 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:

CustomerFormatWhy
Business in GermanyZUGFeRD (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
ConsumerPDFNo e-invoicing mandate
Small invoice up to €250, marked with ->smallAmount()PDFSmall invoices are exempt
Business in another EU countryZUGFeRD (EN 16931)Readable PDF with standard XML
Customer outside the EUPDFNo EU rules

If a customer requires a specific format, name it. For German authorities you need the Leitweg-ID:

InvoiceController.php
$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).

app/Models/User.php
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:

ValidationException
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:

tests/Feature/OrderInvoiceTest.php
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.

InvoiceController.php
$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().

Frequently asked questions

How long does the setup take?

About 15 minutes: install the package, enter your company data, build the first invoice. The official validators for CI are optional on top.

Do I have to write XML myself?

No. You describe the invoice with the builder; Quittio writes CII or UBL, calculates totals and VAT and, for ZUGFeRD, embeds the XML in a PDF/A-3.

Where does the invoice number come from?

You set it with number(). Quittio open source does not assign numbers; gapless numbering is part of Quittio Managed.

Can I change the design?

Yes, on three levels: logo, colours and texts via configuration, override single parts of the template, or use your own template.

Does it work with older Laravel versions?

Quittio supports the current Laravel major version, currently Laravel 13. The core also runs without Laravel.

Ready for your first invoice?

The complete guide with every option is in the documentation.