Installation
Requirements
| What | Needed for |
|---|---|
PHP 8.2 or newer with the dom and libxml extensions | Everything |
The bcmath extension | An IBAN in UblNlBis3Service::addPaymentMeans(), and UblValidator::validateIban() |
The soap extension | ViesService |
| Laravel 11, 12 or 13 | Only the Laravel layer: PeppolService, the PeppolLog model and peppol:cleanup |
Composer checks dom and libxml for you. It does not check bcmath and soap, so check those yourself:
php -m | grep -E "bcmath|soap"
Install in any PHP project
-
Install the package:
composer require darvis/ubl-peppol -
There is no step 2. The builders take everything through their method arguments and read no configuration. Go to Check that it works.
Install in a Laravel application
-
Install the package:
composer require darvis/ubl-peppolLaravel discovers the service provider
Darvis\UblPeppol\UblPeppolServiceProviderby itself. You do not add it tobootstrap/providers.php. -
Only when you want to send invoices: add the address and the login of your access point provider to
.env. An access point provider is the company that delivers your documents to the PEPPOL network. The three values come from that provider, usually from its customer portal or its onboarding mail.PEPPOL_URL=https://provider.example/api/send PEPPOL_USERNAME=your-username PEPPOL_PASSWORD=your-passwordPeppolServicesends the XML as the body of an HTTPPOSTtoPEPPOL_URL, with HTTP Basic authentication and the headerContent-Type: application/xml. Ask your provider whether its API accepts that. A provider that expects JSON or an API key header does not work withPeppolService; you can still build the XML with this package and send it with your own code. -
Optional: publish the config file when you want to change a value in PHP instead of in
.env.php artisan vendor:publish --tag=ubl-peppol-configThis writes
config/ubl-peppol.php:Key Env variable Default What it does log_retention_daysPEPPOL_LOG_RETENTION_DAYS60How many days peppol:cleanupkeeps a log rowpasswordPEPPOL_PASSWORDnone Password for your access point provider urlPEPPOL_URLnone The address PeppolServiceposts the XML tousernamePEPPOL_USERNAMEnone Username for your access point provider -
Optional: create the
peppol_logstable when you want a record of every attempt to send.php artisan vendor:publish --tag=ubl-peppol-migrations php artisan migrateSending works without this table. The result of a send then has
'log_id' => null. -
When your application caches its config, clear the cache after you change
.env:php artisan config:clear
Check that it works
The builder
Create check.php in the root of your project:
<?php
// check.php
require __DIR__.'/vendor/autoload.php';
use Darvis\UblPeppol\UblNlBis3Service;
$ubl = new UblNlBis3Service();
$ubl->createDocument();
$ubl->addInvoiceHeader('TEST-001', '2026-01-15', '2026-02-14');
echo $ubl->generateXml();
Run it:
php check.php
You should see an XML document that starts like this:
<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" ...>
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0</cbc:CustomizationID>
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
<cbc:ID>TEST-001</cbc:ID>
That is only a header, not a valid invoice, but it proves the package is installed and the dom extension works. Delete check.php afterwards.
If you see Class "Darvis\UblPeppol\UblNlBis3Service" not found or Class "DOMDocument" not found, go to Troubleshooting.
The Laravel layer
Run the cleanup command. It needs no credentials and no table:
php artisan peppol:cleanup
Without the peppol_logs table you should see:
There is no peppol_logs table, so there is nothing to clean up. Publish it with: php artisan vendor:publish --tag=ubl-peppol-migrations
With the table you should see Deleting Peppol logs older than 60 days... followed by ✓ 0 log(s) deleted. Either answer proves the service provider is loaded. Command "peppol:cleanup" is not defined. means it is not; see Troubleshooting.
The connection to your provider
After step 2, open php artisan tinker and run:
app(\Darvis\UblPeppol\PeppolService::class)->testConnection();
This sends one GET request to PEPPOL_URL and sends no invoice. A working setup returns 'success' => true. The message is Connection successful for a 2xx answer, or Peppol provider reached (HTTP 405); the credentials were not refused when the address only accepts a POST, which is normal. Every other answer is explained in Sending invoices.
Next step
Your first invoice builds a complete Dutch invoice.