Skip to main content

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() and POST /api/v1/documents/validate: they check different things.