PHP: josemmo/einvoicing
josemmo/einvoicing is an open-source PHP library that writes EN 16931 invoices in UBL, with a Peppol BIS preset. It creates the document; GoRoute delivers it. This page joins the two.
Installโ
composer require josemmo/einvoicing guzzlehttp/guzzle
Guzzle is only used for the HTTP call. Any HTTP client works โ GoRoute is plain HTTPS + JSON.
1. Build the invoiceโ
This is the library's own example, using its Peppol preset. Two details matter for delivery: the buyer's electronic address is the Peppol identifier GoRoute delivers to, and the seller's electronic address should be your own participant.
use Einvoicing\Identifier;
use Einvoicing\Invoice;
use Einvoicing\InvoiceLine;
use Einvoicing\Party;
use Einvoicing\Presets;
use Einvoicing\Writers\UblWriter;
$inv = new Invoice(Presets\Peppol::class);
$inv->setNumber('F-202600012')
->setIssueDate(new DateTime('2026-10-01'))
->setDueDate(new DateTime('2026-10-31'));
$seller = new Party();
$seller->setElectronicAddress(new Identifier('0123456789', '0208')) // your participant
->setName('Seller Name BV')
->setVatNumber('BE0123456789')
->setAddress(['Rue de la Loi 1'])
->setCity('Brussels')
->setCountry('BE');
$inv->setSeller($seller);
$buyer = new Party();
$buyer->setElectronicAddress(new Identifier('9876543210', '0208')) // the receiver
->setName('Buyer Name NV')
->setCountry('BE');
$inv->setBuyer($buyer);
$line = new InvoiceLine();
$line->setName('Consulting')
->setPrice(100)
->setVatRate(21)
->setQuantity(1);
$inv->addLine($line);
$inv->validate(); // the library's EN 16931 checks; throws on failure
$xml = (new UblWriter())->export($inv);
2. Send it through GoRouteโ
GoRoute does not read the receiver from the XML, so pass the buyer's electronic address as
receiver_scheme and receiver_id. Use your invoice number as the Idempotency-Key, so a
retry can never deliver the same invoice twice.
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
$goroute = new Client([
'base_uri' => 'https://app.goroute.ai/peppol-api/', // test: see Environments
'timeout' => 30,
]);
$receiver = $inv->getBuyer()->getElectronicAddress();
try {
$response = $goroute->post('api/v1/documents', [
'headers' => [
'X-API-Key' => getenv('GOROUTE_API_KEY'),
'Idempotency-Key' => $inv->getNumber(),
],
'json' => [
'receiver_scheme' => $receiver->getScheme(),
'receiver_id' => $receiver->getValue(),
'document' => $xml,
],
]);
$result = json_decode((string) $response->getBody(), true); // HTTP 202
echo $result['transaction_id'], ' ', $result['status'], PHP_EOL; // โฆ queued
} catch (ClientException $e) {
$error = json_decode((string) $e->getResponse()->getBody(), true);
// 400 INVALID_REQUEST carries GoRoute's validation messages in $error['message']
error_log(json_encode($error));
}
'json' => โฆ makes Guzzle send Content-Type: application/json and encode the XML as a JSON
string, which is exactly what POST /api/v1/documents expects.
3. Credit notes and other document typesโ
GoRoute sends under the Peppol BIS 3.0 Invoice document type unless you pass another.
If your export produces a UBL CreditNote root element, or a customisation other than
Peppol BIS 3.0, add the matching document_type to the JSON body:
$dom = new DOMDocument();
$dom->loadXML($xml);
$isCreditNote = $dom->documentElement->localName === 'CreditNote';
$documentType = $isCreditNote
? 'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2::CreditNote##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1'
: 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1';
See Send XML you already generate โ Credit notes.
4. Know the outcomeโ
The 202 means GoRoute accepted the document, not that the buyer received it. Subscribe to
transaction.delivered and transaction.failed with a webhook,
or poll GET /api/v1/transactions/{transaction_id}.
Before you go liveโ
- Run the whole flow against a sandbox recipient (
9999:test-โฆ) on the test environment first โ see Test identifiers. - Make sure your own company is registered as a Peppol participant with GoRoute, so buyers can reply and send to you โ see Registration.
- Check
$inv->validate()andPOST /api/v1/documents/validate: they check different things.
Relatedโ
- Send XML you already generate โ every rule and error
- Belgium and the other country guides