# Admin API Source: https://docs.e-invoice.be/admin-api Create tenants, issue API keys and register tenants on Peppol with the organisation API key of a reseller. The Admin API is available only to resellers that have an [organisation API key](/glossary#organisation-api-key). For invoice operations with a tenant API key, see [Create e-invoices](/guides/creating-invoices). ## Overview A reseller uses the Admin API to manage [tenants](/glossary#tenant). A tenant is a company that a reseller manages. With the Admin API you can: * create, read, update and delete tenants * issue, read, rename and revoke the API keys of a tenant * register a tenant on the Peppol network, or keep the tenant [send-only](/glossary#send-only-tenant) * put a test document in the inbox of a test-mode tenant The admin operations are not in the OpenAPI specification, so the generated API reference has no pages for them. This page is the reference. ## Endpoint overview All paths are on the host `https://api.e-invoice.be`. | Method | Path | Purpose | | - | - | - | | `GET` | `/api/admin/ping` | [Health check](#health-check) (no authentication) | | `GET` | `/api/admin/tenants` | [List tenants](#list-tenants) | | `POST` | `/api/admin/tenants` | [Create a tenant](#create-a-tenant) | | `GET` | `/api/admin/tenants/{tenant_id}` | [Get a tenant](#get-a-tenant) | | `PUT` | `/api/admin/tenants/{tenant_id}` | [Update a tenant](#update-a-tenant) | | `DELETE` | `/api/admin/tenants/{tenant_id}` | [Delete a tenant](#delete-a-tenant) | | `GET` | `/api/admin/tenants/{tenant_id}/api-keys` | [List API keys](#list-api-keys) | | `POST` | `/api/admin/tenants/{tenant_id}/api-keys` | [Create an API key](#create-an-api-key) | | `GET` | `/api/admin/tenants/{tenant_id}/api-keys/latest` | [Get the latest API key](#get-the-latest-api-key) | | `GET` | `/api/admin/tenants/{tenant_id}/api-keys/{api_key_id}` | [Get one API key](#get-one-api-key) | | `PUT` | `/api/admin/tenants/{tenant_id}/api-keys/{api_key_id}` | [Update an API key](#update-an-api-key) | | `DELETE` | `/api/admin/tenants/{tenant_id}/api-keys/{api_key_id}` | [Revoke an API key](#revoke-an-api-key) | | `GET` | `/api/admin/tenants/{tenant_id}/peppol/` | [Check the registration status](#check-the-registration-status) | | `POST` | `/api/admin/tenants/{tenant_id}/peppol/register` | [Register on Peppol](#register-on-peppol) | | `PUT` | `/api/admin/tenants/{tenant_id}/peppol/business-card` | [Update the business card](#update-the-business-card) | | `DELETE` | `/api/admin/tenants/{tenant_id}/peppol/unregister` | [Unregister from Peppol](#unregister-from-peppol) | | `POST` | `/api/admin/tenants/{tenant_id}/simulate-inbound` | [Simulate an inbound document](#simulate-an-inbound-document) | ## Authentication Send the organisation API key as a bearer token. A tenant API key does not give access to the Admin API. ```bash theme={null} curl -X GET "https://api.e-invoice.be/api/admin/tenants" \ -H "Authorization: Bearer $E_INVOICE_ORG_API_KEY" ``` The samples on this page read the organisation API key from the environment variable `E_INVOICE_ORG_API_KEY`. Samples that use a tenant API key read it from `E_INVOICE_API_KEY`. An organisation API key gives access only to the tenants of that organisation. A request for a tenant of a different organisation returns `404 Not Found`. Only approved resellers get an organisation API key. Send an email to [support@e-invoice.be](mailto:support@e-invoice.be) to apply for the [reseller programme](/reseller-programme). ## Health check `GET /api/admin/ping` shows that the Admin API is available. This request needs no authentication. ```bash theme={null} curl -X GET "https://api.e-invoice.be/api/admin/ping" ``` ```json theme={null} { "message": "Pong", "status": "ok", "timestamp": "2026-04-16T10:00:00.123456" } ``` ## Tenant management ### List tenants Get the tenants of your organisation, newest first. Deleted tenants are not in the list. ```bash theme={null} curl -X GET "https://api.e-invoice.be/api/admin/tenants?skip=0&limit=100" \ -H "Authorization: Bearer $E_INVOICE_ORG_API_KEY" ``` Number of tenants to skip. Maximum number of tenants to return. ```json theme={null} { "tenants": [ { "name": "e-invoice-bv", "description": "E-INVOICE BV", "plan": "enterprise", "credit_balance": 999999999, "peppol_ids": ["0208:1018265814"], "ibans": null, "company_number": "1018265814", "company_tax_id": "BE1018265814", "company_name": "E-INVOICE BV", "company_address": null, "company_zip": null, "company_city": null, "company_country": "BE", "company_email": "info@e-invoice.be", "smp_registration": false, "smp_registration_date": null, "bcc_recipient_email": null, "id": "ten-9k2m4p7q3w5x8r1t", "created_at": "2026-04-16T10:00:00.000000Z", "updated_at": "2026-04-16T10:00:00.000000Z", "enrollment_state": "not_required", "status_message": null, "route_override": null } ], "total": 1 } ``` `total` is the number of tenants that are not deleted, not the number of tenants in this page. ### Create a tenant ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/admin/tenants" \ -H "Authorization: Bearer $E_INVOICE_ORG_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "e-invoice-bv", "description": "E-INVOICE BV", "company_number": "1018265814", "company_tax_id": "BE1018265814", "company_name": "E-INVOICE BV", "company_country": "BE", "company_email": "info@e-invoice.be", "peppol_ids": ["0208:1018265814"] }' ``` ```javascript Node.js theme={null} const response = await fetch('https://api.e-invoice.be/api/admin/tenants', { method: 'POST', headers: { Authorization: `Bearer ${process.env.E_INVOICE_ORG_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ name: 'e-invoice-bv', description: 'E-INVOICE BV', company_number: '1018265814', company_tax_id: 'BE1018265814', company_name: 'E-INVOICE BV', company_country: 'BE', company_email: 'info@e-invoice.be', peppol_ids: ['0208:1018265814'], }), }); if (!response.ok) { throw new Error(`${response.status}: ${await response.text()}`); } const tenant = await response.json(); console.log(tenant.id); ``` ```python Python theme={null} import os import requests response = requests.post( "https://api.e-invoice.be/api/admin/tenants", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_ORG_API_KEY']}"}, json={ "name": "e-invoice-bv", "description": "E-INVOICE BV", "company_number": "1018265814", "company_tax_id": "BE1018265814", "company_name": "E-INVOICE BV", "company_country": "BE", "company_email": "info@e-invoice.be", "peppol_ids": ["0208:1018265814"], }, timeout=30, ) response.raise_for_status() tenant = response.json() print(tenant["id"]) ``` ```php PHP theme={null} 'e-invoice-bv', 'description' => 'E-INVOICE BV', 'company_number' => '1018265814', 'company_tax_id' => 'BE1018265814', 'company_name' => 'E-INVOICE BV', 'company_country' => 'BE', 'company_email' => 'info@e-invoice.be', 'peppol_ids' => ['0208:1018265814'], ]); $ch = curl_init('https://api.e-invoice.be/api/admin/tenants'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('E_INVOICE_ORG_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => $body, ]); $result = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 201) { throw new RuntimeException("$status: $result"); } $tenant = json_decode($result, true); echo $tenant['id']; ``` ```csharp C# theme={null} using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_ORG_API_KEY")); var response = await client.PostAsJsonAsync("/api/admin/tenants", new { name = "e-invoice-bv", description = "E-INVOICE BV", company_number = "1018265814", company_tax_id = "BE1018265814", company_name = "E-INVOICE BV", company_country = "BE", company_email = "info@e-invoice.be", peppol_ids = new[] { "0208:1018265814" }, }); response.EnsureSuccessStatusCode(); var tenant = await response.Content.ReadFromJsonAsync(); Console.WriteLine(tenant.GetProperty("id").GetString()); ``` #### Request fields Only `name` is required. All other fields are optional. Name of the tenant. The name must be unique among the tenants of your organisation that are not deleted. A name that is in use gives `409 Conflict`. The API does not apply a format rule. A lower-case name without spaces, for example a slug of the company name, is a good convention. Free text that describes the tenant. Plan of the tenant: `starter`, `pro` or `enterprise`. See [Plans and credits](#plans-and-credits). Credit balance of the tenant. See [Plans and credits](#plans-and-credits). The Peppol ID of the tenant in the form `scheme:identifier`. The field is an array, but the platform uses one Peppol ID for each tenant. The tenant needs it to create and send documents: if the sender Peppol ID of a document is not in this list, `POST /api/documents/` returns `406` and `POST /api/documents/{document_id}/send` returns `409`. See [Why you set the Peppol ID when you create the tenant](#why-you-set-the-peppol-id-when-you-create-the-tenant). IBANs of the tenant. Company registration number from the national business register, **without** a country prefix. For Belgium this is the CBE number, for example `1018265814`. VAT number, **with** the country prefix, for example `BE1018265814`. The API removes spaces and the characters `.`, `-`, `/` and `_`, and changes letters to upper case: `BE 1018.265.814` becomes `BE1018265814`. A different character, for example `(`, gives `422 Unprocessable Entity`. Legal name of the company, with the company type, for example `E-INVOICE BV`. Street and number, for example `Brusselsesteenweg 119`. Postal code, for example `1980`. City, for example `Zemst`. Country of the company as a two-letter ISO 3166-1 code in upper case, for example `BE`. The API uses this value to select the delivery route of the outbound documents of the tenant, so do not write the country name. Contact email address of the tenant. For a test-mode tenant this field is necessary: the API sends each outbound document to this address. Without it, each send of the tenant fails with `400 Bad Request`. See [Contact email address of a test-mode tenant](#contact-email-address-of-a-test-mode-tenant). The API ignores fields that are not in this list. You cannot set `test_mode` (see below). `route_override` is accepted only by [Update a tenant](#update-a-tenant); create ignores it. The response has the status `201 Created`: ```json theme={null} { "name": "e-invoice-bv", "description": "E-INVOICE BV", "plan": "enterprise", "credit_balance": 999999999, "peppol_ids": ["0208:1018265814"], "ibans": null, "company_number": "1018265814", "company_tax_id": "BE1018265814", "company_name": "E-INVOICE BV", "company_address": null, "company_zip": null, "company_city": null, "company_country": "BE", "company_email": "info@e-invoice.be", "smp_registration": false, "smp_registration_date": null, "bcc_recipient_email": "e-invoice-bv+3f9a1c7e@outbox.email.e-invoice.be", "id": "ten-9k2m4p7q3w5x8r1t", "created_at": "2026-04-16T10:00:00.000000Z", "updated_at": "2026-04-16T10:00:00.000000Z", "enrollment_state": "not_required", "status_message": null, "route_override": null } ``` You must find and set the correct Peppol ID for each tenant. The API does not validate `peppol_ids` when you create or update a tenant. Creating a tenant does not register the Peppol ID on the Peppol network; see [Register on Peppol](#register-on-peppol). A tenant that you create with the organisation API key of a test-mode organisation is a test-mode tenant. The request body has no `test_mode` field. You cannot change the test mode of a tenant after you create it. See [Test mode and sandbox companies](/environments). #### Contact email address of a test-mode tenant A test-mode tenant does not send documents on the Peppol network. The API sends each outbound document as an email with the UBL XML to the `company_email` of the tenant. Set `company_email` when you create a test-mode tenant. If `company_email` is not set, `POST /api/documents/{document_id}/send` returns `400 Bad Request`: ```json theme={null} { "detail": "Tenant is in test mode but no company email is set" } ``` The API sends no email and no webhook event. But the API changes the state of the document to `SENT`, so a second send of the same document returns `405`. To correct the problem: Call [Update a tenant](#update-a-tenant) with `{"company_email": "info@e-invoice.be"}`. Use an address that you can read. Create a new document with `POST /api/documents/` and send it. #### Tenant response fields Each tenant operation returns the request fields above and these fields: Identifier of the tenant. Use it as `tenant_id` in the paths. Date and time when the tenant was created. Date and time of the last change. Whether the tenant is registered on the [SMP](/glossary#smp) (Service Metadata Publisher) of e-invoice.be. Date and time of the registration on the SMP. Email address of the tenant at `outbox.email.e-invoice.be`. The create and get operations fill this field. Activation state of the tenant for its destination network: `not_required`, `not_enrolled`, `pending`, `enrolled` or `failed`. A tenant that sends through Peppol has `not_required` and can send immediately. The reason when `enrollment_state` is `failed`. The delivery route that is set with [Update a tenant](#update-a-tenant). `null` when the tenant uses the default delivery route for its `company_country`. #### Company number and tax ID The two fields have different formats. The API does not report an error when you mix them up, so check the format in your system. * `company_number` is the registration number from the national business register, without a country prefix (Belgium: CBE number `1018265814`, Netherlands: KvK number). * `company_tax_id` is the VAT number with the country prefix (`BE1018265814`, `NL123456789B01`). For a Belgian company, the Peppol ID is `0208:` plus the CBE number: | Field | Value | | - | - | | `company_number` | `1018265814` | | `company_tax_id` | `BE1018265814` | | `peppol_ids` | `["0208:1018265814"]` | For the schemes of other countries, see [Look up Peppol participants](/guides/lookup-participants) and [Supported schemes](#supported-schemes). #### Why you set the Peppol ID when you create the tenant The API uses the Peppol ID in `peppol_ids` for three functions: * **Creating and sending.** `POST /api/documents/` accepts a document only when the sender Peppol ID is in the `peppol_ids` of the tenant (if not: `406`). `POST /api/documents/{document_id}/send` does the same check (if not: `409`). For a self-billing document, the Peppol sender is the customer, so the check applies to the `customer_*` fields. The comparison ignores letter case and spaces at the start and the end. * **Receiving.** Documents for the Peppol ID go to the tenant, after the ID is registered. * **Registration.** [Register on Peppol](#register-on-peppol) publishes the ID on the SMP and in the Peppol Directory. Tenant create and tenant update do not write to the SMP or the [SML](/glossary#sml) (Service Metadata Locator). Registration is a separate step. Do it when the tenant must receive documents, or do not do it for a [send-only tenant](#send-only-tenants). ### Get a tenant ```bash theme={null} curl -X GET "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r1t" \ -H "Authorization: Bearer $E_INVOICE_ORG_API_KEY" ``` The response has the same shape as the response of [Create a tenant](#create-a-tenant). This operation also returns a tenant that is deleted. ```json theme={null} { "name": "e-invoice-bv", "description": "E-INVOICE BV", "plan": "enterprise", "credit_balance": 999999999, "peppol_ids": ["0208:1018265814"], "ibans": null, "company_number": "1018265814", "company_tax_id": "BE1018265814", "company_name": "E-INVOICE BV", "company_address": null, "company_zip": null, "company_city": null, "company_country": "BE", "company_email": "info@e-invoice.be", "smp_registration": false, "smp_registration_date": null, "bcc_recipient_email": "e-invoice-bv+3f9a1c7e@outbox.email.e-invoice.be", "id": "ten-9k2m4p7q3w5x8r1t", "created_at": "2026-04-16T10:00:00.000000Z", "updated_at": "2026-04-16T10:00:00.000000Z", "enrollment_state": "not_required", "status_message": null, "route_override": null } ``` ### Update a tenant Send only the fields that you want to change. The API keeps the value of each field that is not in the request body. When you send a field with the value `null`, the API clears the field. Do not send `null` for `name`, `plan` or `credit_balance`. This operation also changes a tenant that is deleted. The request accepts all the [request fields of Create a tenant](#request-fields), with these differences: * `name` is optional. * `route_override` is accepted. Create ignores this field. * `test_mode` is not accepted. The test mode of a tenant does not change. Sets the delivery route of the outbound documents of the tenant, instead of the default route for its `company_country`. The values are `peppol-direct` and `fr-superpdp`. A different value gives `422 Unprocessable Entity`. Send `null` to use the default route again. This example adds the contact email address and an IBAN: ```bash theme={null} curl -X PUT "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r1t" \ -H "Authorization: Bearer $E_INVOICE_ORG_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "description": "E-INVOICE BV - accounting integration", "company_email": "info@e-invoice.be", "ibans": ["BE68539007547034"] }' ``` The response has the status `200 OK` and contains the full tenant: ```json theme={null} { "name": "e-invoice-bv", "description": "E-INVOICE BV - accounting integration", "plan": "enterprise", "credit_balance": 999999999, "peppol_ids": ["0208:1018265814"], "ibans": ["BE68539007547034"], "company_number": "1018265814", "company_tax_id": "BE1018265814", "company_name": "E-INVOICE BV", "company_address": null, "company_zip": null, "company_city": null, "company_country": "BE", "company_email": "info@e-invoice.be", "smp_registration": false, "smp_registration_date": null, "bcc_recipient_email": null, "id": "ten-9k2m4p7q3w5x8r1t", "created_at": "2026-04-16T10:00:00.000000Z", "updated_at": "2026-04-16T11:30:00.000000Z", "enrollment_state": "not_required", "status_message": null, "route_override": null } ``` A new `name` that a different tenant of your organisation already has gives `409 Conflict`. ### Delete a tenant This operation marks the tenant as deleted. It does not remove the data of the tenant. ```bash theme={null} curl -X DELETE "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r1t" \ -H "Authorization: Bearer $E_INVOICE_ORG_API_KEY" ``` The response has the status `204 No Content` and no body: ```http theme={null} HTTP/1.1 204 No Content ``` After the delete, [List tenants](#list-tenants) does not show the tenant, and you can give its name to a new tenant. [Get a tenant](#get-a-tenant) continues to return the tenant with `200 OK`. The response has no field that shows that the tenant is deleted. Delete does not unregister the tenant from Peppol and does not revoke the API keys of the tenant. Before you delete a tenant, call [Unregister from Peppol](#unregister-from-peppol) when the tenant is registered. Then call [Revoke an API key](#revoke-an-api-key) for each API key of the tenant. ### Plans and credits You set the plan and the credit balance of a tenant with the fields `plan` and `credit_balance` of [Create a tenant](#create-a-tenant) and [Update a tenant](#update-a-tenant). These two fields are the only plan and billing functions in the Admin API. | Field | Values | Default | | - | - | - | | `plan` | `starter`, `pro`, `enterprise` | `enterprise` | | `credit_balance` | Integer | `999999999` | ```bash theme={null} curl -X PUT "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r1t" \ -H "Authorization: Bearer $E_INVOICE_ORG_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "plan": "pro", "credit_balance": 5000 }' ``` The response is the full tenant with the new values (shortened here): ```json theme={null} { "name": "e-invoice-bv", "plan": "pro", "credit_balance": 5000, "id": "ten-9k2m4p7q3w5x8r1t" } ``` The tenant sees these values in `GET /api/me/`. `GET /api/stats` with the API key of the tenant returns the usage of the tenant and uses `credit_balance` to calculate `budget_estimation_days`. See [Usage statistics and credits](/guides/usage-statistics). ## API key management ### Create an API key ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r1t/api-keys" \ -H "Authorization: Bearer $E_INVOICE_ORG_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "production-key", "description": "Production API key for E-INVOICE BV" }' ``` ```javascript Node.js theme={null} const tenantId = 'ten-9k2m4p7q3w5x8r1t'; const response = await fetch( `https://api.e-invoice.be/api/admin/tenants/${tenantId}/api-keys`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.E_INVOICE_ORG_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ name: 'production-key', description: 'Production API key for E-INVOICE BV', }), }, ); if (!response.ok) { throw new Error(`${response.status}: ${await response.text()}`); } // The id is the bearer token of the tenant. Store it in a secure location. const apiKey = await response.json(); ``` ```python Python theme={null} import os import requests tenant_id = "ten-9k2m4p7q3w5x8r1t" response = requests.post( f"https://api.e-invoice.be/api/admin/tenants/{tenant_id}/api-keys", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_ORG_API_KEY']}"}, json={ "name": "production-key", "description": "Production API key for E-INVOICE BV", }, timeout=30, ) response.raise_for_status() # The id is the bearer token of the tenant. Store it in a secure location. api_key = response.json() ``` ```php PHP theme={null} true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('E_INVOICE_ORG_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'name' => 'production-key', 'description' => 'Production API key for E-INVOICE BV', ]), ]); $result = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 201) { throw new RuntimeException("$status: $result"); } // The id is the bearer token of the tenant. Store it in a secure location. $apiKey = json_decode($result, true); ``` ```csharp C# theme={null} using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; var tenantId = "ten-9k2m4p7q3w5x8r1t"; using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_ORG_API_KEY")); var response = await client.PostAsJsonAsync($"/api/admin/tenants/{tenantId}/api-keys", new { name = "production-key", description = "Production API key for E-INVOICE BV", }); response.EnsureSuccessStatusCode(); // The id is the bearer token of the tenant. Store it in a secure location. var apiKey = await response.Content.ReadFromJsonAsync(); ``` Name of the API key. The name must be unique among the active API keys of the tenant; a name that is in use gives `409 Conflict`. Free text, for example the purpose of the key. The response has the status `201 Created`: ```json theme={null} { "id": "api-3h8f5j2k9l4m7n6p1q5r8s2t4v6w9x3y", "tenant_id": "ten-9k2m4p7q3w5x8r1t", "name": "production-key", "description": "Production API key for E-INVOICE BV", "created_at": "2026-04-16T10:00:00.000000Z", "updated_at": "2026-04-16T10:00:00.000000Z", "is_deleted": false } ``` The `id` field is the API key. There is no separate `key` field. Your customer sends the value of `id` as the bearer token: ``` Authorization: Bearer api-3h8f5j2k9l4m7n6p1q5r8s2t4v6w9x3y ``` Keep the `id` secret. Store it encrypted and give it to your customer through a secure channel. Do not write it to logs, to version control or to client-side code. ### List API keys Get the active API keys of a tenant, newest first. Revoked keys are not in the list. ```bash theme={null} curl -X GET "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r1t/api-keys?skip=0&limit=100" \ -H "Authorization: Bearer $E_INVOICE_ORG_API_KEY" ``` Number of API keys to skip. Maximum number of API keys to return. ```json theme={null} { "api_keys": [ { "id": "api-3h8f5j2k9l4m7n6p1q5r8s2t4v6w9x3y", "tenant_id": "ten-9k2m4p7q3w5x8r1t", "name": "production-key", "description": "Production API key for E-INVOICE BV", "created_at": "2026-04-16T10:00:00.000000Z", "updated_at": "2026-04-16T10:00:00.000000Z", "is_deleted": false } ], "total": 1 } ``` The `id` is the bearer token, so the operations that read API keys return live credentials. Limit who can call them, do not write the response body to logs, and do not show it to end users. ### Get the latest API key Get the active API key of a tenant that was created last. ```bash theme={null} curl -X GET "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r1t/api-keys/latest" \ -H "Authorization: Bearer $E_INVOICE_ORG_API_KEY" ``` ```json theme={null} { "id": "api-3h8f5j2k9l4m7n6p1q5r8s2t4v6w9x3y", "tenant_id": "ten-9k2m4p7q3w5x8r1t", "name": "production-key", "description": "Production API key for E-INVOICE BV", "created_at": "2026-04-16T10:00:00.000000Z", "updated_at": "2026-04-16T10:00:00.000000Z", "is_deleted": false } ``` If the tenant has no active API key, the response is `404 Not Found`: ```json theme={null} { "detail": "No active API keys found for tenant ten-9k2m4p7q3w5x8r1t" } ``` ### Get one API key Get one API key by its ID. Use this operation to see if a key is active or revoked: unlike the list operation, it also returns a revoked key, with `is_deleted: true`. ```bash theme={null} curl -X GET "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r1t/api-keys/api-3h8f5j2k9l4m7n6p1q5r8s2t4v6w9x3y" \ -H "Authorization: Bearer $E_INVOICE_ORG_API_KEY" ``` Response for an active key: ```json theme={null} { "id": "api-3h8f5j2k9l4m7n6p1q5r8s2t4v6w9x3y", "tenant_id": "ten-9k2m4p7q3w5x8r1t", "name": "production-key", "description": "Production API key for E-INVOICE BV", "created_at": "2026-04-16T10:00:00.000000Z", "updated_at": "2026-04-16T10:00:00.000000Z", "is_deleted": false } ``` Response for a revoked key: ```json theme={null} { "id": "api-3h8f5j2k9l4m7n6p1q5r8s2t4v6w9x3y", "tenant_id": "ten-9k2m4p7q3w5x8r1t", "name": "production-key", "description": "Production API key for E-INVOICE BV", "created_at": "2026-04-16T10:00:00.000000Z", "updated_at": "2026-04-16T10:00:00.000000Z", "is_deleted": true } ``` If the key does not exist for this tenant, the response is `404 Not Found`: ```json theme={null} { "detail": "API key with ID api-3h8f5j2k9l4m7n6p1q5r8s2t4v6w9x3y not found for tenant ten-9k2m4p7q3w5x8r1t" } ``` ### Update an API key Change the name or the description of an API key. The `id` (the bearer token) does not change. ```bash theme={null} curl -X PUT "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r1t/api-keys/api-3h8f5j2k9l4m7n6p1q5r8s2t4v6w9x3y" \ -H "Authorization: Bearer $E_INVOICE_ORG_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "production-key-v2", "description": "Production API key, second version" }' ``` New name. A name that a different active key of the tenant has gives `409 Conflict`. New description. ```json theme={null} { "id": "api-3h8f5j2k9l4m7n6p1q5r8s2t4v6w9x3y", "tenant_id": "ten-9k2m4p7q3w5x8r1t", "name": "production-key-v2", "description": "Production API key, second version", "created_at": "2026-04-16T10:00:00.000000Z", "updated_at": "2026-04-16T11:30:00.000000Z", "is_deleted": false } ``` ### Revoke an API key ```bash theme={null} curl -X DELETE "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r1t/api-keys/api-3h8f5j2k9l4m7n6p1q5r8s2t4v6w9x3y" \ -H "Authorization: Bearer $E_INVOICE_ORG_API_KEY" ``` The response has the status `204 No Content` and no body: ```http theme={null} HTTP/1.1 204 No Content ``` The key stops working immediately: a request with it returns `401`. After this call, [Get one API key](#get-one-api-key) returns the key with `is_deleted: true`. ### Rotate an API key Call [Create an API key](#create-an-api-key) with a new name. Keep the returned `id`. Send the key through a secure channel, or change the configuration that you keep for the customer. When the customer uses the new key, call [Revoke an API key](#revoke-an-api-key) for the old key. The two keys are valid together until this step, so there is no downtime. ## Peppol registration [Peppol registration](/glossary#peppol-registration) publishes the Peppol ID of a tenant on the SMP of e-invoice.be, so that the tenant can **receive** documents through e-invoice.be. A tenant that only sends documents does not need a registration; see [Send-only tenants](#send-only-tenants). The Peppol operations are not available with the organisation API key of a test-mode organisation. The four operations in this section then return `403 Forbidden`: ```json theme={null} { "detail": "Peppol operations are not available for organizations in test mode. Please contact support to enable production mode." } ``` ### Check the registration status ```bash theme={null} curl -X GET "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r1t/peppol/" \ -H "Authorization: Bearer $E_INVOICE_ORG_API_KEY" ``` Set to `true` to do a live check on the network. See [Verify a registration](#verify-a-registration). ```json theme={null} { "peppol_id": "0208:1018265814", "state": "e-invoice", "smp": { "service_group": { "participant_identifier": "1018265814", "participant_scheme": "0208", "service_metadata_references": [] }, "business_card": { "participant_identifier": "1018265814", "participant_scheme": "0208", "business_entity": { "name": "E-INVOICE BV", "country_code": "BE", "vat_identifier": "BE1018265814", "website_uri": "https://e-invoice.be/company/1018265814", "contact": { "name": null, "email": "support@e-invoice.be" } } }, "document_types": [ { "document_type_code": "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", "process_identifier": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0", "endpoints": [] } ] }, "last_checked_at": "2026-04-16T10:00:00.000000", "verified": false, "publications": [ { "peppol_id": "0208:1018265814", "state": "published", "latest_event": "directory_entry_created", "source": "phoss", "detail": null, "observed_at": "2026-04-16T09:55:00.000000Z" } ] } ``` `e-invoice`: the Peppol ID is on the SMP of e-invoice.be. `other`: the Peppol ID is found on the network, but not on the SMP of e-invoice.be. `not_registered`: the Peppol ID is not found. A tenant with `other` or `not_registered` can send documents. The service group, the business card and the document types on the SMP of e-invoice.be. The three fields are `null` when the Peppol ID is not on this SMP. `true` when this request did a live check (`verify=true`). One entry for each Peppol ID of the tenant. `state` is `not_published`, `pending` or `published` and comes from the last recorded event for the ID. `latest_event`, `source`, `detail` and `observed_at` describe that event; they are `null` when the ID has no events. If the tenant has no Peppol ID, the response is `400 Bad Request`: ```json theme={null} { "detail": "Tenant does not have a Peppol ID" } ``` ### Verify a registration Without `verify`, the `publications` field comes from the events that the API recorded before (for example at registration). With `verify=true`, the API first does a live DNS lookup for each Peppol ID of the tenant, records the result as a new event, and then returns the status. ```bash theme={null} curl -X GET "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r1t/peppol/?verify=true" \ -H "Authorization: Bearer $E_INVOICE_ORG_API_KEY" ``` The response has the same shape as [Check the registration status](#check-the-registration-status). `verified` is `true`, and each publication shows the result of the live check (the `smp` object is shortened here): ```json theme={null} { "peppol_id": "0208:1018265814", "state": "e-invoice", "smp": { "service_group": { "participant_identifier": "1018265814", "participant_scheme": "0208", "service_metadata_references": [] }, "business_card": null, "document_types": [] }, "last_checked_at": "2026-04-16T10:00:00.000000", "verified": true, "publications": [ { "peppol_id": "0208:1018265814", "state": "published", "latest_event": "publication_verified", "source": "dns", "detail": null, "observed_at": "2026-04-16T10:00:00.000000Z" } ] } ``` When the live check does not find the Peppol ID on the SMP of e-invoice.be, the publication has `state: "not_published"` and `latest_event: "not_found_on_smp"`. A request with `verify=true` adds events to the registration history of the tenant. Use it after a registration or when you examine a problem, not for frequent polling. ### Register on Peppol ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r1t/peppol/register" \ -H "Authorization: Bearer $E_INVOICE_ORG_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "peppol_id": "0208:1018265814" }' ``` ```javascript Node.js theme={null} const tenantId = 'ten-9k2m4p7q3w5x8r1t'; const response = await fetch( `https://api.e-invoice.be/api/admin/tenants/${tenantId}/peppol/register`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.E_INVOICE_ORG_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ peppol_id: '0208:1018265814' }), }, ); if (!response.ok) { throw new Error(`${response.status}: ${await response.text()}`); } const registration = await response.json(); console.log(registration.success, registration.registration.method); ``` ```python Python theme={null} import os import requests tenant_id = "ten-9k2m4p7q3w5x8r1t" response = requests.post( f"https://api.e-invoice.be/api/admin/tenants/{tenant_id}/peppol/register", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_ORG_API_KEY']}"}, json={"peppol_id": "0208:1018265814"}, timeout=60, ) response.raise_for_status() registration = response.json() print(registration["success"], registration["registration"]["method"]) ``` ```php PHP theme={null} true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('E_INVOICE_ORG_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode(['peppol_id' => '0208:1018265814']), ]); $result = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 200) { throw new RuntimeException("$status: $result"); } $registration = json_decode($result, true); echo $registration['registration']['method']; ``` ```csharp C# theme={null} using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; var tenantId = "ten-9k2m4p7q3w5x8r1t"; using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_ORG_API_KEY")); var response = await client.PostAsJsonAsync( $"/api/admin/tenants/{tenantId}/peppol/register", new { peppol_id = "0208:1018265814" }); response.EnsureSuccessStatusCode(); var registration = await response.Content.ReadFromJsonAsync(); Console.WriteLine(registration.GetProperty("registration").GetProperty("method").GetString()); ``` Peppol ID to register, in the form `scheme:identifier`. The scheme must be one of the [supported schemes](#supported-schemes). The API changes the ID to lower case and removes spaces at the start and the end. Set to `true` to do all checks and make no change on the SMP. See [Dry run](#dry-run). The API does these steps: 1. It checks the scheme and the format of the identifier. 2. It looks for the Peppol ID on the network. An ID that is active at a different access point gives `409 Conflict`. 3. It creates the service group on the SMP. 4. It adds the six default document types: invoice, credit note, self-billing invoice, self-billing credit note, invoice response and message level response. 5. It adds a business card. For a Belgian Peppol ID, the company data comes from the KBO (Crossroads Bank for Enterprises). The response has the status `200 OK`: ```json theme={null} { "success": true, "peppol_id": "0208:1018265814", "company_data": { "company_number": "1018265814", "company_name": "E-INVOICE BV", "company_country": "BE", "company_address": "Brusselsesteenweg 119", "company_zip": "1980", "company_city": "Zemst" }, "registration": { "registered": true, "method": "direct_registration", "migration_result": null, "message": null }, "business_card": { "success": true, "business_card_data": { "name": "E-INVOICE BV", "country_code": "BE", "geographical_information": "Zemst, BE", "vat_number": "BE1018265814", "website_url": "https://e-invoice.be/company/1018265814", "contact_email": "support@e-invoice.be" }, "message": null, "error": null, "error_type": null, "dry_run": null }, "document_endpoints": { "success": true, "message": "Document endpoints added successfully", "error": null, "error_type": null, "dry_run": null } } ``` For a Belgian Peppol ID, `company_data` also contains the KBO fields `status`, `juridical_situation`, `type_of_enterprise`, `juridical_form`, `start_date` and `address_type`. For other countries, `company_data` contains only `company_number`; the other fields are `null`. The operation is idempotent. If the Peppol ID is already on the SMP of e-invoice.be, `registration.method` is `already_exists` and the API makes no new service group. | Status | Cause | | - | - | | `400 Bad Request` | The Peppol ID has an incorrect format, the scheme is not supported, or the identifier does not obey the rules of the scheme. | | `403 Forbidden` | The organisation is in test mode. | | `409 Conflict` | The Peppol ID is active at a different access point. | | `502 Bad Gateway` | An external service (the SMP) failed. Try again later. | ### Dry run With `"dry_run": true`, the API does the checks of a registration (scheme, identifier format, lookup on the network, KBO lookup for Belgium) and makes no change on the SMP. Use it to find an incorrect or already registered Peppol ID before the real registration. ```bash theme={null} curl -X POST "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r1t/peppol/register" \ -H "Authorization: Bearer $E_INVOICE_ORG_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "peppol_id": "0208:1018265814", "dry_run": true }' ``` ```json theme={null} { "success": true, "peppol_id": "0208:1018265814", "company_data": { "company_number": "1018265814", "company_name": "E-INVOICE BV", "company_country": "BE", "company_address": "Brusselsesteenweg 119", "company_zip": "1980", "company_city": "Zemst" }, "registration": { "registered": true, "method": "direct_registration", "migration_result": null, "message": null }, "business_card": { "success": true, "business_card_data": null, "message": "DRY RUN: Would add business card", "error": null, "error_type": null, "dry_run": true }, "document_endpoints": { "success": true, "message": "DRY RUN: Would add document endpoints", "error": null, "error_type": null, "dry_run": true } } ``` In a dry run, `registration.registered` is `true` although nothing is registered. Read `business_card.dry_run` and `document_endpoints.dry_run` to identify a dry-run response. A dry run does not add events to `publications`. For a test-mode tenant, the API always does a dry run, whatever the value of `dry_run`. [Update the business card](#update-the-business-card) and [Unregister from Peppol](#unregister-from-peppol) return `{"success": true}` for a test-mode tenant and make no change on the SMP. ### Supported schemes Registration accepts a Peppol ID only when its scheme is in this list. The list is smaller than the list of schemes that you can send to. | Country | Scheme | Identifier | | - | - | - | | Belgium (BE) | `0208` | Enterprise number (CBE), 10 digits with a valid check digit | | Netherlands (NL) | `0106`, `0190`, `9944` | KvK number, OIN (government), VAT number | | Finland (FI) | `0216` | OVT code | | Norway (NO) | `0192` | Organisasjonsnummer | | Sweden (SE) | `0007` | Organisationsnummer | | Denmark (DK) | `0184`, `0198` | CVR number, SE number | | Iceland (IS) | `0196` | Kennitala | | Spain (ES) | `9920` | VAT number | | Ireland (IE) | `9935` | VAT number | | Greece (GR) | `9933` | VAT number | | Poland (PL) | `9945` | VAT number | | Portugal (PT) | `9946` | VAT number | | Bulgaria (BG) | `9926` | VAT number | | Cyprus (CY) | `9928` | VAT number | | Czech Republic (CZ) | `9929` | VAT number | | Croatia (HR) | `9934` | VAT number | | Hungary (HU) | `9910` | VAT number | | Malta (MT) | `9943` | VAT number | | Romania (RO) | `9947` | VAT number | | Slovenia (SI) | `9949` | VAT number | | Slovakia (SK) | `9950` | VAT number | | Liechtenstein (LI) | `9936` | VAT number | | Luxembourg (LU) | `9938`, `0240` | VAT number or matricule, matricule (register of legal persons) | | All countries | `0088` | Global Location Number (GLN) | Schemes of other countries (for example Germany, Austria, Italy, Estonia, Latvia and Lithuania), the Belgian VAT scheme `9925` and the Finnish scheme `0037` are not accepted. The French schemes (`0002`, `0009`, `0225`, `9957`) are accepted only for an organisation that has the French capability, and only after the enrolment of the tenant; send an email to [support@e-invoice.be](mailto:support@e-invoice.be). A scheme that is not supported gives `400 Bad Request`: ```json theme={null} { "detail": "This Peppol ID is currently not supported and cannot be registered. Contact support@e-invoice.be" } ``` The company data lookup (KBO) is for Belgium only. For a Peppol ID of a different country, the API has no company name, so the business card gets only the country of the scheme. The register request has no field for the company name. Set the name with [Update the business card](#update-the-business-card) after the registration. The scheme codes are from the Peppol participant identifier scheme code list, version 9.7. This is the version that the API uses. For the table of the most frequent schemes that you can send to, see [Look up Peppol participants](/guides/lookup-participants). ### Update the business card Change the company name on the business card of a registered Peppol ID. The name is the only field that you can change. ```bash theme={null} curl -X PUT "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r1t/peppol/business-card" \ -H "Authorization: Bearer $E_INVOICE_ORG_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "peppol_id": "0208:1018265814", "name": "E-INVOICE BV" }' ``` Peppol ID of the business card. It must be in the `peppol_ids` of the tenant; if not, the response is `403 Forbidden`. New company name. Include the company type, for example `BV` or `NV`. ```json theme={null} { "success": true } ``` If the Peppol ID has no business card on the SMP, the response is `400 Bad Request`: ```json theme={null} { "detail": "No existing business card found for this Peppol ID" } ``` The response is also `400 Bad Request` in these conditions: * The tenant has no Peppol ID. * The scheme is not a [supported scheme](#supported-schemes). * The identifier does not obey the rules of the scheme. A failure of the SMP gives `502 Bad Gateway`. ### Unregister from Peppol Remove the document types, the business card and the service group of a Peppol ID from the SMP of e-invoice.be. The tenant can then not receive documents through e-invoice.be. It can continue to send documents as a [send-only tenant](#send-only-tenants). ```bash theme={null} curl -X DELETE "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r1t/peppol/unregister" \ -H "Authorization: Bearer $E_INVOICE_ORG_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "peppol_id": "0208:1018265814" }' ``` Peppol ID to unregister. It must be in the `peppol_ids` of the tenant; if not, the response is `403 Forbidden`. ```json theme={null} { "success": true, "warnings": null } ``` The operation is idempotent. If the Peppol ID was already removed, the response is: ```json theme={null} { "success": true, "warnings": ["Participant was already unregistered"] } ``` The scheme must be a [supported scheme](#supported-schemes). The identifier must obey the rules of the scheme. If not, the response is `400 Bad Request`. A failure of the SMP gives `502 Bad Gateway`. Unregister changes no field of the tenant. The Peppol ID stays in `peppol_ids`, so the tenant can continue to send documents with it. In [Check the registration status](#check-the-registration-status), the publication of the ID then has `state: "not_published"` and `latest_event: "manually_removed"`. ### Send-only tenants A tenant can send documents through Peppol without a registration on the SMP and the SML. Use a send-only tenant when the company receives its documents through a different access point, or does not want to receive documents at this time. There is no `send_only` field. A tenant is send-only because [Register on Peppol](#register-on-peppol) was not called for it. #### Create a send-only tenant Call [Create a tenant](#create-a-tenant) and put the Peppol ID in `peppol_ids`. Call [Create an API key](#create-an-api-key) for the tenant. The returned `id` is the bearer token of the tenant. Create a document with `POST /api/documents/` and send it. See [Create e-invoices](/guides/creating-invoices). Do not call the register operation. ```bash theme={null} curl -X POST "https://api.e-invoice.be/api/documents/doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d/send?sender_peppol_scheme=0208&sender_peppol_id=1018265814&receiver_peppol_scheme=0208&receiver_peppol_id=0848934496" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` The response has the status `200 OK` and contains the document. This example shows the first fields: ```json theme={null} { "id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "state": "TRANSIT", "direction": "OUTBOUND" } ``` The API does not validate `peppol_ids` when you create or update a tenant. It accepts an incorrect Peppol ID, for example a `0208` number with 9 digits or with an incorrect check digit. The register operation is the only step that validates the ID, and you do not call it for a send-only tenant. Validate the Peppol ID in your system before you create the tenant. A French send-only tenant must be enrolled, but it can have zero publications. Send an email to [support@e-invoice.be](mailto:support@e-invoice.be) for the enrolment procedure. #### What a send-only tenant can do | Capability | Send-only tenant | | - | - | | Send invoices and credit notes | Possible. The API only checks that the sender Peppol ID is in the `peppol_ids` of the tenant. | | Send [self-billing](/guides/self-billing) documents | Possible. The tenant is the customer and the Peppol sender. The check applies to the Peppol ID from the `customer_*` fields. | | Receive documents through e-invoice.be | Not possible. The Peppol ID is not published, so senders cannot find it, or they find the registration at a different access point. | If the sender Peppol ID (for self-billing documents: the Peppol ID from the `customer_*` fields) is not in the `peppol_ids` of the tenant, the send request fails with `409 Conflict`: ```json theme={null} { "detail": "Derived sender '0208:0848934496' is not in tenant peppol_ids" } ``` #### Status of a send-only tenant [Check the registration status](#check-the-registration-status) shows the Peppol ID of a send-only tenant as not published: ```json theme={null} { "peppol_id": "0208:1018265814", "state": "not_registered", "smp": { "service_group": null, "business_card": null, "document_types": null }, "last_checked_at": "2026-04-16T10:00:00.000000", "verified": false, "publications": [ { "peppol_id": "0208:1018265814", "state": "not_published", "latest_event": null, "source": null, "detail": null, "observed_at": null } ] } ``` `state` is `other` when the Peppol ID is registered at a different access point. #### Become a receiver later Call [Register on Peppol](#register-on-peppol) when the tenant is ready to receive documents. No tenant change is necessary. ## Testing Use the organisation API key of a test-mode organisation to test your integration. Send an email to [support@e-invoice.be](mailto:support@e-invoice.be) to get this key. You cannot set test mode on a tenant yourself. Each tenant that you create with this key is a test-mode tenant: its sends go to email, and no Peppol traffic occurs. See [Test mode and sandbox companies](/environments). Set `company_email` on each test-mode tenant. Without it, each send of the tenant fails with `400 Bad Request`. See [Contact email address of a test-mode tenant](#contact-email-address-of-a-test-mode-tenant). The Peppol registration operations return `403 Forbidden` for a test-mode organisation. See [Peppol registration](#peppol-registration). ### Simulate an inbound document Put a UBL document in the inbox of a test-mode tenant, as if it was received through Peppol. The API creates the document in the `RECEIVED` state and sends the `document.received` [webhook](/essentials/webhooks) event to the webhooks of the tenant. ```bash theme={null} curl -X POST "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r1t/simulate-inbound" \ -H "Authorization: Bearer $E_INVOICE_ORG_API_KEY" \ -F "ubl_file=@invoice.xml" ``` UBL invoice or credit note as an XML file. The request is `multipart/form-data`; a raw XML request body is not accepted. The maximum size is 25 MB. The response has the status `201 Created`: ```json theme={null} { "document_id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "state": "RECEIVED" } ``` | Status | Cause | | - | - | | `400 Bad Request` | The file is empty, or the tenant is not in test mode. | | `413 Request Entity Too Large` | The file is larger than 25 MB. | The API reads the sender and receiver Peppol IDs, the document type and the line items from the UBL. It does not change the receiver Peppol ID to the Peppol ID of the tenant. The API does not validate the file. It also returns `201 Created` and creates a `RECEIVED` document for an XML file that is not an invoice or a credit note. It does the same for a file that is not XML. Read the result with `GET /api/documents/{document_id}` and the tenant API key. For the procedure in the app and for the tenant-side view, see [Testing received documents](/environments#testing-received-documents) and [Receive documents](/guides/receiving-documents). ## Onboarding example The script below creates a tenant, creates an API key and registers the tenant on Peppol. Remove the registration step for a [send-only tenant](#send-only-tenants). ```javascript Node.js theme={null} const BASE_URL = 'https://api.e-invoice.be'; async function admin(method, path, body) { const response = await fetch(`${BASE_URL}${path}`, { method, headers: { Authorization: `Bearer ${process.env.E_INVOICE_ORG_API_KEY}`, 'Content-Type': 'application/json', }, body: body ? JSON.stringify(body) : undefined, }); if (!response.ok) { throw new Error(`${method} ${path}: ${response.status} ${await response.text()}`); } return response.status === 204 ? null : response.json(); } async function onboardCustomer(customer) { // 1. Create the tenant with its Peppol ID. const tenant = await admin('POST', '/api/admin/tenants', { name: customer.slug, description: customer.companyName, company_name: customer.companyName, company_number: customer.companyNumber, company_tax_id: customer.vatNumber, company_country: customer.country, company_email: customer.email, peppol_ids: [customer.peppolId], }); // 2. Create an API key. The id is the bearer token of the tenant. const apiKey = await admin('POST', `/api/admin/tenants/${tenant.id}/api-keys`, { name: 'production-key', }); // 3. Register on Peppol, so that the tenant can receive documents. const registration = await admin( 'POST', `/api/admin/tenants/${tenant.id}/peppol/register`, { peppol_id: customer.peppolId }, ); return { tenantId: tenant.id, apiKey: apiKey.id, registered: registration.success, }; } const result = await onboardCustomer({ slug: 'e-invoice-bv', companyName: 'E-INVOICE BV', companyNumber: '1018265814', vatNumber: 'BE1018265814', country: 'BE', email: 'info@e-invoice.be', peppolId: '0208:1018265814', }); console.log('Tenant:', result.tenantId, 'registered:', result.registered); ``` ```python Python theme={null} import os import requests BASE_URL = "https://api.e-invoice.be" HEADERS = {"Authorization": f"Bearer {os.environ['E_INVOICE_ORG_API_KEY']}"} def admin(method, path, body=None): response = requests.request( method, f"{BASE_URL}{path}", headers=HEADERS, json=body, timeout=60 ) response.raise_for_status() return None if response.status_code == 204 else response.json() def onboard_customer(customer): # 1. Create the tenant with its Peppol ID. tenant = admin( "POST", "/api/admin/tenants", { "name": customer["slug"], "description": customer["company_name"], "company_name": customer["company_name"], "company_number": customer["company_number"], "company_tax_id": customer["vat_number"], "company_country": customer["country"], "company_email": customer["email"], "peppol_ids": [customer["peppol_id"]], }, ) # 2. Create an API key. The id is the bearer token of the tenant. api_key = admin( "POST", f"/api/admin/tenants/{tenant['id']}/api-keys", {"name": "production-key"}, ) # 3. Register on Peppol, so that the tenant can receive documents. registration = admin( "POST", f"/api/admin/tenants/{tenant['id']}/peppol/register", {"peppol_id": customer["peppol_id"]}, ) return { "tenant_id": tenant["id"], "api_key": api_key["id"], "registered": registration["success"], } result = onboard_customer( { "slug": "e-invoice-bv", "company_name": "E-INVOICE BV", "company_number": "1018265814", "vat_number": "BE1018265814", "country": "BE", "email": "info@e-invoice.be", "peppol_id": "0208:1018265814", } ) print("Tenant:", result["tenant_id"], "registered:", result["registered"]) ``` ## Errors The Admin API returns errors as JSON with a `detail` field. For the general error model, see [Errors and troubleshooting](/guides/errors). | Status | `detail` (example) | Cause and solution | | - | - | - | | `401 Unauthorized` | `Admin authentication required` | The request has no `Authorization` header. | | `400 Bad Request` | `Tenant does not have a Peppol ID` | The Peppol operations need a Peppol ID in the `peppol_ids` of the tenant. Other `400` causes: an incorrect Peppol ID or a scheme that is not supported (see [Supported schemes](#supported-schemes)), an empty file or a tenant that is not in test mode in [Simulate an inbound document](#simulate-an-inbound-document). | | `401 Unauthorized` | `Invalid admin authentication` | The key is not an organisation API key. Make sure that you do not send a tenant API key. | | `403 Forbidden` | `Peppol operations are not available for organizations in test mode. Please contact support to enable production mode.` | The organisation is in test mode. The Peppol operations are not available. | | `403 Forbidden` | `This Peppol ID does not belong to this tenant.` | The Peppol ID in the request body is not in the `peppol_ids` of the tenant. | | `404 Not Found` | `Tenant with ID ten-... not found` | The tenant does not exist or is a tenant of a different organisation. For the list, create and latest API key operations, the Peppol operations and Simulate inbound: also when the tenant is deleted. | | `409 Conflict` | `Tenant with name 'e-invoice-bv' already exists` | Use a different tenant name, or find the existing tenant with [List tenants](#list-tenants). | | `409 Conflict` | `API key with name 'production-key' already exists for this tenant` | An active API key of the tenant has this name. Use a different name. | | `409 Conflict` | Text that describes the conflict | [Register on Peppol](#register-on-peppol): the Peppol ID is active at a different access point. | | `413 Request Entity Too Large` | `Uploaded UBL payload exceeds 25 MB limit` | [Simulate an inbound document](#simulate-an-inbound-document): the file is larger than 25 MB. | | `422 Unprocessable Entity` | Array of validation errors | A mandatory field is missing, a field has an incorrect type, or a field value is not permitted (for example `company_tax_id` or `route_override`). | | `502 Bad Gateway` | `External service failed: ...` or `SMP service failed: ...` | The SMP failed during a Peppol operation. Try again later. | Example of a `422` response: ```json theme={null} { "detail": [ { "type": "missing", "loc": ["body", "name"], "msg": "Field required", "input": {} } ] } ``` ## Best practices * The `id` of an API key is the bearer token. Keep it secret from the moment that you receive it. * Do not write organisation API keys or tenant API keys to logs. * Store tenant API keys encrypted. * Give keys to customers only through a secure channel. * Rotate keys at regular intervals. See [Rotate an API key](#rotate-an-api-key). Use tenant names that you can calculate from your own data, for example `cust-` or a slug of the company name. The API refuses a second active tenant with the same name (`409 Conflict`), which helps you to prevent duplicates. If [Create a tenant](#create-a-tenant) returns `409 Conflict`, the tenant exists. Get it from [List tenants](#list-tenants) and continue with the next step. [Register on Peppol](#register-on-peppol) and [Unregister from Peppol](#unregister-from-peppol) are idempotent, so you can repeat them after a network error. Record each admin operation in your system: the operation, the tenant ID, the user and the time. Do not record API keys. Validate the Peppol ID in your system, then call [Register on Peppol](#register-on-peppol) with `"dry_run": true`. Do the real registration only when the dry run is successful. ## Rate limits The Admin API endpoints have no rate limit at this time. The document write and validation endpoints that a tenant calls with its own API key have a rate limit for each API key. See [Errors and troubleshooting](/guides/errors). ## Support For access to the Admin API or for technical questions, send an email to [support@e-invoice.be](mailto:support@e-invoice.be). ## Next Steps Learn how to become a reseller and get an organisation API key. Send documents with the API key of a tenant. Test mode, sandbox companies and simulated inbound documents. Read the usage of a tenant with its API key. # API reference Source: https://docs.e-invoice.be/api-reference Find the host, the authentication, all endpoints, the response formats and the limits of the e-invoice.be API. ## Overview With the e-invoice.be API you create, send and receive Peppol invoices and credit notes. The API converts each document to UBL BIS Billing 3.0 and transmits it on the Peppol network. This page is an overview. The endpoint pages in this tab give the parameters and the schemas of each operation. For the changes to the API and to this documentation, see the [Changelog](/changelog). ## Base URL There is one API host: ``` https://api.e-invoice.be ``` Develop and test with a sandbox company. A sandbox company runs in test mode: the API sends each document as UBL XML to the contact email address of the company, and nothing goes to the Peppol network. The API host and the endpoints are the same as for a production company. See [Test mode and sandbox companies](/environments). The contact email address is the `company_email` field of the company. `GET /api/me/` returns it. If `company_email` is empty, the send call of a sandbox company returns `400` with the `detail` `Tenant is in test mode but no company email is set`. See [Send errors](/guides/errors#send-errors). ## Authentication Send the API key of your company as a bearer token in the `Authorization` header: ```bash theme={null} curl "https://api.e-invoice.be/api/me/" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` Three lookup endpoints accept requests without an API key. See [Authentication](/authentication#endpoints-without-authentication). ## Endpoints ### Documents | Endpoint | Function | | - | - | | `POST /api/documents/` | Create a document from JSON. The document gets the state `DRAFT`. | | `POST /api/documents/ubl` | Create a document from a UBL XML file. | | `POST /api/documents/pdf` | Extract the document JSON (and the UBL XML, when possible) from a PDF file. This endpoint does not store a document. To store it, send the JSON to `POST /api/documents/`. | | `GET /api/documents/{document_id}` | Get a document. | | `DELETE /api/documents/{document_id}` | Delete a document (state `DRAFT` or `FAILED` only). See [Delete a draft](/guides/managing-documents#delete-a-draft). | | `POST /api/documents/{document_id}/validate` | Validate a stored document. | | `POST /api/documents/{document_id}/send` | Send a document. A production company sends it through Peppol. A sandbox company sends it by email to its contact email address. | | `GET /api/documents/{document_id}/ubl` | Get the metadata of the UBL XML of a document. Download the XML from the `signed_url` in the response. | | `GET /api/documents/{document_id}/timeline` | Get the events of a document. | Schemas: [Document schema](/api-reference/schemas/document), [LineItem schema](/api-reference/schemas/line-item). Guides: [Create e-invoices](/guides/creating-invoices), [Create credit notes](/guides/credit-notes), [Self-billing and debit notes](/guides/self-billing), [Advanced invoicing](/guides/advanced-invoicing), [Send UBL documents](/guides/ubl-documents), [Create documents from PDF](/guides/pdf-documents), [Document lifecycle and delivery tracking](/guides/document-lifecycle). ### Attachments | Endpoint | Function | | - | - | | `GET /api/documents/{document_id}/attachments` | List the attachments of a document. | | `GET /api/documents/{document_id}/attachments/{attachment_id}` | Get one attachment. | | `DELETE /api/documents/{document_id}/attachments/{attachment_id}` | Delete one attachment. | | `POST /api/documents/{document_id}/attachments` | Add an attachment. Deprecated: put the attachments in the `attachments` field when you create the document. | Guide: [Attachments and PDF](/guides/attachments). ### Document lists | Endpoint | Function | | - | - | | `GET /api/inbox/` | List received documents. | | `GET /api/inbox/invoices` | List received invoices. | | `GET /api/inbox/credit-notes` | List received credit notes. | | `GET /api/outbox/` | List sent documents. | | `GET /api/drafts/` | List draft documents. | | `GET /api/outbox/drafts` | List draft documents. Deprecated: use `GET /api/drafts/`. | Guides: [Receive documents](/guides/receiving-documents) for requests, responses, inbox filters and the pattern to prevent double processing. [List, filter and manage documents](/guides/managing-documents) for filters, sort options and drafts. ### Mailbox | Endpoint | Function | | - | - | | `GET /api/mailbox/` | List inbound emails. | | `GET /api/mailbox/{inbound_email_id}` | Get one inbound email. | | `GET /api/mailbox/{inbound_email_id}/attachments/{filename}` | Download an attachment of an inbound email. | | `POST /api/mailbox/{inbound_email_id}/reprocess` | Process a failed inbound email again. | Guide: [Send invoices by email (Mailbox)](/guides/mailbox). ### Validation | Endpoint | Function | | - | - | | `POST /api/validate/json` | Validate invoice JSON. | | `POST /api/validate/ubl` | Validate a UBL XML file. | | `GET /api/validate/peppol-id` | Find out if a Peppol ID is registered on the network. | Validation is not a separate mandatory call. `POST /api/documents/` rejects a payload that does not pass the same rules. Use `POST /api/validate/json` while you develop, because it returns all rule failures and the generated UBL. Guides: [Validation during development](/guides/validation), [Look up Peppol participants](/guides/lookup-participants). ### Lookup | Endpoint | Function | | - | - | | `GET /api/lookup` | Get the details of a Peppol participant. | | `GET /api/lookup/participants` | Search for participants in the Peppol Directory. | Guide: [Look up Peppol participants](/guides/lookup-participants). ### PDF conversion | Endpoint | Function | | - | - | | `POST /api/conversion/pdf` | Start the conversion of a PDF file. | | `GET /api/conversion/pdf/{task_id}` | Get the status of a conversion. | | `GET /api/conversion/pdf/{task_id}/result` | Get the result of a conversion. | Guide: [Create documents from PDF](/guides/pdf-documents). ### Webhooks | Endpoint | Function | | - | - | | `GET /api/webhooks/` | List the webhooks. | | `POST /api/webhooks/` | Create a webhook. | | `GET /api/webhooks/{webhook_id}` | Get a webhook. | | `PUT /api/webhooks/{webhook_id}` | Update a webhook. | | `DELETE /api/webhooks/{webhook_id}` | Delete a webhook. | | `GET /api/webhooks/{webhook_id}/history` | Get the delivery history of a webhook. | | `POST /api/webhooks/{webhook_id}/test` | Send a test event to a webhook. | Guide: [Webhooks](/essentials/webhooks). ### Account and usage | Endpoint | Function | | - | - | | `GET /api/me/` | Get the data of the company that owns the API key. | | `GET /api/stats` | Get the usage statistics of the company. | Guides: [Authentication](/authentication), [Usage statistics and credits](/guides/usage-statistics). ## Admin API for resellers Resellers that manage more than one customer company use a separate Admin API. It requires an organisation API key and is available to approved resellers only. The Admin API endpoints are not in the OpenAPI specification. In the Admin API, the name of a company is "tenant". With the Admin API you can: * Create and manage the companies of your customers * Create and rotate the API keys of these companies * Register these companies on the Peppol network Manage the companies of your customers as a reseller See also the [reseller programme](/reseller-programme). ## Request and response format ### Request format Requests with a body use JSON, with the header `Content-Type: application/json`. The endpoints that accept files (`POST /api/documents/ubl`, `POST /api/documents/pdf`, `POST /api/validate/ubl`, `POST /api/conversion/pdf`) use `multipart/form-data`. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/documents/" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "purchase_order": "PO-12345", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email": "billing@e-invoice.be", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "subtotal": 1000.00, "total_tax": 210.00, "invoice_total": 1210.00, "amount_due": 1210.00, "payment_term": "Payment within 30 days", "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "INV-2026-001" } ] }' ``` ```javascript Node.js theme={null} const invoice = { document_type: "INVOICE", invoice_id: "INV-2026-001", invoice_date: "2026-10-01", due_date: "2026-10-31", currency: "EUR", purchase_order: "PO-12345", vendor_name: "E-INVOICE BV", vendor_tax_id: "BE1018265814", vendor_address: "Brusselsesteenweg 119/A, 1980 Zemst, BE", vendor_email: "billing@e-invoice.be", customer_name: "OpenPeppol VZW", customer_tax_id: "BE0848934496", customer_address: "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", items: [ { description: "Professional services", quantity: 10, unit: "C62", unit_price: 100.00, amount: 1000.00, tax_rate: "21.00" } ], subtotal: 1000.00, total_tax: 210.00, invoice_total: 1210.00, amount_due: 1210.00, payment_term: "Payment within 30 days", payment_details: [ { iban: "BE68539007547034", swift: "GEBABEBB", payment_reference: "INV-2026-001" } ] }; const response = await fetch("https://api.e-invoice.be/api/documents/", { method: "POST", headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(invoice), }); const result = await response.json(); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${JSON.stringify(result)}`); } console.log(result); ``` ```python Python theme={null} import os import requests invoice = { "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "purchase_order": "PO-12345", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email": "billing@e-invoice.be", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "subtotal": 1000.00, "total_tax": 210.00, "invoice_total": 1210.00, "amount_due": 1210.00, "payment_term": "Payment within 30 days", "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "INV-2026-001" } ] } response = requests.post( "https://api.e-invoice.be/api/documents/", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, json=invoice, timeout=30, ) if not response.ok: raise SystemExit(f"HTTP {response.status_code}: {response.text}") print(response.json()) ``` ```php PHP theme={null} "INVOICE", "invoice_id" => "INV-2026-001", "invoice_date" => "2026-10-01", "due_date" => "2026-10-31", "currency" => "EUR", "purchase_order" => "PO-12345", "vendor_name" => "E-INVOICE BV", "vendor_tax_id" => "BE1018265814", "vendor_address" => "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email" => "billing@e-invoice.be", "customer_name" => "OpenPeppol VZW", "customer_tax_id" => "BE0848934496", "customer_address" => "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items" => [ [ "description" => "Professional services", "quantity" => 10, "unit" => "C62", "unit_price" => 100.00, "amount" => 1000.00, "tax_rate" => "21.00" ] ], "subtotal" => 1000.00, "total_tax" => 210.00, "invoice_total" => 1210.00, "amount_due" => 1210.00, "payment_term" => "Payment within 30 days", "payment_details" => [ [ "iban" => "BE68539007547034", "swift" => "GEBABEBB", "payment_reference" => "INV-2026-001" ] ] ]; $ch = curl_init("https://api.e-invoice.be/api/documents/"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("E_INVOICE_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode($invoice), ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status >= 400) { throw new RuntimeException("HTTP $status: $body"); } print_r(json_decode($body, true)); ``` ```csharp C# theme={null} using System.Net.Http.Headers; using System.Text; var invoice = """ { "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "purchase_order": "PO-12345", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email": "billing@e-invoice.be", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "subtotal": 1000.00, "total_tax": 210.00, "invoice_total": 1210.00, "amount_due": 1210.00, "payment_term": "Payment within 30 days", "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "INV-2026-001" } ] } """; using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var response = await client.PostAsync( "/api/documents/", new StringContent(invoice, Encoding.UTF8, "application/json")); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {body}"); } Console.WriteLine(body); ``` The API rejects a document whose vendor is not a Peppol ID of your company (`406`). Before you run this example, replace the vendor fields with those of your own company. ### Response format A successful request returns JSON with a `2xx` status code. This example shows the first fields of the response to a create request: ```json theme={null} { "id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "created_at": "2026-10-01T10:00:00Z", "document_type": "INVOICE", "state": "DRAFT", "direction": "OUTBOUND", "invoice_id": "INV-2026-001" } ``` For all fields, see the [Document schema](/api-reference/schemas/document). ### Error responses The API returns these error status codes: `400`, `401`, `403` (Admin API only), `404`, `405`, `406`, `409`, `415`, `422`, `429`, `500`. * `406` means that the document does not obey a business rule, or that its Peppol sender (the vendor, or the customer for self-billing) is not a Peppol ID of your company. * `405` means that the document is not in a state that permits a send. * `409` means that the sender of a send call is not a Peppol ID of your company. * `422` means that the request does not agree with the schema of the endpoint, or that the API cannot read an input file. Most errors have this body: ```json theme={null} { "detail": "Document not found" } ``` For the body formats, the cause of each code and the retry rules, see [Errors and troubleshooting](/guides/errors). ## Pagination The document list endpoints (`/api/inbox/`, `/api/outbox/`, `/api/drafts/`) use `page` (default 1) and `page_size` (default 20, maximum 100). The response contains `items`, `total`, `page`, `page_size`, `pages` and `has_next_page`. For filters, sort options and a loop sample, see [List, filter and manage documents](/guides/managing-documents). The Admin API list endpoints (`/api/admin/tenants` and its API-key routes) use `skip` and `limit` parameters. See the [Admin API](/admin-api). ## Rate limiting The API applies a rate limit for each API key to these endpoints: * `POST /api/documents/` and `POST /api/documents/ubl` * `POST /api/validate/json` and `POST /api/validate/ubl` Each of these endpoints has its own counter. When you exceed the limit, the API returns `429 Too Many Requests` with a `Retry-After` header. The value of `Retry-After` is a fixed number of seconds: the length of the rate-limit window. It is not the time until your counter resets. If the request after the wait also gets `429`, wait longer before the next attempt. Do not write a fixed limit into your code. A successful response of these endpoints contains these headers: * `X-RateLimit-Limit`: the number of requests in one window. * `X-RateLimit-Remaining`: the number of requests that remain in the current window. * `X-RateLimit-Reset`: the Unix time in seconds when the window ends. See [Rate limits](/guides/errors#rate-limits). ## Document states | State | Meaning | | - | - | | `DRAFT` | The document exists but is not sent. `POST /api/documents/` always creates a document in this state. | | `TRANSIT` | The send request is accepted and the transmission is in progress. | | `SENT` | The transmission is complete. | | `FAILED` | The transmission failed. You can send the document again. | | `RECEIVED` | The document came in from another Peppol participant. | You can send a document only when it is in the `DRAFT` or `FAILED` state. For the transitions, the retry behaviour and the webhook events of each state, see [Document lifecycle and delivery tracking](/guides/document-lifecycle). To follow the state changes, use [webhooks](/essentials/webhooks) or the timeline. See [Document lifecycle and delivery tracking](/guides/document-lifecycle) for the state diagrams, the transitions and the timeline. ## Supported currencies The `currency` field accepts these ISO 4217 codes: `EUR`, `USD`, `GBP`, `JPY`, `CHF`, `CAD`, `AUD`, `NZD`, `CNY`, `INR`, `SEK`, `NOK`, `DKK`, `SGD`, `HKD` The default is `EUR`. ## Peppol participant IDs A Peppol ID has the format `scheme:identifier`. For example, a Belgian company uses the scheme `0208` with its enterprise number: `0208:1018265814`. For the list of schemes and for the lookup of a participant, see [Look up Peppol participants](/guides/lookup-participants#peppol-id-schemes). ## OpenAPI specification The reference is generated from the `openapi.json` file in the docs repository, which a CI check compares with the live spec at `https://api.e-invoice.be/api/openapi.json`. You can use the live specification to: * Generate a client library in your programming language * Import the API into a test tool (for example Postman or Insomnia) * Validate request and response structures SDKs and a command-line tool are available: see [SDKs](/sdks) and [peppol CLI](/cli). To connect an AI assistant to your company through MCP, see [Model Context Protocol (MCP)](/essentials/mcp). ## Support Send an email to the support team See the open-source projects ## Next Steps Send a first invoice with a sandbox company Get and use an API key Create and send an e-invoice Validate invoices while you develop # Convert a PDF to a document Source: https://docs.e-invoice.be/api-reference/conversion/convert-a-pdf-to-a-document /openapi.json post /api/conversion/pdf Convert a PDF to a document # Get PDF conversion result Source: https://docs.e-invoice.be/api-reference/conversion/get-pdf-conversion-result /openapi.json get /api/conversion/pdf/{task_id}/result Get the result of a PDF conversion task # Get PDF conversion status Source: https://docs.e-invoice.be/api-reference/conversion/get-pdf-conversion-status /openapi.json get /api/conversion/pdf/{task_id} Get the status of a PDF conversion task # Add Attachment Source: https://docs.e-invoice.be/api-reference/documents/add-attachment /openapi.json post /api/documents/{document_id}/attachments Add one or more attachments to an invoice. Be careful: the attachments ARE NOT ADDED to the UBL! They are only stored in our database and can be downloaded later. To add attachments to the UBL, you need to add the attachment(s) via POST /api/documents # Create Document Source: https://docs.e-invoice.be/api-reference/documents/create-document /openapi.json post /api/documents/ Create a new invoice or credit note # Create Document from PDF Source: https://docs.e-invoice.be/api-reference/documents/create-document-from-pdf /openapi.json post /api/documents/pdf Create a new invoice or credit note from a PDF file. If the 'ubl_document' field is set in the response, it indicates that sufficient details were extracted from the PDF to automatically generate a valid UBL document ready for sending. If 'ubl_document' is not set, human intervention may be required to ensure compliance. # Create Document from UBL Source: https://docs.e-invoice.be/api-reference/documents/create-document-from-ubl /openapi.json post /api/documents/ubl Create a new invoice or credit note from a UBL file # Delete Document Source: https://docs.e-invoice.be/api-reference/documents/delete-document /openapi.json delete /api/documents/{document_id} Delete an invoice or credit note # Delete Document Attachment Source: https://docs.e-invoice.be/api-reference/documents/delete-document-attachment /openapi.json delete /api/documents/{document_id}/attachments/{attachment_id} Delete an attachment from an invoice or credit note # Get Document Source: https://docs.e-invoice.be/api-reference/documents/get-document /openapi.json get /api/documents/{document_id} Get an invoice or credit note by ID # Get Document Attachment Source: https://docs.e-invoice.be/api-reference/documents/get-document-attachment /openapi.json get /api/documents/{document_id}/attachments/{attachment_id} Get attachment details with for an invoice or credit note with link to download file (signed URL, valid for 1 hour) # Get Document Attachments Source: https://docs.e-invoice.be/api-reference/documents/get-document-attachments /openapi.json get /api/documents/{document_id}/attachments Get all attachments for an invoice or credit note # Get Document Lifecycle Source: https://docs.e-invoice.be/api-reference/documents/get-document-lifecycle /openapi.json get /api/documents/{document_id}/lifecycle Get the lifecycle events stated about an invoice or credit note after creation (Peppol Message Level and Invoice Responses, partner status events), oldest first. Each event carries a route-agnostic canonical status next to the untouched native code of its source; native codes without a canonical equivalent have canonical_status null. # Get Document Timeline Source: https://docs.e-invoice.be/api-reference/documents/get-document-timeline /openapi.json get /api/documents/{document_id}/timeline Get the timeline of events for an invoice or credit note, including creation, validation, transmission, and delivery events # Get Document UBL Source: https://docs.e-invoice.be/api-reference/documents/get-document-ubl /openapi.json get /api/documents/{document_id}/ubl Get the UBL for an invoice or credit note # Send Document Source: https://docs.e-invoice.be/api-reference/documents/send-document /openapi.json post /api/documents/{document_id}/send Send an invoice or credit note via Peppol. By default, the sender and receiver Peppol IDs are derived from the company (tax) IDs in the document, regardless of whether the document was created from a UBL with a different endpoint ID. To explicitly set the sender or receiver Peppol ID, provide them via the query parameters (sender_peppol_scheme, sender_peppol_id, receiver_peppol_scheme, receiver_peppol_id). # Validate Document Source: https://docs.e-invoice.be/api-reference/documents/validate-document /openapi.json post /api/documents/{document_id}/validate Validate a UBL document according to Peppol BIS Billing 3.0 # List Draft Documents Source: https://docs.e-invoice.be/api-reference/drafts/list-draft-documents /openapi.json get /api/drafts/ Retrieve a paginated list of draft documents with filtering options including state and text search. # List Received Credit Notes Source: https://docs.e-invoice.be/api-reference/inbox/list-received-credit-notes /openapi.json get /api/inbox/credit-notes Retrieve a paginated list of received credit notes with filtering options. # List Received Documents Source: https://docs.e-invoice.be/api-reference/inbox/list-received-documents /openapi.json get /api/inbox/ Retrieve a paginated list of received documents with filtering options including state, type, sender, date range, and text search. # List Received Invoices Source: https://docs.e-invoice.be/api-reference/inbox/list-received-invoices /openapi.json get /api/inbox/invoices Retrieve a paginated list of received invoices with filtering options. # Lookup Peppol ID Source: https://docs.e-invoice.be/api-reference/lookup/lookup-peppol-id /openapi.json get /api/lookup Lookup Peppol ID. The peppol_id must be in the form of `:`. The scheme is a 4-digit code representing the identifier scheme, and the id is the actual identifier value. For example, for a Belgian company it is `0208:0123456789` (where 0208 is the scheme for Belgian enterprises, followed by the 10 digits of the official BTW / KBO number). # Lookup Peppol participants Source: https://docs.e-invoice.be/api-reference/lookup/lookup-peppol-participants /openapi.json get /api/lookup/participants Lookup Peppol participants by name or other identifiers. You can limit the search to a specific country by providing the country code. # Download Inbound Email Attachment Source: https://docs.e-invoice.be/api-reference/mailbox/download-inbound-email-attachment /openapi.json get /api/mailbox/{inbound_email_id}/attachments/{filename} Download a specific attachment from an inbound email by filename. Returns the binary content with the correct Content-Type. # Get Inbound Email Detail Source: https://docs.e-invoice.be/api-reference/mailbox/get-inbound-email-detail /openapi.json get /api/mailbox/{inbound_email_id} Retrieve a single inbound email by ID, scoped to the caller's tenant. # List Inbound Emails Source: https://docs.e-invoice.be/api-reference/mailbox/list-inbound-emails /openapi.json get /api/mailbox/ Retrieve a paginated list of received inbound emails with filtering, search, and sorting options. # Reprocess Failed Inbound Email Source: https://docs.e-invoice.be/api-reference/mailbox/reprocess-failed-inbound-email /openapi.json post /api/mailbox/{inbound_email_id}/reprocess Retry processing of a previously failed inbound email by replaying the original Postmark payload. # List Draft Documents Source: https://docs.e-invoice.be/api-reference/outbox/list-draft-documents /openapi.json get /api/outbox/drafts Retrieve a paginated list of draft documents with filtering options including state and text search. # List Sent Documents Source: https://docs.e-invoice.be/api-reference/outbox/list-sent-documents /openapi.json get /api/outbox/ Retrieve a paginated list of sent documents with filtering options including state, type, sender, date range, and text search. # Document Source: https://docs.e-invoice.be/api-reference/schemas/document Look up each field of the Document schema and see how the API converts it to UBL BIS Billing 3.0. ## Overview The `Document` schema defines the structure of invoices, credit notes and debit notes in the e-invoice.be API. You send this schema when you call `POST /api/documents/` or `POST /api/validate/json`. The schema requires only the `items` array, with a minimum of one line. All other fields are optional in the schema. But `POST /api/documents/` accepts a document only if it also has: * A vendor identifier (`vendor_tax_id` or `vendor_company_id`) and a customer identifier (`customer_peppol_id`, `customer_tax_id` or `customer_company_id`). The derived Peppol ID of the sender must be one of the `peppol_ids` of your company (`406` if not). * `invoice_id` (rule `BR-02`) and `vendor_name`. * A `description` (or `name`), a `quantity` and a `unit_price` for each line. A line without `quantity` or `unit_price` counts as 0.00 in the totals. * For an invoice with a positive amount due: `due_date` or `payment_term` (rule `BR-CO-25`). When a field is absent, the API uses these values: `invoice_date` is the current date, `unit` is `NAR`, a missing address becomes `Unknown Street` and `Unknown City`, and a line with `tax_rate` `0.00` gets tax category `Z` when `tax_code` is `S`. Validation is not a separate mandatory call. `POST /api/documents/` rejects a payload that does not pass the same rules. Use `POST /api/validate/json` while you develop, because it returns all rule failures and the generated UBL. ## Document metadata Type of document to create. * `INVOICE` - Standard invoice * `CREDIT_NOTE` - Credit note (refund or correction) * `DEBIT_NOTE` - Debit note * `SELFBILLING_INVOICE` - Self-billed invoice * `SELFBILLING_CREDIT_NOTE` - Self-billed credit note See [Self-billing and debit notes](/guides/self-billing) for the two self-billing types and for debit notes. Document state. See [Document states](#document-states). Read-only: `POST /api/documents/` always sets `DRAFT` and ignores the value that you send. Document direction. Read-only: `POST /api/documents/` always sets `OUTBOUND` and ignores the value that you send. * `OUTBOUND` - Document that you send to a customer * `INBOUND` - Document that you receive from a supplier ### Document states | State | Meaning | | - | - | | `DRAFT` | The document exists but is not sent. `POST /api/documents/` always creates a document in this state. | | `TRANSIT` | The send request is accepted and the transmission is in progress. | | `SENT` | The transmission is complete. | | `FAILED` | The transmission failed. You can send the document again. | | `RECEIVED` | The document came in from another Peppol participant. | You can send a document only when it is in the `DRAFT` or `FAILED` state. For the transitions, the retry behaviour and the webhook events of each state, see [Document lifecycle and delivery tracking](/guides/document-lifecycle). See [Document lifecycle and delivery tracking](/guides/document-lifecycle) for the state diagrams, the transitions, the retries and the related webhook events. ## Vendor (supplier) information Legal name of the vendor. Example: `"E-INVOICE BV"` VAT number of the vendor, with the country prefix. The API changes the value to uppercase and removes all characters other than A-Z and 0-9. See [Normalisation and rounding](#normalisation-and-rounding). Example: `"BE1018265814"` The API derives the Peppol ID of the vendor from this value. For the document types other than self-billing, the vendor is the Peppol sender. See [Peppol ID routing](/guides/creating-invoices#peppol-id-routing). Company registration number of the vendor, for example the Belgian enterprise number (CBE) or the Dutch KVK number. The API uses it to derive the Peppol ID of the vendor when `vendor_tax_id` is absent. Example: `"1018265814"` Address of the vendor as one string. See [Addresses](#addresses) for the format that the API can split correctly. Example: `"Brusselsesteenweg 119/A, 1980 Zemst, BE"` Department or person at the vendor address. The API writes this value as the contact name only when `vendor_email` is present. Example: `"Accounts Department"` Contact email address of the vendor. Example: `"billing@e-invoice.be"` ## Customer (buyer) information Legal name of the customer. Example: `"OpenPeppol VZW"` VAT number of the customer, with the country prefix. The API changes the value to uppercase and removes all characters other than A-Z and 0-9. Example: `"BE0848934496"` Company registration number of the customer. The API uses it to derive the Peppol ID of the customer when `customer_peppol_id` and `customer_tax_id` are absent. Example: `"0848934496"` Peppol participant ID of the customer, in the format `scheme:identifier`. When you send this field, the API uses it as the Peppol ID of the customer and does not derive it from `customer_tax_id` or `customer_company_id`. The customer is the Peppol receiver, except in a self-billing document, where the customer is the Peppol sender. Example: `"0208:0848934496"` The API does not store this field with the document. The value is kept only as the receiver of the generated UBL. See [Look up Peppol participants](/guides/lookup-participants) to find and check a Peppol ID before you send. Your internal reference for the customer. The API writes it as the party identification of the customer. It is also the fallback value of `BuyerReference` when `buyer_reference` and `purchase_order` are absent. The API does not use it to find the Peppol ID of the customer. Example: `"CUST-12345"` Address of the customer as one string. See [Addresses](#addresses). Example: `"Rond-point Schuman 6, 1040 Brussels, BE"` Department or person at the customer address. The API writes this value as the contact name only when `customer_email` is present. Example: `"Accounts Payable"` Contact email address of the customer. Example: `"ap@openpeppol.example"` ## Invoice details Unique invoice number. Example: `"INV-2026-001"` Issue date in ISO 8601 format (`YYYY-MM-DD`). When it is absent, the API uses the current date. Example: `"2026-10-01"` Payment due date in ISO 8601 format (`YYYY-MM-DD`). The API does not write this field to the UBL of a credit note. Example: `"2026-10-31"` Buyer reference (BT-10), for example a cost centre or a reference that the buyer asks for. The API writes it to `cbc:BuyerReference`. Example: `"CC-4410"` Purchase order reference of the customer (BT-13). The API writes it to `cac:OrderReference/cbc:ID`, and to `cbc:BuyerReference` when `buyer_reference` is absent. Do not use it for the number of the original invoice of a credit note: use `preceding_invoice_id`. Example: `"PO-12345"` Sales order reference of the vendor (BT-14). The API writes it to `cac:OrderReference/cbc:SalesOrderID`. When `purchase_order` is absent, the API writes `NA` as the order reference ID. Example: `"SO-2026-0042"` Number of the invoice that this document corrects (BT-25), for example the invoice that a credit note credits. The API writes it to `cac:BillingReference/cac:InvoiceDocumentReference/cbc:ID`. Example: `"INV-2026-001"` Issue date of the preceding invoice (BT-26) in ISO 8601 format (`YYYY-MM-DD`). The API writes it only together with `preceding_invoice_id`. Example: `"2026-10-01"` Free-text note. Example: `"Full refund - goods returned"` Description of the payment terms. Example: `"Payment due within 30 days"` ## Financial fields The API calculates `subtotal`, `total_discount`, `total_tax`, `invoice_total` and `amount_due` when they are absent. When you send them, the API compares them with its own calculation. See [Invoice totals and calculations](/guides/invoice-totals) for the formulas. Currency code (ISO 4217). The `currency` field accepts these ISO 4217 codes: `EUR`, `USD`, `GBP`, `JPY`, `CHF`, `CAD`, `AUD`, `NZD`, `CNY`, `INR`, `SEK`, `NOK`, `DKK`, `SGD`, `HKD` The default is `EUR`. Example: `"EUR"` Taxable base amount (after document-level allowances and charges, before tax). Corresponds to UBL `cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount` Example: `1000.00` Total of the document-level allowances (discounts only, not charges). Corresponds to UBL `cac:LegalMonetaryTotal/cbc:AllowanceTotalAmount` Example: `50.00` Total VAT amount. Corresponds to UBL `cac:TaxTotal/cbc:TaxAmount` Example: `210.00` Total amount with tax (`subtotal` + `total_tax`). Corresponds to UBL `cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount` Example: `1210.00` Amount due for payment after prepayments. Corresponds to UBL `cac:LegalMonetaryTotal/cbc:PayableAmount` Example: `1210.00` Previous outstanding balance. This is an internal field: the API does not write it to the generated UBL. Example: `100.00` ## Tax information Tax category code (UNCL5305). * `S` - Standard rate (most common) * `Z` - Zero rated * `E` - Exempt from tax * `AE` - VAT reverse charge * `K`, `G`, `O`, `L`, `M`, `B` - Other special cases The API derives the tax category of each line from its `tax_rate`: a rate more than `0.00` gives `S`. A rate of `0.00` gives the `tax_code` of the document, or `Z` when `tax_code` is `S`. When `tax_code` is `K` (intra-community supply), each line item must have a `tax_rate` of `0.00` (`422` if not). VAT exemption reason code (when `tax_code` is `E`, `AE`, `K`, `G`, `O`, `L`, `M` or `B`). Example: `"VATEX-EU-IC"` for an intra-community supply, `"VATEX-EU-AE"` for a reverse charge When `vatex` is `VATEX-EU-O` (not subject to VAT), `vendor_tax_id` and `customer_tax_id` must be empty (`406` if not). Text that explains the VAT exemption. When you send `vatex_note`, the UBL contains only this text as the exemption reason, and not the `vatex` code. Example: `"Reverse charge applies - Art. 196 EU VAT Directive"` ## Service period Start date of the service period (ISO 8601: `YYYY-MM-DD`). Example: `"2026-09-01"` End date of the service period (ISO 8601: `YYYY-MM-DD`). Example: `"2026-09-30"` ## Additional addresses Of these fields, the API writes only `shipping_address` and `shipping_address_recipient` to the generated UBL (as `cac:Delivery`). The billing, service and remittance fields are kept with the document, but the receiver does not get them in the UBL. Billing address (if different from the customer address). Example: `"Rond-point Schuman 6, 1040 Brussels, BE"` Recipient at the billing address. Example: `"Accounts Payable Department"` Delivery address. The API writes it to `cac:Delivery/cac:DeliveryLocation/cac:Address` with the same split rule as the party addresses. When `tax_code` is `K` and there is no shipping address, the API writes only the country of the customer as the delivery location. Example: `"Industrieweg 5, 3000 Leuven, BE"` Recipient at the delivery address. The API writes it as the name of the delivery party. Example: `"Warehouse Manager"` Address of the service location. Example: `"Industrieweg 5, 3000 Leuven, BE"` Recipient at the service address. Remittance (payment) address. Example: `"Brusselsesteenweg 119/A, 1980 Zemst, BE"` Recipient at the remittance address. ## Line items Array of line items (minimum 1). See the [LineItem schema](/api-reference/schemas/line-item) for the fields. Example: ```json theme={null} [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": 21.00 } ] ``` ## Payment details Array of payment instructions. The API writes one `cac:PaymentMeans` element for each entry, always with payment means code `30` (credit transfer). International Bank Account Number. The API changes the value to uppercase and removes all characters other than A-Z and 0-9. Example: `"BE68539007547034"` SWIFT/BIC code of the bank. The API writes it only when `iban` or `bank_account_number` is present. Example: `"GEBABEBB"` Bank account number for an account that has no IBAN. The API writes it as the payee account only when `iban` is absent. The API does not normalise this value. Example: `"12345678"` Payment reference, for example a Belgian structured communication. When it is absent, the API writes `invoice_id` as the payment reference. Example: `"+++010/8068/17183+++"` Example: ```json theme={null} [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "INV-2026-001" } ] ``` ## Allowances and charges Document-level allowances (discounts). Allowance amount without VAT, with a maximum of 2 decimals. The API does not calculate this value from `multiplier_factor` and `base_amount`. You must send it. Example: `100.00` Reason for the allowance as text. Example: `"Volume discount"` Allowance reason code (UNCL5189). Permitted values: `41`, `42`, `60`, `62`, `63`, `64`, `65`, `66`, `67`, `68`, `70`, `71`, `88`, `95`, `100`, `102`, `103`, `104`, `105`. Send the code as a string. Example: `"95"` (discount) Percentage of the allowance, from 0 to 100, with a maximum of 2 decimals. To state 10%, send `10`. Send it together with `base_amount`. Example: `10` Amount to which the percentage applies, with a maximum of 2 decimals. Send it together with `multiplier_factor`. Example: `1000.00` VAT category code that applies to the allowance. VAT rate that applies to the allowance, as a percentage from 0 to 100. Example of a 10% allowance: ```json theme={null} [ { "reason": "Volume discount", "reason_code": "95", "multiplier_factor": 10, "base_amount": 1000.00, "amount": 100.00, "tax_code": "S", "tax_rate": 21.00 } ] ``` Document-level charges (fees). Charge amount without VAT, with a maximum of 2 decimals. The API does not calculate this value from `multiplier_factor` and `base_amount`. You must send it. Example: `25.00` Reason for the charge as text. Example: `"Shipping and handling"` Charge reason code (UNCL7161). The full list of permitted values is in the `ChargeReasonCode` schema of the [API reference](/api-reference). Example: `"ABL"` (additional packaging) Percentage of the charge, from 0 to 100, with a maximum of 2 decimals. To state 2.5%, send `2.5`. Send it together with `base_amount`. Example: `2.5` Amount to which the percentage applies, with a maximum of 2 decimals. Send it together with `multiplier_factor`. Example: `1000.00` VAT category code that applies to the charge. VAT rate that applies to the charge, as a percentage from 0 to 100. Example: ```json theme={null} [ { "reason": "Additional packaging", "reason_code": "ABL", "amount": 25.00, "tax_code": "S", "tax_rate": 21.00 } ] ``` See [Advanced invoicing](/guides/advanced-invoicing) for the effect of allowances and charges on the totals. ## Tax details Tax breakdown for each rate. Optional. The API stores and returns the entries as you send them. It does not calculate them, and it does not use them for the tax breakdown in the UBL (the API always calculates that breakdown). Each entry has `amount` (number, the tax amount) and `rate` (string, for example `"21.00"`). ## Attachments Files in this array are embedded in the UBL. See [Attachments and PDF](/guides/attachments) for the permitted file types and for the `construct_pdf` option. Example: ```json theme={null} [ { "file_name": "timesheet.pdf", "file_type": "application/pdf", "file_data": "" } ] ``` ## How fields map to UBL The API converts the JSON document to UBL BIS Billing 3.0. The table shows the conversions that are not self-evident. The response of `POST /api/validate/json` contains the generated UBL, thus you can examine the result for your own payload. | JSON field | UBL element | Rule | | - | - | - | | `buyer_reference`, `purchase_order`, `customer_id` | `cbc:BuyerReference` | The first of these three fields that is present. When the three are absent, the text `Unknown`. | | `purchase_order`, `sales_order` | `cac:OrderReference/cbc:ID`, `cbc:SalesOrderID` | When only `sales_order` is present, the order reference ID is `NA`. | | `preceding_invoice_id`, `preceding_invoice_date` | `cac:BillingReference/cac:InvoiceDocumentReference/cbc:ID`, `cbc:IssueDate` | For a credit note of a Dutch vendor without `preceding_invoice_id`, the API writes `UNSPECIFIED` (rule `NL-R-001`). | | `payment_details[].payment_reference` | `cac:PaymentMeans/cbc:PaymentID` | When the reference is absent, the value is `invoice_id`. | | `payment_details[]` | `cac:PaymentMeans/cbc:PaymentMeansCode` | Always `30` (credit transfer). | | `payment_details[].iban`, `bank_account_number` | `cac:PayeeFinancialAccount/cbc:ID` | `iban` when it is present, else `bank_account_number`. | | `payment_details[].swift` | `cac:PayeeFinancialAccount/cac:FinancialInstitutionBranch/cbc:ID` | Only when an account is present. | | `payment_term` | `cac:PaymentTerms/cbc:Note` | | | `due_date` | `cbc:DueDate` | Invoices only. Not written for credit notes. | | `service_start_date`, `service_end_date` | `cac:InvoicePeriod/cbc:StartDate`, `cbc:EndDate` | Written when one or the two dates are present. | | `vendor_email`, `customer_email` | `cac:Party/cac:Contact/cbc:ElectronicMail` | | | `vendor_address_recipient`, `customer_address_recipient` | `cac:Party/cac:Contact/cbc:Name` | Only when the email of the same party is present. | | `customer_id` | `cac:AccountingCustomerParty/cac:Party/cac:PartyIdentification/cbc:ID` | | | `shipping_address` | `cac:Delivery/cac:DeliveryLocation/cac:Address` | | | `shipping_address_recipient` | `cac:Delivery/cac:DeliveryParty/cac:PartyName/cbc:Name` | | | `allowances[]`, `charges[]` | `cac:AllowanceCharge` | `reason_code` goes to `cbc:AllowanceChargeReasonCode`, `multiplier_factor` to `cbc:MultiplierFactorNumeric`, `base_amount` to `cbc:BaseAmount`. | | `items[].product_code` | `cac:Item/cac:SellersItemIdentification/cbc:ID` | | | `items[].item_attributes[]` | `cac:Item/cac:AdditionalItemProperty` | An attribute without `value` is not written. | | `items[].unit_price`, `items[].price_base_quantity` | `cac:Price/cbc:PriceAmount`, `cac:Price/cbc:BaseQuantity` | `price_base_quantity` is `1` when it is absent. | | `attachments[]` | `cac:AdditionalDocumentReference` | See the MIME type rule below. | The MIME type of an attachment in the UBL is `file_type` when it is one of `application/pdf`, `image/png`, `image/jpeg`, `text/csv`, `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` (xlsx) or `application/vnd.oasis.opendocument.spreadsheet` (ods). For a different `file_type`, the API uses the extension of `file_name` (`.pdf`, `.png`, `.jpg`, `.jpeg`, `.csv`, `.xlsx`, `.ods`) to find the MIME type. ## Addresses `vendor_address`, `customer_address` and `shipping_address` are single strings. The API splits each string into the parts of a UBL address with these rules: 1. The API splits the string on new lines. If there is only one line, it splits the string on a comma followed by a space (`", "`). 2. Part 1 is the street (`cbc:StreetName`). 3. Part 2 is the postal code and the city. When the part has the form "postal code, space, city", the API writes `cbc:PostalZone` and `cbc:CityName`. If not, the full part is the city. 4. The last part is the country (`cac:Country/cbc:IdentificationCode`) only if it has 2 or 3 letters and no other characters. The API changes it to uppercase. 5. For the vendor and the customer, if the last part is not a country code, the API uses the first two characters of the tax ID of that party. If there is no tax ID, the country is `BE`. For `shipping_address`, the API does not use the tax ID: the country is `BE` when the string has no country code. Use this format, with the ISO 3166-1 alpha-2 country code as the last part: ```json theme={null} { "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "customer_address": "Rond-point Schuman 6, 1040 Brussels, BE" } ``` The first address gives this UBL: ```xml theme={null} Brusselsesteenweg 119/A Zemst 1980 BE ``` The API does not read a country name as a country. With `"..., 1000 Brussels, Belgium"`, the API ignores `Belgium` and takes the country from the tax ID prefix, or uses `BE`. An address in a different country, with a country name and without a tax ID, gets the country `BE`. Always end the address with the two-letter country code. Do not put a comma followed by a space in the street part (for example `"Main Street 1, box 5"`). The API reads the text after the comma as the postal code and city. A last part that has only 2 or 3 letters is always read as a country code. When the address is absent or has no usable parts, the API writes `Unknown Street` and `Unknown City`. ## Normalisation and rounding The API changes some values before it stores the document and generates the UBL. | Field | Rule | | - | - | | `vendor_tax_id`, `customer_tax_id` | Changed to uppercase. All characters other than A-Z and 0-9 are removed. `"be 0848.934.496"` becomes `"BE0848934496"`. | | `payment_details[].iban` | Same rule. `"BE68 5390 0754 7034"` becomes `"BE68539007547034"`. | | `amount`, `tax` and `tax_rate` of a line item | 2 decimals. | | `quantity` and `unit_price` of a line item | 4 decimals. | | `amount`, `base_amount`, `multiplier_factor` and `tax_rate` of an allowance or charge | 2 decimals. | Rounding of monetary amounts is half-up to 2 decimals (`0.005` becomes `0.01`). The API rounds in this sequence: 1. Each line `amount` is rounded. 2. For each tax group, the API adds the rounded line amounts, subtracts the document-level allowances and adds the document-level charges. A tax group is one combination of tax category and tax rate. 3. The tax of each group is `taxable amount × rate / 100`, rounded. 4. `total_tax` is the sum of the group taxes. The API thus calculates VAT for each tax group, not for each line. If you calculate VAT for each line and add the results, your `total_tax` can be different by some cents. See [Invoice totals and calculations](/guides/invoice-totals). ## Example Complete invoice: ```json Invoice theme={null} { "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "purchase_order": "PO-12345", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email": "billing@e-invoice.be", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "subtotal": 1000.00, "total_tax": 210.00, "invoice_total": 1210.00, "amount_due": 1210.00, "payment_term": "Payment within 30 days", "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "INV-2026-001" } ] } ``` Replace the vendor fields with the data of your company. The API rejects a document if the Peppol ID of the vendor is not one of the `peppol_ids` of your company. ## Next Steps Look up the fields of an invoice line. Create and send an invoice with this schema. Calculate the totals that the API checks. Validate a payload and examine the generated UBL. # LineItem Source: https://docs.e-invoice.be/api-reference/schemas/line-item Look up each field of an invoice line, with its type, its rules and an example value. ## Overview The `LineItem` schema defines one line of an invoice or credit note. Each line is a product, a service or a different billable item. You send line items in the `items` array of the [Document schema](/api-reference/schemas/document). All fields are optional in the schema. But a line without `description` (or `name`) fails rule `BR-25`, and a line without `quantity` or `unit_price` counts as 0.00 in the totals. For a complete line, send `description`, `quantity`, `unit`, `unit_price`, `amount` and `tax_rate`. ## Basic fields Description of the product or service (BT-154). The API writes it to `cac:Item/cbc:Description`. When `name` is absent, the API also writes it as the item name. Example: `"Consulting hours - September 2026"` Name of the item (BT-153), for example the product name. The API writes it to `cac:Item/cbc:Name`. When it is absent, the API uses `description` as the item name. Example: `"Wireless mouse"` Product code or SKU of the seller. The API writes it to `cac:Item/cac:SellersItemIdentification/cbc:ID`. Example: `"PROD-001"` Reserved field. In the current OpenAPI specification this field accepts only `null`, and the API does not write it to the generated UBL. Do not send a value. For the period of a delivery or service, use `service_start_date` and `service_end_date` on the document. Example: `null` ## Quantity and unit Quantity of items, with a maximum of 4 decimals. It can be negative for credit notes or corrections. Example: `10`, `2.5` [Unit code](/glossary#unit-code) of the unit of measure (UN/ECE Recommendation 20). Common values: * `C62` - Units or pieces (most common) * `HUR` - Hours * `DAY` - Days * `MTR` - Metres * `KGM` - Kilograms * `LTR` - Litres * `MTK` - Square metres * `MTQ` - Cubic metres Example: `"HUR"` The API accepts only the codes of its `UnitOfMeasureCode` list (see the [API reference](/api-reference)). A code that is not in the list, for example `KWH`, gives `422`. When `unit` is absent, the API uses `NAR` (number of articles). ## Pricing Net price of one unit without VAT, with a maximum of 4 decimals. This is the item net price (BT-146), after an item price discount and before line allowances and charges. Example: `100.00`, `0.0125` Number of units to which `unit_price` applies (BT-149), for example `100` when the price is the price of 100 units. Must be more than 0. The API writes it to `cac:Price/cbc:BaseQuantity`. Example: `1`, `100` Net amount of the line without VAT (BT-131), with a maximum of 2 decimals. Line allowances are subtracted and line charges are added. Calculation: `(quantity × unit_price / price_base_quantity) - allowances + charges` The API writes `amount` to the UBL as the line extension amount and uses it for the tax breakdown. The totals check uses `quantity`, `unit_price`, `price_base_quantity` and the line allowances and charges. It can be negative for credit notes or corrections. When `amount` is absent, the API sets it to `quantity × unit_price / price_base_quantity`, without the line allowances and charges. Thus, when a line has allowances or charges, you must send `amount`. If not, the UBL fails rule `PEPPOL-EN16931-R120` and the API returns `406`. Example: `1000.00` ## Tax VAT rate as a percentage from 0 to 100, with 2 decimals. Send a number. A string such as `"21.00"` is also accepted for backward compatibility. Common Belgian rates: * `21.00` - Standard rate * `6.00` - Reduced rate * `0.00` - Zero-rated Example: `21.00` VAT amount of the line, with a maximum of 2 decimals. Calculation: `amount × (tax_rate / 100)` When it is absent and you send `amount`, the API calculates it from `amount` and `tax_rate`. The value is for information only: the API does not write it to the UBL and does not use it for `total_tax`. Example: `210.00` (21% of 1000.00) ## Item attributes Properties of the item, such as colour or size (BG-32). The API writes each attribute to `cac:Item/cac:AdditionalItemProperty`. Name of the attribute (BT-160). Example: `"Colour"` Value of the attribute (BT-161). The API does not write an attribute without a value to the UBL. Example: `"Blue"` Example: ```json theme={null} [ { "name": "Colour", "value": "Blue" }, { "name": "Size", "value": "XL" } ] ``` ## Allowances and charges Line-level allowances (discounts) for this item only, for example a bulk discount or a promotion. Allowance amount without VAT, with a maximum of 2 decimals. The API does not calculate this value from `multiplier_factor` and `base_amount`. You must send it. Example: `500.00` Reason for the allowance as text. Example: `"Bulk discount"` Allowance reason code (UNCL5189). Permitted values: `41`, `42`, `60`, `62`, `63`, `64`, `65`, `66`, `67`, `68`, `70`, `71`, `88`, `95`, `100`, `102`, `103`, `104`, `105`. Send the code as a string. Example: `"95"` (discount) Percentage of the allowance, from 0 to 100, with a maximum of 2 decimals. To state 10%, send `10`. Send it together with `base_amount`. Example: `10` Amount to which the percentage applies, with a maximum of 2 decimals. Send it together with `multiplier_factor`. Example: `5000.00` VAT category code. The API does not write a tax category on a line-level allowance in the UBL. The line uses the tax category of the line item. VAT rate as a percentage. The API does not write it on a line-level allowance in the UBL. Example of a 10% allowance: ```json theme={null} [ { "reason": "Bulk discount", "reason_code": "95", "multiplier_factor": 10, "base_amount": 5000.00, "amount": 500.00 } ] ``` Line-level charges (fees) for this item only, for example special handling or customisation. Charge amount without VAT, with a maximum of 2 decimals. The API does not calculate this value from `multiplier_factor` and `base_amount`. You must send it. Example: `50.00` Reason for the charge as text. Example: `"Additional packaging"` Charge reason code (UNCL7161). The full list of permitted values is in the `ChargeReasonCode` schema of the [API reference](/api-reference). Example: `"ABL"` (additional packaging) Percentage of the charge, from 0 to 100, with a maximum of 2 decimals. To state 10%, send `10`. Send it together with `base_amount`. Example: `10` Amount to which the percentage applies, with a maximum of 2 decimals. Send it together with `multiplier_factor`. Example: `500.00` VAT category code. The API does not write a tax category on a line-level charge in the UBL. VAT rate as a percentage. The API does not write it on a line-level charge in the UBL. Example: ```json theme={null} [ { "reason": "Additional packaging", "reason_code": "ABL", "amount": 50.00 } ] ``` See [Advanced invoicing](/guides/advanced-invoicing) for document-level allowances and charges and their effect on the totals. ## Examples ### Simple line item ```json theme={null} { "description": "Product A", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": 21.00 } ``` * Amount: 10 × 100.00 = 1000.00 * Tax: 1000.00 × 21% = 210.00 * Total with tax: 1210.00 ### Service line item ```json theme={null} { "description": "Consulting services - September 2026", "quantity": 40, "unit": "HUR", "unit_price": 150.00, "amount": 6000.00, "tax_rate": 21.00 } ``` * Amount: 40 hours × 150.00 = 6000.00 * Tax: 6000.00 × 21% = 1260.00 * Total with tax: 7260.00 ### Line item with item attributes ```json theme={null} { "description": "T-shirt with logo", "product_code": "TS-BLUE-XL", "quantity": 25, "unit": "C62", "unit_price": 12.00, "amount": 300.00, "tax_rate": 21.00, "item_attributes": [ { "name": "Colour", "value": "Blue" }, { "name": "Size", "value": "XL" } ] } ``` The receiver gets the attributes in the UBL: ```xml theme={null} Colour Blue Size XL ``` ### Line item with a percentage allowance ```json theme={null} { "description": "Premium product B", "quantity": 100, "unit": "C62", "unit_price": 50.00, "amount": 4500.00, "tax_rate": 21.00, "allowances": [ { "reason": "Bulk discount", "reason_code": "95", "multiplier_factor": 10, "base_amount": 5000.00, "amount": 500.00 } ] } ``` * Base: 100 × 50.00 = 5000.00 * Allowance: 5000.00 × 10% = 500.00 * Amount: 4500.00 * Tax: 4500.00 × 21% = 945.00 * Total with tax: 5445.00 ### Line item with a charge ```json theme={null} { "description": "Fragile equipment", "quantity": 1, "unit": "C62", "unit_price": 500.00, "amount": 550.00, "tax_rate": 21.00, "charges": [ { "reason": "Additional packaging", "reason_code": "ABL", "amount": 50.00 } ] } ``` * Base: 1 × 500.00 = 500.00 * Charge: 50.00 * Amount: 550.00 * Tax: 550.00 × 21% = 115.50 * Total with tax: 665.50 ### Lines with different tax rates ```json theme={null} { "items": [ { "description": "Standard product", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": 21.00 }, { "description": "Reduced rate product (books)", "quantity": 5, "unit": "C62", "unit_price": 20.00, "amount": 100.00, "tax_rate": 6.00 }, { "description": "Zero-rated product", "quantity": 3, "unit": "C62", "unit_price": 200.00, "amount": 600.00, "tax_rate": 0.00 } ] } ``` * Line 1: 1000.00 + 210.00 tax (21%) = 1210.00 * Line 2: 100.00 + 6.00 tax (6%) = 106.00 * Line 3: 600.00 + 0.00 tax (0%) = 600.00 * Total with tax: 1916.00 ## Calculation flow 1. Base amount: `quantity × unit_price / price_base_quantity` 2. Subtract the line-level allowances. 3. Add the line-level charges. 4. Line amount: `base - allowances + charges`, rounded half-up to 2 decimals. This is the value of `amount`. 5. Tax: `amount × (tax_rate / 100)` Example with an allowance and a charge: ```json theme={null} { "description": "Configured product", "quantity": 20, "unit": "C62", "unit_price": 100.00, "amount": 1850.00, "tax_rate": 21.00, "allowances": [ { "amount": 200.00, "reason": "Volume discount" } ], "charges": [ { "amount": 50.00, "reason": "Customisation fee" } ] } ``` 1. Base: 20 × 100.00 = 2000.00 2. Allowance: 200.00 3. Charge: 50.00 4. Amount: 1850.00 5. Tax (21%): 388.50 In the document totals, the API calculates VAT for each tax group ([tax category](/glossary#tax-category-code) and rate), not for each line. See [Normalisation and rounding](/api-reference/schemas/document#normalisation-and-rounding). ## Best practices A specific description tells the customer what the line is for. Recommended: ```json theme={null} { "description": "Consulting services - Project XYZ - September 2026" } ``` Not recommended: ```json theme={null} { "description": "Services" } ``` Use the unit that agrees with the item: * Products and goods: `"C62"` (pieces) * Services by time: `"HUR"` (hours) or `"DAY"` (days) * Materials by weight: `"KGM"` (kilograms) * Materials by volume: `"LTR"` (litres) `tax_rate` is a number from 0 to 100. A string is accepted for backward compatibility, but a number with 2 decimals is preferred. Preferred: ```json theme={null} { "tax_rate": 21.00 } ``` Also accepted: ```json theme={null} { "tax_rate": "21.00" } ``` * Unit prices: a maximum of 4 decimals * Quantities: a maximum of 4 decimals * Amounts and tax rates: 2 decimals ## Next Steps Look up the document fields and their conversion to UBL. Apply allowances and charges on the line and on the document. Calculate the totals from the line amounts. Create and send an invoice with line items. # Get information about your account Source: https://docs.e-invoice.be/api-reference/tenant/get-information-about-your-account /openapi.json get /api/me/ Retrieve information about your account. # Get Usage Statistics Source: https://docs.e-invoice.be/api-reference/tenant/get-usage-statistics /openapi.json get /api/stats Retrieve usage statistics for billing purposes. Returns statistics for document sent and document received actions, grouped by aggregation period (day/week/month). If no date parameters are provided, returns daily stats for all available data. # Validate JSON Document Source: https://docs.e-invoice.be/api-reference/validate/validate-json-document /openapi.json post /api/validate/json Validate if the JSON document can be converted to a valid UBL document # Validate Peppol ID Source: https://docs.e-invoice.be/api-reference/validate/validate-peppol-id /openapi.json get /api/validate/peppol-id Validate if a Peppol ID exists in the Peppol network and retrieve supported document types. The peppol_id must be in the form of `:`. The scheme is a 4-digit code representing the identifier scheme, and the id is the actual identifier value. For example, for a Belgian company it is `0208:0123456789` (where 0208 is the scheme for Belgian enterprises, followed by the 10 digits of the official BTW / KBO number). # Validate UBL Document Source: https://docs.e-invoice.be/api-reference/validate/validate-ubl-document /openapi.json post /api/validate/ubl Validate the correctness of a UBL document # Create Webhook Source: https://docs.e-invoice.be/api-reference/webhooks/create-webhook /openapi.json post /api/webhooks/ Create a new webhook # Delete Webhook Source: https://docs.e-invoice.be/api-reference/webhooks/delete-webhook /openapi.json delete /api/webhooks/{webhook_id} Delete a webhook # Get All Webhooks Source: https://docs.e-invoice.be/api-reference/webhooks/get-all-webhooks /openapi.json get /api/webhooks/ Get all webhooks for the current tenant # Get Webhook Source: https://docs.e-invoice.be/api-reference/webhooks/get-webhook /openapi.json get /api/webhooks/{webhook_id} Get a webhook by ID # Get Webhook History Source: https://docs.e-invoice.be/api-reference/webhooks/get-webhook-history /openapi.json get /api/webhooks/{webhook_id}/history Get the history of a webhook # Test Webhook Source: https://docs.e-invoice.be/api-reference/webhooks/test-webhook /openapi.json post /api/webhooks/{webhook_id}/test Send a test event to a webhook for testing and debugging purposes # Update Webhook Source: https://docs.e-invoice.be/api-reference/webhooks/update-webhook /openapi.json put /api/webhooks/{webhook_id} Update a webhook by ID # Authentication Source: https://docs.e-invoice.be/authentication Authenticate your API requests with the API key of your company as a Bearer token. ## Overview The e-invoice.be API uses Bearer token authentication. Each [company](/glossary#company) has its own API key. Send the key in the `Authorization` header of each request to `https://api.e-invoice.be`. The API key identifies the company. Thus the key also controls if a send goes to the Peppol network ([production company](/glossary#production-company)) or to email ([sandbox company](/glossary#sandbox-company)). Develop and test with a sandbox company. A sandbox company runs in test mode: the API sends each document as UBL XML to the contact email address of the company, and nothing goes to the Peppol network. The API host and the endpoints are the same as for a production company. See [Test mode and sandbox companies](/environments). ## Get your API key Sign in to [app.e-invoice.be](https://app.e-invoice.be). Select the company that you want to use. A sandbox company and a production company have different keys. Open **API Settings** and copy the API key. Keep your API key secret. Do not put it in version control, in client-side code or in a public location. An API key starts with `api-`, followed by 32 characters. The key has no expiry date. It stops working only when it is reset or deleted. ## Make an authenticated request Add the header `Authorization: Bearer ` to the request. The samples read the key from the environment variable `E_INVOICE_API_KEY` and call `GET /api/me/`, which returns the data of the company that owns the key. ```bash cURL theme={null} curl "https://api.e-invoice.be/api/me/" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```javascript Node.js theme={null} const response = await fetch("https://api.e-invoice.be/api/me/", { headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }, }); const result = await response.json(); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${JSON.stringify(result)}`); } console.log(result); ``` ```python Python theme={null} import os import requests response = requests.get( "https://api.e-invoice.be/api/me/", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, timeout=30, ) if not response.ok: raise SystemExit(f"HTTP {response.status_code}: {response.text}") print(response.json()) ``` ```php PHP theme={null} true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("E_INVOICE_API_KEY"), ], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status >= 400) { throw new RuntimeException("HTTP $status: $body"); } print_r(json_decode($body, true)); ``` ```csharp C# theme={null} using System.Net.Http.Headers; using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var response = await client.GetAsync("/api/me/"); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {body}"); } Console.WriteLine(body); ``` A correct key gives a `200` response: ```json theme={null} { "name": "E-INVOICE BV", "description": null, "plan": "starter", "credit_balance": 100, "peppol_ids": ["0208:1018265814"], "ibans": null, "company_number": "1018265814", "company_tax_id": "BE1018265814", "company_name": "E-INVOICE BV", "company_address": "Brusselsesteenweg 119/A", "company_zip": "1980", "company_city": "Zemst", "company_country": "Belgium", "company_email": "billing@e-invoice.be", "smp_registration": true, "smp_registration_date": "2025-01-15T10:30:00Z", "bcc_recipient_email": null } ``` The response also contains the plan and the credit balance. See [Usage statistics and credits](/guides/usage-statistics). ## Endpoints without authentication Three read-only Peppol lookup operations do not require an API key: `GET /api/validate/peppol-id`, `GET /api/lookup` and `GET /api/lookup/participants`. All other operations require the `Authorization` header. ## Best practices ### Keep the key in an environment variable Do not write the key in your source code. Read it from an environment variable or from a secret manager. ```bash theme={null} # .env (add this file to .gitignore) E_INVOICE_API_KEY=your_api_key E_INVOICE_BASE_URL=https://api.e-invoice.be ``` ### Use a different key for tests and for production Use the key of a sandbox company in your development and test systems. Use the key of a production company only in your production system. The base URL and your code are the same for the two. ### Reset a key that is no longer safe If a key is possibly known to other persons, open **API Settings** in the app and select **Reset API key**. The app creates a new key and the previous key stops working. Then update your applications with the new key. ## Error responses A request with no valid key returns `401 Unauthorized` with the header `WWW-Authenticate: Bearer`. If the `Authorization` header is missing: ```json theme={null} { "detail": "Authentication required" } ``` If the key is not correct or is deleted: ```json theme={null} { "detail": "Invalid authentication" } ``` ### Troubleshooting The header value must be `Bearer`, one space, then the key. Make sure that the key has no spaces or line breaks before or after it. A sandbox company and a production company have different keys. A key that was reset in the app does not work. Call `GET /api/me/` with the `-v` option and read the response status. ```bash theme={null} curl -v "https://api.e-invoice.be/api/me/" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` For all status codes and error formats, see [Errors and troubleshooting](/guides/errors). ## Next Steps Create a sandbox company and learn what test mode does. Make your first API call. Create and send an e-invoice. Validate invoice payloads during development. # Changelog Source: https://docs.e-invoice.be/changelog See which API behaviour and documentation changed, and which endpoints and parameters are deprecated. This page lists changes that have an effect on an existing integration. The API has one version and no version number in its paths. The current version of the OpenAPI specification is `1.1.0`. The date of each entry is the date on which the change was published in this documentation. The Admin API documentation now lists all fields of tenant create and tenant update. A tenant in test mode must have a `company_email`. If `company_email` is not set, the send request returns `400` with the message `Tenant is in test mode but no company email is set`. The guides were compared with the current API, and the incorrect statements were corrected. Changes that can have an effect on an integration: * Credit notes: give the original invoice in `preceding_invoice_id` and `preceding_invoice_date`. Do not use `purchase_order` for this. * `POST /api/validate/json` and `POST /api/validate/ubl` return `422` for a request error. Before, some of these errors returned `500`. * The unit code `KWH` is not accepted. A unit code that is not in the list gives `422`. * The `enabled` field of a webhook does not stop delivery. To stop delivery, delete the webhook. * The OpenAPI specification now contains `GET /api/documents/{document_id}/lifecycle`. See [Admin API](/admin-api), [Test mode and sandbox companies](/environments) and [Errors and troubleshooting](/guides/errors). The Admin API documentation now describes send-only tenants. A tenant can send documents on Peppol without a registration on the SMP. Tenant creation and tenant update do not register the tenant on the Peppol network. Registration is a separate step that is necessary only when the tenant must receive documents. See [Admin API](/admin-api#send-only-tenants). The OpenAPI specification now contains the Mailbox (inbound email) endpoints: * `GET /api/mailbox/` * `GET /api/mailbox/{inbound_email_id}` * `GET /api/mailbox/{inbound_email_id}/attachments/{filename}` * `POST /api/mailbox/{inbound_email_id}/reprocess` See [Send invoices by email (Mailbox)](/guides/mailbox). The documentation now describes one API host, `https://api.e-invoice.be`. References to a second host for development were removed. Tests are done with a sandbox company. A sandbox company is in test mode: outbound documents go to email, and inbound documents are added with Simulate inbound. Test mode is fixed when the company is created. A sandbox company cannot be converted into a production company. See [Test mode and sandbox companies](/environments) and [Go-live checklist](/going-live). ## Deprecated items These items are marked as deprecated in the API. They continue to operate, but do not use them in new integrations. | Item | Type | | - | - | | `GET /api/outbox/drafts` | Endpoint | | `POST /api/documents/{document_id}/attachments` | Endpoint | | `email` on `POST /api/documents/{document_id}/send` | Query parameter | | `sender` on `GET /api/outbox/` | Query parameter | ## Next Steps Read the reference for all endpoints Learn about the API host and sandbox companies Move from a sandbox company to a production company # peppol CLI Source: https://docs.e-invoice.be/cli Install the peppol command-line tool and validate, create and send Peppol documents from a terminal or a script. `peppol` is the command-line tool for the e-invoice.be API. Use it to send a test invoice without code, to examine received documents, or to automate tasks in a shell script. The source is in the repository [e-invoice-be/peppol-cli](https://github.com/e-invoice-be/peppol-cli). The CLI calls the single API host `https://api.e-invoice.be`. The API key selects the mode: a key of a sandbox company runs the commands in test mode. ## Install ```bash theme={null} brew tap e-invoice-be/tap brew install e-invoice-be/tap/peppol-cli ``` Use Homebrew: ```bash theme={null} brew tap e-invoice-be/tap brew install e-invoice-be/tap/peppol-cli ``` As an alternative, download the `linux_amd64` or `linux_arm64` archive from [GitHub Releases](https://github.com/e-invoice-be/peppol-cli/releases), extract it, and move the `peppol` file to a directory in your `PATH`. Download the `.zip` file for Windows from [GitHub Releases](https://github.com/e-invoice-be/peppol-cli/releases), extract it, and add `peppol.exe` to your `PATH`. Make sure that the installation is correct: ```bash theme={null} peppol version ``` ## Authenticate The CLI can get the API key from two sources. ```bash theme={null} peppol auth ``` The command opens the API settings page of the app in your browser and asks you to paste the API key. It calls `GET /api/me/` to make sure that the key is valid, and then stores the key in a workspace. ```bash theme={null} # Show the authentication status peppol auth status # Remove the stored key of the active workspace peppol auth logout ``` ```bash theme={null} export PEPPOL_API_KEY="your-api-key" ``` Use this method in scripts and in CI. The CLI stores nothing. Show the account of the key: ```bash theme={null} peppol me ``` `PEPPOL_API_KEY` has priority over all stored keys. While the variable is set, the `--workspace` flag and the active workspace do not change the key that the CLI uses. ## Workspaces A workspace is a name for one stored API key. Each company has its own API key, thus you use one workspace for each company. The CLI stores workspaces in `~/.config/peppol-cli`, or in `$XDG_CONFIG_HOME/peppol-cli` if that variable is set. | Command | Function | | - | - | | `peppol workspace add ` | Asks for an API key, makes sure that it is valid, and stores it with this name | | `peppol workspace list` | Shows all workspaces and the active workspace | | `peppol workspace use ` | Makes a workspace the active workspace | | `peppol workspace remove ` | Removes a workspace and its stored key | `peppol auth` also makes a workspace. Its name comes from the company name, or from the `--workspace` flag if you give it. ### Use a sandbox company and a production company together Paste the API key of the related company when the command asks for it. ```bash theme={null} peppol workspace add sandbox peppol workspace add production ``` ```bash theme={null} peppol workspace use sandbox ``` All commands now use the sandbox company. The global flag `--workspace` (short form `-w`) applies to one command only. The active workspace does not change. ```bash theme={null} peppol outbox list -w production ``` Keep the workspace of the sandbox company as the active workspace. Then a command without `-w` cannot send a document on the Peppol network. ## Validate, create and send an invoice This procedure uses a file `invoice.json` with the invoice JSON. The [Quickstart](/quickstart) shows how to make this file. Validation is not a separate mandatory call. `POST /api/documents/` rejects a payload that does not pass the same rules. Use `POST /api/validate/json` while you develop, because it returns all rule failures and the generated UBL. ```bash theme={null} peppol validate json invoice.json ``` The command shows `Validation: PASSED`, or `Validation: FAILED` with the number of errors and warnings and a table with the columns `SEVERITY`, `RULE ID`, `MESSAGE` and `LOCATION`. If the document is not valid, the exit code is `3`. To read the JSON from standard input, use `--file -`: ```bash theme={null} cat invoice.json | peppol validate json --file - ``` ```bash theme={null} peppol document create json invoice.json ``` The command creates a document in the `DRAFT` state and shows its details. Record the document ID. Add `--construct-pdf` to let the API make a PDF from the document. ```bash theme={null} peppol document send ``` With a sandbox company, the send does not use the Peppol network. See [Test mode and sandbox companies](/environments). ```bash theme={null} peppol document get peppol document timeline ``` In a script, use `--json` and get the document ID from the output. This sample uses `jq`: ```bash theme={null} DOCUMENT_ID=$(peppol document create json invoice.json --json | jq -r '.id') peppol document send "$DOCUMENT_ID" --json ``` ### Set the Peppol IDs for a send `peppol document send` accepts flags that give the sender and the receiver explicitly: Peppol scheme of the sender, for example `0208`. Peppol ID of the sender. Peppol scheme of the receiver. Peppol ID of the receiver. Email address for the `email` parameter of the send call. ## Other common tasks ### Create a document from UBL or PDF ```bash theme={null} # From a UBL XML file peppol document create ubl invoice.xml # From a PDF file peppol document create pdf invoice.pdf --vendor-tax-id BE1018265814 --customer-tax-id BE0848934496 ``` The flags `--vendor-tax-id` and `--customer-tax-id` are optional. See [Send UBL documents](/guides/ubl-documents) and [Create documents from PDF](/guides/pdf-documents). ### Validate ```bash theme={null} # A UBL XML file peppol validate ubl invoice.xml # A document that is already in your account peppol document validate # A Peppol ID: format and registration on the network peppol validate peppol-id 0208:0848934496 ``` For the Peppol ID check, see [Look up Peppol participants](/guides/lookup-participants). ### Look up a participant ```bash theme={null} peppol lookup 0208:0848934496 peppol lookup search "OpenPeppol" --country BE ``` ### List documents ```bash theme={null} peppol inbox list peppol inbox invoices peppol inbox credit-notes peppol outbox list peppol drafts list ``` The list commands accept the flags `--page`, `--page-size`, `--from`, `--to`, `--search`, `--type`, `--sort-by` and `--sort-order`. The inbox commands also accept `--sender`. `peppol outbox list` also accepts `--receiver`. ### Download, delete and attach ```bash theme={null} # Download the UBL XML of a document peppol document ubl -o invoice.xml # Delete a draft document peppol document delete # Attachments peppol document attachment list peppol document attachment add terms.pdf peppol document attachment get -o terms.pdf ``` ### Usage statistics ```bash theme={null} peppol stats peppol stats --from 2026-01-01 --to 2026-03-01 --aggregation MONTH ``` `--aggregation` accepts `DAY`, `WEEK` or `MONTH`. See [Usage statistics and credits](/guides/usage-statistics). ### Back up all documents ```bash theme={null} peppol backup ./peppol-archive ``` The command writes each document, its attachments, its UBL XML and its timeline to the directory. ## Automation ### Global flags These flags are available on all commands: | Flag | Short form | Function | | - | - | - | | `--json` | `-j` | Writes the output as JSON | | `--quiet` | `-q` | Does not write output that is not essential | | `--verbose` | `-v` | Writes more information | | `--no-color` | | Does not use colours in the output | | `--workspace ` | `-w` | Uses this workspace for the command | ### Confirmation prompts `peppol document delete` and `peppol document attachment delete` ask for a confirmation. In a script, add `--yes` to these two commands. `--yes` is not a global flag. ```bash theme={null} peppol document delete --yes ``` ### Exit codes | Code | Meaning | | - | - | | `0` | The command was successful | | `1` | General error, for example a file that does not exist or an error response from the API | | `2` | Authentication error: no API key is available, or the API does not accept the key | | `3` | Validation failed: `peppol validate json` or `peppol validate ubl` found that the document is not valid | | `4` | Not found: the document or the attachment does not exist | With `--json`, `peppol validate json` and `peppol validate ubl` write the validation result and stop with exit code `0`, also when the document is not valid. In a script that uses `--json`, read the field `is_valid` from the output. `peppol document validate` does not use exit code `3` in any mode. When a command fails and `--json` is set, the CLI writes the error as JSON to standard error: ```json theme={null} { "error": "document not found", "code": 4 } ``` ### Sample script ```bash theme={null} #!/usr/bin/env bash set -euo pipefail # PEPPOL_API_KEY is set in the environment if ! peppol validate json invoice.json --quiet; then echo "The invoice is not valid" >&2 exit 1 fi DOCUMENT_ID=$(peppol document create json invoice.json --json | jq -r '.id') peppol document send "$DOCUMENT_ID" --json ``` ### Shell completion `peppol completion bash`, `peppol completion zsh`, `peppol completion fish` and `peppol completion powershell` write a completion script for your shell. ## Use with AI agents An AI agent that can run shell commands can use the CLI with `--json`, `--yes` and the exit codes. To connect an AI assistant directly to your account without the CLI, use the MCP server. See [Model Context Protocol (MCP)](/essentials/mcp). ## Report a problem Report a problem with the CLI in the [issues of the repository](https://github.com/e-invoice-be/peppol-cli/issues). ## Next Steps Call the API with a typed client in your language Learn how a sandbox company runs in test mode Understand the validation result Connect an AI assistant to your account # Test mode and sandbox companies Source: https://docs.e-invoice.be/environments Use a sandbox company to build and test your integration on the single API host without Peppol traffic. ## Overview e-invoice.be has one API host: `https://api.e-invoice.be`. There is no separate host for development or tests. The company that you authenticate as controls what a send does: * A [production company](/glossary#production-company) sends and receives documents on the Peppol network. * A [sandbox company](/glossary#sandbox-company) runs in [test mode](/glossary#test-mode). The API sends each document to an email address, and you simulate inbound documents. No Peppol traffic occurs. Each company has its own API key. To change from tests to production, you change the API key. The base URL and your code stay the same. ## Terminology The documentation uses these terms: | Term | Meaning | | - | - | | **Sandbox company** | A company that runs in test mode. It has its own API key and does not exchange documents on the Peppol network. | | **Production company** | A company that sends and receives documents on the Peppol network. | | **Company** | The unit that sends and receives documents in the app and in the API. A company is a sandbox company or a production company. | | **Tenant** | The name of a company in the Admin API and in the `tenant_id` field of a webhook payload. A tenant is a company that a [reseller](/reseller-programme) manages. | | **Organisation** | The account of a reseller. An organisation has one Admin API key and manages many tenants. | | **Contact email address** | The email address of the company (the `company_email` field). In test mode, the API sends each outbound document to this address. A reseller sets it with the Admin API. | | **Test mode** | The operating mode of a sandbox company. It is set when you create the company and you cannot change it. | ## API host Use one base URL for all calls: ```bash theme={null} # .env E_INVOICE_API_KEY=your_api_key E_INVOICE_BASE_URL=https://api.e-invoice.be ``` Put the API key of a sandbox company in `E_INVOICE_API_KEY` during development. Put the API key of a production company there when you go live. ## Test mode When a company is in test mode: * The API does not send documents on the Peppol network. It sends an email with the UBL XML as an attachment to the contact email address of the company. * The company does not receive documents from the Peppol network. You add inbound documents with [Simulate inbound](/glossary#simulate-inbound) (see [Testing received documents](#testing-received-documents)). * The Peppol registration operations of the Admin API return `403 Forbidden` for a test-mode organisation. For a test-mode tenant of a production organisation, they make no change on the SMP. See [Peppol registration](/admin-api#peppol-registration). * Document creation, validation, webhooks, the inbox, the outbox and all other endpoints operate as they do for a production company. Test mode is set when the company is created and does not change. You cannot convert a sandbox company into a production company, or a production company into a sandbox company. Create the type of company that you need. ## Sandbox companies A sandbox company is a company in test mode. In the app and in the API it behaves as a production company does: it has settings, documents, webhooks and an API key. * **Test mode is permanent.** Outbound documents go to the contact email address and inbound documents are simulated. * **Synthetic identifiers.** The app suggests a Belgian VAT number that is not real. You can change it. The app does not do a KBO, VIES, email, telephone or payment verification. * **Webhooks operate.** The `document.sent` and `document.received` events are sent as they are for a production company. * **No billing.** `GET /api/me/` also returns `plan` and `credit_balance` for a sandbox company, but these values have no effect in test mode. * **Own API key.** Use the key as the Bearer token for `https://api.e-invoice.be`. ### Create a sandbox company Sign in to the app at [app.e-invoice.be](https://app.e-invoice.be). Open **Companies** and select **Create sandbox company**. The suggested values are safe placeholders and you can change them. Select **Suggest valid VAT** to get a synthetic Belgian VAT number. Enter a **Contact email** that you can read: this address receives the documents that you send. Open **API Settings** in the sandbox company and copy the API key. ### What a send does in test mode `POST /api/documents/` operates as it does for a production company. The document gets the `DRAFT` state. `POST /api/documents/{document_id}/send` changes the state to `TRANSIT`. The API then sends an email with the UBL XML to the contact email address of the company and changes the state to `SENT`. No Peppol transmission occurs. The `document.sent` webhook is sent as it is for a production company. If the company has no contact email address (`company_email`), `POST /api/documents/{document_id}/send` returns `400 Bad Request`: ```json theme={null} { "detail": "Tenant is in test mode but no company email is set" } ``` The API sends no email and no webhook event. But the API changes the state of the document to `SENT`, so you cannot send the same document again. Set the contact email address, then create the document again and send it. * In the app: enter the **Contact email** when you [create the sandbox company](#create-a-sandbox-company). * With the Admin API: set `company_email` when you [create a tenant](/admin-api#create-a-tenant) in test mode. To add it later, call [Update a tenant](/admin-api#update-a-tenant) with `company_email`. ## Testing received documents Use **Simulate inbound** to test the inbox. It puts a UBL document in the inbox of your sandbox company as if the document came from the Peppol network. The document appears in `GET /api/inbox/` with the `RECEIVED` state, and the `document.received` webhooks are sent. For the requests that read the inbox, see [Receive documents](/guides/receiving-documents). You cannot put a document in your own inbox with a send to yourself. In test mode, a send goes to email, so the document gets the `SENT` state and does not come back in the inbox. The API also ignores the `direction` field in `POST /api/documents/`: this endpoint always creates `OUTBOUND` drafts. Simulate inbound is the only method to fill the inbox of a sandbox company. ### In the app In a sandbox company, open **Inbox** and select **Simulate inbound**. Then do one of these: * Add the built-in sample invoice. * Upload your own UBL XML file. The app replaces the receiver identifiers with the identifiers of your company, so that the document is addressed to you. If the **Simulate inbound** button does not show, make sure that the active company is a sandbox company, or contact [support@e-invoice.be](mailto:support@e-invoice.be). ### With the Admin API A reseller can do the same for each sandbox company that the organisation manages. See [Simulate an inbound document](/admin-api#simulate-an-inbound-document) in the Admin API. ## Recommended development workflow Use its API key with `https://api.e-invoice.be`. Use `POST /api/validate/json` while you develop. Test more than one invoice scenario and examine the tax rates and the totals. See [Validation during development](/guides/validation). Create and send documents and read the UBL in the email. Use Simulate inbound to test the receive flow. Make sure that your webhook handler processes the events. Create a separate production company and use its API key. You cannot convert a sandbox company. Follow the [go-live checklist](/going-live). ## Frequently asked questions Create a sandbox company in the app and use its API key with `https://api.e-invoice.be`. No document goes to the Peppol network. There is one host: `https://api.e-invoice.be`. Use it with a sandbox company for tests and with a production company for production. No. The type of company is set at creation. Create a production company and use its API key. Replace the API key of the sandbox company with the API key of a production company. The base URL and your code stay the same. The API then sends documents on the Peppol network. See the [go-live checklist](/going-live). Yes. Webhooks operate the same in test mode. You get `document.sent` after a send (by email) and `document.received` after a Simulate inbound. ## Next Steps Make your first API call. Validate invoice payloads during development. Receive notifications for document events. Move from a sandbox company to a production company. # Model Context Protocol (MCP) Source: https://docs.e-invoice.be/essentials/mcp Connect an AI assistant to your e-invoice.be company with the Model Context Protocol (MCP) and read your documents, Peppol data and usage statistics. The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) is an open standard that lets AI assistants use external services. The e-invoice.be MCP server gives AI agents access to your invoices, credit notes, Peppol network data and usage statistics. All MCP tools are read-only. An MCP client cannot create, change or delete data. To create or send documents, use the [API reference](/api-reference). To create or send documents from a terminal, or from an AI agent that can run shell commands, use the [peppol CLI](/cli). ## Prerequisites Before you set up MCP, you need: 1. An e-invoice.be account. Sign up at [app.e-invoice.be](https://app.e-invoice.be). 2. The API key of your company. Open **API Settings** in the app and copy the key. See [Authentication](/authentication). The server URL is `https://api.e-invoice.be/mcp`. It is the same for all clients. The transport is streamable HTTP, and you authenticate with your API key as a Bearer token. The server does not check the API key when the client connects or lists the tools. The API checks the key at each tool call. Thus a client with a wrong key can connect and show the tools, but each tool call that needs a key gives HTTP `401`. ## Setup [Claude Code](https://docs.anthropic.com/en/docs/claude-code) supports remote MCP servers with streamable HTTP. Use the command line (recommended): ```bash theme={null} claude mcp add --transport http e-invoice https://api.e-invoice.be/mcp \ --header "Authorization: Bearer YOUR_API_KEY" ``` This command registers the server at local scope: only you can use it, and only in the current project. To use it in all your projects, add `--scope user`. To share it with your team through the repository, add `--scope project`. Project scope writes the server to the file `.mcp.json` in the project root. Do not commit a real API key in `.mcp.json`. As an alternative, add this JSON to `.mcp.json` in the project root (project scope): ```json theme={null} { "mcpServers": { "e-invoice": { "type": "http", "url": "https://api.e-invoice.be/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } } ``` Then restart Claude Code or run `/mcp` and make sure that the server is in the list. Official documentation for MCP in Claude Code Claude Desktop does not connect directly to remote streamable HTTP MCP servers. Use [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) as a bridge. It needs Node.js 18 or later. Select **Claude → Settings → Developer → Edit Config**. The file location is: | OS | Configuration file | | - | - | | macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` | | Windows | `%APPDATA%\Claude\claude_desktop_config.json` | | Linux | `~/.config/Claude/claude_desktop_config.json` | Add the `mcpServers` key, or merge the entry into the key that is there: ```json theme={null} { "mcpServers": { "e-invoice": { "command": "npx", "args": [ "mcp-remote@latest", "https://api.e-invoice.be/mcp", "--header", "Authorization: Bearer YOUR_API_KEY" ] } } } ``` The e-invoice tools then show in the tools menu of the composer. Official documentation for connecting MCP servers to Claude Desktop ChatGPT supports remote MCP servers as custom connectors on the Plus, Pro, Business and Enterprise plans. Custom connectors in developer mode are not available for all account types in the EEA, the United Kingdom and Switzerland. Make sure that your ChatGPT plan supports this feature before you continue. Open [chatgpt.com](https://chatgpt.com) and go to **Settings → Connectors → Advanced**. Set **Developer mode** to on. Select **Create → Add custom connector** and enter these values: * **Name:** `e-invoice` * **Description:** Read and look up Peppol invoices * **MCP server URL:** `https://api.e-invoice.be/mcp` * **Authentication:** Custom header, with header name `Authorization` and header value `Bearer YOUR_API_KEY` Save the connector and accept the trust prompt. In each chat, select **+ → Connectors** and enable the connector. Learn about connectors in ChatGPT Enable custom MCP connectors Add this JSON to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (one project): ```json theme={null} { "mcpServers": { "e-invoice": { "url": "https://api.e-invoice.be/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } } ``` Save the file, then restart Cursor or open a new chat session. The tools show in the MCP tools list. Official documentation for MCP in Cursor For GitHub Copilot in VS Code, add this JSON to `.vscode/mcp.json` in the root of your project: ```json theme={null} { "servers": { "e-invoice": { "type": "http", "url": "https://api.e-invoice.be/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } } ``` As an alternative, add the server to your user or workspace `settings.json` below the `mcp.servers` key. Official documentation for MCP in VS Code Add this JSON to `~/.codeium/windsurf/mcp_config.json`: ```json theme={null} { "mcpServers": { "e-invoice": { "type": "http", "url": "https://api.e-invoice.be/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } } ``` Save the file, then restart Windsurf. If you have no API key, see [Authentication](/authentication). ## Available tools ### Documents | Tool | Description | | - | - | | `get_document` | Get one invoice or credit note by ID, with line items, tax details, payment data and attachments | | `get_document_attachments` | List all attachments of a document (see [Attachments and PDF](/guides/attachments)) | | `get_document_attachment` | Get one attachment with a signed download URL (valid for 1 hour) | | `get_document_timeline` | Get the event history of a document in time sequence (creation, validation, transmission, delivery). See [Document lifecycle and delivery tracking](/guides/document-lifecycle) | ### Inbox | Tool | Description | | - | - | | `get_inbox` | List all received documents; filter by type, sender, date range and search text | | `get_inbox_invoices` | List only received invoices | | `get_inbox_credit_notes` | List only received credit notes | ### Outbox | Tool | Description | | - | - | | `get_outbox` | List all sent documents; filter by receiver, date range and search text | ### Drafts | Tool | Description | | - | - | | `get_drafts` | List draft documents; filter by state (`DRAFT`, `TRANSIT`, `FAILED`), type and date range | ### Peppol lookup These tools are public and need no authentication. | Tool | Description | | - | - | | `get_lookup_peppol_id` | Check if a participant is registered on the Peppol network and get the document types that it supports | | `get_lookup_participants` | Search for Peppol participants by name, with an optional country filter | ### Statistics | Tool | Description | | - | - | | `get_stats` | Get send and receive statistics by day, week or month. See [Usage statistics and credits](/guides/usage-statistics) | ## Example prompts When the client is connected, you can ask questions about your company in natural language: * *"Show me all invoices received this month"* * *"Get the details and timeline of document doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"* * *"Is the company with VAT number BE0848934496 registered on Peppol?"* * *"Search for Peppol participants named 'OpenPeppol' in Belgium"* * *"Show my send and receive statistics for the last 3 months, by month"* * *"List all failed documents in my drafts"* * *"What attachments are on invoice doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d?"* ## Troubleshooting 1. Restart the application. Most MCP clients load the server configuration only when they start. 2. Make sure that the server URL is `https://api.e-invoice.be/mcp`, with no slash at the end. 3. Make sure that the configuration file is valid JSON. A wrong API key does not prevent the tools from showing. See **Authentication errors** below. * Look for errors in the log file: * macOS: `tail -n 50 ~/Library/Logs/Claude/mcp.log` * Windows: open `%APPDATA%\Claude\logs\mcp.log` * Make sure that Node.js 18 or later is installed and on your `PATH`: `node --version`. * Run `mcp-remote` manually to see authentication errors: ```bash theme={null} npx mcp-remote@latest https://api.e-invoice.be/mcp \ --header "Authorization: Bearer $E_INVOICE_API_KEY" ``` * Make sure that the JSON is valid. If the file has a comma at the end of a list or a missing quotation mark, Claude Desktop does not load the server and shows no error. * Custom connectors need **Developer mode**. Enable it in **Settings → Connectors → Advanced**. * Developer mode is not available on all plans and in all regions. Limits can apply in the EEA, the United Kingdom and Switzerland. * Enable the connector in each new chat with the **+ → Connectors** menu. A wrong or absent API key gives HTTP `401` at the tool call, not when the client connects. * Make sure that your API key is correct. This request must return your company data: ```bash theme={null} curl -s https://api.e-invoice.be/api/me/ \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` * Make sure that you copied the full API key. A company API key starts with `api-`. * An organisation API key for the [Admin API](/admin-api) does not work with MCP. Use the API key of a company. * If you reset your key, update the configuration file and restart the client. * For the HTTP status codes of the API, see [Errors and troubleshooting](/guides/errors). ## Security Your API key gives an MCP client access to all data of your company that the read-only tools can return. Protect it as you protect all API credentials. * **Read-only access:** MCP tools cannot create, change or delete data. * **Company scope:** the API key controls which company the tools can read. An API key belongs to one company. * **Public lookup tools:** `get_lookup_peppol_id` and `get_lookup_participants` operate without authentication. * **Sandbox company first:** use the API key of a sandbox company while you try MCP. See [Test mode and sandbox companies](/environments). ## Next Steps Get your API key and learn how the API authenticates requests Create and send documents with the REST API Create, validate and send documents from a terminal or a script Receive a notification for each document event # Webhooks Source: https://docs.e-invoice.be/essentials/webhooks Create a webhook, verify the signature of each event in your receiver, and debug deliveries with the test call and the delivery history. A [webhook](/glossary#webhook) is an HTTP `POST` request that the API sends to your server when a document event occurs. Use webhooks to learn that a document was sent, received or failed without polling the API. Each webhook belongs to one company. A sandbox company and a production company each have their own API key and their own webhooks. ## Set up a webhook Send the URL of your receiver and the events that you want. See [Create a webhook](#create-a-webhook). The response contains the `secret`. Put it in a secret manager or an environment variable. See [Get the signing secret](#get-the-signing-secret). Verify the `X-Signature` header, return a `2xx` status, then process the event. See [Implement the receiver](#implement-the-receiver). Call the test endpoint to deliver an event that you define. See [Send a test event](#send-a-test-event). Read the response of the test call and the [delivery history](#delivery-history). ## Create a webhook The address of your receiver. See [Receiver URL requirements](#receiver-url-requirements). The event types that this webhook receives. See [Events](#events). An unknown value gives HTTP `422`. A flag that the API stores and returns with the webhook. The API does not use this flag when it delivers events: a webhook with `enabled` set to `false` continues to receive events. To stop the deliveries, delete the webhook. See [Delete a webhook](#delete-a-webhook). ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/webhooks/" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/webhook", "events": ["document.received", "document.sent", "document.sent.failed"], "enabled": true }' ``` ```javascript Node.js theme={null} const response = await fetch("https://api.e-invoice.be/api/webhooks/", { method: "POST", headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ url: "https://example.com/webhook", events: ["document.received", "document.sent", "document.sent.failed"], enabled: true, }), }); const webhook = await response.json(); console.log(webhook.id); ``` ```python Python theme={null} import os import requests response = requests.post( "https://api.e-invoice.be/api/webhooks/", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, json={ "url": "https://example.com/webhook", "events": ["document.received", "document.sent", "document.sent.failed"], "enabled": True, }, timeout=30, ) response.raise_for_status() webhook = response.json() print(webhook["id"]) ``` ```php PHP theme={null} true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("E_INVOICE_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "url" => "https://example.com/webhook", "events" => ["document.received", "document.sent", "document.sent.failed"], "enabled" => true, ]), ]); $webhook = json_decode(curl_exec($curl), true); curl_close($curl); echo $webhook["id"]; ``` ```csharp C# theme={null} using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var response = await client.PostAsJsonAsync("/api/webhooks/", new { url = "https://example.com/webhook", events = new[] { "document.received", "document.sent", "document.sent.failed" }, enabled = true, }); response.EnsureSuccessStatusCode(); var webhook = await response.Content.ReadFromJsonAsync(); Console.WriteLine(webhook.GetProperty("id").GetString()); ``` The API returns HTTP `201`: ```json theme={null} { "id": "webhook-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "url": "https://example.com/webhook", "events": ["document.received", "document.sent", "document.sent.failed"], "enabled": true, "secret": "5f2b8c1e9d4a7036b1c8e5f2a9d6037441be7c0a3d8f6e2915c4b7a0d3e6f921" } ``` A company can have more than one webhook. Each webhook that subscribes to an event type receives its own request for that event. ## Get the signing secret The API makes the secret when you create the webhook. The secret is a string of 64 hexadecimal characters. You cannot supply your own value. * The `secret` field is in the response of the create, get, list and update calls. You can read it again at any time. * You cannot change the secret of a webhook. To rotate the secret, create a new webhook, deploy the new secret to your receiver, then delete the old webhook. * Keep the secret in a secret manager or an environment variable. Do not put it in source code, in logs or in client-side code. Each person or system that has the API key of the company can read the secret with the get and list calls. Protect the API key with the same care as the secret. ## Receiver URL requirements * **Method**: the receiver accepts `POST` requests with a JSON body. * **Protocol**: `http` and `https` URLs are accepted. Use `https` for a production company. * **Certificate**: an `https` receiver must have a certificate from a trusted certificate authority. Delivery to a receiver with a self-signed certificate fails. If your receiver must use a self-signed certificate, contact support. * **Redirects**: the API does not follow redirects. A `3xx` response is a failed delivery. * **Basic authentication**: you can put credentials in the URL, for example `https://username:password@example.com/webhook`. Register the final address of your receiver. If the address answers with a redirect (for example from `http://` to `https://`, or from the apex domain to `www`), each delivery fails. The get and list calls return the webhook URL in clear text, credentials included. If you use Basic authentication in the URL, use credentials that are only for this receiver. ## Event payload The API sends each event as a `POST` request with these headers: | Header | Value | | - | - | | `X-Signature` | `sha256=` followed by the HMAC-SHA256 digest in hexadecimal. See [Verify the signature](#verify-the-signature). | | `X-Event-Type` | The event type, for example `document.sent`. | | `Content-Type` | `application/json` | | `User-Agent` | `e-invoice-be-webhook-service` | The body is a JSON object: ```json theme={null} { "id": "evt-e7wyc7gtpqx4z73x2wqmwhebhbb3r8n3ovfhcsdbulxr3s2awf49de76yglrnri3", "tenant_id": "ten-k3m9x2p7q4w8z5n1", "created_at": 1762780249, "type": "document.sent", "data": { "document_id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d" }, "text": "\u26a1\ufe0f New webhook event: document.sent\n\n{\n \"document_id\": \"doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d\"\n}" } ``` | Field | Type | Description | | - | - | - | | `id` | string | Identifier of this delivery attempt. It is not stable across retries. See [Duplicates and order](#duplicates-and-order). | | `tenant_id` | string | Identifier of the company ([tenant](/glossary#tenant)) that owns the webhook. | | `created_at` | integer | Unix time in seconds at which the API built this delivery attempt. | | `type` | string | The event type. | | `data` | object | The event data. For document events it contains `document_id` only. | | `text` | string | A text summary of the event. It contains non-ASCII characters and line breaks. | The payload does not contain the document. Use `data.document_id` to get it: ```bash cURL theme={null} curl "https://api.e-invoice.be/api/documents/doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` For a failure event, get the document and read its state. To find the cause, see [A document is in state FAILED](/guides/errors#a-document-is-in-state-failed) and [Document lifecycle and delivery tracking](/guides/document-lifecycle). ## Events Document events are sent to your webhooks. The `data` object of each one is `{"document_id": "doc-..."}`. | Event | Sent when | `data` fields | | - | - | - | | `document.received` | The API received a document for your company and stored it. In a sandbox company, Simulate inbound sends this event. | `document_id` | | `document.received.failed` | The API could not process a document that it received for your company. Not all receive failures give this event. | `document_id` | | `document.sent` | A document was sent. In a sandbox company, the email delivery of a send gives this event. | `document_id` | | `document.sent.failed` | The transmission of a document failed and the document went to state `FAILED`. | `document_id` | To get the document that a `document.received` event announces, see [Receive documents](/guides/receiving-documents). Some failures do not give an event: * In a sandbox company, if the email delivery of a send fails, the API sends no event and the document stays in state `TRANSIT`. * An error in the send request itself (for example HTTP `400`, `405` or `409`) is in the HTTP response. It gives no event. If a document stays in state `TRANSIT`, get the document with `GET /api/documents/{document_id}` and read its `state`. See [Document lifecycle and delivery tracking](/guides/document-lifecycle). The `events` field of the create call also accepts the values below. They are reserved: do not build a receiver that depends on them. `document.error`, `email.received`, `email.received.failed`, `email.sent`, `email.sent.failed`, `peppol.received`, `peppol.received.failed`, `peppol.sent`, `peppol.sent.failed` The OpenAPI specification gives `events` as a list of strings without an enumeration. This page is the reference for the permitted values. ## Verify the signature Each request has an `X-Signature` header. Verify it before you process the event. A request without a correct signature did not come from e-invoice.be. The API computes the signature as follows: 1. It serialises the complete event (all fields, not only `data`) to a canonical JSON string. 2. It encodes that string as UTF-8. 3. It computes the HMAC-SHA256 of those bytes with the webhook secret as the key. 4. It sends `sha256=` followed by the digest in lowercase hexadecimal. ### Canonical JSON string The canonical string is the output of Python `json.dumps(payload, sort_keys=True)`: * The keys of each object are in ascending order, at each level. * The separator between items is a comma followed by one space (`, `). The separator between a key and its value is a colon followed by one space (`: `). There are no line breaks and there is no other white space. * Each character outside the printable ASCII range is written as a `\uXXXX` escape with lowercase hexadecimal digits. A character above U+FFFF is written as a surrogate pair. * The characters `"` and `\` are escaped with a backslash. Line feed, carriage return, tab, backspace and form feed are written as `\n`, `\r`, `\t`, `\b` and `\f`. The character `/` is not escaped. Do not compute the HMAC of the raw request body. The body that the API sends is not the canonical string: it has no spaces after the separators, its keys are not sorted and non-ASCII characters are not escaped. Parse the body, build the canonical string, then compute the HMAC. `JSON.stringify` in JavaScript, `json_encode` in PHP and `JsonSerializer` in C# do not give the canonical string. They do not sort the keys and they do not put a space after the separators. Use the serialiser functions in the samples below. ### Test vector Use this vector to test your code before you connect it to the API. Secret: ```text theme={null} secret ``` Event: the payload in [Event payload](#event-payload). Canonical string (one line): ```text theme={null} {"created_at": 1762780249, "data": {"document_id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"}, "id": "evt-e7wyc7gtpqx4z73x2wqmwhebhbb3r8n3ovfhcsdbulxr3s2awf49de76yglrnri3", "tenant_id": "ten-k3m9x2p7q4w8z5n1", "text": "\u26a1\ufe0f New webhook event: document.sent\n\n{\n \"document_id\": \"doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d\"\n}", "type": "document.sent"} ``` In this string, `\n`, `\"` and `\u26a1\ufe0f` are literal escape sequences. They are not line breaks or decoded characters. Signature: ```text theme={null} sha256=a0b509e3466347b9c227767ff860769c15bf2d6c975a5ae706813aeb22a8694e ``` ## Implement the receiver Each sample is a complete receiver. It reads the secret from the environment variable `E_INVOICE_WEBHOOK_SECRET`, parses the body, builds the canonical string, compares the signature in constant time and returns a status code. ```javascript Node.js theme={null} import { createHmac, timingSafeEqual } from "node:crypto"; import { createServer } from "node:http"; const secret = process.env.E_INVOICE_WEBHOOK_SECRET; // Write a string as Python json.dumps does (ensure_ascii=True). function quote(text) { let out = '"'; for (const unit of text.split("")) { const code = unit.charCodeAt(0); if (unit === '"') out += '\\"'; else if (unit === "\\") out += "\\\\"; else if (unit === "\n") out += "\\n"; else if (unit === "\r") out += "\\r"; else if (unit === "\t") out += "\\t"; else if (unit === "\b") out += "\\b"; else if (unit === "\f") out += "\\f"; else if (code < 0x20 || code > 0x7e) out += "\\u" + code.toString(16).padStart(4, "0"); else out += unit; } return out + '"'; } // Sorted keys, ", " between items, ": " between key and value. function canonicalJson(value) { if (value === null) return "null"; if (typeof value === "string") return quote(value); if (typeof value !== "object") return String(value); if (Array.isArray(value)) return "[" + value.map(canonicalJson).join(", ") + "]"; const members = Object.keys(value) .sort() .map((key) => quote(key) + ": " + canonicalJson(value[key])); return "{" + members.join(", ") + "}"; } function expectedSignature(body, secret) { const canonical = canonicalJson(JSON.parse(body)); return "sha256=" + createHmac("sha256", secret).update(canonical, "utf8").digest("hex"); } function isValid(received, expected) { const a = Buffer.from(received); const b = Buffer.from(expected); return a.length === b.length && timingSafeEqual(a, b); } createServer((request, response) => { if (request.method !== "POST" || request.url !== "/webhook") { response.writeHead(404).end(); return; } const chunks = []; request.on("data", (chunk) => chunks.push(chunk)); request.on("end", () => { const body = Buffer.concat(chunks).toString("utf8"); const received = request.headers["x-signature"] ?? ""; let expected; try { expected = expectedSignature(body, secret); } catch { response.writeHead(400).end(); return; } if (!isValid(received, expected)) { response.writeHead(401).end(); return; } // Reply first, then do the work. response.writeHead(200).end(); const event = JSON.parse(body); setImmediate(() => handleEvent(event)); }); }).listen(3000); function handleEvent(event) { // Put the event on your queue. Get the document with event.data.document_id. console.log(event.type, event.data.document_id); } ``` ```python Python theme={null} import hashlib import hmac import json import os from flask import Flask, request app = Flask(__name__) SECRET = os.environ["E_INVOICE_WEBHOOK_SECRET"] def expected_signature(body: bytes, secret: str) -> str: # json.dumps with sort_keys=True gives the canonical string. # Do not add separators= or ensure_ascii=False. canonical = json.dumps(json.loads(body), sort_keys=True) digest = hmac.new( secret.encode("utf-8"), canonical.encode("utf-8"), hashlib.sha256 ).hexdigest() return f"sha256={digest}" @app.post("/webhook") def webhook(): body = request.get_data() received = request.headers.get("X-Signature", "") try: expected = expected_signature(body, SECRET) except ValueError: return "", 400 if not hmac.compare_digest(received.encode("utf-8"), expected.encode("utf-8")): return "", 401 event = json.loads(body) enqueue(event) # Keep this fast. Do the work in a background job. return "", 200 def enqueue(event: dict) -> None: # Put the event on your queue. Get the document with event["data"]["document_id"]. print(event["type"], event["data"]["document_id"]) ``` ```php PHP theme={null} $member) { $parts[] = canonical_string((string) $key) . ': ' . canonical_json($member); } return '{' . implode(', ', $parts) . '}'; } if (is_array($value)) { return '[' . implode(', ', array_map('canonical_json', $value)) . ']'; } if (is_string($value)) { return canonical_string($value); } return json_encode($value); } function expected_signature(string $body, string $secret): string { // Decode objects as stdClass, so that {} and [] stay different. $canonical = canonical_json(json_decode($body, false, 512, JSON_THROW_ON_ERROR)); return 'sha256=' . hash_hmac('sha256', $canonical, $secret); } $secret = getenv('E_INVOICE_WEBHOOK_SECRET'); $body = file_get_contents('php://input'); $received = $_SERVER['HTTP_X_SIGNATURE'] ?? ''; try { $expected = expected_signature($body, $secret); } catch (JsonException $e) { http_response_code(400); exit; } if (!hash_equals($expected, $received)) { http_response_code(401); exit; } $event = json_decode($body, true); // Put the event on your queue. Get the document with $event['data']['document_id']. // Keep this fast. Do the work in a background job. http_response_code(200); ``` ```csharp C# theme={null} using System.Security.Cryptography; using System.Text; using System.Text.Json; var secret = Environment.GetEnvironmentVariable("E_INVOICE_WEBHOOK_SECRET")!; var app = WebApplication.CreateBuilder(args).Build(); app.MapPost("/webhook", async (HttpRequest request) => { using var reader = new StreamReader(request.Body, Encoding.UTF8); var body = await reader.ReadToEndAsync(); var received = request.Headers["X-Signature"].ToString(); JsonDocument document; try { document = JsonDocument.Parse(body); } catch (JsonException) { return Results.BadRequest(); } using (document) { var expected = ExpectedSignature(document.RootElement, secret); if (!CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(received), Encoding.UTF8.GetBytes(expected))) { return Results.Unauthorized(); } var type = document.RootElement.GetProperty("type").GetString(); // Put the event on your queue. Keep this fast. Do the work in a background job. Console.WriteLine(type); } return Results.Ok(); }); app.Run(); static string ExpectedSignature(JsonElement payload, string secret) { var canonical = Canonical(payload); using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); var digest = hmac.ComputeHash(Encoding.UTF8.GetBytes(canonical)); return "sha256=" + Convert.ToHexString(digest).ToLowerInvariant(); } // Sorted keys, ", " between items, ": " between key and value. static string Canonical(JsonElement element) => element.ValueKind switch { JsonValueKind.Object => "{" + string.Join(", ", element.EnumerateObject() .OrderBy(member => member.Name, StringComparer.Ordinal) .Select(member => Quote(member.Name) + ": " + Canonical(member.Value))) + "}", JsonValueKind.Array => "[" + string.Join(", ", element.EnumerateArray().Select(Canonical)) + "]", JsonValueKind.String => Quote(element.GetString()!), JsonValueKind.Number => element.GetRawText(), JsonValueKind.True => "true", JsonValueKind.False => "false", _ => "null", }; // Write a string as Python json.dumps does (ensure_ascii=True). static string Quote(string text) { var builder = new StringBuilder("\""); foreach (var unit in text) { switch (unit) { case '"': builder.Append("\\\""); break; case '\\': builder.Append("\\\\"); break; case '\n': builder.Append("\\n"); break; case '\r': builder.Append("\\r"); break; case '\t': builder.Append("\\t"); break; case '\b': builder.Append("\\b"); break; case '\f': builder.Append("\\f"); break; default: if (unit < 0x20 || unit > 0x7e) builder.Append("\\u").Append(((int)unit).ToString("x4")); else builder.Append(unit); break; } } return builder.Append('"').ToString(); } ``` The Node.js sample uses only built-in modules. The Python sample uses Flask. The PHP sample is a script behind a web server. The C# sample is an ASP.NET Core minimal API. The document events contain only strings and integers, and the samples give the correct canonical string for them. If you send a test event with decimal numbers in `data`, the JavaScript serialiser can give a different result, because JavaScript does not keep the difference between `1.0` and `1`. Rules for the receiver: * Verify the signature before you use the payload. * Compare the signatures with a constant-time function. * Return a `2xx` status in less than 10 seconds. Do slow work (get the document, update your database) in a background job. * Return `401` for a wrong signature. Do not return a redirect. ## Delivery and retries * A delivery is successful when your receiver returns a `2xx` status in less than 10 seconds. * Each other result is a failure: a status that is not `2xx` (redirects included), a timeout, a connection error or a certificate error. * The API makes a maximum of 3 attempts for each event. The wait time between attempts increases. After the last failed attempt, the API does not send the event again. * The wait time between attempts is not a fixed value. Do not build logic that depends on it. If your receiver was unavailable for a longer time, get the missed documents with the inbox and outbox calls. See [Receive documents](/guides/receiving-documents). ## Duplicates and order * **Duplicates**: your receiver can get the same event more than once, for example when it processed the request but answered too late. Each attempt has a new `id`, a new `created_at` and a new signature. Thus the `id` cannot identify a duplicate. De-duplicate on the combination of `type` and `data.document_id`. * **More than one webhook**: if two webhooks subscribe to the same event type, each one gets a request with its own `id`. * **Order**: the order of delivery is not guaranteed. A `document.sent.failed` event can arrive after a later `document.sent` event for the same document. Do not derive the state of a document from the order of the events. Get the document and read its `state`. ## Test on your computer Run one of the receiver samples on a local port, for example port 3000. The API cannot reach `localhost`. Use a tunnel tool (for example ngrok or Cloudflare Tunnel) to get a public `https` address that forwards to your local port. Use the API key of a sandbox company and the public address of the tunnel as `url`. Set `E_INVOICE_WEBHOOK_SECRET` to the `secret` from the response and start the receiver again. Send a test event with the test call below. To get a `document.received` event with a document that you can get from the API, use Simulate inbound in the sandbox company. To get a `document.sent` event, send a document from the sandbox company. See [Test mode and sandbox companies](/environments#testing-received-documents). Before you open a tunnel, run your signature function on the [test vector](#test-vector). If the result is not the given digest, the canonical string is wrong. ### Send a test event The test call sends an event with the `event_type` and `data` that you supply. The webhook must subscribe to the event type. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/webhooks/webhook-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d/test" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "event_type": "document.sent", "data": { "document_id": "doc-test-123" } }' ``` The API returns HTTP `200`. The response has one of two forms. If the API delivered the event during the call, `webhook_delivery_result` contains the result of that one attempt: ```json theme={null} { "success": true, "message": "Webhook test completed successfully", "webhook_delivery_result": { "success": true, "status_code": 200, "error": null, "elapsed_seconds": 0.21 } } ``` If the API put the event on the delivery queue, the response confirms only that: ```json theme={null} { "success": true, "message": "Webhook test event enqueued for delivery via the retry queue", "webhook_delivery_result": { "success": true, "enqueued": true, "max_retries": 3 } } ``` A failed delivery during the call also gives HTTP `200`, with `success` set to `false`: ```json theme={null} { "success": false, "message": "Webhook test completed with delivery failure", "webhook_delivery_result": { "success": false, "status_code": 401, "error": "HTTP 401: Unauthorized", "elapsed_seconds": 0.18 } } ``` The `error` field contains the status code and the first 100 characters of the response body of your receiver. An `event_type` that is unknown, or that the webhook does not subscribe to, gives HTTP `422`. The `document_id` in a test event is the value that you supply. It is not necessarily a document that exists. ## Delivery history The history call returns the recorded delivery results of one webhook. ```bash cURL theme={null} curl "https://api.e-invoice.be/api/webhooks/webhook-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d/history" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```json theme={null} { "history": [ { "event_type": "document.sent", "event_data": { "document_id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d" }, "result": { "success": true, "status_code": 200, "error": null, "elapsed_seconds": 0.42 } }, { "event_type": "document.received", "event_data": { "document_id": "doc-7h2k9m4p6r8t3v5x1z0b2d4f6h8j0l2n" }, "result": { "success": false, "status_code": 500, "error": "HTTP 500: Internal Server Error", "elapsed_seconds": 0.37 } } ] } ``` | Field | Description | | - | - | | `event_type` | The event type. | | `event_data` | The `data` object of the event. | | `result.success` | `true` if the receiver returned a `2xx` status. | | `result.status_code` | The HTTP status of the receiver, or `null` if there was no response. | | `result.error` | `null` on success. On failure: the status code and the first 100 characters of the response body, or the timeout or connection error. | | `result.elapsed_seconds` | The duration of the attempt in seconds. | Limits of the history: * There is no pagination and there are no filters. The call returns all recorded rows of the webhook. * A row has no timestamp and no event `id`. * The history has one row for each event: the successful attempt, or the last failed attempt. Failed attempts that were followed by a retry are not recorded. * Not each delivery is recorded. An empty history is not proof that the API sent no events. * An unknown webhook identifier gives an empty `history` list, not HTTP `404`. ## Manage webhooks Each call needs the API key of the company that owns the webhook. A wrong or absent key gives HTTP `401`. An unknown webhook identifier gives HTTP `404` on the get, update, delete and test calls. See [Errors and troubleshooting](/guides/errors). ### List webhooks ```bash cURL theme={null} curl "https://api.e-invoice.be/api/webhooks/" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```json theme={null} [ { "id": "webhook-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "url": "https://example.com/webhook", "events": ["document.received", "document.sent", "document.sent.failed"], "enabled": true, "secret": "5f2b8c1e9d4a7036b1c8e5f2a9d6037441be7c0a3d8f6e2915c4b7a0d3e6f921" } ] ``` ### Get a webhook ```bash cURL theme={null} curl "https://api.e-invoice.be/api/webhooks/webhook-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```json theme={null} { "id": "webhook-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "url": "https://example.com/webhook", "events": ["document.received", "document.sent", "document.sent.failed"], "enabled": true, "secret": "5f2b8c1e9d4a7036b1c8e5f2a9d6037441be7c0a3d8f6e2915c4b7a0d3e6f921" } ``` ### Update a webhook The update call accepts `url`, `events` and `enabled`. All fields are optional; the API changes only the fields that you send. The secret does not change. ```bash cURL theme={null} curl -X PUT "https://api.e-invoice.be/api/webhooks/webhook-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/webhooks/e-invoice", "events": ["document.received", "document.received.failed"] }' ``` ```json theme={null} { "id": "webhook-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "url": "https://example.com/webhooks/e-invoice", "events": ["document.received", "document.received.failed"], "enabled": true, "secret": "5f2b8c1e9d4a7036b1c8e5f2a9d6037441be7c0a3d8f6e2915c4b7a0d3e6f921" } ``` Send only the event types from the [Events](#events) section. The update call does not check the values in `events` in the same way as the create call. ### Delete a webhook ```bash cURL theme={null} curl -X DELETE "https://api.e-invoice.be/api/webhooks/webhook-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```json theme={null} { "is_deleted": true } ``` After the delete call, the webhook receives no new events and the get call gives HTTP `404`. To stop all deliveries to a receiver, delete its webhook. ## Security checklist * Use an `https` URL with a certificate from a trusted certificate authority. * Verify the `X-Signature` header of each request, with a constant-time comparison. * Keep the secret out of source code and logs. * Treat the webhook URL as readable by each holder of the API key. Credentials in the URL are returned in clear text by the get and list calls. * Do not trust the payload for business decisions. Get the document from the API with `data.document_id`. * To rotate the secret, create a new webhook and delete the old one. ## Next Steps Get the documents that a `document.received` event announces. Read the document states behind the sent and failed events. Find the cause of an error response from the API. Test webhooks in a sandbox company with Simulate inbound. # Glossary Source: https://docs.e-invoice.be/glossary Find the definition of each Peppol, e-invoicing and e-invoice.be term that this documentation uses. Each entry gives a short definition and a link to the page that uses the term. ## Peppol network ### Peppol An international network for the exchange of electronic business documents. Each participant connects through an access point, and participants on different access points can exchange documents. See [Introduction](/index). ### Access Point A service provider that sends and receives documents on the Peppol network for its customers. e-invoice.be is an access point. See [Introduction](/index). ### SMP Service Metadata Publisher. A registry that holds, for each participant, the document types that the participant can receive and the access point that receives them. e-invoice.be operates an SMP. See [Look up Peppol participants](/guides/lookup-participants). ### SML Service Metadata Locator. The central DNS-based service of Peppol that tells a sender which SMP holds the data of a participant. See [Admin API](/admin-api#peppol-registration). ### Peppol Directory A public, searchable index of Peppol participants. It is built from the business cards that the SMPs publish. See [Look up Peppol participants](/guides/lookup-participants). ### Business card The public company data (for example name and country) that an SMP publishes for a participant. `GET /api/validate/peppol-id` returns `business_card_valid` and, when available, `business_card`. See [Look up Peppol participants](/guides/lookup-participants). ### Peppol ID The address of a participant on the Peppol network. It is also called participant ID. The format is `scheme:identifier`, for example `0208:1018265814`. See [Look up Peppol participants](/guides/lookup-participants). ### Scheme The four-digit code in front of a Peppol ID that tells which register issued the identifier. It is also called ICD (International Code Designator). Belgian companies use `0208`. See [Look up Peppol participants](/guides/lookup-participants). ### Peppol registration The publication of a Peppol ID on an SMP, so that the participant can receive documents. A company that only sends documents does not need a registration. See [Admin API](/admin-api#peppol-registration). ## Company identifiers ### VAT number The tax identifier of a company, with a country prefix, for example `BE1018265814`. In the API it is `vendor_tax_id` and `customer_tax_id`. In Belgium the Dutch name is "btw-nummer" (BTW). See [Create e-invoices](/guides/creating-invoices). ### Enterprise number The company registration number. In the API it is `vendor_company_id` and `customer_company_id`. For a Belgian company it is the CBE number: the ten digits of the VAT number without the `BE` prefix. See [Create e-invoices](/guides/creating-invoices). ### CBE and KBO The Belgian company register. CBE (Crossroads Bank for Enterprises) is the English name and KBO (Kruispuntbank van Ondernemingen) is the Dutch name. The CBE number with scheme `0208` is the Peppol ID of a Belgian company. See [Look up Peppol participants](/guides/lookup-participants). ## Standards and formats ### EN 16931 The European standard that defines the data model of an electronic invoice. See [Validation during development](/guides/validation). ### UBL Universal Business Language. An XML format for business documents. Peppol invoices and credit notes are UBL documents. The API converts JSON to UBL. See [Send UBL documents](/guides/ubl-documents). ### BIS Billing 3.0 The Peppol specification for invoices and credit notes. It applies EN 16931 to UBL and adds Peppol rules. Each document that the API sends must comply with it. See [Validation during development](/guides/validation). ### Schematron A rule language for XML documents. The rules of EN 16931 and BIS Billing 3.0 are published as Schematron rules, and validation reports a failure with the identifier of the rule. See [Validation during development](/guides/validation). ### Tax category code A code from the code list UNCL5305 that gives the VAT treatment of a line or a tax subtotal, for example `S` (standard rate), `Z` (zero rate), `E` (exempt) and `AE` (reverse charge). See [LineItem schema](/api-reference/schemas/line-item). ### VATEX The code list for VAT exemption reasons. A VATEX code tells why no VAT is charged. See [Invoice totals and calculations](/guides/invoice-totals). ### Unit code A code from UN/ECE Recommendation 20 that gives the unit of measure of a line quantity, for example `C62` (one, piece) and `HUR` (hour). See [LineItem schema](/api-reference/schemas/line-item). ## Document types ### Invoice A request for payment from a vendor to a customer. See [Create e-invoices](/guides/creating-invoices). ### Credit note A document that cancels or decreases an invoice. See [Create credit notes](/guides/credit-notes). ### Self-billing A procedure in which the customer issues the invoice or credit note for the vendor. The customer is then the sender of the document on Peppol. See [Self-billing and debit notes](/guides/self-billing). ### Document state The position of a document in its lifecycle: `DRAFT`, `TRANSIT`, `SENT`, `FAILED` or `RECEIVED`. See [Document lifecycle and delivery tracking](/guides/document-lifecycle). ## e-invoice.be terms ### Company The account unit in the app and in the guides. A company has its own API key, Peppol IDs, documents and webhooks. See [Authentication](/authentication). ### Tenant The name of a company in the Admin API and in the reseller pages. A tenant is a company that a [reseller](/reseller-programme) manages. The webhook payload field `tenant_id` identifies the company. See [Admin API](/admin-api#tenant-management). ### Organisation API key The API key of a reseller. It gives access to the Admin API and to all tenants of the reseller. It is different from the API key of one company. See [Admin API](/admin-api#authentication). ### Test mode The mode of a company that has no connection to the Peppol network. Outbound documents go by email to the contact email address of the company (the `company_email` field), and inbound documents are simulated. Test mode is fixed when the company is created. See [Test mode and sandbox companies](/environments#test-mode). ### Sandbox company A company in test mode. Use it for development and tests. A sandbox company cannot be converted into a production company. See [Test mode and sandbox companies](/environments#sandbox-companies). ### Production company A company that sends and receives documents on the Peppol network. See [Go-live checklist](/going-live). ### Simulate inbound The function that puts a document into the inbox of a sandbox company, as if it was received from Peppol. See [Testing received documents](/environments#testing-received-documents). ### Send-only tenant A tenant that sends documents on Peppol and has no Peppol registration through e-invoice.be. See [Admin API](/admin-api#send-only-tenants). ### Mailbox The record of the emails that are sent to the `@outbox.email.e-invoice.be` address of a company. e-invoice.be makes a draft document from the attachment of each email. The Mailbox endpoints list the emails, give access to their attachments and retry a failed email. See [Send invoices by email (Mailbox)](/guides/mailbox). ### Webhook An HTTP request that e-invoice.be sends to your server when a document event occurs. See [Webhooks](/essentials/webhooks). ## Next Steps Send a first document from a sandbox company Find and check Peppol IDs Check a document against the BIS Billing 3.0 rules Move from a sandbox company to a production company # Go-live checklist Source: https://docs.e-invoice.be/going-live Move an integration from a sandbox company to a production company that sends and receives documents on the Peppol network. ## Overview A sandbox company cannot be converted into a production company. Test mode is fixed when a company is created. To go live, you create a separate production company and repeat the configuration that belongs to a company: the API key, the Peppol ID and the webhooks. The API host does not change. Sandbox companies and production companies both use `https://api.e-invoice.be`. See [Test mode and sandbox companies](/environments). A production company sends documents on the Peppol network. Each document that you send from a production company reaches a real recipient. ## What changes and what stays the same | Item | Sandbox company | Production company | | - | - | - | | API host | `https://api.e-invoice.be` | `https://api.e-invoice.be` | | API key | Key of the sandbox company | New key of the production company | | Outbound documents | Sent by email as UBL XML | Sent on the Peppol network | | Inbound documents | Simulate inbound only | Received from the Peppol network | | Webhooks | Belong to the sandbox company | Must be created again | | Company verification | Not applicable | Applicable | | Plan and credits | Not applicable | Applicable | | Request and response formats | Identical | Identical | ## Checklist Sign in to [app.e-invoice.be](https://app.e-invoice.be) and create a company that is not a sandbox company. Complete the company verification steps that the app shows. The sandbox company stays available. Keep it for development and for regression tests. See [Test mode and sandbox companies](/environments#sandbox-companies) for the differences between the two company types. Call `GET /api/me/` with the production API key and read `peppol_ids`. The sender of each document must be one of these Peppol IDs. If it is not, `POST /api/documents/` returns `406` and `POST /api/documents/{document_id}/send` returns `409`. For self-billing documents, the Peppol sender is the customer, so the check applies to the `customer_*` fields. If the company must receive documents, it must also be registered on the Peppol network. Check the registration with `GET /api/validate/peppol-id`. See [Look up Peppol participants](/guides/lookup-participants) and the [Peppol ID](/glossary#peppol-id) entry in the glossary. Copy the API key of the production company from its settings in the app. Store the key in a secret manager and load it from the environment variable `E_INVOICE_API_KEY`. Do not use the sandbox key in production, and do not use the production key in tests. See [Authentication](/authentication). Webhooks belong to one company. The webhooks of the sandbox company do not exist in the production company. Create each webhook again with `POST /api/webhooks/`. The server generates a new secret for each webhook. Store the new secret and use it to verify the `X-Signature` header. A signature check that uses the sandbox secret fails for production events. See [Webhooks](/essentials/webhooks). Call `GET /api/me/` and read `plan` and `credit_balance`. A sandbox company has no billing, so this is the first time that these values apply to your integration. See [Usage statistics and credits](/guides/usage-statistics). Set `sender_peppol_scheme`, `sender_peppol_id`, `receiver_peppol_scheme` and `receiver_peppol_id` on each call to `POST /api/documents/{document_id}/send`. Explicit routing prevents delivery failures that occur when the API derives a Peppol ID from the company identifiers in the document. See [Create e-invoices](/guides/creating-invoices). Call `GET /api/validate/peppol-id` for the recipient. The response contains `is_valid`, `dns_valid`, `business_card_valid` and `supported_document_types`. Send only when the recipient is registered and supports the document type. In a sandbox company this check has no effect on delivery, because documents go to email. In a production company the document can only be delivered to a recipient that is registered on the Peppol network. See [Look up Peppol participants](/guides/lookup-participants). The document write endpoints return `429` when the rate limit for the API key is exceeded. Read the `Retry-After` header and wait that number of seconds before you send the request again. A document that cannot be delivered gets the state `FAILED`. Subscribe to the `document.sent.failed` and `document.received.failed` events, and make sure that an operator sees these documents. See [Errors and troubleshooting](/guides/errors) and [Document lifecycle and delivery tracking](/guides/document-lifecycle). Send one document from the production company to a participant that you know, for example a company of your own organisation or a partner who expects the test. Then call `GET /api/documents/{document_id}/timeline` and confirm that the document reaches the state `SENT`. See [Document lifecycle and delivery tracking](/guides/document-lifecycle). Monitor these items from the first production day: * Webhook deliveries that fail. `GET /api/webhooks/{webhook_id}/history` shows the delivery history of a webhook. * Documents in the state `FAILED`. * Your usage and the value of `credit_balance` from `GET /api/me/`. See [Webhooks](/essentials/webhooks), [List, filter and manage documents](/guides/managing-documents) and [Usage statistics and credits](/guides/usage-statistics). ## Frequently asked questions No. Test mode is fixed when the company is created. Create a separate production company. The host, the endpoints and the payloads are identical. The API key and the webhook secrets change. Recipient checks and the handling of `FAILED` documents become important, because documents go to real recipients. No. Each company has its own documents, webhooks and API key. No. `GET /api/me/` does not return the test mode of the company. Label each key when you store it, so that a sandbox key and a production key cannot be confused. ## Next Steps Compare sandbox companies and production companies Handle error responses and rate limits Follow a document from draft to delivery Receive events for sent and received documents # Advanced invoicing Source: https://docs.e-invoice.be/guides/advanced-invoicing Add allowances (discounts) and charges (fees) to an invoice at document level and at line level. ## Overview An invoice can contain two types of adjustment: * **Allowances** (discounts) decrease the invoice amount. * **Charges** (fees) increase the invoice amount. You can apply each type at two levels: 1. **Document level**: the adjustment applies to the full invoice (for example an early payment discount or a handling fee). 2. **Line level**: the adjustment applies to one line item (for example a bulk discount on one product). The API calculates the totals of the document from the lines, the allowances and the charges. [Invoice totals and calculations](/guides/invoice-totals) gives the formulas. ## Send the request All request bodies on this page go to the same two endpoints: `POST /api/validate/json` (to examine the body) and `POST /api/documents/` (to create the document). Only the JSON body changes. The request below creates the basic invoice; replace its body with a body from this page. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/documents/" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "purchase_order": "PO-12345", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email": "billing@e-invoice.be", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "subtotal": 1000.00, "total_tax": 210.00, "invoice_total": 1210.00, "amount_due": 1210.00, "payment_term": "Payment within 30 days", "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "INV-2026-001" } ] }' ``` ```javascript Node.js theme={null} const invoice = { document_type: "INVOICE", invoice_id: "INV-2026-001", invoice_date: "2026-10-01", due_date: "2026-10-31", currency: "EUR", purchase_order: "PO-12345", vendor_name: "E-INVOICE BV", vendor_tax_id: "BE1018265814", vendor_address: "Brusselsesteenweg 119/A, 1980 Zemst, BE", vendor_email: "billing@e-invoice.be", customer_name: "OpenPeppol VZW", customer_tax_id: "BE0848934496", customer_address: "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", items: [ { description: "Professional services", quantity: 10, unit: "C62", unit_price: 100.00, amount: 1000.00, tax_rate: "21.00" } ], subtotal: 1000.00, total_tax: 210.00, invoice_total: 1210.00, amount_due: 1210.00, payment_term: "Payment within 30 days", payment_details: [ { iban: "BE68539007547034", swift: "GEBABEBB", payment_reference: "INV-2026-001" } ] }; const response = await fetch("https://api.e-invoice.be/api/documents/", { method: "POST", headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(invoice), }); const result = await response.json(); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${JSON.stringify(result)}`); } console.log(result); ``` ```python Python theme={null} import os import requests invoice = { "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "purchase_order": "PO-12345", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email": "billing@e-invoice.be", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "subtotal": 1000.00, "total_tax": 210.00, "invoice_total": 1210.00, "amount_due": 1210.00, "payment_term": "Payment within 30 days", "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "INV-2026-001" } ] } response = requests.post( "https://api.e-invoice.be/api/documents/", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, json=invoice, timeout=30, ) if not response.ok: raise SystemExit(f"HTTP {response.status_code}: {response.text}") print(response.json()) ``` ```php PHP theme={null} "INVOICE", "invoice_id" => "INV-2026-001", "invoice_date" => "2026-10-01", "due_date" => "2026-10-31", "currency" => "EUR", "purchase_order" => "PO-12345", "vendor_name" => "E-INVOICE BV", "vendor_tax_id" => "BE1018265814", "vendor_address" => "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email" => "billing@e-invoice.be", "customer_name" => "OpenPeppol VZW", "customer_tax_id" => "BE0848934496", "customer_address" => "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items" => [ [ "description" => "Professional services", "quantity" => 10, "unit" => "C62", "unit_price" => 100.00, "amount" => 1000.00, "tax_rate" => "21.00" ] ], "subtotal" => 1000.00, "total_tax" => 210.00, "invoice_total" => 1210.00, "amount_due" => 1210.00, "payment_term" => "Payment within 30 days", "payment_details" => [ [ "iban" => "BE68539007547034", "swift" => "GEBABEBB", "payment_reference" => "INV-2026-001" ] ] ]; $ch = curl_init("https://api.e-invoice.be/api/documents/"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("E_INVOICE_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode($invoice), ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status >= 400) { throw new RuntimeException("HTTP $status: $body"); } print_r(json_decode($body, true)); ``` ```csharp C# theme={null} using System.Net.Http.Headers; using System.Text; var invoice = """ { "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "purchase_order": "PO-12345", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email": "billing@e-invoice.be", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "subtotal": 1000.00, "total_tax": 210.00, "invoice_total": 1210.00, "amount_due": 1210.00, "payment_term": "Payment within 30 days", "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "INV-2026-001" } ] } """; using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var response = await client.PostAsync( "/api/documents/", new StringContent(invoice, Encoding.UTF8, "application/json")); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {body}"); } Console.WriteLine(body); ``` Replace the vendor fields with the data of your company. The API rejects a document on create (`406`) if the Peppol ID of the vendor is not one of the Peppol IDs of your company. The API returns `201` with the document in state `DRAFT`. The response contains the calculated totals as strings: ```json Response (shortened) theme={null} { "id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "created_at": "2026-10-01T09:30:00Z", "document_type": "INVOICE", "state": "DRAFT", "direction": "OUTBOUND", "invoice_id": "INV-2026-001", "currency": "EUR", "subtotal": "1000.00", "total_discount": "0.00", "total_tax": "210.00", "invoice_total": "1210.00", "amount_due": "1210.00" } ``` Validation is not a separate mandatory call. `POST /api/documents/` rejects a payload that does not pass the same rules. Use `POST /api/validate/json` while you develop, because it returns all rule failures and the generated UBL. Many examples on this page are fragments: they show only the `items`, `allowances` or `charges` part of the body. Add them to a complete document before you send them. ## Document-level allowances A document-level allowance decreases the invoice amount after the line items are added together. ### Early payment discount ```json theme={null} { "document_type": "INVOICE", "invoice_id": "INV-2026-010", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Product A", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "allowances": [ { "amount": 50.00, "reason": "Early payment discount (5%)", "tax_code": "S", "tax_rate": "21.00" } ] } ``` Calculation: * Line items: €1,000.00 * Allowance: -€50.00 * `subtotal`: €950.00 * `total_tax` (21%): €199.50 * `invoice_total`: €1,149.50 ### More than one document-level allowance ```json theme={null} { "allowances": [ { "amount": 50.00, "reason": "Volume discount", "tax_code": "S", "tax_rate": "21.00" }, { "amount": 25.00, "reason": "Promotional discount", "tax_code": "S", "tax_rate": "21.00" } ] } ``` Calculation with line items of €1,000.00: * Volume discount: -€50.00 * Promotional discount: -€25.00 * `subtotal`: €925.00 * `total_tax` (21%): €194.25 * `invoice_total`: €1,119.25 To state an allowance as a percentage, refer to [Percentage allowances and charges](#percentage-allowances-and-charges). ## Document-level charges A document-level charge increases the invoice amount. ### Shipping fee ```json theme={null} { "document_type": "INVOICE", "invoice_id": "INV-2026-011", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Product B", "quantity": 5, "unit": "C62", "unit_price": 200.00, "amount": 1000.00, "tax_rate": "21.00" } ], "charges": [ { "amount": 25.00, "reason": "Shipping and handling", "tax_code": "S", "tax_rate": "21.00" } ] } ``` Calculation: * Line items: €1,000.00 * Charge: +€25.00 * `subtotal`: €1,025.00 * `total_tax` (21%): €215.25 * `invoice_total`: €1,240.25 ### More than one charge ```json theme={null} { "charges": [ { "amount": 25.00, "reason": "Shipping", "tax_code": "S", "tax_rate": "21.00" }, { "amount": 15.00, "reason": "Insurance", "tax_code": "S", "tax_rate": "21.00" } ] } ``` ## Combine allowances and charges One invoice can contain allowances and charges: ```json theme={null} { "document_type": "INVOICE", "invoice_id": "INV-2026-012", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Product C", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "allowances": [ { "amount": 100.00, "reason": "Bulk purchase discount (10%)", "tax_code": "S", "tax_rate": "21.00" } ], "charges": [ { "amount": 30.00, "reason": "Express delivery", "tax_code": "S", "tax_rate": "21.00" } ] } ``` Calculation: * Line items: €1,000.00 * Allowance: -€100.00 * Charge: +€30.00 * `subtotal`: €930.00 * `total_tax` (21%): €195.30 * `invoice_total`: €1,125.30 ## Line-level allowances A line-level allowance applies to one line item. The `amount` of a line is the net amount of the line: `quantity × unit_price`, minus the allowances of the line, plus the charges of the line. The API writes `amount` to the UBL as the line amount, and calculates the document totals from `quantity`, `unit_price` and the line allowances and charges. Make sure that the two results are equal. ### Discount on one product ```json theme={null} { "items": [ { "description": "Product D", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 900.00, "tax_rate": "21.00", "allowances": [ { "amount": 100.00, "reason": "10% discount on this product" } ] }, { "description": "Product E", "quantity": 5, "unit": "C62", "unit_price": 50.00, "amount": 250.00, "tax_rate": "21.00" } ] } ``` Calculation: * Line 1: (10 × €100) - €100 = €900.00 * Line 2: 5 × €50 = €250.00 * `subtotal`: €1,150.00 * `total_tax` (21%): €241.50 * `invoice_total`: €1,391.50 A line-level allowance or charge uses the tax rate of its line. It has no tax category of its own in the UBL. ### More than one allowance on one line ```json theme={null} { "items": [ { "description": "Product F", "quantity": 20, "unit": "C62", "unit_price": 100.00, "amount": 1850.00, "tax_rate": "21.00", "allowances": [ { "amount": 100.00, "reason": "Volume discount (5%)" }, { "amount": 50.00, "reason": "Loyalty programme discount" } ] } ] } ``` Calculation of line 1: * Base: 20 × €100 = €2,000.00 * Volume discount: -€100.00 * Loyalty discount: -€50.00 * Line amount: €1,850.00 ## Line-level charges A line-level charge applies to one line item. ```json theme={null} { "items": [ { "description": "Fragile equipment", "quantity": 1, "unit": "C62", "unit_price": 500.00, "amount": 550.00, "tax_rate": "21.00", "charges": [ { "amount": 50.00, "reason": "Special handling - fragile item" } ] } ] } ``` Calculation of line 1: * Base: 1 × €500 = €500.00 * Special handling: +€50.00 * Line amount: €550.00 ## Complete example with the two levels This invoice uses allowances and charges at document level and at line level: ```json theme={null} { "document_type": "INVOICE", "invoice_id": "INV-2026-013", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "payment_term": "Net 30 days", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Premium product A", "quantity": 20, "unit": "C62", "unit_price": 100.00, "amount": 1800.00, "tax_rate": "21.00", "allowances": [ { "amount": 200.00, "reason": "Bulk discount (10%)" } ] }, { "description": "Standard product B", "quantity": 10, "unit": "C62", "unit_price": 50.00, "amount": 500.00, "tax_rate": "21.00" }, { "description": "Heavy equipment", "quantity": 1, "unit": "C62", "unit_price": 1000.00, "amount": 1100.00, "tax_rate": "21.00", "charges": [ { "amount": 100.00, "reason": "Oversized item handling" } ] } ], "allowances": [ { "amount": 150.00, "reason": "Early payment discount", "tax_code": "S", "tax_rate": "21.00" } ], "charges": [ { "amount": 50.00, "reason": "Delivery service", "tax_code": "S", "tax_rate": "21.00" } ] } ``` Line items: * Line 1: (20 × €100) - €200 = €1,800.00 * Line 2: 10 × €50 = €500.00 * Line 3: €1,000 + €100 = €1,100.00 * Sum of the lines: €3,400.00 Document level: * Allowance: -€150.00 * Charge: +€50.00 * `subtotal`: €3,300.00 * `total_discount`: €150.00 Tax and total: * `total_tax` (21% of €3,300.00): €693.00 * `invoice_total`: €3,993.00 ## Tax on allowances and charges Each document-level allowance and charge has a `tax_rate` (default `21.00`) and a `tax_code` (default `S`). In the UBL tax breakdown, the API puts it in the tax group of the lines that have the same tax category and the same rate. An allowance decreases the tax base of that group, and a charge increases it. An allowance or charge with `tax_code` `S` and `tax_rate` `0.00` gets the category `E`. ### Same tax rate If all lines have one tax rate, use that rate for the allowance or charge: ```json theme={null} { "items": [ { "description": "Product A", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "allowances": [ { "amount": 50.00, "reason": "Volume discount", "tax_rate": "21.00", "tax_code": "S" } ] } ``` ### Different tax rates If the invoice has more than one tax rate, give each allowance or charge the rate of the lines to which it applies: ```json theme={null} { "items": [ { "description": "Standard rate item", "quantity": 1, "unit": "C62", "unit_price": 100.00, "amount": 100.00, "tax_rate": "21.00" }, { "description": "Reduced rate item", "quantity": 1, "unit": "C62", "unit_price": 50.00, "amount": 50.00, "tax_rate": "6.00" } ], "allowances": [ { "amount": 50.00, "reason": "General discount on standard rate items", "tax_rate": "21.00", "tax_code": "S" } ] } ``` ### Allowances and charges without VAT For an allowance or charge on items that are exempt from VAT, send `tax_rate` `0.00` and the applicable tax category code: ```json theme={null} { "allowances": [ { "amount": 100.00, "reason": "Discount on exempt items", "tax_rate": "0.00", "tax_code": "E" } ] } ``` An allowance or charge with `tax_rate` `0.00` changes `subtotal` but does not change `total_tax`. ## Validation of the result The API converts the JSON body to a Peppol BIS Billing 3.0 document. The rules of that standard depend on the content of the invoice: the country of the vendor, the document type, the tax category codes and the tax rates. For example, each document-level allowance must have a reason or a reason code, and its tax category must agree with its rate. Validation is not a separate mandatory call. `POST /api/documents/` rejects a payload that does not pass the same rules. Use `POST /api/validate/json` while you develop, because it returns all rule failures and the generated UBL. Refer to the [validation guide](/guides/validation) for the response format and to [Errors and troubleshooting](/guides/errors) for the rules that fail most frequently. ## Recommended practices Give each allowance and charge a reason that the customer can understand. Clear: ```json theme={null} { "reason": "Early payment discount (2% if paid within 10 days)" } ``` Not clear: ```json theme={null} { "reason": "Discount" } ``` Use the document level when the allowance or charge applies to the full order: * Early payment discounts * Shipping for the full order * Promotional discounts on the full order Use the line level when it applies to one product: * Bulk discount on one product * Special handling for a fragile item * Promotion on one product When you calculate an amount from a percentage, round the result to 2 decimals before you send it: ```javascript theme={null} const lineItemsTotal = 1000.0; const discountPercent = 5; const discountAmount = Math.round(lineItemsTotal * (discountPercent / 100) * 100) / 100; // 50.00 ``` Give each document-level allowance and charge the tax category code and the tax rate of the items to which it applies. When you give an early payment discount, state the terms in `payment_term`: ```json theme={null} { "payment_term": "Net 30 days. 2% discount if paid within 10 days.", "allowances": [ { "amount": 50.00, "reason": "Early payment discount (2%)", "tax_code": "S", "tax_rate": "21.00" } ] } ``` ## Frequent cases ```json theme={null} { "items": [ { "description": "Product X", "quantity": 100, "unit": "C62", "unit_price": 10.00, "amount": 900.00, "tax_rate": "21.00", "allowances": [ { "amount": 100.00, "reason": "Volume discount - 100+ units (10%)" } ] } ] } ``` ```json theme={null} { "charges": [ { "amount": 15.00, "reason": "Standard shipping", "tax_code": "S", "tax_rate": "21.00" }, { "amount": 5.00, "reason": "Handling fee", "tax_code": "S", "tax_rate": "21.00" } ] } ``` ```json theme={null} { "allowances": [ { "amount": 25.00, "reason": "Gold member discount (5%)", "tax_code": "S", "tax_rate": "21.00" } ] } ``` ```json theme={null} { "charges": [ { "amount": 2.50, "reason": "Credit card processing fee (2.5%)", "tax_code": "S", "tax_rate": "21.00" } ] } ``` ## Percentage allowances and charges To state an allowance or a charge as a percentage, send `multiplier_factor` and `base_amount` together with `amount`. `multiplier_factor` is the percentage (send `10` for 10%; minimum 0, maximum 100, 2 decimals maximum). The API does not calculate `amount` from `multiplier_factor` and `base_amount`, and it does not compare the three values. You must send `amount` (`base_amount × multiplier_factor / 100`, rounded to 2 decimals). ```json theme={null} { "document_type": "INVOICE", "invoice_id": "INV-2026-014", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "allowances": [ { "reason": "Volume discount", "reason_code": "95", "multiplier_factor": 10, "base_amount": 1000.00, "amount": 100.00, "tax_code": "S", "tax_rate": "21.00" } ] } ``` Totals that the API calculates: `subtotal` 900.00, `total_discount` 100.00, `total_tax` 189.00, `invoice_total` 1089.00. A charge accepts the same three fields (`multiplier_factor`, `base_amount`, `amount`). ### Allowance reason codes The `reason_code` of an allowance accepts these UNCL5189 codes (send the code as a string): | Code | Meaning | | - | - | | `41` | Bonus for works ahead of schedule | | `42` | Other bonus | | `60` | Manufacturer's consumer discount | | `62` | Due to military status | | `63` | Due to work accident | | `64` | Special agreement | | `65` | Production error discount | | `66` | New outlet discount | | `67` | Sample discount | | `68` | End-of-range discount | | `70` | Incoterm discount | | `71` | Point of sales threshold allowance | | `88` | Material surcharge/deduction | | `95` | Discount | | `100` | Special rebate | | `102` | Fixed long term | | `103` | Temporary | | `104` | Standard | | `105` | Yearly turnover | The `reason_code` of a charge accepts the UNCL7161 codes. The full list is in the `ChargeReasonCode` schema of the [API reference](/api-reference). ## Next Steps Read how the API calculates and examines the totals Create credit notes with allowances Examine complex invoices while you develop See all fields of a line item, an allowance and a charge # Attachments and PDF Source: https://docs.e-invoice.be/guides/attachments Add files to an invoice so that the receiver gets them in the UBL, let the platform generate a PDF, and download attachments and UBL files. ## Overview A Peppol invoice is a UBL XML file. A file that the receiver must get (for example a PDF copy of the invoice or a timesheet) must be embedded in that UBL. There are three methods to add a file to a document. Only two of them put the file in the UBL. | Method | Goes into the UBL | When to use | | - | - | - | | `attachments[]` in the body of `POST /api/documents/` | Yes | You have the file and the receiver must get it. | | `construct_pdf=true` on `POST /api/documents/` | Yes | You have no PDF and you want the platform to generate a readable copy of the invoice. | | `POST /api/documents/{document_id}/attachments` (deprecated) | No | Do not use for new integrations. The file is stored with the document only. | This page is about files that go out with a document. To use a PDF as the input from which a document is created, see [Create documents from PDF](/guides/pdf-documents). ## Add attachments when you create the document Add an `attachments` array to the body of `POST /api/documents/`. The API embeds each file in the UBL as an `AdditionalDocumentReference` and stores it with the document. File name with extension, for example `timesheet-2026-09.pdf`. The receiver sees this name. MIME type of the file. See the permitted types below. File content, base64 encoded. Always supply this field when you create a document. Optional. The API calculates the stored size from the decoded `file_data`. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/documents/" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Consulting services", "quantity": 10, "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "attachments": [ { "file_name": "timesheet-2026-09.pdf", "file_type": "application/pdf", "file_data": "JVBERi0xLjQKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFI..." } ] }' ``` ```javascript Node.js theme={null} import { readFile } from "node:fs/promises"; const fileData = (await readFile("timesheet-2026-09.pdf")).toString("base64"); const response = await fetch("https://api.e-invoice.be/api/documents/", { method: "POST", headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ document_type: "INVOICE", invoice_id: "INV-2026-001", invoice_date: "2026-10-01", due_date: "2026-10-31", currency: "EUR", vendor_name: "E-INVOICE BV", vendor_tax_id: "BE1018265814", vendor_address: "Brusselsesteenweg 119/A, 1980 Zemst, BE", customer_name: "OpenPeppol VZW", customer_tax_id: "BE0848934496", customer_address: "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", items: [ { description: "Consulting services", quantity: 10, unit_price: 100.0, amount: 1000.0, tax_rate: "21.00", }, ], attachments: [ { file_name: "timesheet-2026-09.pdf", file_type: "application/pdf", file_data: fileData, }, ], }), }); console.log(response.status, await response.json()); ``` ```python Python theme={null} import base64 import os import requests with open("timesheet-2026-09.pdf", "rb") as f: file_data = base64.b64encode(f.read()).decode("ascii") response = requests.post( "https://api.e-invoice.be/api/documents/", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, json={ "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Consulting services", "quantity": 10, "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00", } ], "attachments": [ { "file_name": "timesheet-2026-09.pdf", "file_type": "application/pdf", "file_data": file_data, } ], }, ) print(response.status_code, response.json()) ``` ```php PHP theme={null} 'INVOICE', 'invoice_id' => 'INV-2026-001', 'invoice_date' => '2026-10-01', 'due_date' => '2026-10-31', 'currency' => 'EUR', 'vendor_name' => 'E-INVOICE BV', 'vendor_tax_id' => 'BE1018265814', 'vendor_address' => 'Brusselsesteenweg 119/A, 1980 Zemst, Belgium', 'customer_name' => 'OpenPeppol VZW', 'customer_tax_id' => 'BE0848934496', 'customer_address' => 'Robert Schumanplein 6 bus 5, 1040 Brussel, Belgium', 'items' => [[ 'description' => 'Consulting services', 'quantity' => 10, 'unit_price' => 100.00, 'amount' => 1000.00, 'tax_rate' => '21.00', ]], 'attachments' => [[ 'file_name' => 'timesheet-2026-09.pdf', 'file_type' => 'application/pdf', 'file_data' => base64_encode(file_get_contents('timesheet-2026-09.pdf')), ]], ]; $ch = curl_init('https://api.e-invoice.be/api/documents/'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('E_INVOICE_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode($payload), ]); $body = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE) . PHP_EOL . $body . PHP_EOL; curl_close($ch); ``` ```csharp C# theme={null} using System.Net.Http.Headers; using System.Net.Http.Json; var fileData = Convert.ToBase64String(await File.ReadAllBytesAsync("timesheet-2026-09.pdf")); using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var payload = new { document_type = "INVOICE", invoice_id = "INV-2026-001", invoice_date = "2026-10-01", due_date = "2026-10-31", currency = "EUR", vendor_name = "E-INVOICE BV", vendor_tax_id = "BE1018265814", vendor_address = "Brusselsesteenweg 119/A, 1980 Zemst, BE", customer_name = "OpenPeppol VZW", customer_tax_id = "BE0848934496", customer_address = "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", items = new[] { new { description = "Consulting services", quantity = 10, unit_price = 100.00, amount = 1000.00, tax_rate = "21.00" } }, attachments = new[] { new { file_name = "timesheet-2026-09.pdf", file_type = "application/pdf", file_data = fileData } } }; var response = await client.PostAsJsonAsync("/api/documents/", payload); Console.WriteLine((int)response.StatusCode); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` Before you run the sample, replace the vendor fields with the data of your company. The API rejects a document if the Peppol ID of the vendor is not one of the `peppol_ids` of your company. The API returns `201 Created` with the document in the `DRAFT` state. The response below shows only the fields that are related to attachments. ```json theme={null} { "id": "doc-8f3a2b1c9d4e5f60718293a4b5c6d7e8", "state": "DRAFT", "document_type": "INVOICE", "invoice_id": "INV-2026-001", "attachments": [ { "id": "doc-att-1a2b3c4d5e6f708192a3b4c5d6e7f809", "file_name": "timesheet-2026-09.pdf", "file_type": "application/pdf", "file_size": 48213 } ] } ``` ### Permitted file types Peppol rule BR-CL-24 permits only these MIME types for a file that is embedded in the UBL: | File | `file_type` | | - | - | | PDF | `application/pdf` | | PNG image | `image/png` | | JPEG image | `image/jpeg` | | CSV | `text/csv` | | Excel workbook (`.xlsx`) | `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` | | OpenDocument spreadsheet (`.ods`) | `application/vnd.oasis.opendocument.spreadsheet` | If `file_type` is not one of these values, the API uses the extension of `file_name` (`.pdf`, `.png`, `.jpg`, `.jpeg`, `.csv`, `.xlsx`, `.ods`) to find the MIME type that it writes to the UBL. Give each file a correct extension. ### File size The API does not publish a size limit for the files in `attachments[]`. Base64 makes the request body approximately one third larger than the file, and the receiving Access Point must accept the full UBL. Keep attachments small, and send large supporting files through a different channel. The limits of 5 MB for each file and 25 MB for each batch apply to the PDF conversion endpoint only. See [Create documents from PDF](/guides/pdf-documents). Validation is not a separate mandatory call. `POST /api/documents/` rejects a payload that does not pass the same rules. Use `POST /api/validate/json` while you develop, because it returns all rule failures and the generated UBL. ## Let the platform make the PDF Add the query parameter `construct_pdf=true` to `POST /api/documents/`. The API then does these steps: The API converts the JSON body to UBL and validates it. The API makes a PDF from that UBL. The API adds the PDF to the attachments of the document, generates the UBL again with the PDF embedded, and validates the result. The receiver gets a UBL that contains a readable PDF copy of the invoice. If the body also has an `attachments` array, the generated PDF is added after those files, and the UBL contains all of them. If `true`, the API generates a PDF from the document, stores it as an attachment and embeds it in the UBL. The samples read the invoice JSON from a file `invoice.json`. Save this payload in that file: ```json Invoice theme={null} { "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "purchase_order": "PO-12345", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email": "billing@e-invoice.be", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "subtotal": 1000.00, "total_tax": 210.00, "invoice_total": 1210.00, "amount_due": 1210.00, "payment_term": "Payment within 30 days", "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "INV-2026-001" } ] } ``` Replace the vendor fields with the data of your company. The API rejects a document if the Peppol ID of the vendor is not one of the `peppol_ids` of your company. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/documents/?construct_pdf=true" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -H "Content-Type: application/json" \ -d @invoice.json ``` ```javascript Node.js theme={null} import { readFile } from "node:fs/promises"; const response = await fetch( "https://api.e-invoice.be/api/documents/?construct_pdf=true", { method: "POST", headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}`, "Content-Type": "application/json", }, body: await readFile("invoice.json", "utf8"), }, ); console.log(response.status, await response.json()); ``` ```python Python theme={null} import json import os import requests with open("invoice.json") as f: invoice = json.load(f) response = requests.post( "https://api.e-invoice.be/api/documents/", params={"construct_pdf": "true"}, headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, json=invoice, ) print(response.status_code, response.json()) ``` ```php PHP theme={null} true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('E_INVOICE_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => file_get_contents('invoice.json'), ]); $body = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE) . PHP_EOL . $body . PHP_EOL; curl_close($ch); ``` ```csharp C# theme={null} using System.Net.Http.Headers; using System.Text; using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var content = new StringContent( await File.ReadAllTextAsync("invoice.json"), Encoding.UTF8, "application/json"); var response = await client.PostAsync("/api/documents/?construct_pdf=true", content); Console.WriteLine((int)response.StatusCode); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ```json theme={null} { "id": "doc-8f3a2b1c9d4e5f60718293a4b5c6d7e8", "state": "DRAFT", "document_type": "INVOICE", "invoice_id": "INV-2026-001", "attachments": [ { "id": "doc-att-9e8d7c6b5a4f30211203f4a5b6c7d8e9", "file_name": "INV-2026-001-generated.pdf", "file_type": "application/pdf", "file_size": 31876 } ] } ``` ### File name of the generated PDF * The name is `-generated.pdf`. * Each character of `invoice_id` that is not a letter, a digit, `-`, `_` or `.` becomes `-`. For example, `INV/2026 001` gives `INV-2026-001-generated.pdf`. * If the document has no `invoice_id`, the name is a random UUID followed by `-generated.pdf`. ### Errors | Status | Cause | | - | - | | `406` | The UBL is not valid, before or after the PDF is embedded. The `detail` field gives the rule failures. | | `500` | The API could not generate the PDF. The `detail` field is `Could not generate constructed PDF`. No document is created. | The [command-line tool](/cli) has the same option: `peppol document create json invoice.json --construct-pdf`. ## Deprecated upload endpoint `POST /api/documents/{document_id}/attachments` is deprecated. A file that you upload with this endpoint is stored with the document, but it is not added to the UBL. The receiver does not get it. To send a file to the receiver, put it in `attachments[]` when you create the document, or use `construct_pdf=true`. Existing integrations that use this endpoint send a `multipart/form-data` request with one file in the form field `file`: ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/documents/doc-8f3a2b1c9d4e5f60718293a4b5c6d7e8/attachments" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -F "file=@delivery-note.pdf;type=application/pdf" ``` ```json theme={null} { "id": "doc-att-4c5d6e7f8091a2b3c4d5e6f708192a3b", "file_name": "delivery-note.pdf", "file_type": "application/pdf", "file_size": 20480 } ``` ## List, download and delete attachments These calls work for documents that you created and for documents that you received. ### List the attachments of a document ```bash cURL theme={null} curl "https://api.e-invoice.be/api/documents/doc-8f3a2b1c9d4e5f60718293a4b5c6d7e8/attachments" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```json theme={null} [ { "id": "doc-att-1a2b3c4d5e6f708192a3b4c5d6e7f809", "file_name": "timesheet-2026-09.pdf", "file_type": "application/pdf", "file_size": 48213, "file_url": "https://storage.example.com/timesheet-2026-09.pdf?X-Amz-Expires=3600&X-Amz-Signature=..." } ] ``` If the document has no attachments, this call returns `404` with the detail `Document attachments not found`. It does not return an empty array. Treat this response as an empty list. For a sample in five languages that lists and downloads all attachments, see [Download the attachments and the PDF](/guides/receiving-documents#download-the-attachments-and-the-pdf). ### Download one attachment Get the attachment to receive a `file_url`. This is a signed URL that is valid for 1 hour. Download the file from that URL with a plain `GET` request, without the `Authorization` header. ```bash cURL theme={null} curl "https://api.e-invoice.be/api/documents/doc-8f3a2b1c9d4e5f60718293a4b5c6d7e8/attachments/doc-att-1a2b3c4d5e6f708192a3b4c5d6e7f809" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```json theme={null} { "id": "doc-att-1a2b3c4d5e6f708192a3b4c5d6e7f809", "file_name": "timesheet-2026-09.pdf", "file_type": "application/pdf", "file_size": 48213, "file_url": "https://storage.example.com/timesheet-2026-09.pdf?X-Amz-Expires=3600&X-Amz-Signature=..." } ``` ```bash cURL theme={null} curl -o timesheet-2026-09.pdf "" ``` Do not store `file_url`. Request a new URL each time that you need the file. ### Delete an attachment You can delete an attachment only while the document is in the `DRAFT` or `FAILED` state. ```bash cURL theme={null} curl -X DELETE "https://api.e-invoice.be/api/documents/doc-8f3a2b1c9d4e5f60718293a4b5c6d7e8/attachments/doc-att-1a2b3c4d5e6f708192a3b4c5d6e7f809" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```json theme={null} { "is_deleted": true } ``` | Status | Cause | | - | - | | `400` | The document is not in the `DRAFT` or `FAILED` state. The detail is `Document is not in draft state`. | | `404` | The document or the attachment does not exist. | This call removes the stored attachment only. It does not generate the UBL again. To send a document without a file that is embedded in its UBL, delete the document and create it again without that file. For the states of a document, see [Document lifecycle and delivery tracking](/guides/document-lifecycle). ## Download the UBL `GET /api/documents/{document_id}/ubl` returns the metadata of the UBL file of a document and a signed URL from which you can download the XML. Use it to see what the receiver gets, or to archive the UBL of a received document. ```bash cURL theme={null} curl "https://api.e-invoice.be/api/documents/doc-8f3a2b1c9d4e5f60718293a4b5c6d7e8/ubl" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```json theme={null} { "id": "ubl-5d6e7f8091a2b3c4d5e6f708192a3b4c", "file_name": "doc-8f3a2b1c9d4e5f60718293a4b5c6d7e8.xml", "file_size": 73412, "signed_url": "https://storage.example.com/doc-8f3a2b1c9d4e5f60718293a4b5c6d7e8.xml?X-Amz-Expires=3600&X-Amz-Signature=...", "sender_peppol_scheme": "0208", "sender_peppol_id": "1018265814", "receiver_peppol_scheme": "0208", "receiver_peppol_id": "0848934496", "validated_at": "2026-10-01T09:15:42Z" } ``` | Field | Description | | - | - | | `signed_url` | Signed URL of the XML file. It is valid for 1 hour. Download it without the `Authorization` header. | | `file_name`, `file_size` | Name and size in bytes of the stored XML file. | | `sender_peppol_scheme`, `sender_peppol_id`, `receiver_peppol_scheme`, `receiver_peppol_id` | The Peppol IDs that are stored with the UBL. | | `validated_at` | Time at which the UBL passed validation. | Fields that have no value are not in the response. The call returns `404` if the document has no UBL. Do not store `signed_url`. For a download sample in five languages, see [Download the original UBL](/guides/receiving-documents#download-the-original-ubl). ## Uploaded UBL and received documents You do not add attachments separately in these two cases. The files are already in the UBL. * **Documents that you create from UBL.** When you upload a UBL file with `POST /api/documents/ubl`, the API extracts each embedded file and stores it as an attachment of the document. The UBL is not changed. See [Send UBL documents](/guides/ubl-documents). * **Received documents.** The API extracts each file that the sender embedded and stores it as an attachment. If the received UBL contains no PDF, the API generates a PDF from the UBL and stores it as an attachment with the name `.pdf`. Thus a received document usually has a PDF that you can show to a person. See [Receive documents](/guides/receiving-documents). Use the list and download calls above to get these files. ## Test with a sandbox company Develop and test with a sandbox company. A sandbox company runs in test mode: the API sends each document as UBL XML to the contact email address of the company, and nothing goes to the Peppol network. The API host and the endpoints are the same as for a production company. See [Test mode and sandbox companies](/environments). Create a document with `attachments[]` or `construct_pdf=true` in a sandbox company, send it, and open the XML from the email. Each file is in an `AdditionalDocumentReference` element as base64 content. ## Next Steps Build the invoice body to which you add attachments. Use a PDF as the input for a new document. Read received invoices and their attachments. Examine the generated UBL before you create a document. # Create e-invoices Source: https://docs.e-invoice.be/guides/creating-invoices Validate, create and send an invoice on the Peppol network with the e-invoice.be API. ## Overview With the e-invoice.be API you create an [invoice](/glossary#invoice) from JSON. The API converts the JSON to UBL that complies with the European e-invoicing standard (EN 16931) and transmits the document on the Peppol network. ## Before you start Develop and test with a sandbox company. A sandbox company runs in test mode: the API sends each document as UBL XML to the contact email address of the company, and nothing goes to the Peppol network. The API host and the endpoints are the same as for a production company. See [Test mode and sandbox companies](/environments). You need the API key of the company. See [Authentication](/authentication). ## Workflow To send an e-invoice, do these steps: 1. **Validate** the JSON invoice data while you develop. 2. **Create** the document. The API accepts only an invoice that it can convert to valid UBL BIS Billing 3.0. 3. **Send** the document. A production company sends on the Peppol network. A sandbox company sends an email. Validation is not a separate mandatory call. `POST /api/documents/` rejects a payload that does not pass the same rules. Use `POST /api/validate/json` while you develop, because it returns all rule failures and the generated UBL. ## Validate the invoice data The examples on this page use this payload: ```json Invoice theme={null} { "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "purchase_order": "PO-12345", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email": "billing@e-invoice.be", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "subtotal": 1000.00, "total_tax": 210.00, "invoice_total": 1210.00, "amount_due": 1210.00, "payment_term": "Payment within 30 days", "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "INV-2026-001" } ] } ``` Replace the vendor fields with the data of your company. The API rejects a document if the Peppol ID of the vendor is not one of the `peppol_ids` of your company. `POST /api/validate/json` does not create a document and does not check the vendor. Thus you can validate the example without changes. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/validate/json" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "purchase_order": "PO-12345", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email": "billing@e-invoice.be", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "subtotal": 1000.00, "total_tax": 210.00, "invoice_total": 1210.00, "amount_due": 1210.00, "payment_term": "Payment within 30 days", "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "INV-2026-001" } ] }' ``` ```javascript Node.js theme={null} const invoice = { document_type: "INVOICE", invoice_id: "INV-2026-001", invoice_date: "2026-10-01", due_date: "2026-10-31", currency: "EUR", purchase_order: "PO-12345", vendor_name: "E-INVOICE BV", vendor_tax_id: "BE1018265814", vendor_address: "Brusselsesteenweg 119/A, 1980 Zemst, BE", vendor_email: "billing@e-invoice.be", customer_name: "OpenPeppol VZW", customer_tax_id: "BE0848934496", customer_address: "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", items: [ { description: "Professional services", quantity: 10, unit: "C62", unit_price: 100.00, amount: 1000.00, tax_rate: "21.00" } ], subtotal: 1000.00, total_tax: 210.00, invoice_total: 1210.00, amount_due: 1210.00, payment_term: "Payment within 30 days", payment_details: [ { iban: "BE68539007547034", swift: "GEBABEBB", payment_reference: "INV-2026-001" } ] }; const response = await fetch("https://api.e-invoice.be/api/validate/json", { method: "POST", headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(invoice), }); const result = await response.json(); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${JSON.stringify(result)}`); } console.log(result); ``` ```python Python theme={null} import os import requests invoice = { "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "purchase_order": "PO-12345", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email": "billing@e-invoice.be", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "subtotal": 1000.00, "total_tax": 210.00, "invoice_total": 1210.00, "amount_due": 1210.00, "payment_term": "Payment within 30 days", "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "INV-2026-001" } ] } response = requests.post( "https://api.e-invoice.be/api/validate/json", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, json=invoice, timeout=30, ) if not response.ok: raise SystemExit(f"HTTP {response.status_code}: {response.text}") print(response.json()) ``` ```php PHP theme={null} "INVOICE", "invoice_id" => "INV-2026-001", "invoice_date" => "2026-10-01", "due_date" => "2026-10-31", "currency" => "EUR", "purchase_order" => "PO-12345", "vendor_name" => "E-INVOICE BV", "vendor_tax_id" => "BE1018265814", "vendor_address" => "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email" => "billing@e-invoice.be", "customer_name" => "OpenPeppol VZW", "customer_tax_id" => "BE0848934496", "customer_address" => "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items" => [ [ "description" => "Professional services", "quantity" => 10, "unit" => "C62", "unit_price" => 100.00, "amount" => 1000.00, "tax_rate" => "21.00" ] ], "subtotal" => 1000.00, "total_tax" => 210.00, "invoice_total" => 1210.00, "amount_due" => 1210.00, "payment_term" => "Payment within 30 days", "payment_details" => [ [ "iban" => "BE68539007547034", "swift" => "GEBABEBB", "payment_reference" => "INV-2026-001" ] ] ]; $ch = curl_init("https://api.e-invoice.be/api/validate/json"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("E_INVOICE_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode($invoice), ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status >= 400) { throw new RuntimeException("HTTP $status: $body"); } print_r(json_decode($body, true)); ``` ```csharp C# theme={null} using System.Net.Http.Headers; using System.Text; var invoice = """ { "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "purchase_order": "PO-12345", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email": "billing@e-invoice.be", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "subtotal": 1000.00, "total_tax": 210.00, "invoice_total": 1210.00, "amount_due": 1210.00, "payment_term": "Payment within 30 days", "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "INV-2026-001" } ] } """; using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var response = await client.PostAsync( "/api/validate/json", new StringContent(invoice, Encoding.UTF8, "application/json")); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {body}"); } Console.WriteLine(body); ``` A valid payload gives `is_valid: true`, an empty `issues` list and the generated UBL in `ubl_document`. The UBL is shortened here. ```json Valid theme={null} { "id": "b55354b0-5c69-489b-a8f7-44be7d5bdd6b", "file_name": "b55354b0-5c69-489b-a8f7-44be7d5bdd6b.xml", "is_valid": true, "issues": [], "ubl_document": " (shortened) " } ``` A payload that breaks a rule gives `is_valid: false`. Each entry in `issues` has a `message`, a `type` (`error` or `warning`) and the name of the rule set in `schematron`. The fields `rule_id`, `flag`, `location` and `test` are optional. ```json Not valid theme={null} { "id": "b55354b0-5c69-489b-a8f7-44be7d5bdd6b", "file_name": "b55354b0-5c69-489b-a8f7-44be7d5bdd6b.xml", "is_valid": false, "issues": [ { "message": "Belgian enterprise number MUST be stated in the correct format.", "type": "error", "rule_id": "PEPPOL-COMMON-R043", "flag": "fatal", "schematron": "PEPPOL-EN16931-UBL" } ] } ``` See the [validation guide](/guides/validation) for the rules and the corrections. For all status codes and error formats, see [Errors and troubleshooting](/guides/errors). ## Create the invoice Send the same JSON payload to `POST /api/documents/`. Replace the vendor fields with the data of your company before you run this example: the API returns `406` if the Peppol ID of the vendor is not one of the `peppol_ids` of your company. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/documents/" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "purchase_order": "PO-12345", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email": "billing@e-invoice.be", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "subtotal": 1000.00, "total_tax": 210.00, "invoice_total": 1210.00, "amount_due": 1210.00, "payment_term": "Payment within 30 days", "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "INV-2026-001" } ] }' ``` ```javascript Node.js theme={null} const invoice = { document_type: "INVOICE", invoice_id: "INV-2026-001", invoice_date: "2026-10-01", due_date: "2026-10-31", currency: "EUR", purchase_order: "PO-12345", vendor_name: "E-INVOICE BV", vendor_tax_id: "BE1018265814", vendor_address: "Brusselsesteenweg 119/A, 1980 Zemst, BE", vendor_email: "billing@e-invoice.be", customer_name: "OpenPeppol VZW", customer_tax_id: "BE0848934496", customer_address: "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", items: [ { description: "Professional services", quantity: 10, unit: "C62", unit_price: 100.00, amount: 1000.00, tax_rate: "21.00" } ], subtotal: 1000.00, total_tax: 210.00, invoice_total: 1210.00, amount_due: 1210.00, payment_term: "Payment within 30 days", payment_details: [ { iban: "BE68539007547034", swift: "GEBABEBB", payment_reference: "INV-2026-001" } ] }; const response = await fetch("https://api.e-invoice.be/api/documents/", { method: "POST", headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(invoice), }); const result = await response.json(); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${JSON.stringify(result)}`); } console.log(result); ``` ```python Python theme={null} import os import requests invoice = { "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "purchase_order": "PO-12345", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email": "billing@e-invoice.be", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "subtotal": 1000.00, "total_tax": 210.00, "invoice_total": 1210.00, "amount_due": 1210.00, "payment_term": "Payment within 30 days", "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "INV-2026-001" } ] } response = requests.post( "https://api.e-invoice.be/api/documents/", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, json=invoice, timeout=30, ) if not response.ok: raise SystemExit(f"HTTP {response.status_code}: {response.text}") print(response.json()) ``` ```php PHP theme={null} "INVOICE", "invoice_id" => "INV-2026-001", "invoice_date" => "2026-10-01", "due_date" => "2026-10-31", "currency" => "EUR", "purchase_order" => "PO-12345", "vendor_name" => "E-INVOICE BV", "vendor_tax_id" => "BE1018265814", "vendor_address" => "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email" => "billing@e-invoice.be", "customer_name" => "OpenPeppol VZW", "customer_tax_id" => "BE0848934496", "customer_address" => "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items" => [ [ "description" => "Professional services", "quantity" => 10, "unit" => "C62", "unit_price" => 100.00, "amount" => 1000.00, "tax_rate" => "21.00" ] ], "subtotal" => 1000.00, "total_tax" => 210.00, "invoice_total" => 1210.00, "amount_due" => 1210.00, "payment_term" => "Payment within 30 days", "payment_details" => [ [ "iban" => "BE68539007547034", "swift" => "GEBABEBB", "payment_reference" => "INV-2026-001" ] ] ]; $ch = curl_init("https://api.e-invoice.be/api/documents/"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("E_INVOICE_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode($invoice), ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status >= 400) { throw new RuntimeException("HTTP $status: $body"); } print_r(json_decode($body, true)); ``` ```csharp C# theme={null} using System.Net.Http.Headers; using System.Text; var invoice = """ { "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "purchase_order": "PO-12345", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email": "billing@e-invoice.be", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "subtotal": 1000.00, "total_tax": 210.00, "invoice_total": 1210.00, "amount_due": 1210.00, "payment_term": "Payment within 30 days", "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "INV-2026-001" } ] } """; using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var response = await client.PostAsync( "/api/documents/", new StringContent(invoice, Encoding.UTF8, "application/json")); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {body}"); } Console.WriteLine(body); ``` The API returns `201 Created` with the complete document in the `DRAFT` state. Amounts are strings in the response. The response is shortened here. ```json theme={null} { "id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "created_at": "2026-10-01T09:15:23.482913Z", "document_type": "INVOICE", "state": "DRAFT", "direction": "OUTBOUND", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "subtotal": "1000.00", "total_tax": "210.00", "invoice_total": "1210.00", "amount_due": "1210.00" } ``` Keep the `id`. You use it to send the document. If the API rejects the payload, see [Create document errors](/guides/errors#create-document-errors-406). ## Invoice structure ### Main fields Only the `items` array is mandatory in the schema. But the API accepts a document only if the generated UBL passes the Peppol BIS Billing 3.0 rules. Thus a complete invoice also needs the fields that this table marks as mandatory, the vendor and customer identifiers, and `vendor_name`. For the full list, see the [document schema](/api-reference/schemas/document). | Field | Description | Mandatory | | - | - | - | | `items` | Array of invoice lines | Yes (minimum 1) | | `document_type` | `INVOICE`, `CREDIT_NOTE`, `DEBIT_NOTE`, `SELFBILLING_INVOICE` or `SELFBILLING_CREDIT_NOTE`. Default: `INVOICE`. See [Self-billing and debit notes](/guides/self-billing). | No | | `invoice_id` | Your unique invoice number | Yes (rule `BR-02`) | | `invoice_date` | Invoice date (`YYYY-MM-DD`). Default: the current date. | Recommended | | `due_date` | Payment due date (`YYYY-MM-DD`) | For an invoice with a positive amount due, `due_date` or `payment_term` is mandatory (rule `BR-CO-25`) | | `currency` | Currency code, for example `EUR`. Default: `EUR`. | No | For all fields, see the [document schema](/api-reference/schemas/document). For the amounts (`subtotal`, `total_tax`, `invoice_total`, `amount_due`), see [Invoice totals and calculations](/guides/invoice-totals). ### Vendor fields The vendor is the seller. This is your company. | Field | Description | Example | | - | - | - | | `vendor_name` | Company name | `"E-INVOICE BV"` | | `vendor_tax_id` | [VAT number](/glossary#vat-number) with the country prefix | `"BE1018265814"` | | `vendor_company_id` | Company registration number, for example the Belgian [enterprise number](/glossary#enterprise-number) | `"1018265814"` | | `vendor_address` | Full address | `"Brusselsesteenweg 119/A, 1980 Zemst, BE"` | | `vendor_email` | Contact email address | `"billing@e-invoice.be"` | The `vendor_tax_id` is the VAT number of the company. It is not the Peppol ID. For a Belgian company, give the full VAT number with the `BE` prefix, for example `BE1018265814`. The API derives the Peppol ID from it when you send the document. ### Customer fields The customer is the buyer. | Field | Description | Example | | - | - | - | | `customer_name` | Company name | `"OpenPeppol VZW"` | | `customer_tax_id` | VAT number with the country prefix | `"BE0848934496"` | | `customer_company_id` | Company registration number | `"0848934496"` | | `customer_peppol_id` | Peppol ID of the customer (`scheme:identifier`) | `"0208:0848934496"` | | `customer_address` | Full address | `"Robert Schumanplein 6 bus 5, 1040 Brussel, BE"` | | `customer_email` | Contact email address | `"info@openpeppol.org"` | ### Line items Each object in the `items` array is one product or service: ```json theme={null} { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ``` | Field | Description | | - | - | | `description` | The product or service that you sell | | `quantity` | Quantity | | `unit` | [Unit code](/glossary#unit-code). Frequent codes: `C62` (unit or piece), `HUR` (hour), `DAY` (day), `MTR` (metre), `KGM` (kilogram). | | `unit_price` | Price for one unit, without VAT | | `amount` | Line total without VAT: `quantity` x `unit_price`, minus the line allowances, plus the line charges. When a line has allowances or charges, you must send `amount`. See [Line-level allowances](/guides/advanced-invoicing#line-level-allowances). | | `tax_rate` | VAT percentage from 0 to 100, as a number or a string, for example `21.00`, `"6.00"` or `"0.00"` | For all line fields, see the [LineItem schema](/api-reference/schemas/line-item). ### Optional fields ```json theme={null} { "payment_term": "Payment within 30 days", "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "INV-2026-001" } ] } ``` Of these fields, only `shipping_address` and `shipping_address_recipient` go into the UBL (as the delivery). The API stores the billing, service and remittance addresses with the document, but the receiver does not get them. ```json theme={null} { "billing_address": "Billing Street 1, 1000 Brussels, BE", "billing_address_recipient": "Accounts Payable Department", "shipping_address": "Delivery Street 2, 2000 Antwerp, BE", "shipping_address_recipient": "Warehouse Manager", "service_address": "Service Location 3, 3000 Leuven, BE" } ``` ```json theme={null} { "allowances": [ { "amount": 50.00, "reason": "Early payment discount", "tax_code": "S", "tax_rate": "21.00" } ], "charges": [ { "amount": 25.00, "reason": "Shipping costs", "tax_code": "S", "tax_rate": "21.00" } ] } ``` See [Advanced invoicing](/guides/advanced-invoicing). ```json theme={null} { "buyer_reference": "CC-4410", "purchase_order": "PO-12345", "sales_order": "SO-2026-0042", "note": "Thank you for your business" } ``` `buyer_reference` is the reference that the buyer asks for (BT-10). When it is absent, the API writes `purchase_order` as the buyer reference. For the reference to a preceding invoice, see [Create credit notes](/guides/credit-notes). To send a PDF or a different file with the invoice, see [Attachments and PDF](/guides/attachments). ## Send the document Send the document with `POST /api/documents/{document_id}/send`. Set the environment variable `DOCUMENT_ID` to the `id` from the create response. Replace the sender Peppol ID in the query with the Peppol ID of your company. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/documents/$DOCUMENT_ID/send?sender_peppol_scheme=0208&sender_peppol_id=1018265814&receiver_peppol_scheme=0208&receiver_peppol_id=0848934496" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```javascript Node.js theme={null} const documentId = process.env.DOCUMENT_ID; // the "id" from the create response const query = new URLSearchParams({ sender_peppol_scheme: "0208", sender_peppol_id: "1018265814", receiver_peppol_scheme: "0208", receiver_peppol_id: "0848934496", }); const response = await fetch( `https://api.e-invoice.be/api/documents/${documentId}/send?${query}`, { method: "POST", headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }, }, ); const result = await response.json(); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${JSON.stringify(result)}`); } console.log(result.state); ``` ```python Python theme={null} import os import requests document_id = os.environ["DOCUMENT_ID"] # the "id" from the create response response = requests.post( f"https://api.e-invoice.be/api/documents/{document_id}/send", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, params={ "sender_peppol_scheme": "0208", "sender_peppol_id": "1018265814", "receiver_peppol_scheme": "0208", "receiver_peppol_id": "0848934496", }, timeout=30, ) if not response.ok: raise SystemExit(f"HTTP {response.status_code}: {response.text}") print(response.json()["state"]) ``` ```php PHP theme={null} "0208", "sender_peppol_id" => "1018265814", "receiver_peppol_scheme" => "0208", "receiver_peppol_id" => "0848934496", ]); $ch = curl_init("https://api.e-invoice.be/api/documents/$documentId/send?$query"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("E_INVOICE_API_KEY"), ], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status >= 400) { throw new RuntimeException("HTTP $status: $body"); } echo json_decode($body, true)["state"], PHP_EOL; ``` ```csharp C# theme={null} using System.Net.Http.Headers; var documentId = Environment.GetEnvironmentVariable("DOCUMENT_ID"); // the "id" from the create response using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var query = "sender_peppol_scheme=0208&sender_peppol_id=1018265814&receiver_peppol_scheme=0208&receiver_peppol_id=0848934496"; var response = await client.PostAsync($"/api/documents/{documentId}/send?{query}", null); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {body}"); } Console.WriteLine(body); ``` The API returns `200 OK` with the document. The state is now `TRANSIT`, because the delivery runs in the background. The response is shortened here. ```json theme={null} { "id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "created_at": "2026-10-01T09:15:23.482913Z", "document_type": "INVOICE", "state": "TRANSIT", "direction": "OUTBOUND", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "invoice_total": "1210.00" } ``` For the errors of this call, see [Send errors](/guides/errors#send-errors). ### Peppol ID routing When you send a document, the API finds each of the four Peppol values (sender scheme and identifier, receiver scheme and identifier) in this order: 1. The query parameter of the send request. 2. The Peppol ID that the API stored with the UBL of the document when you created it. For a document that you created from UBL, this is the `cbc:EndpointID` of the party. 3. The derivation from the identifiers in the document: `customer_peppol_id` first for the receiver, then `vendor_company_id` / `customer_company_id` and `vendor_tax_id` / `customer_tax_id`. For `SELFBILLING_INVOICE` and `SELFBILLING_CREDIT_NOTE`, the direction is reversed: the API derives the sender from the `customer_*` fields and the receiver from the `vendor_*` fields. For a Belgian party, the API derives scheme `0208` and the enterprise number: the tax ID `BE1018265814` gives the Peppol ID `0208:1018265814`. If a tax ID has no country prefix, the API uses the country code at the end of the address (for example `1980 Zemst, BE`). Always give the VAT number with its country prefix, because rule `BR-CO-09` rejects a VAT number without it. For the scheme that the API derives in other countries, see [Peppol ID schemes](/guides/lookup-participants#peppol-id-schemes). When the API sends the document on the Peppol network, it writes the final sender and receiver values in the `cbc:EndpointID` elements of the UBL that it transmits. The other content of the UBL does not change. The derived scheme can be different from the scheme that the receiver registered. Thus the best practice is to give all four query parameters: ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/documents/$DOCUMENT_ID/send?sender_peppol_scheme=0208&sender_peppol_id=1018265814&receiver_peppol_scheme=0208&receiver_peppol_id=0848934496" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` Scheme of the sender Peppol ID. Example: `0208`. Identifier of the sender, without the scheme. Example: `1018265814`. Scheme of the receiver Peppol ID. Example: `0208`. Identifier of the receiver, without the scheme. Example: `0848934496`. The API returns `400` if it cannot find a complete sender and receiver Peppol ID. It returns `409` if the sender Peppol ID is not one of the `peppol_ids` of your company. Before you send, make sure that the receiver is registered on the Peppol network with the Peppol ID that you use. See [Look up Peppol participants](/guides/lookup-participants). For the Peppol schemes and the check of a receiver, see [Look up Peppol participants](/guides/lookup-participants). ### After the send request The result depends on the type of company: * **Production company:** the API transmits the document on the Peppol network. The receiver gets it in the software that is connected to its Access Point. * **Sandbox company:** the API sends the UBL XML to the contact email address of the company. Nothing goes to the Peppol network. See [What a send does in test mode](/environments#what-a-send-does-in-test-mode). The code is the same for the two types of company. Only the API key is different. ### Document states | State | Meaning | | - | - | | `DRAFT` | The document exists but is not sent. `POST /api/documents/` always creates a document in this state. | | `TRANSIT` | The send request is accepted and the transmission is in progress. | | `SENT` | The transmission is complete. | | `FAILED` | The transmission failed. You can send the document again. | | `RECEIVED` | The document came in from another Peppol participant. | You can send a document only when it is in the `DRAFT` or `FAILED` state. For the transitions, the retry behaviour and the webhook events of each state, see [Document lifecycle and delivery tracking](/guides/document-lifecycle). To follow the delivery, use [webhooks](/essentials/webhooks) or request `GET /api/documents/{document_id}` until the state is `SENT` or `FAILED`. If the state is `FAILED`, see [A document is in state FAILED](/guides/errors#a-document-is-in-state-failed). ## Complete example These programs do the three steps in sequence. Save the example payload from [Validate the invoice data](#validate-the-invoice-data) as `invoice.json`, and replace the vendor fields and the Peppol IDs with your data. ```javascript Node.js theme={null} import { readFile } from "node:fs/promises"; const BASE_URL = "https://api.e-invoice.be"; async function post(path, body) { const response = await fetch(`${BASE_URL}${path}`, { method: "POST", headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}`, "Content-Type": "application/json", }, body, }); const result = await response.json(); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${JSON.stringify(result)}`); } return result; } const invoice = await readFile("invoice.json", "utf8"); // 1. Validate (while you develop) const validation = await post("/api/validate/json", invoice); if (!validation.is_valid) { console.error("Validation failed:", validation.issues); process.exit(1); } console.log("OK: the invoice JSON is valid"); // 2. Create (same payload) const document = await post("/api/documents/", invoice); console.log("OK: document created:", document.id); // 3. Send. Replace the Peppol IDs with those of your company and of the receiver. const query = new URLSearchParams({ sender_peppol_scheme: "0208", sender_peppol_id: "1018265814", receiver_peppol_scheme: "0208", receiver_peppol_id: "0848934496", }); const sent = await post(`/api/documents/${document.id}/send?${query}`); console.log("OK: send request accepted, state:", sent.state); ``` ```python Python theme={null} import json import os import sys import requests BASE_URL = "https://api.e-invoice.be" HEADERS = {"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"} def post(path, **kwargs): response = requests.post(f"{BASE_URL}{path}", headers=HEADERS, timeout=30, **kwargs) if not response.ok: raise SystemExit(f"HTTP {response.status_code}: {response.text}") return response.json() with open("invoice.json") as f: invoice = json.load(f) # 1. Validate (while you develop) validation = post("/api/validate/json", json=invoice) if not validation["is_valid"]: print("Validation failed:", validation["issues"]) sys.exit(1) print("OK: the invoice JSON is valid") # 2. Create (same payload) document = post("/api/documents/", json=invoice) print("OK: document created:", document["id"]) # 3. Send. Replace the Peppol IDs with those of your company and of the receiver. sent = post( f"/api/documents/{document['id']}/send", params={ "sender_peppol_scheme": "0208", "sender_peppol_id": "1018265814", "receiver_peppol_scheme": "0208", "receiver_peppol_id": "0848934496", }, ) print("OK: send request accepted, state:", sent["state"]) ``` ## Credit notes and other document types A credit note has the same structure as an invoice. Set `document_type` to `CREDIT_NOTE`: ```json Credit note theme={null} { "document_type": "CREDIT_NOTE", "invoice_id": "CN-2026-001", "invoice_date": "2026-10-15", "due_date": "2026-11-14", "currency": "EUR", "purchase_order": "PO-12345", "preceding_invoice_id": "INV-2026-001", "preceding_invoice_date": "2026-10-01", "note": "Credit note for invoice INV-2026-001: 2 units returned", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email": "billing@e-invoice.be", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services (credit)", "quantity": 2, "unit": "C62", "unit_price": 100.00, "amount": 200.00, "tax_rate": "21.00" } ], "subtotal": 200.00, "total_tax": 42.00, "invoice_total": 242.00, "amount_due": 242.00 } ``` This credit note has positive amounts. Replace the vendor fields with the data of your company. The API rejects a document if the Peppol ID of the vendor is not one of the `peppol_ids` of your company. See [Create credit notes](/guides/credit-notes) for partial credits and corrections, and [Self-billing and debit notes](/guides/self-billing) for the other values of `document_type`. ## Next Steps Follow the states and the delivery of a document Find the Peppol ID of a customer List, find and delete documents Get a notification for each delivery result # Create credit notes Source: https://docs.e-invoice.be/guides/credit-notes Create a credit note that cancels or decreases an invoice, and send it through Peppol. ## Overview A [credit note](/glossary#credit-note) decreases or cancels an invoice that you issued. Typical causes are: * **Refund** - the customer returns goods or cancels a service. * **Correction** - the invoice had a wrong price, quantity or amount. * **Discount after the invoice** - you give a price adjustment for an invoice that is already sent. * **Cancellation** - the invoice was issued in error. A credit note uses the same endpoints, the same JSON fields and the same UBL BIS Billing 3.0 rules as an invoice. If you did not send an invoice before, read [Create e-invoices](/guides/creating-invoices) first. For a credit note that your company issues as the buyer, see [Self-billing credit notes](/guides/self-billing#self-billing-credit-notes). ## Differences from an invoice | Field | Credit note | Invoice | | - | - | - | | `document_type` | `"CREDIT_NOTE"` | `"INVOICE"` (default) | | `invoice_id` | The credit note number, for example `CN-2026-001` | The invoice number | | `preceding_invoice_id`, `preceding_invoice_date` | Recommended: the number and the date of the original invoice | Optional (for a corrected invoice) | | `note` | Recommended: the reason for the credit | Optional | | `items` and totals | The credited quantities and amounts, as positive values | The invoiced quantities and amounts | Put the number of the original invoice in `preceding_invoice_id` and its date in `preceding_invoice_date`. The API writes them to `cac:BillingReference/cac:InvoiceDocumentReference` in the UBL. Do not use `purchase_order` for this: it is the order number of the customer, and the API writes it to the order reference and the buyer reference of the UBL document. For a Dutch vendor, a credit note must have a billing reference (rule `NL-R-001`). If `preceding_invoice_id` is absent, the API writes the text `UNSPECIFIED`. ## Example credit note The original invoice `INV-2026-001` has 10 units at €100.00. The customer returns 2 units. The credit note is for €242.00 (2 × €100.00 plus 21% VAT). ```json Credit note theme={null} { "document_type": "CREDIT_NOTE", "invoice_id": "CN-2026-001", "invoice_date": "2026-10-15", "due_date": "2026-11-14", "currency": "EUR", "purchase_order": "PO-12345", "preceding_invoice_id": "INV-2026-001", "preceding_invoice_date": "2026-10-01", "note": "Credit note for invoice INV-2026-001: 2 units returned", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email": "billing@e-invoice.be", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services (credit)", "quantity": 2, "unit": "C62", "unit_price": 100.00, "amount": 200.00, "tax_rate": "21.00" } ], "subtotal": 200.00, "total_tax": 42.00, "invoice_total": 242.00, "amount_due": 242.00 } ``` This credit note has positive amounts. Replace the vendor fields with the data of your company. The API rejects a document if the Peppol ID of the vendor is not one of the `peppol_ids` of your company. Save this payload as `credit-note.json`. The code samples on this page read that file. ## Create and send a credit note Validation is not a separate mandatory call. `POST /api/documents/` rejects a payload that does not pass the same rules. Use `POST /api/validate/json` while you develop, because it returns all rule failures and the generated UBL. To validate the credit note, send the same body to `POST /api/validate/json`. See [Validation during development](/guides/validation). Develop and test with a sandbox company. A sandbox company runs in test mode: the API sends each document as UBL XML to the contact email address of the company, and nothing goes to the Peppol network. The API host and the endpoints are the same as for a production company. See [Test mode and sandbox companies](/environments). Send the payload to `POST /api/documents/`. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/documents/" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -H "Content-Type: application/json" \ -d @credit-note.json ``` ```javascript Node.js theme={null} import { readFile } from "node:fs/promises"; const creditNote = await readFile("credit-note.json", "utf8"); const response = await fetch("https://api.e-invoice.be/api/documents/", { method: "POST", headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}`, "Content-Type": "application/json", }, body: creditNote, }); const result = await response.json(); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${JSON.stringify(result)}`); } console.log(result.id, result.state); ``` ```python Python theme={null} import json import os import requests with open("credit-note.json", encoding="utf-8") as file: credit_note = json.load(file) response = requests.post( "https://api.e-invoice.be/api/documents/", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, json=credit_note, timeout=30, ) if not response.ok: raise SystemExit(f"HTTP {response.status_code}: {response.text}") result = response.json() print(result["id"], result["state"]) ``` ```php PHP theme={null} true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("E_INVOICE_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => $creditNote, ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 201) { throw new RuntimeException("HTTP $status: $body"); } $result = json_decode($body, true); echo $result["id"] . " " . $result["state"] . PHP_EOL; ``` ```csharp C# theme={null} using System.Net.Http.Headers; using System.Text; using System.Text.Json; var creditNote = await File.ReadAllTextAsync("credit-note.json"); using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var response = await client.PostAsync( "https://api.e-invoice.be/api/documents/", new StringContent(creditNote, Encoding.UTF8, "application/json")); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { throw new Exception($"HTTP {(int)response.StatusCode}: {body}"); } using var result = JsonDocument.Parse(body); Console.WriteLine( $"{result.RootElement.GetProperty("id")} {result.RootElement.GetProperty("state")}"); ``` The API returns `201` and the document in the `DRAFT` state. The response has all fields of the document. This example shows a selection. Amounts in the response are strings. ```json Response (201) theme={null} { "id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "created_at": "2026-10-15T09:30:00Z", "document_type": "CREDIT_NOTE", "state": "DRAFT", "direction": "OUTBOUND", "invoice_id": "CN-2026-001", "invoice_date": "2026-10-15", "due_date": "2026-11-14", "currency": "EUR", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "subtotal": "200.00", "total_tax": "42.00", "invoice_total": "242.00", "amount_due": "242.00" } ``` Keep the `id`. You use it in the next step. Call `POST /api/documents/{document_id}/send` with the `id` from the create response. The call is the same as for an invoice. Replace the sender Peppol ID in the query parameters with the Peppol ID of your company. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/documents/$DOCUMENT_ID/send?sender_peppol_scheme=0208&sender_peppol_id=1018265814&receiver_peppol_scheme=0208&receiver_peppol_id=0848934496" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```javascript Node.js theme={null} const documentId = process.env.DOCUMENT_ID; // the "id" from the create response const query = new URLSearchParams({ sender_peppol_scheme: "0208", sender_peppol_id: "1018265814", receiver_peppol_scheme: "0208", receiver_peppol_id: "0848934496", }); const response = await fetch( `https://api.e-invoice.be/api/documents/${documentId}/send?${query}`, { method: "POST", headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }, }, ); const result = await response.json(); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${JSON.stringify(result)}`); } console.log(result.state); ``` ```python Python theme={null} import os import requests document_id = os.environ["DOCUMENT_ID"] # the "id" from the create response response = requests.post( f"https://api.e-invoice.be/api/documents/{document_id}/send", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, params={ "sender_peppol_scheme": "0208", "sender_peppol_id": "1018265814", "receiver_peppol_scheme": "0208", "receiver_peppol_id": "0848934496", }, timeout=30, ) if not response.ok: raise SystemExit(f"HTTP {response.status_code}: {response.text}") print(response.json()["state"]) ``` ```php PHP theme={null} "0208", "sender_peppol_id" => "1018265814", "receiver_peppol_scheme" => "0208", "receiver_peppol_id" => "0848934496", ]); $ch = curl_init("https://api.e-invoice.be/api/documents/$documentId/send?$query"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("E_INVOICE_API_KEY"), ], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status >= 400) { throw new RuntimeException("HTTP $status: $body"); } echo json_decode($body, true)["state"], PHP_EOL; ``` ```csharp C# theme={null} using System.Net.Http.Headers; var documentId = Environment.GetEnvironmentVariable("DOCUMENT_ID"); // the "id" from the create response using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var query = "sender_peppol_scheme=0208&sender_peppol_id=1018265814&receiver_peppol_scheme=0208&receiver_peppol_id=0848934496"; var response = await client.PostAsync($"/api/documents/{documentId}/send?{query}", null); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {body}"); } Console.WriteLine(body); ``` The API returns `200` and the document. The state is no longer `DRAFT`. ```json Response (200) theme={null} { "id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "created_at": "2026-10-15T09:30:00Z", "document_type": "CREDIT_NOTE", "state": "TRANSIT", "direction": "OUTBOUND", "invoice_id": "CN-2026-001", "invoice_total": "242.00" } ``` The document goes to `SENT` or to `FAILED`. Use [webhooks](/essentials/webhooks) (`document.sent` and `document.sent.failed`) or read the timeline of the document. See [Document lifecycle and delivery tracking](/guides/document-lifecycle). ## Document states A credit note has the same states as an invoice. | State | Meaning | | - | - | | `DRAFT` | The document exists but is not sent. `POST /api/documents/` always creates a document in this state. | | `TRANSIT` | The send request is accepted and the transmission is in progress. | | `SENT` | The transmission is complete. | | `FAILED` | The transmission failed. You can send the document again. | | `RECEIVED` | The document came in from another Peppol participant. | You can send a document only when it is in the `DRAFT` or `FAILED` state. For the transitions, the retry behaviour and the webhook events of each state, see [Document lifecycle and delivery tracking](/guides/document-lifecycle). ## Variants of the example Each block below shows only the fields that are different from the [example credit note](#example-credit-note). All other fields stay the same. Give each credit note its own `invoice_id`. ### Full credit To cancel the invoice fully, include each line of the original invoice with the same quantity and the same price. ```json theme={null} { "note": "Credit note for invoice INV-2026-001: invoice issued in error", "items": [ { "description": "Professional services (credit)", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "subtotal": 1000.00, "total_tax": 210.00, "invoice_total": 1210.00, "amount_due": 1210.00 } ``` ### Price correction To correct a price, credit the difference. In this example the invoice price was €10.00 too high for each unit. The credit is €121.00 (10 × €10.00 plus 21% VAT). ```json theme={null} { "note": "Credit note for invoice INV-2026-001: price correction of 10.00 EUR for each unit", "items": [ { "description": "Professional services (price adjustment)", "quantity": 10, "unit": "C62", "unit_price": 10.00, "amount": 100.00, "tax_rate": "21.00" } ], "subtotal": 100.00, "total_tax": 21.00, "invoice_total": 121.00, "amount_due": 121.00 } ``` ### More than one line In this example the customer returns 5 units of the first product and 3 units of the second product. The credit is €786.50. ```json theme={null} { "note": "Credit note for invoice INV-2026-001: return of two products", "items": [ { "description": "Product A (returned)", "quantity": 5, "unit": "C62", "unit_price": 100.00, "amount": 500.00, "tax_rate": "21.00" }, { "description": "Product B (damaged)", "quantity": 3, "unit": "C62", "unit_price": 50.00, "amount": 150.00, "tax_rate": "21.00" } ], "subtotal": 650.00, "total_tax": 136.50, "invoice_total": 786.50, "amount_due": 786.50 } ``` ### Service cancellation In this example you credit one day of a service that the customer did not use. ```json theme={null} { "note": "Credit note for invoice INV-2026-001: one consulting day not used", "items": [ { "description": "Consulting services (day not used)", "quantity": 1, "unit": "DAY", "unit_price": 500.00, "amount": 500.00, "tax_rate": "21.00" } ], "subtotal": 500.00, "total_tax": 105.00, "invoice_total": 605.00, "amount_due": 605.00 } ``` ### Refund details To tell the customer how you pay the refund, add `payment_term` and `payment_details`. ```json theme={null} { "payment_term": "Refund within 14 days", "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "CN-2026-001" } ] } ``` ### Allowances and charges A credit note accepts the same `allowances` and `charges` as an invoice, at document level and at line level. See [Advanced invoicing](/guides/advanced-invoicing) for the fields and [Invoice totals and calculations](/guides/invoice-totals) for the calculation of the totals. ## Good practice Send the number and the date of the original invoice in `preceding_invoice_id` and `preceding_invoice_date`, and the reason for the credit in `note`. The customer can then match the credit note with the invoice. ```json theme={null} { "preceding_invoice_id": "INV-2026-001", "preceding_invoice_date": "2026-10-01", "note": "Credit note for invoice INV-2026-001: goods returned damaged" } ``` If you credit specific lines of the original invoice, use the same descriptions and unit prices. Add the reason to the description. ```json theme={null} { "description": "Professional services (returned)", "quantity": 2, "unit": "C62", "unit_price": 100.00, "amount": 200.00, "tax_rate": "21.00" } ``` Use the tax rates and tax categories of the original invoice, also if the rates changed after the invoice date. Give the credited quantities and amounts as positive values. The document type `CREDIT_NOTE` tells the receiver that the document decreases the amount due. The API has no sign rule of its own, but a negative `unit_price` fails rule `BR-27`. ```json theme={null} { "quantity": 2, "unit_price": 100.00, "amount": 200.00 } ``` ## Troubleshooting ### The API rejects the credit note `POST /api/documents/` returns `406` if the payload does not pass the validation rules or if the Peppol ID of the vendor is not a Peppol ID of your company. It returns `422` if a field has a wrong type or format. No document is created. Send the body to `POST /api/validate/json` to see the rule failures, and see [Create document errors](/guides/errors#create-document-errors-406). ### The total of the credit note is more than the total of the invoice The API does not compare a credit note with the original invoice. A higher total can be correct, for example when you also credit return shipping costs. Make sure that your accounting system and that of the customer accept this. ### The customer did not receive the credit note 1. Make sure that the Peppol ID of the customer is correct. See [Look up Peppol participants](/guides/lookup-participants). 2. Get the state of the document with `GET /api/documents/{document_id}`. 3. Get the delivery history with `GET /api/documents/{document_id}/timeline`. See [Document lifecycle and delivery tracking](/guides/document-lifecycle). 4. Examine the webhook events for the document. For all status codes and error formats, see [Errors and troubleshooting](/guides/errors). ## Next Steps Follow the states and the delivery of a credit note Issue credit notes as the buyer, and debit notes Add allowances and charges Find the cause of a rejected or failed document # Document lifecycle and delivery tracking Source: https://docs.e-invoice.be/guides/document-lifecycle Follow a document through its states, read its timeline, and interpret the Peppol responses that the receiver returns. ## Overview Each document has a [state](/glossary#document-state) that shows where it is in its life. The timeline endpoint gives the events behind that state: creation, each transmission, and each response from the receiver. Use this page to find out why a document is `FAILED` or to see if the buyer accepted an invoice. ## Document states A document has one of five states: `DRAFT`, `TRANSIT`, `SENT`, `FAILED` and `RECEIVED`. The `state` field of the document contains the value. An outbound document uses the first four states. An inbound document is `RECEIVED`, or `FAILED` when its processing failed. ### Outbound flow ```mermaid theme={null} stateDiagram-v2 direction LR [*] --> DRAFT: Create document DRAFT --> TRANSIT: Send TRANSIT --> SENT: Transmission successful TRANSIT --> FAILED: Transmission failed FAILED --> TRANSIT: Send again DRAFT --> [*]: Delete FAILED --> [*]: Delete SENT --> [*] ``` | State | Next states | Webhook event on entry | Production company | Sandbox company | | - | - | - | - | - | | `DRAFT` | `TRANSIT`, or deleted | None | `POST /api/documents/` creates the draft. | Same. | | `TRANSIT` | `SENT`, `FAILED` | None | The send request puts the document in the queue for Peppol transmission. | The send request puts the document in the queue for email delivery. | | `SENT` | None (final) | `document.sent` | The access point of the receiver accepted the document. | The UBL XML was sent by email to the contact email address of the company. No Peppol traffic occurs. | | `FAILED` | `TRANSIT`, or deleted | `document.sent.failed` | The transmission failed. You can send the document again. | Not used. If the email delivery fails, the document stays in `TRANSIT` and the API sends no webhook event. | ### Inbound flow ```mermaid theme={null} stateDiagram-v2 direction LR [*] --> RECEIVED: Document received and processed [*] --> FAILED: Processing failed RECEIVED --> [*] ``` | State | Next states | Webhook event on entry | Production company | Sandbox company | | - | - | - | - | - | | `RECEIVED` | None (final) | `document.received` | The document arrived through Peppol and is in the inbox. | The document is seeded with Simulate inbound. A sandbox company receives no Peppol traffic. | | `FAILED` | None | `document.received.failed` (not for all failures) | The document arrived, but the processing failed. | Not applicable. | See [Webhooks](/essentials/webhooks) for the payload of each event. ### Retry behaviour For a production company, the API makes a maximum of 3 attempts to deliver the document to the access point of the receiver, with 5 seconds between the attempts. If the last attempt fails, the document goes to `FAILED`. If the API cannot prepare the document for transmission, the document goes to `FAILED` with no attempt. Thus a document can be `FAILED` some seconds after the send request. The API does not try again later. To try again, send the document again: see [Read a failed transmission](#read-a-failed-transmission). ## State rules * **Send.** `POST /api/documents/{document_id}/send` is permitted only for a document in the state `DRAFT` or `FAILED`. For other states the API returns `405 Method Not Allowed`. * **Delete.** `DELETE /api/documents/{document_id}` is permitted only for a document in the state `DRAFT` or `FAILED`. For other states the API returns `400 Bad Request` with the detail `Document is not in draft state`. * **Update.** There is no endpoint that changes the content of a draft. Delete the draft and create a new document. See [List, filter and manage documents](/guides/managing-documents) for the related procedures. ## Get the timeline The timeline endpoint returns all events of one document in chronological order. The ID of the document. ```bash cURL theme={null} curl "https://api.e-invoice.be/api/documents/doc-4f8a1c2e9b7d4a6f8e3c5b1a2d9f7e60/timeline" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```javascript Node.js theme={null} const documentId = "doc-4f8a1c2e9b7d4a6f8e3c5b1a2d9f7e60"; const response = await fetch( `https://api.e-invoice.be/api/documents/${documentId}/timeline`, { headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` } }, ); if (!response.ok) { throw new Error(`Request failed: ${response.status}`); } const timeline = await response.json(); for (const event of timeline.events) { console.log(event.timestamp, event.event_type); } ``` ```python Python theme={null} import os import requests document_id = "doc-4f8a1c2e9b7d4a6f8e3c5b1a2d9f7e60" response = requests.get( f"https://api.e-invoice.be/api/documents/{document_id}/timeline", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, timeout=30, ) response.raise_for_status() for event in response.json()["events"]: print(event["timestamp"], event["event_type"]) ``` ```php PHP theme={null} true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('E_INVOICE_API_KEY')], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 200) { throw new RuntimeException("Request failed: {$status}"); } foreach (json_decode($body, true)['events'] as $event) { echo $event['timestamp'] . ' ' . $event['event_type'] . PHP_EOL; } ``` ```csharp C# theme={null} using System.Net.Http.Headers; using System.Text.Json; var documentId = "doc-4f8a1c2e9b7d4a6f8e3c5b1a2d9f7e60"; using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var response = await client.GetAsync($"/api/documents/{documentId}/timeline"); response.EnsureSuccessStatusCode(); using var timeline = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); foreach (var timelineEvent in timeline.RootElement.GetProperty("events").EnumerateArray()) { Console.WriteLine( $"{timelineEvent.GetProperty("timestamp")} {timelineEvent.GetProperty("event_type")}"); } ``` The response for an invoice that a production company sent, and that the buyer accepted: ```json theme={null} { "document_id": "doc-4f8a1c2e9b7d4a6f8e3c5b1a2d9f7e60", "events": [ { "event_type": "document_created", "timestamp": "2026-10-01T09:14:02.118000Z", "id": "doc-4f8a1c2e9b7d4a6f8e3c5b1a2d9f7e60", "details": { "invoice_id": "INV-2026-001", "vendor_tax_id": "BE1018265814", "customer_tax_id": "BE0848934496", "sender_peppol_id": "0208:1018265814", "receiver_peppol_id": "0208:0848934496" } }, { "event_type": "send_success", "timestamp": "2026-10-01T09:14:05.402000Z", "id": "7c1d2a90-5b3e-4f6a-9d2c-8e1f0a4b6c3d", "details": { "sender_peppol_id": "0208:1018265814", "receiver_peppol_id": "0208:0848934496" } }, { "event_type": "mlr_received", "timestamp": "2026-10-01T09:14:31.870000Z", "id": "ar-9a2b4c6d8e0f1a3b5c7d9e1f2a4b6c8d", "details": { "response_type": "mlr", "response_code": "AB", "sender_name": "OpenPeppol VZW" } }, { "event_type": "imr_received", "timestamp": "2026-10-03T14:02:47.006000Z", "id": "ar-1f3e5d7c9b0a2c4e6f8a0b1c3d5e7f9a", "details": { "response_type": "invoice_response", "response_code": "AP", "status_reason_code": "NON", "sender_name": "OpenPeppol VZW" } } ] } ``` For a [sandbox company](/environments), the timeline of an outbound document contains only the `document_created` event. The email delivery makes no transmission record, and Peppol responses are not delivered to sandbox companies. Read the `state` of the document to see the result of the send. ### Response fields The ID of the document. The events, sorted by `timestamp` from oldest to newest. One of the nine event types in the table below. The date and time of the event (ISO 8601). The ID of the record behind the event: the email, the document, the transmission or the response. More data about the event. The keys are different for each event type. A key is absent when it has no value, and the object is absent when it has no keys. The API returns `404 Not Found` when the document does not exist in your company. With the command-line tool, `peppol document timeline ` shows the same events. See [peppol CLI](/cli). ## Timeline events | Event type | Meaning | Keys in `details` | | - | - | - | | `email_received` | An inbound email that is linked to the document was received. See [Send invoices by email (Mailbox)](/guides/mailbox). | `subject`, `attachments` | | `email_processed` | The email was processed. | `subject`, `attachments` | | `document_created` | The document record was created. | `invoice_id`, `vendor_tax_id`, `customer_tax_id`, `vendor_company_id`, `customer_company_id`, `sender_peppol_id`, `receiver_peppol_id` | | `send_attempted` | The newest transmission is in progress (the document is `TRANSIT`). | `sender_peppol_id`, `receiver_peppol_id` | | `send_failed` | A transmission failed. | `sender_peppol_id`, `receiver_peppol_id` | | `send_success` | The transmission was successful (the document is `SENT`). | `sender_peppol_id`, `receiver_peppol_id` | | `receive_success` | The document was received through Peppol. | `sender_peppol_id`, `receiver_peppol_id` | | `mlr_received` | A Message Level Response arrived for the document. | `response_type`, `response_code`, `status_reason_code`, `status_reason`, `sender_name` | | `imr_received` | An Invoice Response arrived for the document. | `response_type`, `response_code`, `status_reason_code`, `status_reason`, `sender_name` | ## Read a failed transmission Each send request that reaches the transmission step makes one transmission record, and thus one event in the timeline. The API sets the event type of these records as follows: * Each record before the newest one is `send_failed`. * The newest record is `send_success` when the document is `SENT`, `send_attempted` when the document is `TRANSIT`, and `send_failed` in all other cases. A document that failed two times and was then delivered shows two `send_failed` events and one `send_success` event. The timeline does not show each automatic attempt. The maximum of 3 attempts in one send request (see [Retry behaviour](#retry-behaviour)) gives one event. If the preparation of the document fails before the transmission step, the document goes to `FAILED` and the timeline gets no send event for that request. Call `GET /api/documents/{document_id}` and make sure that `state` is `FAILED`. You also get the `document.sent.failed` webhook event when the transmission fails. Find the `send_failed` events. Examine `details.receiver_peppol_id` to make sure that the document went to the correct participant. Use [Look up Peppol participants](/guides/lookup-participants) to make sure that the receiver is registered on Peppol and can receive the document type. Call `POST /api/documents/{document_id}/send` again. Give the Peppol IDs as query parameters if the routing was incorrect. The document goes back to `TRANSIT` and the timeline gets a new event. The timeline does not contain the cause of a failed transmission. If the cause is not clear after these steps, contact support and give the document ID. ## Peppol responses After a successful transmission, the receiver can return two types of response. The API attaches each response to the original document and shows it in the timeline. | | Message Level Response (MLR) | Invoice Response | | - | - | - | | Timeline event | `mlr_received` | `imr_received` | | `details.response_type` | `mlr` | `invoice_response` | | Subject | The technical result: the receiving system accepted or rejected the message. | The business status: the buyer processes, accepts, rejects or pays the invoice. | Companies that you register through e-invoice.be are registered on Peppol as receivers of the two response types. No configuration is necessary. Responses are optional in Peppol. Many receivers send no Message Level Response and no Invoice Response. A timeline without response events does not mean that the buyer rejected or ignored the invoice. ### Response codes `details.response_code` contains one of these codes (UNCL4343 subset for Peppol BIS Invoice Response 3.0). | Code | Meaning | | - | - | | `AB` | Message acknowledgement | | `IP` | In process | | `UQ` | Under query | | `CA` | Conditionally accepted | | `AP` | Accepted | | `RE` | Rejected | | `PD` | Paid | ### Status reason codes `details.status_reason_code` gives the reason for the status (OpenPeppol `OPStatusReason` code list). `details.status_reason` contains the free text of the sender, if there is one. | Code | Meaning | | - | - | | `NON` | No issue | | `REF` | References incorrect | | `LEG` | Legal information incorrect | | `REC` | Receiver unknown | | `QUA` | Item quality insufficient | | `DEL` | Delivery issues | | `PRI` | Prices incorrect | | `QTY` | Quantity incorrect | | `ITM` | Items incorrect | | `PAY` | Payment terms incorrect | | `UNR` | Not recognised | | `FIN` | Finance incorrect | | `PPD` | Partially paid | | `OTH` | Other | ## Limits * **No webhook event for responses.** There is no webhook event type for a Message Level Response or an Invoice Response. To follow the business status of an invoice, poll the timeline of the document. * **No outbound Invoice Response.** The API has no endpoint to send an Invoice Response for a document that you received. * **No responses for sandbox companies.** A sandbox company has no Peppol traffic, thus its documents get no `mlr_received` or `imr_received` events. * **A response does not change the state.** The `state` of a document stays `SENT` when a response arrives, also when the response code is `RE`. ## Next Steps Receive an event when a document is sent, received or failed. Find the cause of an API error and correct it. Read and process the documents in your inbox. Use the lists, the filters and the draft operations. # Errors and troubleshooting Source: https://docs.e-invoice.be/guides/errors Read the error formats and status codes of the e-invoice.be API, correct frequent validation failures and retry failed requests safely. ## Overview This page is the reference for error handling. It shows the error body formats, the status codes of each endpoint, the frequent validation rules and the correct retry behaviour. ## Error body formats The API uses three formats. Read the HTTP status code first, then parse the body. ### Error with a text message Most errors return a JSON object with one field, `detail`, that contains a string. ```json 404 Not Found theme={null} { "detail": "Document not found" } ``` ### Request validation error (422) When a request does not match the schema of the endpoint (a missing field, a wrong type, a wrong date format), `detail` is an array. Each item identifies one field. ```json 422 Unprocessable Entity theme={null} { "detail": [ { "type": "missing", "loc": ["body", "file"], "msg": "Field required", "input": null } ] } ``` The path to the field. The first item is the part of the request (`body`, `query` or `path`). The description of the problem. The machine-readable error type, for example `missing`. Some endpoints also return `422` with a text message in `detail`, for example `"Invalid file type"`. Your code must accept a string and an array in `detail`. ### Validation result The validation endpoints (`POST /api/validate/json`, `POST /api/validate/ubl` and `POST /api/documents/{document_id}/validate`) return a success status when the document breaks a rule: `201 Created` for the two `/api/validate/` endpoints and `200 OK` for the validation of a stored document. The result is in the body: `is_valid` is `false` and `issues` contains the rule failures. ```json 201 Created theme={null} { "id": "b55354b0-5c69-489b-a8f7-44be7d5bdd6b", "file_name": "b55354b0-5c69-489b-a8f7-44be7d5bdd6b.xml", "is_valid": false, "issues": [ { "message": "Belgian enterprise number MUST be stated in the correct format.", "type": "error", "rule_id": "PEPPOL-COMMON-R043", "flag": "fatal", "schematron": "PEPPOL-EN16931-UBL" } ] } ``` The text of the rule that failed. The issue type, for example `error`. The identifier of the rule, for example `BR-CO-15` or `PEPPOL-EN16931-R120`. A failure of the XML schema has the value `xsd-validation`. The severity. An issue with the value `fatal` makes the document invalid. An issue with the `rule_id` `xsd-validation` also makes the document invalid. A result with `is_valid` `true` can contain issues with other values, for example warnings. The position in the generated UBL where the rule failed. The test expression of the rule. The rule set that found the issue: `XSD`, `CEN-EN16931-UBL`, `PEPPOL-EN16931-UBL` or `PEPPOL-EN16931-UBL-SB`. ## HTTP status codes | Code | Endpoints | Typical `detail` | Action | | - | - | - | - | | `400` | Send document | `Could not derive Peppol sender/receiver information from document. Provide via API params or ensure document has valid tax/company IDs.` | Give the four Peppol routing parameters in the send call. | | `400` | Send document (sandbox company) | `Tenant is in test mode but no company email is set` | Set the contact email address (`company_email`) of the sandbox company. Then create and send the document again. See [Send errors](#send-errors). | | `400` | Validate a stored document | `Could not derive Peppol sender or receiver information` | Correct the vendor and customer identifiers of the document. | | `400` | Delete document, delete attachment | `Document is not in draft state` | Delete only documents in state `DRAFT` or `FAILED`. See [Delete a draft](/guides/managing-documents#delete-a-draft). | | `400` | Usage statistics | `end_date must be greater than or equal to start_date` | Correct the date range. | | `401` | All endpoints with authentication | `Authentication required`, `Invalid authentication` | Send a valid API key. See [Authentication](/authentication). | | `403` | Admin API only | `This Peppol ID does not belong to this tenant.` | See [Admin API](/admin-api). The other endpoints do not return `403`. | | `404` | Endpoints with an ID in the path | `Document not found`, `Webhook not found`, `Inbound email not found` | Make sure that the ID is correct and that it belongs to the company of the API key. | | `404` | Usage statistics | `No documents found for this tenant` | Give `start_date` and `end_date`. See [Usage statistics and credits](/guides/usage-statistics#errors). | | `404` | List attachments | `Document attachments not found` | The document has no attachments. Treat the response as an empty list. See [Attachments and PDF](/guides/attachments#list-the-attachments-of-a-document). | | `405` | Send document | `Document is not in a valid state to send (DRAFT, FAILED)` | Do not send the document again. See [Send errors](#send-errors). | | `406` | Create document, validate JSON | `Document is not valid: [...]` | Correct the payload. See [Create document errors](#create-document-errors-406). | | `409` | Send document | `Derived sender '0208:0848934496' is not in tenant peppol_ids` | Send from a Peppol ID of your company. See [Ownership rule](/guides/self-billing#ownership-rule). | | `409` | Reprocess inbound email | `Email was already processed successfully` | Do not reprocess. See [Send invoices by email (Mailbox)](/guides/mailbox). | | `415` | Create document from UBL | `UBL file is empty`, `UBL file is not valid and contains errors` | Validate the file with `POST /api/validate/ubl` and correct it. | | `422` | All endpoints with parameters or a body | Array with `loc`, `msg`, `type` | Correct the field in `loc`. | | `422` | Create document from UBL or PDF, validate UBL, validate JSON, webhooks | `UBL File could not be parsed`, `CustomizationID '...' matches no registered format profile`, `PDF could not be OCRed`, `PDF could not be converted to a document`, `Invalid file type`, `Invalid vendor tax id`, `Invalid customer tax id`, `Invalid webhook event` | Correct the input. | | `429` | Create document, create document from UBL, validate JSON, validate UBL | `Too many requests, please try again later.` | Wait for the number of seconds in the `Retry-After` header. See [Rate limits](#rate-limits). | | `500` | All endpoints | `Internal server error` | Retry with a delay. If the error continues, contact support. | The same status code can have different causes on different endpoints. A failure to derive the Peppol IDs gives `406` when you create a document and `400` when you send it. A Peppol ID that is not owned by your company gives `406` when you create a document and `409` when you send it. ## Create document errors (406) `POST /api/documents/` returns `406 Not Acceptable` for four causes. No document is created. ```json theme={null} { "detail": "Could not derive Peppol sender or receiver information" } ``` The API derives the Peppol IDs from the tax IDs and the company IDs of the vendor and the customer. Make sure that `vendor_tax_id` and `customer_tax_id` (or the company IDs) are present and correct. ```json theme={null} { "detail": "Derived sender '0208:0848934496' is not in tenant peppol_ids" } ``` The Peppol ID that the API derived from the vendor is not registered for the company of the API key. The comparison ignores upper and lower case and spaces at the start and the end. Use the vendor identifiers of your company. Use `GET /api/me/` to see the Peppol IDs of your company. For a self-billing document, the customer is the sender on Peppol. Thus the API examines the Peppol ID of the customer, and the message also starts with `Derived sender`. See [Ownership rule](/guides/self-billing#ownership-rule). ```json theme={null} { "detail": "Document is not valid: ['Subtotal mismatch: provided 100.0, calculated 110.00']" } ``` The API calculates the totals from the lines and compares them with your values. The message can also start with `Total tax mismatch`, `Total discount mismatch`, `Invoice total mismatch` or `Amount due`. See [Invoice totals and calculations](/guides/invoice-totals). ```json theme={null} { "detail": "Provided document details lead to an invalid UBL document: {'Belgian enterprise number MUST be stated in the correct format.'}" } ``` `detail` contains the messages of the rules that failed, without the rule identifiers. Send the same payload to `POST /api/validate/json` to get the full list of issues with `rule_id`. Then see [Frequent validation rules](#frequent-validation-rules). Validation is not a separate mandatory call. `POST /api/documents/` rejects a payload that does not pass the same rules. Use `POST /api/validate/json` while you develop, because it returns all rule failures and the generated UBL. ## Send errors `POST /api/documents/{document_id}/send` does these checks in sequence. | Code | `detail` | Cause | Correction | | - | - | - | - | | `404` | `Document not found` | The ID is wrong, or the document belongs to a different company. | Use the `id` from the create response. | | `405` | `Document is not in a valid state to send (DRAFT, FAILED)` | The state is `TRANSIT`, `SENT` or `RECEIVED`. | Do not send again. Read the state with `GET /api/documents/{document_id}`. | | `404` | `UBL document not found` | The document has no generated UBL. | Create the document again. | | `400` | `Could not derive Peppol sender/receiver information from document. Provide via API params or ensure document has valid tax/company IDs.` | The API cannot find a full sender and receiver Peppol ID. | Give `sender_peppol_scheme`, `sender_peppol_id`, `receiver_peppol_scheme` and `receiver_peppol_id` as query parameters. | | `409` | `Derived sender '0208:0848934496' is not in tenant peppol_ids` | The sender is not a Peppol ID of your company. For self-billing, the sender is the customer. | Send from a Peppol ID of your company. | | `400` | `Tenant is in test mode but no company email is set` | A sandbox company sends documents to its contact email address (the `company_email` field), and this field is empty. | Set the contact email address in app.e-invoice.be. A reseller can set `company_email` with the [Admin API](/admin-api). Then create and send the document again. | When a send call returns `400` with `Tenant is in test mode but no company email is set`, the API has already changed the state of the document to `SENT`. The API sends no email and no webhook event. A new send call for this document returns `405`. Thus create a new document after you set the contact email address. ### Peppol ID routing When you send a document, the API finds each of the four Peppol values (sender scheme and identifier, receiver scheme and identifier) in this order: 1. The query parameter of the send request. 2. The Peppol ID that the API stored with the UBL of the document when you created it. For a document that you created from UBL, this is the `cbc:EndpointID` of the party. 3. The derivation from the identifiers in the document: `customer_peppol_id` first for the receiver, then `vendor_company_id` / `customer_company_id` and `vendor_tax_id` / `customer_tax_id`. For `SELFBILLING_INVOICE` and `SELFBILLING_CREDIT_NOTE`, the direction is reversed: the API derives the sender from the `customer_*` fields and the receiver from the `vendor_*` fields. For a Belgian party, the API derives scheme `0208` and the enterprise number: the tax ID `BE1018265814` gives the Peppol ID `0208:1018265814`. If a tax ID has no country prefix, the API uses the country code at the end of the address (for example `1980 Zemst, BE`). Always give the VAT number with its country prefix, because rule `BR-CO-09` rejects a VAT number without it. For the scheme that the API derives in other countries, see [Peppol ID schemes](/guides/lookup-participants#peppol-id-schemes). When the API sends the document on the Peppol network, it writes the final sender and receiver values in the `cbc:EndpointID` elements of the UBL that it transmits. The other content of the UBL does not change. The derived scheme can be different from the scheme that the receiver registered. Thus the best practice is to give all four query parameters: ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/documents/$DOCUMENT_ID/send?sender_peppol_scheme=0208&sender_peppol_id=1018265814&receiver_peppol_scheme=0208&receiver_peppol_id=0848934496" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` Scheme of the sender Peppol ID. Example: `0208`. Identifier of the sender, without the scheme. Example: `1018265814`. Scheme of the receiver Peppol ID. Example: `0208`. Identifier of the receiver, without the scheme. Example: `0848934496`. The API returns `400` if it cannot find a complete sender and receiver Peppol ID. It returns `409` if the sender Peppol ID is not one of the `peppol_ids` of your company. Before you send, make sure that the receiver is registered on the Peppol network with the Peppol ID that you use. See [Look up Peppol participants](/guides/lookup-participants). ## Frequent validation rules | Rule | Cause | Correction in the JSON | | - | - | - | | `PEPPOL-COMMON-R043` | A Belgian enterprise number does not have the correct format or fails the modulo 97 check. | Use a real enterprise number of 10 digits in `vendor_tax_id` and `customer_tax_id`, for example `BE1018265814`. | | `BR-S-08` | The taxable amount of a VAT rate is not equal to the sum of the line net amounts with that rate. | Give `amount` on each line, and make sure that `subtotal` is the sum of the line amounts. | | `PEPPOL-EN16931-R120` | The net amount of a line is not equal to quantity × unit price, plus line charges, minus line allowances. | Set `amount` to `quantity` × `unit_price` on each line. For allowances and charges, see [Advanced invoicing](/guides/advanced-invoicing). | | `BR-CO-15` | The total with VAT is not equal to the total without VAT plus the VAT amount. | Make sure that `invoice_total` = `subtotal` + `total_tax`. See [Invoice totals and calculations](/guides/invoice-totals). | | `BR-CO-25` | The amount due is more than zero, and the document has no due date and no payment terms. | Add `due_date` or `payment_term`. | | No rule (`422`) | A date does not have the format `YYYY-MM-DD`. The request fails before the rules run. | Send dates as `2026-10-01`. The array in `detail` gives the field in `loc`. | For the full procedure, see [Validation during development](/guides/validation). ## A document is in state `FAILED` After an accepted send call, the document is in state `TRANSIT`. The transmission occurs in the background. If all transmission attempts fail, the state becomes `FAILED` and the `document.sent.failed` webhook event is sent. See [Retry behaviour](/guides/document-lifecycle#retry-behaviour) for the number of attempts. `GET /api/documents/{document_id}/timeline` returns the events of the document. Each transmission attempt that failed is an event with `event_type` `send_failed`. The `details` object shows the sender and receiver Peppol IDs that the API used. ```bash cURL theme={null} curl "https://api.e-invoice.be/api/documents/doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d/timeline" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```json 200 OK theme={null} { "document_id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "events": [ { "event_type": "document_created", "timestamp": "2026-10-01T09:00:00Z", "id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d" }, { "event_type": "send_failed", "timestamp": "2026-10-01T09:00:05Z", "id": "7c1d2a90-5b3e-4f6a-9d2c-8e1f0a4b6c3d", "details": { "sender_peppol_id": "0208:1018265814", "receiver_peppol_id": "0208:0848934496" } } ] } ``` The values in this example are illustrative. Make sure that the receiver Peppol ID in `details` is correct. Then call `GET /api/validate/peppol-id?peppol_id=0208:0848934496` to see if the receiver is registered on the Peppol network. See [Look up Peppol participants](/guides/lookup-participants). Call `POST /api/documents/{document_id}/send` again. The API accepts a send call for a document in state `FAILED`. If the Peppol IDs were wrong, give the correct IDs as query parameters. See [Send a failed document again](/guides/managing-documents#send-a-failed-document-again). The timeline does not contain the technical reason of a failed transmission. If the receiver is registered and the document fails again, contact [support@e-invoice.be](mailto:support@e-invoice.be) with the document ID. For all states and transitions, see [Document lifecycle and delivery tracking](/guides/document-lifecycle). ## Retries and idempotency ### Which errors to retry | Response | Retry | How | | - | - | - | | `429` | Yes | Wait for the number of seconds in the `Retry-After` header. If you get `429` again, wait longer. | | `500` and network errors | Yes | Use a delay that increases with each attempt. Before you retry a create call, do the check below. | | `400`, `401`, `404`, `405`, `406`, `409`, `415`, `422` | No | The same request gives the same error. Correct the request first. | ### Rate limits The rate limit applies to each API key and only to these endpoints: * `POST /api/documents/` * `POST /api/documents/ubl` * `POST /api/validate/json` * `POST /api/validate/ubl` Each endpoint has its own counter. The counter starts again at the end of each time window. When you exceed the limit, the API returns `429` with a `Retry-After` header. The value of `Retry-After` is fixed. It is the length of the time window in seconds, not the time until your counter starts again. If the request after the wait also gets `429`, wait longer before the next attempt. ```json 429 Too Many Requests theme={null} { "detail": "Too many requests, please try again later." } ``` Do not write a fixed limit into your code. A successful response of these endpoints contains these headers: | Header | Value | | - | - | | `X-RateLimit-Limit` | The number of requests in one time window | | `X-RateLimit-Remaining` | The number of requests that remain in the current window | | `X-RateLimit-Reset` | The Unix time in seconds when the current window ends | Use these headers to slow down before you get `429`. ### No idempotency key The API has no idempotency key. If you repeat `POST /api/documents/`, the API creates a second draft with the same `invoice_id`. A send call is different: a second send call for a document in state `TRANSIT` or `SENT` returns `405` and does not transmit the document again. If a create call ends without a response (a timeout or a network error), find out if the document exists before you create it again. Search the drafts for your invoice number. This list contains documents in the states `DRAFT`, `TRANSIT` and `FAILED`. For documents that are sent, use `GET /api/outbox/` with the same `search` parameter. [Find duplicates before a retry](/guides/managing-documents#find-duplicates-before-a-retry) gives the full procedure. ```bash cURL theme={null} curl "https://api.e-invoice.be/api/drafts/?search=INV-2026-001" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```javascript Node.js theme={null} const params = new URLSearchParams({ search: "INV-2026-001" }); const response = await fetch( `https://api.e-invoice.be/api/drafts/?${params}`, { headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }, } ); const { items } = await response.json(); const existing = items.find((doc) => doc.invoice_id === "INV-2026-001"); console.log(existing ? existing.id : "Not found. It is safe to create the document."); ``` ```python Python theme={null} import os import requests response = requests.get( "https://api.e-invoice.be/api/drafts/", params={"search": "INV-2026-001"}, headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, ) response.raise_for_status() existing = next( (doc for doc in response.json()["items"] if doc["invoice_id"] == "INV-2026-001"), None, ) print(existing["id"] if existing else "Not found. It is safe to create the document.") ``` ```php PHP theme={null} 'INV-2026-001']); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('E_INVOICE_API_KEY')], ]); $body = json_decode(curl_exec($ch), true); curl_close($ch); $existing = null; foreach ($body['items'] as $doc) { if ($doc['invoice_id'] === 'INV-2026-001') { $existing = $doc; break; } } echo $existing ? $existing['id'] : 'Not found. It is safe to create the document.'; ``` ```csharp C# theme={null} using System.Net.Http.Headers; using System.Text.Json; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var json = await client.GetStringAsync( "https://api.e-invoice.be/api/drafts/?search=INV-2026-001"); using var body = JsonDocument.Parse(json); string? existingId = null; foreach (var doc in body.RootElement.GetProperty("items").EnumerateArray()) { if (doc.GetProperty("invoice_id").GetString() == "INV-2026-001") { existingId = doc.GetProperty("id").GetString(); break; } } Console.WriteLine(existingId ?? "Not found. It is safe to create the document."); ``` ```json 200 OK theme={null} { "items": [ { "id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "state": "DRAFT", "document_type": "INVOICE", "invoice_id": "INV-2026-001", "vendor_name": "E-INVOICE BV", "customer_name": "OpenPeppol VZW" } ], "total": 1, "page": 1, "page_size": 20, "pages": 1, "has_next_page": false } ``` The response is shortened. The `search` parameter finds a part of the invoice number, the vendor name, the customer name or the note, thus compare `invoice_id` for an exact match. ## Troubleshooting The endpoint expects a `multipart/form-data` upload with a part that has the name `file`. You sent the XML or the PDF as the raw request body. With cURL, use `-F "file=@invoice.xml"` and do not set the `Content-Type` header manually. See [Send UBL documents](/guides/ubl-documents) and [Create documents from PDF](/guides/pdf-documents). `POST /api/validate/ubl` accepts only a `file` part with the content type `application/xml` or `text/xml`. Some HTTP clients send a file part as `application/octet-stream`. Set the content type of the part. With cURL, use `-F "file=@invoice.xml;type=application/xml"`. With Python `requests`, use `files={"file": ("invoice.xml", open("invoice.xml", "rb"), "application/xml")}`. `Authentication required` means that the request has no `Authorization: Bearer` header. `Invalid authentication` means that the API key is not known or is deleted. Make sure that the header has the format `Authorization: Bearer ` and that the key is the key of the company that you want to use. See [Authentication](/authentication). The create and validate endpoints have a rate limit for each API key. Read the `Retry-After` header, wait, then continue. Send the requests in sequence, not in parallel. Read the state with `GET /api/documents/{document_id}`. * `DRAFT`: the document was not sent. Call the send endpoint. * `TRANSIT`: the transmission is in progress. * `FAILED`: see [A document is in state `FAILED`](#a-document-is-in-state-failed). * `SENT`: the access point of the receiver accepted the document. Ask the customer to look in the software that is connected to their Peppol ID. Read the timeline to see which receiver Peppol ID was used. If you use a sandbox company, the document is not sent on the Peppol network. See [Test mode and sandbox companies](/environments). A sandbox company sends each document as an email with the UBL file to the contact email address of your company, not to the customer. This address is the `company_email` field of `GET /api/me/`. Make sure that it is set, and look in the spam folder. If `company_email` is empty, the send call returns `400` with `Tenant is in test mode but no company email is set`. See [Send errors](#send-errors) and [Test mode and sandbox companies](/environments). The signature is calculated on the full event as JSON with sorted keys, not on the `data` field only. Make sure that you use the secret of the correct webhook and that you serialise the payload as the [Webhooks](/essentials/webhooks) guide shows. A sandbox company does not receive documents from the Peppol network, and a document that you send to your own company does not go into the inbox. Use **Simulate inbound** to put a document into the inbox. See [Receive documents](/guides/receiving-documents). ## Next Steps Find all rule failures of a payload before you create a document. Learn the states of a document and the transitions between them. Receive an event when a document is sent or fails. Calculate totals that pass the validation rules. # Invoice totals and calculations Source: https://docs.e-invoice.be/guides/invoice-totals Calculate the totals of an invoice in the same way as the API, so that the API accepts the values that you send. ## Overview Each invoice and credit note has five total fields. You can send them or omit them: * If you omit a field, the API calculates it from the line items, the document-level allowances and the document-level charges. * If you send a field, the API compares it with its own calculation and rejects the document if the difference is too large. This page gives the formulas that the API uses. To add allowances and charges to a document, refer to [Advanced invoicing](/guides/advanced-invoicing). ## Total fields | Field | Description | | - | - | | `subtotal` | The amount without VAT, after document-level allowances and charges | | `total_discount` | The sum of the document-level allowances | | `total_tax` | The total VAT amount | | `invoice_total` | The amount with VAT | | `amount_due` | The amount that the customer must pay after prepayments | The API returns these fields as strings (for example `"950.00"`). ## Basic formula ``` line total = sum of (quantity × unit_price - line allowances + line charges) subtotal = line total - document-level allowances + document-level charges invoice_total = subtotal + total_tax amount_due = invoice_total - prepayment ``` The API rounds each line result and each total to 2 decimals (half up). ## Each field in detail ### Subtotal `subtotal` is the amount of the invoice without VAT. ``` subtotal = line total - sum of all document-level allowances + sum of all document-level charges ``` * The API calculates the line total from `quantity`, `unit_price` and the line-level allowances and charges of each item. It does not use the `amount` of the item for this sum. Send an `amount` that agrees with this calculation. * All document-level allowances and charges are in the subtotal, also those with a `tax_rate` of `0.00`. Example: ``` Line items: €1,000.00 Commercial discount: -€100.00 Shipping charge: +€50.00 Subtotal: €950.00 ``` ### Total tax `total_tax` is the sum of the VAT of each tax rate. 1. The API adds the line results together for each `tax_rate`. 2. A document-level allowance with a `tax_rate` more than `0.00` decreases the tax base of that rate. A document-level charge with a `tax_rate` more than `0.00` increases it. Allowances and charges with a `tax_rate` of `0.00` do not change a tax base. 3. For each rate: tax = tax base × rate / 100, rounded to 2 decimals. 4. `total_tax` is the sum of these amounts. Example: ``` Tax base at 21%: €950.00 Total tax: €199.50 (950.00 × 0.21) ``` A document-level allowance or charge has a default `tax_rate` of `21.00`. Send the `tax_rate` and the [tax category code](/glossary#tax-category-code) of the items to which it applies. Items without VAT use a category such as `E`, and an exemption can need a [VATEX](/glossary#vatex) code. ### Total discount `total_discount` is the sum of all document-level allowances. ``` total_discount = sum of all document-level allowances ``` * These allowances are already subtracted in `subtotal`. Do not subtract `total_discount` again. * Line-level allowances and document-level charges are not in this field. Example: ``` Line items: €1,000.00 Early payment discount (21% VAT): -€50.00 Total discount: €50.00 Subtotal: €950.00 Tax (21%): €199.50 Invoice total: €1,149.50 ``` ### Invoice total `invoice_total` is the amount of the invoice with VAT, before prepayments. ``` invoice_total = subtotal + total_tax ``` ### Amount due `amount_due` is the amount that the customer must pay. If you omit it, the API sets it to `invoice_total`. There is no separate field for a prepayment. To state a prepayment, send an `amount_due` that is lower than `invoice_total`. If the difference is more than 0.01, the API writes the difference to the UBL as the prepaid amount. ``` Invoice total: €1,199.50 Amount due: €999.50 Prepaid amount in the UBL: €200.00 ``` ## Document-level and line-level adjustments ### Document-level allowances and charges * Change `subtotal`. * Apply to the full invoice, after the line items are added together. * Change the tax base of their `tax_rate` (if that rate is more than `0.00`). * Allowances are added to `total_discount`. Charges are not. * Examples: early payment discounts, shipping for the full order, handling fees. ### Line-level allowances and charges * Change the amount of one line item. * Are not in `total_discount`. * Examples: bulk discount on one product, special handling for a fragile item. ## Complete example This fragment shows the parts of a document that the totals are calculated from: ```json theme={null} { "items": [ { "description": "Product A", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "allowances": [ { "reason": "Commercial discount", "amount": 200.00, "tax_code": "S", "tax_rate": "21.00" }, { "reason": "Early payment discount", "amount": 50.00, "tax_code": "S", "tax_rate": "21.00" } ], "charges": [ { "reason": "Shipping", "amount": 50.00, "tax_code": "S", "tax_rate": "21.00" } ] } ``` Calculation: 1. Line total: €1,000.00 2. Document-level allowances: -€200.00 and -€50.00 3. Document-level charge: +€50.00 4. `subtotal`: €800.00 5. `total_tax`: €800.00 × 21% = €168.00 6. `total_discount`: €250.00 (allowances only) 7. `invoice_total`: €800.00 + €168.00 = €968.00 8. `amount_due`: €968.00 (no prepayment) The totals that you can send with this document: ```json theme={null} { "subtotal": 800.00, "total_discount": 250.00, "total_tax": 168.00, "invoice_total": 968.00, "amount_due": 968.00 } ``` ## UBL mapping | API field | UBL element | | - | - | | `subtotal` | `cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount` | | `total_tax` | `cac:TaxTotal/cbc:TaxAmount` | | `total_discount` | `cac:LegalMonetaryTotal/cbc:AllowanceTotalAmount` | | `invoice_total` | `cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount` | | `amount_due` | `cac:LegalMonetaryTotal/cbc:PayableAmount` | The API also writes `LineExtensionAmount` (the sum of the line amounts), `ChargeTotalAmount` (the sum of the document-level charges) and, for a prepayment, `PrepaidAmount`. These elements have no API field. ## Validation rules `POST /api/documents/` and `POST /api/validate/json` do these checks on the totals: 1. `subtotal`, `total_tax`, `total_discount` and `invoice_total`: each value that you send must agree with the value that the API calculates. The API permits a maximum difference of 1.00. 2. `amount_due` must not be more than the calculated `invoice_total` plus 1.00. The API does not write the `subtotal`, `total_tax`, `total_discount` and `invoice_total` that you send to the UBL. The UBL always contains the values that the API calculates. The API stores and returns the values that you send. Thus the UBL can be different from the response by a maximum of 1.00. Only `amount_due` goes to the UBL as you send it. Do not send an `amount_due` that is more than `invoice_total`. If the difference is more than 0.01, the totals check accepts it, but the generated UBL fails rule `BR-CO-16` and the API returns `406`. If a check fails, the API returns `406` and names the field, the value that you sent and the calculated value (for example `Subtotal mismatch: provided 900.00, calculated 800.00`). Refer to [Errors and troubleshooting](/guides/errors) for the error format. After these checks, the generated UBL must also pass the Peppol BIS Billing 3.0 rules. Refer to [Validation during development](/guides/validation). ## Frequently asked questions A document-level allowance applies to the full invoice and is in `total_discount`. A line-level allowance applies to one line item, decreases the amount of that line, and is not in `total_discount`. The totals check of the API has no minimum value for `subtotal`, `total_tax`, `total_discount`, `invoice_total` or `amount_due`. It only compares each value with the calculated value. Thus `invoice_total` is negative when the document-level allowances are larger than the line total plus the charges, or when the lines have negative quantities. To refund or correct an invoice, send a [credit note](/guides/credit-notes) with positive amounts, not an invoice with a negative total. The API calculates each of `subtotal`, `total_tax`, `total_discount` and `invoice_total` that you omit, and sets `amount_due` to `invoice_total`. The response of the create call contains the calculated values. A document-level charge (for example a shipping fee) increases `subtotal`. If its `tax_rate` is more than `0.00`, it also increases the tax base of that rate. It is not in `total_discount`. ## Next Steps Add allowances and charges at document level and line level Examine a document before you create it Read the error formats and the frequent rule failures See all fields of a document # Look up Peppol participants Source: https://docs.e-invoice.be/guides/lookup-participants Find the Peppol ID of a customer and make sure that the customer can receive invoices before you send. A receiver must have a [Peppol ID](/glossary#peppol-id) and a registration with an Access Point before you can send a document on the Peppol network. The API has three operations to find and check a receiver. All three work without an API key: the `Authorization` header is not necessary. ## Compare the operations | | `GET /api/validate/peppol-id` | `GET /api/lookup` | `GET /api/lookup/participants` | | - | - | - | - | | **Purpose** | Yes-or-no check before you send | Full technical record of a participant | Search by name | | **Input** | Exact Peppol ID | Exact Peppol ID | Name or part of an identifier | | **Data source** | SML and [SMP](/glossary#smp) | SML and SMP | [Peppol Directory](/glossary#peppol-directory) | | **Result** | `is_valid` and the supported document types | DNS data, endpoints, certificates, [business card](/glossary#business-card) | List of possible participants | | **Complete** | Yes, for registered participants | Yes, for registered participants | No | The two operations with an exact Peppol ID use the same lookup and the same cache. The API keeps each result for 15 minutes. The Peppol Directory can be incomplete. Publication in the directory is optional for an Access Point, so a registered participant can be absent from the search results. An empty search result does not show that a company is not on Peppol. Use the directory search to find candidates only, then check the Peppol ID. ## Peppol ID schemes A Peppol ID has the format `scheme:identifier`, for example `0208:1018265814`. The [scheme](/glossary#scheme) is a four-digit code that tells which register issued the identifier. | Country | ISO code | Scheme | Identifier type | Example | | - | - | - | - | - | | Belgium | BE | `0208` | Enterprise number (CBE/KBO), 10 digits. This is the VAT number without `BE`. | `0208:1018265814` | | Netherlands | NL | `0106` | KVK number | `0106:12345678` | | Netherlands | NL | `9944` | VAT number | `9944:NL123456789B01` | | Luxembourg | LU | `9938` | VAT number | `9938:LU12345678` | | Luxembourg | LU | `0240` | Register of legal persons (matricule) | `0240:12345678910` | | France | FR | `0002` | SIREN number | `0002:123456789` | | France | FR | `0009` | SIRET number | `0009:12345678901234` | | France | FR | `9957` | VAT number | `9957:FR12345678901` | | Germany | DE | `0204` | Leitweg-ID (public sector) | `0204:991-12345-67` | | Germany | DE | `9930` | VAT number | `9930:DE123456789` | | Italy | IT | `0211` | VAT number (Partita IVA) | `0211:IT12345678901` | | United Kingdom | GB | `9932` | VAT number | `9932:GB123456789` | | Denmark | DK | `0184` | CVR number | `0184:12345678` | | Denmark | DK | `0198` | SE number | `0198:DK12345678` | | Norway | NO | `0192` | Organisation number | `0192:123456789` | | Sweden | SE | `0007` | Organisation number | `0007:1234567890` | | Finland | FI | `0216` | OVT code | `0216:003712345678` | | International | - | `0088` | Global Location Number (GLN) | `0088:1234567890123` | | International | - | `0060` | D-U-N-S number | `0060:123456789` | This table shows the most frequent schemes of the Peppol participant identifier scheme code list, version 9.7. This is the version that the API uses. For all schemes, see the [Peppol code lists](https://docs.peppol.eu/edelivery/codelists/). When you do not give the Peppol IDs, the API derives one scheme for each country from the tax ID or the company ID: | Country | Derived scheme | | - | - | | Belgium | `0208`, with `*_company_id` or else the tax ID without `BE` | | Netherlands | `0106` with `*_company_id`; else `9944` with the VAT number | | Denmark | `0198`, with the identifier `DK` + 8 digits | | Norway | `0192`, with the 9-digit organisation number (the API removes `MVA`) | | Sweden | `0007`, with the 10-digit organisation number | | Finland | `0216`, with the OVT code (`0037` + the 8-digit business ID) | | Italy | `0211` | | Other countries | The VAT scheme of the country, for example `9930` (Germany), `9957` (France), `9938` (Luxembourg) or `9932` (United Kingdom) | The scheme that the API derives can be different from the scheme that the receiver registered. Check the Peppol ID of the receiver, and give it explicitly when you send. The schemes that you can register for your own company are a smaller set. See [Supported schemes](/admin-api#supported-schemes). For a Belgian company, the identifier is the [enterprise number](/glossary#cbe-and-kbo): the [VAT number](/glossary#vat-number) without `BE`. The VAT number `BE1018265814` gives the Peppol ID `0208:1018265814`. ## Pre-send check Before you send a document, call `GET /api/validate/peppol-id` with the Peppol ID of the receiver. Send only if `is_valid` is `true` and `supported_document_types` contains the document type that you send. This is the only place where this rule is written; other pages link to it. Peppol ID in the format `scheme:identifier`, for example `0208:1018265814`. ```bash cURL theme={null} curl "https://api.e-invoice.be/api/validate/peppol-id?peppol_id=0208:1018265814" ``` ```javascript Node.js theme={null} const params = new URLSearchParams({ peppol_id: '0208:1018265814' }); const response = await fetch(`https://api.e-invoice.be/api/validate/peppol-id?${params}`); const result = await response.json(); console.log(result.is_valid, result.supported_document_types); ``` ```python Python theme={null} import requests url = "https://api.e-invoice.be/api/validate/peppol-id" result = requests.get(url, params={"peppol_id": "0208:1018265814"}).json() print(result["is_valid"], result["supported_document_types"]) ``` ```php PHP theme={null} '0208:1018265814']); $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $result = json_decode(curl_exec($ch), true); var_dump($result['is_valid']); ``` ```csharp C# theme={null} using var client = new HttpClient(); var json = await client.GetStringAsync("https://api.e-invoice.be/api/validate/peppol-id?peppol_id=" + Uri.EscapeDataString("0208:1018265814")); Console.WriteLine(json); ``` ```json theme={null} { "is_valid": true, "dns_valid": true, "business_card_valid": true, "supported_document_types": [ "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", "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" ], "business_card": { "name": "E-INVOICE BV", "country_code": "BE", "registration_date": "2025-01-15" } } ``` The SML has a DNS record for the Peppol ID, so an SMP knows the participant. The SMP publishes a business card with a minimum of one entity. `true` if `dns_valid` or `business_card_valid` is `true`. It does not tell which document types the receiver accepts; read `supported_document_types` for that. `null` if there is no business card. For a Peppol ID that is not registered, the status is `200` and `is_valid` is `false`. ## Get the full record of a participant `GET /api/lookup` returns the complete result of the SML and SMP lookup. Use it when you need the endpoints, the transport profile or the certificates of a receiver, for example to find the cause of a failed send. It has the same `peppol_id` query parameter as the pre-send check. ```bash cURL theme={null} curl "https://api.e-invoice.be/api/lookup?peppol_id=0208:1018265814" ``` ```javascript Node.js theme={null} const params = new URLSearchParams({ peppol_id: '0208:1018265814' }); const response = await fetch(`https://api.e-invoice.be/api/lookup?${params}`); const record = await response.json(); console.log(record.dnsInfo.status, record.businessCard.entities[0]?.name); ``` ```python Python theme={null} import requests url = "https://api.e-invoice.be/api/lookup" record = requests.get(url, params={"peppol_id": "0208:1018265814"}).json() print(record["dnsInfo"]["status"]) ``` ```php PHP theme={null} '0208:1018265814']); $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $record = json_decode(curl_exec($ch), true); echo $record['dnsInfo']['status']; ``` ```csharp C# theme={null} using var client = new HttpClient(); var json = await client.GetStringAsync("https://api.e-invoice.be/api/lookup?peppol_id=" + Uri.EscapeDataString("0208:1018265814")); Console.WriteLine(json); ``` ```json theme={null} { "queryMetadata": { "identifierScheme": "iso6523-actorid-upis", "identifierValue": "0208:1018265814", "smlDomain": "participant.sml.prod.tech.peppol.org", "timestamp": "2026-01-12T14:32:10.123456", "version": "1.0.0" }, "status": "success", "errors": [], "dnsInfo": { "status": "success", "smpHostname": "smp.e-invoice.be", "smlHostname": "participant.sml.prod.tech.peppol.org", "dnsRecords": [{ "ip": "193.140.151.175" }] }, "serviceMetadata": { "status": "success", "queryTimeMs": 210.4, "endpoints": [ { "status": "success", "url": "https://smp.e-invoice.be/iso6523-actorid-upis::0208:1018265814/services/busdox-docid-qns::urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice%23%23urn:cen.eu:en16931:2017%23compliant%23urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1", "documentTypes": [ { "scheme": "busdox-docid-qns", "value": "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" } ] } ] }, "businessCard": { "status": "success", "entities": [{ "name": "E-INVOICE BV", "countryCode": "BE" }], "queryTimeMs": 123.45 }, "certificates": [], "executionTimeMs": 895 } ``` This example is shortened: it shows one endpoint, and no `processes` and `certificates` content. The status is `200` also for a Peppol ID that is not registered. In that case `dnsInfo.status` is `error`. This response has no single field that says "registered"; the pre-send check gives that answer. ## Search the Peppol Directory `GET /api/lookup/participants` searches the public [Peppol Directory](https://directory.peppol.eu). Use it when you know the name of a company but not its Peppol ID, for example for a search field in your application. Search text: a company name or a part of an identifier. ISO 3166-1 alpha-2 country code, for example `BE`. Without this parameter the search is global. ```bash cURL theme={null} curl "https://api.e-invoice.be/api/lookup/participants?query=E-INVOICE&country_code=BE" ``` ```javascript Node.js theme={null} const params = new URLSearchParams({ query: 'E-INVOICE', country_code: 'BE' }); const response = await fetch(`https://api.e-invoice.be/api/lookup/participants?${params}`); const { participants } = await response.json(); console.log(participants.map((p) => p.peppol_id)); ``` ```python Python theme={null} import requests url = "https://api.e-invoice.be/api/lookup/participants" result = requests.get(url, params={"query": "E-INVOICE", "country_code": "BE"}).json() for p in result["participants"]: print(p["peppol_id"]) ``` ```php PHP theme={null} 'E-INVOICE', 'country_code' => 'BE']); $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $result = json_decode(curl_exec($ch), true); echo count($result['participants']); ``` ```csharp C# theme={null} using var client = new HttpClient(); var json = await client.GetStringAsync("https://api.e-invoice.be/api/lookup/participants?query=" + Uri.EscapeDataString("E-INVOICE") + "&country_code=BE"); Console.WriteLine(json); ``` ```json theme={null} { "total_count": 1, "used_count": 1, "query_terms": "q=E-INVOICE&country=BE", "search_date": "2026-01-12T14:32:10.123Z", "participants": [ { "peppol_id": "0208:1018265814", "peppol_scheme": "iso6523-actorid-upis", "entities": [ { "name": "E-INVOICE BV", "country_code": "BE", "registration_date": "2025-01-15", "identifiers": [] } ], "document_types": [ { "scheme": "busdox-docid-qns", "value": "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" } ] } ] } ``` The `peppol_id` is the complete Peppol ID (`scheme:identifier`). Use it as it is for the pre-send check. The `peppol_scheme` is the identifier scheme of the Peppol Directory (`iso6523-actorid-upis`), not the four-digit scheme: do not join it to `peppol_id`. The API copies the values from the Peppol Directory: `query_terms` is the query that the API sent to the directory, and `search_date` is the time of the search in UTC. The response has a maximum of 20 participants and no paging. Make the search text more specific if the participant that you look for is not in the list. ## Find and check a receiver This pattern combines the operations. If you know the identifier, it checks the Peppol ID directly. If you know only the name, it searches the directory and checks each candidate. ```javascript Node.js theme={null} const BASE = 'https://api.e-invoice.be'; const INVOICE = 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##'; async function get(path, params) { const response = await fetch(`${BASE}${path}?${new URLSearchParams(params)}`); if (!response.ok) throw new Error(`${path} returned ${response.status}`); return response.json(); } // Pre-send check: registered and able to receive invoices. async function checkReceiver(peppolId) { const result = await get('/api/validate/peppol-id', { peppol_id: peppolId }); const canReceive = result.is_valid && result.supported_document_types.some((t) => t.startsWith(INVOICE)); return { peppolId, canReceive, name: result.business_card?.name ?? null }; } // Directory search: candidates only. async function searchDirectory(name, countryCode) { const result = await get('/api/lookup/participants', { query: name, country_code: countryCode }); return (result.participants ?? []).map((p) => p.peppol_id); } async function findReceiver({ peppolId, name, countryCode = 'BE' }) { const candidates = peppolId ? [peppolId] : await searchDirectory(name, countryCode); const checked = await Promise.all(candidates.map(checkReceiver)); return checked.filter((c) => c.canReceive); } console.log(await findReceiver({ peppolId: '0208:0848934496' })); console.log(await findReceiver({ name: 'OpenPeppol' })); ``` ```python Python theme={null} import requests BASE = "https://api.e-invoice.be" INVOICE = "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##" def get(path, params): response = requests.get(f"{BASE}{path}", params=params, timeout=30) response.raise_for_status() return response.json() def check_receiver(peppol_id): """Pre-send check: registered and able to receive invoices.""" result = get("/api/validate/peppol-id", {"peppol_id": peppol_id}) types = result["supported_document_types"] can_receive = result["is_valid"] and any(t.startswith(INVOICE) for t in types) card = result.get("business_card") or {} return {"peppol_id": peppol_id, "can_receive": can_receive, "name": card.get("name")} def search_directory(name, country_code): """Directory search: candidates only.""" result = get("/api/lookup/participants", {"query": name, "country_code": country_code}) return [p["peppol_id"] for p in result.get("participants") or []] def find_receiver(peppol_id=None, name=None, country_code="BE"): candidates = [peppol_id] if peppol_id else search_directory(name, country_code) return [c for c in map(check_receiver, candidates) if c["can_receive"]] print(find_receiver(peppol_id="0208:0848934496")) print(find_receiver(name="OpenPeppol")) ``` If the list is empty after a search by name, ask the customer for the enterprise number or VAT number and check the Peppol ID directly. If the direct check also gives `is_valid: false`, the customer cannot receive documents on Peppol; use a different delivery method. ## Integration checklist * Do the [pre-send check](#pre-send-check) for each new receiver. * Use the directory search only to find candidates, and check each candidate. * Do not use an empty directory result as proof that a company is not registered. * Show the user a clear message when the search finds nothing, and ask for the identifier. * Keep a positive result in your own cache for a limited time; a receiver can change its Access Point or stop its registration. * Set the Peppol IDs explicitly when you send. See [Create e-invoices](/guides/creating-invoices). * For the status codes and the error body of these operations, see [Errors and troubleshooting](/guides/errors). ## Next Steps Create a document and send it to the receiver that you checked. Follow the state of a document after the send. Find the cause of a failed send. Read the definitions of the Peppol terms. # Send invoices by email (Mailbox) Source: https://docs.e-invoice.be/guides/mailbox Create draft e-invoices from emails with a UBL or PDF attachment, and monitor, download and retry those emails with the Mailbox API. ## Overview Each company has a personal `@outbox.email.e-invoice.be` address. When you send an email with an invoice or [credit note](/guides/credit-notes) attachment (UBL XML or PDF) to this address, e-invoice.be makes a draft document from the attachment. The [Mailbox](/glossary#mailbox) is the record of these emails: it shows which emails arrived, which document each email made, and why an email failed. Use the Mailbox when: * Your ERP or accounting tool can email invoices but cannot call an API. * You want to add the address in BCC to the emails that you already send to your customers. * You want to start with e-invoicing before you build an API integration. The email itself needs no API call. The Mailbox API is for monitoring: list the emails, read one email, download an attachment and retry a failed email. The Mailbox is for documents that you send (outbound). To read documents that you receive through Peppol, see [Receive documents](/guides/receiving-documents). ## Find the mailbox address The address is unique for each company. You can get it in two ways: * **In the app**: open the company settings in [app.e-invoice.be](https://app.e-invoice.be). The card **E-Invoice Email Conversion** shows the **BCC Email Address** with a copy button. * **With the API**: call `GET /api/me/` and read the field `bcc_recipient_email`. ```bash cURL theme={null} curl -X GET "https://api.e-invoice.be/api/me/" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```json Response (extract) theme={null} { "company_name": "E-INVOICE BV", "company_tax_id": "BE1018265814", "bcc_recipient_email": "e-invoice-bv+k3x9q7m2@outbox.email.e-invoice.be" } ``` If `bcc_recipient_email` is `null`, the Mailbox is not available for the company. Keep the address private. The part after the `+` identifies your company, and an email to this address makes a document in your company. ## Send an email to the Mailbox Attach the document as UBL XML or as PDF. One email makes one document. For more than one invoice, send more than one email. Put the mailbox address in the BCC field (recommended), or in the To or CC field. With BCC, your customer does not see the address. The email shows in the Mailbox. Processing of a PDF can take some minutes, because the data is extracted from the PDF. When the processing is complete, the email has a `document_id`. The document is in state `DRAFT`. Review it, then send it with `POST /api/documents/{document_id}/send`. ## What happens to an email ### Selection of the main attachment | Attachments in the email | Result | | - | - | | One UBL XML file or one PDF file | This file is the main document. | | UBL XML and PDF files | UBL has precedence. The PDF is not used to make the document. | | More than one candidate of the same type | The file that is most likely the invoice is selected automatically, based on the file names. | | No UBL XML and no PDF file | The email is not converted. | The main attachment can be 10 MB at most. Send one invoice file for each email and give it a clear file name, for example `invoice-INV-2026-001.pdf`. Then the selection is not ambiguous. ### Other attachments * **PDF as main attachment**: the PDF and the other attachments with a file type that Peppol permits (rule BR-CL-24: PDF, PNG, JPEG, CSV, XLSX, ODS) become attachments of the document. Other file types, and images that are part of the email body (for example a logo in a signature), are ignored. * **UBL as main attachment**: only the UBL is used. Other attachments in the email are not added. Embed the attachments in the UBL itself. See [Attachments and PDF](/guides/attachments). ### Conversion and validation * A UBL file is validated against Peppol BIS Billing 3.0. An invalid UBL makes the email fail. * A PDF is converted in the same way as in [Create documents from PDF](/guides/pdf-documents). If the extracted data does not make a valid e-invoice, the email fails. * A successful email makes one document in state `DRAFT`. The Mailbox does not send the document through Peppol. You send it yourself. ## Email status The status of an email is not a field in the response. It is derived from `processed` and `error_message`. You can filter on it with the `status` parameter. | Status | `processed` | `error_message` | Meaning | | - | - | - | - | | `pending` | `false` | `null` | The email arrived and is not processed yet. | | `success` | `true` | `null` | The document was made. `document_id` has the ID of the document. | | `failed` | any | text | The processing failed. `error_message` gives the cause. | ## List the emails `GET /api/mailbox/` returns the emails of your company, with the newest email first. Page number. Minimum 1. Number of emails for each page. Minimum 1, maximum 100. Filter by status: `pending`, `success` or `failed`. Has precedence over `processed`. Filter by the `processed` field. Ignored when `status` is set. Inclusive lower limit of the received date, for example `2026-10-01T00:00:00Z`. If an email has no `received_at`, the filter uses `created_at`. Inclusive upper limit of the received date. If an email has no `received_at`, the filter uses `created_at`. Case-insensitive search in the sender email address, the subject, the message ID and the attachment file names. Sort field: `received_at` or `created_at`. Sort direction: `asc` or `desc`. The example lists the failed emails. ```bash cURL theme={null} curl -X GET "https://api.e-invoice.be/api/mailbox/?status=failed&page=1&page_size=20" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```javascript Node.js theme={null} const params = new URLSearchParams({ status: 'failed', page: '1', page_size: '20', }); const response = await fetch(`https://api.e-invoice.be/api/mailbox/?${params}`, { headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }, }); if (!response.ok) { throw new Error(`Request failed: ${response.status} ${await response.text()}`); } const { items, total } = await response.json(); console.log(`${total} failed emails`); for (const email of items) { console.log(email.id, email.subject, email.error_message); } ``` ```python Python theme={null} import os import requests response = requests.get( "https://api.e-invoice.be/api/mailbox/", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, params={"status": "failed", "page": 1, "page_size": 20}, timeout=30, ) response.raise_for_status() data = response.json() print(f"{data['total']} failed emails") for email in data["items"]: print(email["id"], email["subject"], email["error_message"]) ``` ```php PHP theme={null} 'failed', 'page' => 1, 'page_size' => 20, ]); $ch = curl_init("https://api.e-invoice.be/api/mailbox/?$query"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('E_INVOICE_API_KEY')], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 200) { throw new RuntimeException("Request failed: $status $body"); } $data = json_decode($body, true); echo $data['total'] . " failed emails\n"; foreach ($data['items'] as $email) { echo $email['id'] . ' ' . $email['subject'] . ' ' . $email['error_message'] . "\n"; } ``` ```csharp C# theme={null} using System.Net.Http.Headers; using System.Text.Json; using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var response = await client.GetAsync("/api/mailbox/?status=failed&page=1&page_size=20"); response.EnsureSuccessStatusCode(); using var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); Console.WriteLine($"{json.RootElement.GetProperty("total").GetInt32()} failed emails"); foreach (var email in json.RootElement.GetProperty("items").EnumerateArray()) { Console.WriteLine($"{email.GetProperty("id")} {email.GetProperty("subject")} {email.GetProperty("error_message")}"); } ``` ```json Response theme={null} { "items": [ { "id": "mail-8f3k2j5h7g9d1s4a6q0w2e5r8t1y3u7i", "message_id": "b7bc2f4a-e38e-4336-af7d-e6c392c2f817", "sender_email": "billing@e-invoice.be", "sender_name": "E-INVOICE BV", "to_addresses": "accounts@openpeppol.org", "cc_addresses": null, "bcc_addresses": "e-invoice-bv+k3x9q7m2@outbox.email.e-invoice.be", "subject": "Invoice INV-2026-001", "attachments": [ { "filename": "invoice-INV-2026-001.pdf", "size": 184320, "content_type": "application/pdf" } ], "attachment_count": 1, "processed": false, "error_message": "Not able to generate valid e-invoice from 'invoice-INV-2026-001.pdf'", "received_at": "2026-10-01T09:14:22Z", "created_at": "2026-10-01T09:14:23Z", "processed_at": null, "document_id": null } ], "total": 1, "page": 1, "page_size": 20, "pages": 1, "has_next_page": false } ``` ### Response fields | Field | Type | Description | | - | - | - | | `id` | string | ID of the email in the Mailbox. Starts with `mail-`. | | `message_id` | string | Message ID that the mail system gave to the email. | | `sender_email` | string | Email address of the sender. | | `sender_name` | string or null | Display name of the sender. | | `to_addresses`, `cc_addresses`, `bcc_addresses` | string or null | Recipients of the email. | | `subject` | string or null | Subject of the email. | | `attachments` | array | For each attachment: `filename`, `size` (bytes), `content_type`. | | `attachment_count` | integer | Number of attachments. | | `processed` | boolean | `true` when the document was made. | | `error_message` | string or null | Cause of the failure. `null` when there is no failure. | | `received_at` | date-time or null | Time at which the email arrived. | | `created_at` | date-time | Time at which the Mailbox record was made. | | `processed_at` | date-time or null | Time at which the processing was complete. | | `document_id` | string or null | ID of the document that the email made. | ## Get one email `GET /api/mailbox/{inbound_email_id}` returns one email. Use it to poll an email until `document_id` has a value or `error_message` has a value. ```bash cURL theme={null} curl -X GET "https://api.e-invoice.be/api/mailbox/mail-8f3k2j5h7g9d1s4a6q0w2e5r8t1y3u7i" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```json Response theme={null} { "id": "mail-8f3k2j5h7g9d1s4a6q0w2e5r8t1y3u7i", "message_id": "b7bc2f4a-e38e-4336-af7d-e6c392c2f817", "sender_email": "billing@e-invoice.be", "sender_name": "E-INVOICE BV", "to_addresses": "accounts@openpeppol.org", "cc_addresses": null, "bcc_addresses": "e-invoice-bv+k3x9q7m2@outbox.email.e-invoice.be", "subject": "Invoice INV-2026-001", "attachments": [ { "filename": "invoice-INV-2026-001.pdf", "size": 184320, "content_type": "application/pdf" } ], "attachment_count": 1, "processed": true, "error_message": null, "received_at": "2026-10-01T09:14:22Z", "created_at": "2026-10-01T09:14:23Z", "processed_at": "2026-10-01T09:15:41Z", "document_id": "doc-4n7c2x9v1b5m8k3j6h0g2f4d7s1a9q5w" } ``` An ID that does not exist, or that belongs to a different company, gives `404` with `{"detail": "Inbound email not found"}`. ## Download an attachment `GET /api/mailbox/{inbound_email_id}/attachments/{filename}` returns the original file from the email. The response body is the binary content, with the `Content-Type` of the attachment and a `Content-Disposition: attachment` header. The `filename` must be exactly the same as `attachments[].filename` in the email. URL-encode the file name. ```bash cURL theme={null} curl -X GET "https://api.e-invoice.be/api/mailbox/mail-8f3k2j5h7g9d1s4a6q0w2e5r8t1y3u7i/attachments/invoice-INV-2026-001.pdf" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ --output invoice-INV-2026-001.pdf ``` ```javascript Node.js theme={null} import { writeFile } from 'node:fs/promises'; const emailId = 'mail-8f3k2j5h7g9d1s4a6q0w2e5r8t1y3u7i'; const filename = 'invoice-INV-2026-001.pdf'; const response = await fetch( `https://api.e-invoice.be/api/mailbox/${emailId}/attachments/${encodeURIComponent(filename)}`, { headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` } }, ); if (!response.ok) { throw new Error(`Download failed: ${response.status} ${await response.text()}`); } await writeFile(filename, Buffer.from(await response.arrayBuffer())); console.log(`Saved ${filename} (${response.headers.get('content-type')})`); ``` ```python Python theme={null} import os from urllib.parse import quote import requests email_id = "mail-8f3k2j5h7g9d1s4a6q0w2e5r8t1y3u7i" filename = "invoice-INV-2026-001.pdf" response = requests.get( f"https://api.e-invoice.be/api/mailbox/{email_id}/attachments/{quote(filename)}", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, timeout=60, ) response.raise_for_status() with open(filename, "wb") as file: file.write(response.content) print(f"Saved {filename} ({response.headers['Content-Type']})") ``` ```php PHP theme={null} true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('E_INVOICE_API_KEY')], ]); $content = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 200) { throw new RuntimeException("Download failed: $status $content"); } file_put_contents($filename, $content); echo "Saved $filename\n"; ``` ```csharp C# theme={null} using System.Net.Http.Headers; var emailId = "mail-8f3k2j5h7g9d1s4a6q0w2e5r8t1y3u7i"; var filename = "invoice-INV-2026-001.pdf"; using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var response = await client.GetAsync( $"/api/mailbox/{emailId}/attachments/{Uri.EscapeDataString(filename)}"); response.EnsureSuccessStatusCode(); await File.WriteAllBytesAsync(filename, await response.Content.ReadAsByteArrayAsync()); Console.WriteLine($"Saved {filename}"); ``` ```http Response theme={null} HTTP/1.1 200 OK Content-Type: application/pdf Content-Disposition: attachment; filename="invoice-INV-2026-001.pdf"; filename*=UTF-8''invoice-INV-2026-001.pdf ``` The endpoint gives `404` when the email does not exist, when the email has no attachment with this file name, or when the original email is no longer in storage. ## Retry a failed email `POST /api/mailbox/{inbound_email_id}/reprocess` processes a failed email again from the stored original. Use it after you corrected the cause of the failure on your side, or when the failure was temporary (for example a timeout of the PDF conversion). The request has no body. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/mailbox/mail-8f3k2j5h7g9d1s4a6q0w2e5r8t1y3u7i/reprocess" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```javascript Node.js theme={null} const emailId = 'mail-8f3k2j5h7g9d1s4a6q0w2e5r8t1y3u7i'; const response = await fetch(`https://api.e-invoice.be/api/mailbox/${emailId}/reprocess`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }, }); if (response.status !== 202) { const error = await response.json(); throw new Error(`Reprocess refused: ${response.status} ${error.detail}`); } const email = await response.json(); console.log(`Reprocessing started for ${email.id}`); ``` ```python Python theme={null} import os import requests email_id = "mail-8f3k2j5h7g9d1s4a6q0w2e5r8t1y3u7i" response = requests.post( f"https://api.e-invoice.be/api/mailbox/{email_id}/reprocess", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, timeout=30, ) if response.status_code != 202: raise RuntimeError( f"Reprocess refused: {response.status_code} {response.json()['detail']}" ) print(f"Reprocessing started for {response.json()['id']}") ``` ```php PHP theme={null} true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('E_INVOICE_API_KEY')], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $data = json_decode($body, true); if ($status !== 202) { throw new RuntimeException("Reprocess refused: $status " . $data['detail']); } echo 'Reprocessing started for ' . $data['id'] . "\n"; ``` ```csharp C# theme={null} using System.Net; using System.Net.Http.Headers; using System.Text.Json; var emailId = "mail-8f3k2j5h7g9d1s4a6q0w2e5r8t1y3u7i"; using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var response = await client.PostAsync($"/api/mailbox/{emailId}/reprocess", null); using var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); if (response.StatusCode != HttpStatusCode.Accepted) { throw new InvalidOperationException( $"Reprocess refused: {(int)response.StatusCode} {json.RootElement.GetProperty("detail")}"); } Console.WriteLine($"Reprocessing started for {json.RootElement.GetProperty("id")}"); ``` ```json Response (202 Accepted) theme={null} { "id": "mail-8f3k2j5h7g9d1s4a6q0w2e5r8t1y3u7i", "message_id": "b7bc2f4a-e38e-4336-af7d-e6c392c2f817", "sender_email": "billing@e-invoice.be", "sender_name": "E-INVOICE BV", "to_addresses": "accounts@openpeppol.org", "cc_addresses": null, "bcc_addresses": "e-invoice-bv+k3x9q7m2@outbox.email.e-invoice.be", "subject": "Invoice INV-2026-001", "attachments": [ { "filename": "invoice-INV-2026-001.pdf", "size": 184320, "content_type": "application/pdf" } ], "attachment_count": 1, "processed": false, "error_message": null, "received_at": "2026-10-01T09:14:22Z", "created_at": "2026-10-01T09:14:23Z", "processed_at": null, "document_id": null } ``` `202 Accepted` means that the processing started again, not that it is complete. The previous `error_message` is cleared and the email goes back to `pending`. Poll `GET /api/mailbox/{inbound_email_id}` to get the result. The endpoint refuses the request in these conditions: | Status code | `detail` | Condition | | - | - | - | | `404` | `Inbound email not found` | The ID does not exist or belongs to a different company. | | `409` | `Email is still pending processing` | The email is `pending`. Wait for the result. | | `409` | `Email was already processed successfully` | The email is `success`. A second document is not made. | | `409` | `Email failed but a document was already created — manual intervention required` | The email has an `error_message` and also a `document_id`. Examine the document, or contact support. | | `422` | `Original email payload is not available in storage — cannot reprocess` | The stored original of the email is gone. Send the email again. | ## Track the result in the document timeline When an email made a document, the timeline of that document contains two Mailbox events: `email_received` and `email_processed`. The `id` of each event is the ID of the email, and `details` has the subject and the attachment metadata. ```bash cURL theme={null} curl -X GET "https://api.e-invoice.be/api/documents/doc-4n7c2x9v1b5m8k3j6h0g2f4d7s1a9q5w/timeline" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```json Response (extract) theme={null} { "document_id": "doc-4n7c2x9v1b5m8k3j6h0g2f4d7s1a9q5w", "events": [ { "event_type": "email_received", "timestamp": "2026-10-01T09:14:22Z", "id": "mail-8f3k2j5h7g9d1s4a6q0w2e5r8t1y3u7i", "details": { "subject": "Invoice INV-2026-001" } }, { "event_type": "email_processed", "timestamp": "2026-10-01T09:15:41Z", "id": "mail-8f3k2j5h7g9d1s4a6q0w2e5r8t1y3u7i", "details": { "subject": "Invoice INV-2026-001" } } ] } ``` See [Document lifecycle and delivery tracking](/guides/document-lifecycle) for the other events and the document states. ## Troubleshooting Make sure that the full mailbox address, with the part after the `+`, is in the To, CC or BCC field. An email to an address that does not match a company is not stored in a Mailbox. If the company has a list of allowed senders, the From address of the email must be in that list. The UBL attachment does not pass the Peppol BIS Billing 3.0 rules. Use `POST /api/validate/ubl` to get the rule failures, correct the file and send a new email. See [Send UBL documents](/guides/ubl-documents). The data extracted from the PDF does not make a valid invoice (for example, a tax ID or a total is missing). Download the attachment to examine it. You can also convert the PDF with `POST /api/documents/pdf`, correct the JSON and create the document with `POST /api/documents/`. See [Create documents from PDF](/guides/pdf-documents). When an email has UBL and PDF files, UBL has precedence. When an email has more than one file of the same type, the selection uses the file names. Send one invoice file for each email. ## Next Steps Learn how a PDF is converted into an e-invoice. Send UBL XML that your system already makes. Follow a document from draft to delivery. Get a notification when a document is sent. # List, filter and manage documents Source: https://docs.e-invoice.be/guides/managing-documents List your documents with pagination, filters and sort options, and validate, send or delete drafts. ## Overview The API has three document lists. Each list returns the same paginated response and accepts the same pagination and sort parameters. The filters are different for each list. Use this guide when you build a document overview, a reconciliation job or a retry procedure. ## The three lists | List | Endpoint | Direction | States in the list | | - | - | - | - | | Drafts | `GET /api/drafts/` | Outbound | `DRAFT`, `TRANSIT` and `FAILED` (default). One state when you set `state`. See [Drafts](#drafts). | | Outbox | `GET /api/outbox/` | Outbound | `SENT` only | | Inbox | `GET /api/inbox/` | Inbound | `RECEIVED` only | `GET /api/outbox/` does not return drafts, documents in transit or failed documents. An outbound document stays in the drafts list until its state is `SENT`. To see all outbound documents, read the drafts list and the outbox. | State | Meaning | | - | - | | `DRAFT` | The document exists but is not sent. `POST /api/documents/` always creates a document in this state. | | `TRANSIT` | The send request is accepted and the transmission is in progress. | | `SENT` | The transmission is complete. | | `FAILED` | The transmission failed. You can send the document again. | | `RECEIVED` | The document came in from another Peppol participant. | You can send a document only when it is in the `DRAFT` or `FAILED` state. For the transitions, the retry behaviour and the webhook events of each state, see [Document lifecycle and delivery tracking](/guides/document-lifecycle). For the inbox and the procedure to receive documents, see [Receive documents](/guides/receiving-documents). ## Pagination All three lists use page-based pagination. Page number. The first page is `1`. Number of documents on each page. Minimum `1`, maximum `100`. A value out of this range gives `422 Unprocessable Entity`. ```bash cURL theme={null} curl -X GET "https://api.e-invoice.be/api/outbox/?page=1&page_size=20" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```javascript Node.js theme={null} const response = await fetch( 'https://api.e-invoice.be/api/outbox/?page=1&page_size=20', { headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` } } ); const result = await response.json(); console.log(result.total, result.items.length); ``` ```python Python theme={null} import os import requests response = requests.get( "https://api.e-invoice.be/api/outbox/", params={"page": 1, "page_size": 20}, headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, ) result = response.json() print(result["total"], len(result["items"])) ``` ```php PHP theme={null} The response has the same shape for each list. Each entry in `items` is a full document object (see the [document schema](/api-reference/schemas/document)). The example shows only some of the document fields. ```json theme={null} { "items": [ { "id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "created_at": "2026-10-01T09:15:42Z", "document_type": "INVOICE", "state": "SENT", "direction": "OUTBOUND", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "currency": "EUR", "invoice_total": "1210.00" } ], "total": 250, "page": 1, "page_size": 20, "pages": 13, "has_next_page": true } ``` | Field | Description | | - | - | | `items` | Documents on this page | | `total` | Number of documents that agree with the filters, on all pages | | `page` | Page number of this response | | `page_size` | Page size of this response | | `pages` | Number of pages | | `has_next_page` | `true` when `page` is less than `pages` | ### Read all pages Increase `page` until `has_next_page` is `false`. ```javascript Node.js theme={null} const headers = { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }; const documents = []; let page = 1; while (true) { const url = new URL('https://api.e-invoice.be/api/outbox/'); url.searchParams.set('page', String(page)); url.searchParams.set('page_size', '100'); const response = await fetch(url, { headers }); if (!response.ok) { throw new Error(`List request failed: ${response.status}`); } const result = await response.json(); documents.push(...result.items); if (!result.has_next_page) break; page += 1; } console.log(`${documents.length} documents`); ``` ```python Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"} documents = [] page = 1 while True: response = requests.get( "https://api.e-invoice.be/api/outbox/", params={"page": page, "page_size": 100}, headers=headers, ) response.raise_for_status() result = response.json() documents.extend(result["items"]) if not result["has_next_page"]: break page += 1 print(f"{len(documents)} documents") ``` Pagination uses a page offset, not a cursor. With the default sort (`created_at`, newest first), a document that is added during the loop moves the subsequent documents one position. A document can then be on two pages. Remove duplicates by `id`, or sort with `sort_order=asc` for a stable sequence. ## Filters Filters are combined with a logical AND. Text filters are not case-sensitive and match a part of the value. ### Drafts `GET /api/drafts/` | Parameter | Type | Description | | - | - | - | | `state` | string | One document state. Use `DRAFT`, `TRANSIT` or `FAILED`. The parameter also accepts `SENT`, but use the outbox for sent documents. Without this parameter, the list contains the states `DRAFT`, `TRANSIT` and `FAILED`. | | `type` | string | Document type: `INVOICE`, `CREDIT_NOTE`, `DEBIT_NOTE`, `SELFBILLING_INVOICE`, `SELFBILLING_CREDIT_NOTE`. Without this parameter, the list contains all types. | | `date_from` | date-time | Issue date (`invoice_date`) on or after this date | | `date_to` | date-time | Issue date (`invoice_date`) on or before this date | | `search` | string | Text search. See [Search](#search). | ### Outbox `GET /api/outbox/` | Parameter | Type | Description | | - | - | - | | `type` | string | Document type. Same values as for drafts. | | `receiver` | string | Matches `customer_name`, `customer_email`, `customer_tax_id`, `customer_company_id` or `customer_id` | | `date_from` | date-time | Issue date (`invoice_date`) on or after this date | | `date_to` | date-time | Issue date (`invoice_date`) on or before this date | | `search` | string | Text search. See [Search](#search). | | `sender` | string | Deprecated. See [Deprecated items](#deprecated-items). | The outbox has no `state` parameter. It always returns documents in state `SENT`. ### Inbox `GET /api/inbox/` | Parameter | Type | Description | | - | - | - | | `type` | string | Document type. Same values as for drafts. | | `sender` | string | Matches `vendor_name`, `vendor_email`, `vendor_tax_id` or `vendor_company_id` | | `date_from` | date-time | Issue date (`invoice_date`) on or after this date | | `date_to` | date-time | Issue date (`invoice_date`) on or before this date | | `search` | string | Text search. See [Search](#search). | The inbox has no `state` parameter. It always returns documents in state `RECEIVED`. See [Receive documents](/guides/receiving-documents) for the full procedure. ### Date filters `date_from` and `date_to` compare with the issue date of the document (`invoice_date`), not with the date of creation or transmission. Both limits are inclusive. The API uses only the date part of the value and ignores the time part. ```bash theme={null} curl -G "https://api.e-invoice.be/api/outbox/" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ --data-urlencode "date_from=2026-09-01T00:00:00" \ --data-urlencode "date_to=2026-09-30T00:00:00" \ --data-urlencode "type=INVOICE" ``` The response is the paginated shape from [Pagination](#pagination), with only the invoices that have an issue date in September 2026. ### Search `search` is available on all three lists. It matches a part of one of these fields: * `invoice_id` (the invoice number) * `customer_name` * `vendor_name` * `note` ```bash theme={null} curl -G "https://api.e-invoice.be/api/drafts/" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ --data-urlencode "search=INV-2026-001" ``` `search` is not an exact match. `search=INV-2026-001` also returns `INV-2026-0010`. Compare `invoice_id` in your code when you must have an exact match. ## Sort All three lists accept the same sort parameters. Field to sort by. One of `created_at`, `invoice_date`, `due_date`, `invoice_total`, `customer_name`, `vendor_name`, `invoice_id`. Sort direction: `asc` or `desc`. The default is `created_at` in descending order: the newest document is first. A value that is not in the list gives `422 Unprocessable Entity`. ```bash theme={null} curl -G "https://api.e-invoice.be/api/outbox/" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ --data-urlencode "sort_by=invoice_total" \ --data-urlencode "sort_order=desc" ``` ## Manage a draft A document that you create with `POST /api/documents/` starts in state `DRAFT`. These are the operations that the API permits on a draft. | Operation | Endpoint | Permitted states | Response in other states | | - | - | - | - | | Read | `GET /api/documents/{document_id}` | All | - | | Validate | `POST /api/documents/{document_id}/validate` | All | - | | Send | `POST /api/documents/{document_id}/send` | `DRAFT`, `FAILED` | `405 Method Not Allowed` | | Delete | `DELETE /api/documents/{document_id}` | `DRAFT`, `FAILED` | `400 Bad Request` | | Update | Not available | - | - | Create the document with `POST /api/documents/`. See [Create e-invoices](/guides/creating-invoices). Keep the `id` from the response. This step is optional. `POST /api/documents/{document_id}/validate` validates the stored document against Peppol BIS Billing 3.0 and returns the UBL XML. Send the document with `POST /api/documents/{document_id}/send`. The document stays in the drafts list until its state is `SENT`. Then it is in the outbox. Delete the document with `DELETE /api/documents/{document_id}`. ### Validate a stored draft ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/documents/doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d/validate" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```javascript Node.js theme={null} const documentId = 'doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d'; const response = await fetch( `https://api.e-invoice.be/api/documents/${documentId}/validate`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }, } ); const result = await response.json(); console.log(result.is_valid, result.issues); ``` ```python Python theme={null} import os import requests document_id = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d" response = requests.post( f"https://api.e-invoice.be/api/documents/{document_id}/validate", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, ) result = response.json() print(result["is_valid"], result["issues"]) ``` ```php PHP theme={null} ```json theme={null} { "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab", "file_name": null, "is_valid": true, "issues": [], "ubl_document": "..." } ``` When the document has rule failures, `is_valid` is `false` and `issues` contains one entry for each failure. The `id` in this response identifies the validation result, not the document. See [Validation during development](/guides/validation) for the issue fields. ### Delete a draft ```bash cURL theme={null} curl -X DELETE "https://api.e-invoice.be/api/documents/doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```javascript Node.js theme={null} const documentId = 'doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d'; const response = await fetch( `https://api.e-invoice.be/api/documents/${documentId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }, } ); console.log(response.status, await response.json()); ``` ```python Python theme={null} import os import requests document_id = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d" response = requests.delete( f"https://api.e-invoice.be/api/documents/{document_id}", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, ) print(response.status_code, response.json()) ``` ```php PHP theme={null} ```json theme={null} { "is_deleted": true } ``` You can delete a document only in state `DRAFT` or `FAILED`. For a document in state `TRANSIT`, `SENT` or `RECEIVED`, the API returns `400 Bad Request`: ```json theme={null} { "detail": "Document is not in draft state" } ``` A document ID that does not exist in your company gives `404 Not Found`. A deleted document is not in the lists and you cannot get it back through the API. ### Change a draft The API has no endpoint to update a document. To change a draft, delete it and create it again with the corrected data. The new document has a new `id`. Validation is not a separate mandatory call. `POST /api/documents/` rejects a payload that does not pass the same rules. Use `POST /api/validate/json` while you develop, because it returns all rule failures and the generated UBL. A payload that you validate before you create the document does not make a draft that you must delete. See [Validation during development](/guides/validation). ### Send a failed document again You can send a document from state `DRAFT` or `FAILED` only. To try a failed document again, call `POST /api/documents/{document_id}/send` again with the same document ID. A new document is not necessary. For a document in a different state, the API returns `405 Method Not Allowed`: ```json theme={null} { "detail": "Document is not in a valid state to send (DRAFT, FAILED)" } ``` Find the failed documents with the `state` filter: ```bash theme={null} curl -G "https://api.e-invoice.be/api/drafts/" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ --data-urlencode "state=FAILED" ``` The response is the paginated shape from [Pagination](#pagination). See [Errors and troubleshooting](/guides/errors) for the causes of a failed send. ## Find duplicates before a retry The API has no idempotency key. If a create request has a timeout and you send it again, the result can be two drafts with the same `invoice_id`. The API does not reject the second one. Before you repeat a create request, look for the invoice number in the drafts list and in the outbox: Call `GET /api/drafts/?search=INV-2026-001`. This finds documents in state `DRAFT`, `TRANSIT` and `FAILED`. Call `GET /api/outbox/?search=INV-2026-001`. This finds documents in state `SENT`. `search` matches a part of the value. In the results, keep only the documents whose `invoice_id` is equal to your invoice number. If you find a document, use its `id` and do not create a new one. If you find more than one draft, delete the extra drafts. ## Deprecated items | Deprecated | Replacement | Remarks | | - | - | - | | `GET /api/outbox/drafts` | `GET /api/drafts/` | The deprecated endpoint has no `date_from` and `date_to` filters. | | `sender` parameter on `GET /api/outbox/` | `receiver` parameter | The API uses `sender` as `receiver` when `receiver` is not set. | ## Next Steps Learn the document states and the transitions between them. Read the inbox and process inbound documents. Find the cause of an error response or a failed send. # Create documents from PDF Source: https://docs.e-invoice.be/guides/pdf-documents Upload a PDF invoice, get the extracted invoice data and a UBL document, and send the result through Peppol. ## Overview The API can extract the invoice data from a PDF file and convert it to a UBL document. Use this method when: * You have PDF invoices that you want to send through Peppol. * Your system generates PDF invoices but no structured data. * You migrate from PDF invoicing to e-invoicing. If your ERP can only send invoices by email, see [Send invoices by email (Mailbox)](/guides/mailbox). To attach a PDF to a document that you create from JSON, see [Attachments and PDF](/guides/attachments). ## How it works A PDF upload does not create a document in your company. The API returns the extracted invoice data as JSON, with these fields in addition to the invoice fields: | Field | Description | | - | - | | `success` | `true` if the PDF was converted into a compliant e-invoice. | | `ubl_document` | The UBL XML as a string. Present only if the API could generate the UBL. It can also be present when `success` is `false`: then the UBL does not pass the validation rules. | | `error_message`, `error_type` | The cause when `success` is `false`. | After the upload, you do these steps: 1. Examine the extracted data. The extraction can contain errors. 2. Create the document: from the `ubl_document` with `POST /api/documents/ubl`, or from the corrected JSON with `POST /api/documents/`. 3. Send the document. There are two upload methods: | Method | Endpoint | Use | | - | - | - | | Synchronous | `POST /api/documents/pdf` | One request that stays open until the extraction is complete. This can take up to 8 minutes, so set a client timeout of 8 minutes or more. | | Asynchronous | `POST /api/conversion/pdf` | Returns a task ID immediately. You poll for the result. Use it when your client cannot keep a connection open for a long time. | All upload requests use `multipart/form-data` with one form field with the name `file`. ## Synchronous upload `POST /api/documents/pdf` has two optional query parameters. Tax ID of the vendor. If you do not give it, the API uses the tax ID of your company. Tax ID of the customer. The API uses it as input for the extraction. The samples use the house example: E-INVOICE BV (`BE1018265814`) as vendor and OpenPeppol VZW (`BE0848934496`) as customer. Replace the vendor tax ID with that of your company. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/documents/pdf?vendor_tax_id=BE1018265814&customer_tax_id=BE0848934496" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -F "file=@invoice.pdf;type=application/pdf" ``` ```javascript Node.js theme={null} import { readFile } from "node:fs/promises"; const query = new URLSearchParams({ vendor_tax_id: "BE1018265814", customer_tax_id: "BE0848934496", }); const form = new FormData(); form.append( "file", new Blob([await readFile("invoice.pdf")], { type: "application/pdf" }), "invoice.pdf", ); // Do not set the Content-Type header. fetch adds it with the multipart boundary. const response = await fetch(`https://api.e-invoice.be/api/documents/pdf?${query}`, { method: "POST", headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }, body: form, }); const extracted = await response.json(); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${JSON.stringify(extracted)}`); } console.log(extracted.success, extracted.invoice_id, extracted.invoice_total); ``` ```python Python theme={null} import os import requests # Do not set the Content-Type header. requests adds it with the multipart boundary. with open("invoice.pdf", "rb") as f: response = requests.post( "https://api.e-invoice.be/api/documents/pdf", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, params={"vendor_tax_id": "BE1018265814", "customer_tax_id": "BE0848934496"}, files={"file": ("invoice.pdf", f, "application/pdf")}, timeout=600, ) response.raise_for_status() extracted = response.json() print(extracted["success"], extracted.get("invoice_id"), extracted.get("invoice_total")) ``` ```php PHP theme={null} "BE1018265814", "customer_tax_id" => "BE0848934496", ]); $ch = curl_init("https://api.e-invoice.be/api/documents/pdf?" . $query); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 600, CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("E_INVOICE_API_KEY")], // An array as body makes the request multipart/form-data. CURLOPT_POSTFIELDS => [ "file" => new CURLFile("invoice.pdf", "application/pdf", "invoice.pdf"), ], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 201) { throw new RuntimeException("HTTP $status: $body"); } $extracted = json_decode($body, true); echo $extracted["invoice_id"] . " " . $extracted["invoice_total"] . PHP_EOL; ``` ```csharp C# theme={null} using System.Net.Http.Headers; using var client = new HttpClient { Timeout = TimeSpan.FromMinutes(10) }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var file = new ByteArrayContent(await File.ReadAllBytesAsync("invoice.pdf")); file.Headers.ContentType = new MediaTypeHeaderValue("application/pdf"); using var form = new MultipartFormDataContent(); form.Add(file, "file", "invoice.pdf"); var response = await client.PostAsync( "https://api.e-invoice.be/api/documents/pdf?vendor_tax_id=BE1018265814&customer_tax_id=BE0848934496", form); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {body}"); } Console.WriteLine(body); ``` Response (`201`, shortened). This block is a response and not a request body. The UBL string is shortened, so do not use the block as input. ```json theme={null} { "success": true, "document_type": "INVOICE", "state": "DRAFT", "direction": "OUTBOUND", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "subtotal": "1000.00", "total_tax": "210.00", "invoice_total": "1210.00", "items": [ { "description": "Professional services", "quantity": "10", "unit_price": "100.00", "amount": "1000.00", "tax_rate": "21.00" } ], "ubl_document": "(shortened)" } ``` The response has no `id`, because the upload does not store a document. The values `state` and `direction` are the values that the document gets when you create it. If the API cannot extract the line items, the response has `success: false`, `error_type: "ExtractionError"` and one placeholder line with the description `Line items could not be extracted from PDF`. ## Create and send the document Check `success` first. If `success` is `true` and the response has a `ubl_document`, save the string to a file and create the document from it. If `success` is `false`, do not use the `ubl_document`: `POST /api/documents/ubl` rejects a UBL that does not pass the rules. `POST /api/documents/ubl` accepts `multipart/form-data` with a `file` field, not a raw XML body. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/documents/ubl" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -F "file=@extracted_invoice.xml;type=application/xml" ``` The response is the created document in state `DRAFT`, with an `id` such as `doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d`. For this request in five languages and for the response, see [Send UBL documents](/guides/ubl-documents#create-the-document-from-ubl). If `success` is `false`, if the response has no `ubl_document`, or if the extracted data is not correct, correct the JSON and create the document with `POST /api/documents/`. See [Create e-invoices](/guides/creating-invoices). Then send the document. The samples read the document ID from the environment variable `DOCUMENT_ID`. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/documents/$DOCUMENT_ID/send?sender_peppol_scheme=0208&sender_peppol_id=1018265814&receiver_peppol_scheme=0208&receiver_peppol_id=0848934496" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```javascript Node.js theme={null} const documentId = process.env.DOCUMENT_ID; // the "id" from the create response const query = new URLSearchParams({ sender_peppol_scheme: "0208", sender_peppol_id: "1018265814", receiver_peppol_scheme: "0208", receiver_peppol_id: "0848934496", }); const response = await fetch( `https://api.e-invoice.be/api/documents/${documentId}/send?${query}`, { method: "POST", headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }, }, ); const result = await response.json(); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${JSON.stringify(result)}`); } console.log(result.state); ``` ```python Python theme={null} import os import requests document_id = os.environ["DOCUMENT_ID"] # the "id" from the create response response = requests.post( f"https://api.e-invoice.be/api/documents/{document_id}/send", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, params={ "sender_peppol_scheme": "0208", "sender_peppol_id": "1018265814", "receiver_peppol_scheme": "0208", "receiver_peppol_id": "0848934496", }, timeout=30, ) if not response.ok: raise SystemExit(f"HTTP {response.status_code}: {response.text}") print(response.json()["state"]) ``` ```php PHP theme={null} "0208", "sender_peppol_id" => "1018265814", "receiver_peppol_scheme" => "0208", "receiver_peppol_id" => "0848934496", ]); $ch = curl_init("https://api.e-invoice.be/api/documents/$documentId/send?$query"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("E_INVOICE_API_KEY"), ], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status >= 400) { throw new RuntimeException("HTTP $status: $body"); } echo json_decode($body, true)["state"], PHP_EOL; ``` ```csharp C# theme={null} using System.Net.Http.Headers; var documentId = Environment.GetEnvironmentVariable("DOCUMENT_ID"); // the "id" from the create response using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var query = "sender_peppol_scheme=0208&sender_peppol_id=1018265814&receiver_peppol_scheme=0208&receiver_peppol_id=0848934496"; var response = await client.PostAsync($"/api/documents/{documentId}/send?{query}", null); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {body}"); } Console.WriteLine(body); ``` Response (`200`, shortened): ```json theme={null} { "id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "created_at": "2026-10-01T12:00:00Z", "document_type": "INVOICE", "state": "TRANSIT", "direction": "OUTBOUND", "invoice_id": "INV-2026-001" } ``` Before you send, make sure that the receiver is registered on the Peppol network. See [Look up Peppol participants](/guides/lookup-participants). ## Asynchronous upload The asynchronous upload has no tax ID parameters. The API uses the tax ID of your company as the vendor tax ID. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/conversion/pdf" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -F "file=@invoice.pdf;type=application/pdf" ``` Response (`201`): ```json theme={null} { "task_id": "0b5f4b0e-7c1d-4e7a-9d57-2f6c1a3e8b90" } ``` The response has no `status` field. The API never returns the status `pending`. ```bash cURL theme={null} curl "https://api.e-invoice.be/api/conversion/pdf/$TASK_ID" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` While the task is in progress, this request returns `404` with the detail `PDF conversion task not found or expired`. Continue to poll. When the task is complete, the response is `200`: ```json theme={null} { "task_id": "0b5f4b0e-7c1d-4e7a-9d57-2f6c1a3e8b90", "status": "completed" } ``` The `status` is `completed` or `failed`. ```bash cURL theme={null} curl "https://api.e-invoice.be/api/conversion/pdf/$TASK_ID/result" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` The response (`200`) has the same structure as the response of the synchronous upload. For a failed task, `success` is `false` and `error_message` and `error_type` give the cause. The API keeps the result for 5 minutes after the task is complete. After that time, the status request and the result request return `404`. Stop the poll loop after a maximum time, because a `404` can mean "not complete" or "expired". ```javascript Node.js theme={null} import { readFile } from "node:fs/promises"; const BASE_URL = "https://api.e-invoice.be"; const headers = { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }; async function convertPdf(pdfPath, { intervalMs = 5000, maxWaitMs = 10 * 60 * 1000 } = {}) { // 1. Start the conversion const form = new FormData(); form.append( "file", new Blob([await readFile(pdfPath)], { type: "application/pdf" }), "invoice.pdf", ); const start = await fetch(`${BASE_URL}/api/conversion/pdf`, { method: "POST", headers, body: form, }); if (!start.ok) { throw new Error(`HTTP ${start.status}: ${await start.text()}`); } const { task_id: taskId } = await start.json(); console.log("Conversion started:", taskId); // 2. Poll the status. 404 means that the task is not complete. const deadline = Date.now() + maxWaitMs; while (Date.now() < deadline) { await new Promise((resolve) => setTimeout(resolve, intervalMs)); const status = await fetch(`${BASE_URL}/api/conversion/pdf/${taskId}`, { headers }); if (status.status === 404) continue; if (!status.ok) { throw new Error(`HTTP ${status.status}: ${await status.text()}`); } // 3. Get the result (also for a failed task: it contains the cause) const result = await fetch(`${BASE_URL}/api/conversion/pdf/${taskId}/result`, { headers }); if (!result.ok) { throw new Error(`HTTP ${result.status}: ${await result.text()}`); } return result.json(); } throw new Error(`Conversion ${taskId} was not complete after ${maxWaitMs} ms`); } const extracted = await convertPdf("./invoice.pdf"); if (extracted.success) { console.log("OK:", extracted.invoice_id, extracted.invoice_total); } else { console.error("Failed:", extracted.error_type, extracted.error_message); } ``` ```python Python theme={null} import os import time import requests BASE_URL = "https://api.e-invoice.be" HEADERS = {"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"} def convert_pdf(pdf_path, interval=5, max_wait=600): # 1. Start the conversion with open(pdf_path, "rb") as f: start = requests.post( f"{BASE_URL}/api/conversion/pdf", headers=HEADERS, files={"file": ("invoice.pdf", f, "application/pdf")}, timeout=60, ) start.raise_for_status() task_id = start.json()["task_id"] print("Conversion started:", task_id) # 2. Poll the status. 404 means that the task is not complete. deadline = time.monotonic() + max_wait while time.monotonic() < deadline: time.sleep(interval) status = requests.get( f"{BASE_URL}/api/conversion/pdf/{task_id}", headers=HEADERS, timeout=60 ) if status.status_code == 404: continue status.raise_for_status() # 3. Get the result (also for a failed task: it contains the cause) result = requests.get( f"{BASE_URL}/api/conversion/pdf/{task_id}/result", headers=HEADERS, timeout=60 ) result.raise_for_status() return result.json() raise TimeoutError(f"Conversion {task_id} was not complete after {max_wait} s") extracted = convert_pdf("./invoice.pdf") if extracted["success"]: print("OK:", extracted.get("invoice_id"), extracted.get("invoice_total")) else: print("Failed:", extracted.get("error_type"), extracted.get("error_message")) ``` ## Complete example The example uploads a PDF, creates the document from the generated UBL and sends it. It needs only the environment variable `E_INVOICE_API_KEY`. In a production integration, examine the extracted data before the send step. ```javascript Node.js theme={null} import { readFile } from "node:fs/promises"; const BASE_URL = "https://api.e-invoice.be"; const headers = { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }; async function call(path, options = {}) { const response = await fetch(`${BASE_URL}${path}`, { ...options, headers }); const body = await response.json(); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${JSON.stringify(body)}`); } return body; } async function processAndSendPdf(pdfPath, vendorTaxId, customerTaxId) { // 1. Upload the PDF and extract the data const query = new URLSearchParams(); if (vendorTaxId) query.set("vendor_tax_id", vendorTaxId); if (customerTaxId) query.set("customer_tax_id", customerTaxId); const pdfForm = new FormData(); pdfForm.append( "file", new Blob([await readFile(pdfPath)], { type: "application/pdf" }), "invoice.pdf", ); const extracted = await call(`/api/documents/pdf?${query}`, { method: "POST", body: pdfForm, }); console.log("OK: PDF processed:", extracted.invoice_id, extracted.currency, extracted.invoice_total); // 2. Stop if no UBL was generated if (!extracted.success || !extracted.ubl_document) { console.error("Failed: no UBL document.", extracted.error_type, extracted.error_message); return null; } // 3. Create the document from the UBL string (no temporary file is necessary) const ublForm = new FormData(); ublForm.append( "file", new Blob([extracted.ubl_document], { type: "application/xml" }), "invoice.xml", ); const document = await call("/api/documents/ubl", { method: "POST", body: ublForm }); console.log("OK: document created:", document.id); // 4. Send the document const sent = await call(`/api/documents/${document.id}/send`, { method: "POST" }); console.log("OK: document sent, state:", sent.state); return sent; } // Replace the vendor tax ID with that of your company. await processAndSendPdf("./invoice.pdf", "BE1018265814", "BE0848934496"); ``` ```python Python theme={null} import os import requests BASE_URL = "https://api.e-invoice.be" HEADERS = {"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"} def process_and_send_pdf(pdf_path, vendor_tax_id=None, customer_tax_id=None): # 1. Upload the PDF and extract the data params = {} if vendor_tax_id: params["vendor_tax_id"] = vendor_tax_id if customer_tax_id: params["customer_tax_id"] = customer_tax_id with open(pdf_path, "rb") as f: response = requests.post( f"{BASE_URL}/api/documents/pdf", headers=HEADERS, params=params, files={"file": ("invoice.pdf", f, "application/pdf")}, timeout=600, ) response.raise_for_status() extracted = response.json() print("OK: PDF processed:", extracted.get("invoice_id"), extracted.get("invoice_total")) # 2. Stop if no UBL was generated if not extracted.get("success") or not extracted.get("ubl_document"): print("Failed: no UBL document.", extracted.get("error_type"), extracted.get("error_message")) return None # 3. Create the document from the UBL string (no temporary file is necessary) response = requests.post( f"{BASE_URL}/api/documents/ubl", headers=HEADERS, files={ "file": ("invoice.xml", extracted["ubl_document"].encode("utf-8"), "application/xml") }, timeout=60, ) response.raise_for_status() document = response.json() print("OK: document created:", document["id"]) # 4. Send the document response = requests.post( f"{BASE_URL}/api/documents/{document['id']}/send", headers=HEADERS, timeout=60 ) response.raise_for_status() sent = response.json() print("OK: document sent, state:", sent["state"]) return sent # Replace the vendor tax ID with that of your company. process_and_send_pdf("./invoice.pdf", "BE1018265814", "BE0848934496") ``` ## PDF requirements * The file must be a PDF that is not empty and not damaged. * A text-based PDF (generated by software) gives better results than a scanned document. * Put one invoice in each PDF. * Use a clear layout with separate sections for the vendor, the customer and the line items. * All mandatory invoice fields must be visible in the PDF. The API does not publish a size limit for these two upload endpoints. Keep the files small, and use the asynchronous upload if your client or proxy has a short timeout. ## Troubleshooting | Symptom | Cause and solution | | - | - | | `422` with detail `PDF could not be OCRed` | The API could not read the file. Make sure that the file is a PDF that is not empty and not damaged. | | `422` with detail `PDF could not be converted to a document` | The API could not make an invoice from the content. The detail can list the fields that are missing. Use a PDF with complete invoice data, or create the document from JSON. | | `422` with `Field required` for `body.file` | The request is not `multipart/form-data` with a `file` field. | | `success` is `false`, or there is no `ubl_document` | The PDF did not have sufficient data for a compliant document. Read `error_message`, give `vendor_tax_id` and `customer_tax_id`, or correct the extracted JSON and create the document with `POST /api/documents/`. | | Amounts, dates or tax IDs are not correct | Give `vendor_tax_id` and `customer_tax_id`, use a text-based PDF, and examine the extracted data before you create the document. | | `404` on the status or result request | The task is not complete, or the result expired (5 minutes). | | `401` | The API key is missing or not correct. See [Authentication](/authentication). | For all status codes and error formats, see [Errors and troubleshooting](/guides/errors). ## Best practices * Give `vendor_tax_id` and `customer_tax_id` with the synchronous upload. * Check `success` and `ubl_document` before you continue. If `success` is `false` or `ubl_document` is missing, a person must examine the data. * Examine the important fields (`invoice_id`, `invoice_total`, the tax IDs and the line items) before you send. * Use the asynchronous upload when you convert many PDF files, and limit the number of conversions that run at the same time. * Keep the original PDF files for your audit records. * Develop and test with a sandbox company. See [Test mode and sandbox companies](/environments). ## Next Steps Create and send a document from UBL XML Add a PDF or other files to a JSON document Let your ERP email invoices to e-invoice.be Status codes, error formats and retry rules # Receive documents Source: https://docs.e-invoice.be/guides/receiving-documents Find the documents that your suppliers send to you through Peppol, read their data, and download the original UBL, the PDF and the attachments. ## Overview When a supplier sends an invoice or a [credit note](/guides/credit-notes) to your Peppol ID, e-invoice.be receives the document, stores it, and puts it in the inbox of your company. This guide shows how to find new documents, how to read their data, and how to download the original UBL file, the PDF and the other attachments. A received document has `direction: "INBOUND"` and `state: "RECEIVED"`. ## Prerequisites * An API key for the company that receives the documents. See [Authentication](/authentication). * A production company must be registered as a receiver on the Peppol network. Without this registration, senders cannot find your Peppol ID and no document arrives. * A sandbox company does not receive documents from the Peppol network. Use **Simulate inbound** to put documents in its inbox. See [Test with a sandbox company](#test-with-a-sandbox-company). Resellers: a send-only tenant has no Peppol registration, thus its inbox stays empty. See [Send-only tenants](/admin-api#send-only-tenants) in the Admin API guide. ## How a document arrives The access point receives the Peppol message and finds the company from the receiver Peppol ID. The original UBL XML is stored. Each file that the sender embedded in the UBL is extracted and stored as an attachment of the document. If the UBL contains no PDF attachment, e-invoice.be generates a PDF from the UBL and adds it as an attachment with the file name `{document_id}.pdf`. The document data (parties, totals, line items) is available as JSON, and the document is in the inbox. Each webhook of the company that subscribes to `document.received` gets an event with the `document_id`. ## Find new documents There are two methods to find new documents: webhooks and polling. Webhooks are the recommended method. Polling is a good safety net in addition to webhooks. The API has no "mark as processed" endpoint and no "unread" filter. A document stays in the inbox after you read it. Your system must record which documents it has processed. See [Prevent double processing](#prevent-double-processing). ### Webhook (recommended) Create a webhook that subscribes to `document.received`. e-invoice.be calls your URL when a document is in the `RECEIVED` state. [Webhooks](/essentials/webhooks) gives the setup procedure and the signature verification. ```json theme={null} { "id": "evt-e7wyc7gtpqx4z73x2wqmwhebhbb3r8n3ovfhcsdbulxr3s2awf49de76yglrnri3", "tenant_id": "ten-5x7y9z1a3b5c7d9e", "created_at": 1790846142, "type": "document.received", "data": { "document_id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d" }, "text": "\u26a1\ufe0f New webhook event: document.received\n\n{\n \"document_id\": \"doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d\"\n}" } ``` The event contains only the `document_id`. Use this ID to [get the document](#get-the-document). The event `document.received.failed` tells you that e-invoice.be could not process an incoming document. The `data` object has the same shape. Such a document is not in the `RECEIVED` state, thus `GET /api/inbox/` does not list it. Not all receive failures give this event. ### Polling Call `GET /api/inbox/` at a fixed interval. The default sort is `created_at` in descending order, thus the newest documents are on the first page. ```bash cURL theme={null} curl -X GET "https://api.e-invoice.be/api/inbox/?sort_by=created_at&sort_order=desc&page=1&page_size=50" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```javascript Node.js theme={null} const params = new URLSearchParams({ sort_by: "created_at", sort_order: "desc", page: "1", page_size: "50", }); const response = await fetch(`https://api.e-invoice.be/api/inbox/?${params}`, { headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }, }); if (!response.ok) { throw new Error(`Inbox request failed: ${response.status}`); } const inbox = await response.json(); console.log(inbox.total, inbox.items.map((doc) => doc.id)); ``` ```python Python theme={null} import os import requests response = requests.get( "https://api.e-invoice.be/api/inbox/", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, params={ "sort_by": "created_at", "sort_order": "desc", "page": 1, "page_size": 50, }, timeout=30, ) response.raise_for_status() inbox = response.json() print(inbox["total"], [doc["id"] for doc in inbox["items"]]) ``` ```php PHP theme={null} 'created_at', 'sort_order' => 'desc', 'page' => 1, 'page_size' => 50, ]); $ch = curl_init("https://api.e-invoice.be/api/inbox/?{$query}"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('E_INVOICE_API_KEY')], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 200) { throw new RuntimeException("Inbox request failed: {$status}"); } $inbox = json_decode($body, true); echo $inbox['total'] . PHP_EOL; ``` ```csharp C# theme={null} using System.Net.Http.Headers; using System.Text.Json; using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var response = await client.GetAsync( "/api/inbox/?sort_by=created_at&sort_order=desc&page=1&page_size=50"); response.EnsureSuccessStatusCode(); using var inbox = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); Console.WriteLine(inbox.RootElement.GetProperty("total").GetInt32()); ``` The response is one page of documents. Each item has the same shape as the response of `GET /api/documents/{document_id}`. ```json theme={null} { "items": [ { "id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "created_at": "2026-10-01T09:15:42.118Z", "document_type": "INVOICE", "state": "RECEIVED", "direction": "INBOUND", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "subtotal": "1000.00", "total_tax": "210.00", "invoice_total": "1210.00", "amount_due": "1210.00" } ], "total": 1, "page": 1, "page_size": 50, "pages": 1, "has_next_page": false } ``` Read the subsequent page while `has_next_page` is `true`. ### Prevent double processing Because the API does not record what you have processed, keep this record in your own system. Use one of these patterns, or the two together. Store the `id` of each document after you process it. Before you process a document, look for its `id` in your store and skip the document if the `id` is there. This pattern is correct for webhooks and for polling. Use it in each webhook handler, so that a repeated event does not process the same document twice. Store the `created_at` value and the `id` of the newest document that you processed. At each poll, read the inbox with `sort_by=created_at` and `sort_order=desc`, page by page, and stop when you find a document whose `created_at` is not later than the stored value. Process the documents that you collected from the oldest to the newest, then store the new cursor. `date_from` and `date_to` filter on the issue date of the invoice (`invoice_date`), not on the time of receipt. Do not use them as a cursor: a supplier can send an invoice with an issue date in the past. These programs read the inbox from the newest document to the stored cursor, download the UBL of each new document, and then move the cursor. The cursor is in a local JSON file; use your database in a production system. ```javascript Node.js theme={null} import { readFile, writeFile } from "node:fs/promises"; const BASE_URL = "https://api.e-invoice.be"; const HEADERS = { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }; const CURSOR_FILE = "inbox-cursor.json"; async function api(path) { const response = await fetch(`${BASE_URL}${path}`, { headers: HEADERS }); if (!response.ok) { throw new Error(`GET ${path} failed: ${response.status}`); } return response.json(); } async function loadCursor() { try { return JSON.parse(await readFile(CURSOR_FILE, "utf8")); } catch { return { createdAt: null, processedIds: [] }; } } async function collectNewDocuments(cursor) { const documents = []; let page = 1; while (true) { const inbox = await api( `/api/inbox/?sort_by=created_at&sort_order=desc&page=${page}&page_size=100` ); for (const doc of inbox.items) { if (cursor.createdAt && new Date(doc.created_at) < new Date(cursor.createdAt)) { return documents; } if (!cursor.processedIds.includes(doc.id)) { documents.push(doc); } } if (!inbox.has_next_page) { return documents; } page += 1; } } async function processDocument(doc) { const ubl = await api(`/api/documents/${doc.id}/ubl`); const download = await fetch(ubl.signed_url); if (!download.ok) { throw new Error(`UBL download failed: ${download.status}`); } await writeFile(ubl.file_name, Buffer.from(await download.arrayBuffer())); console.log(`Processed ${doc.id} (${doc.document_type} ${doc.invoice_id})`); } const cursor = await loadCursor(); const documents = await collectNewDocuments(cursor); // Oldest first, so that the cursor only moves forward. for (const doc of documents.reverse()) { await processDocument(doc); const sameInstant = cursor.createdAt === doc.created_at; cursor.processedIds = sameInstant ? [...cursor.processedIds, doc.id] : [doc.id]; cursor.createdAt = doc.created_at; await writeFile(CURSOR_FILE, JSON.stringify(cursor)); } ``` ```python Python theme={null} import json import os from datetime import datetime from pathlib import Path import requests BASE_URL = "https://api.e-invoice.be" HEADERS = {"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"} CURSOR_FILE = Path("inbox-cursor.json") def api(path, **params): response = requests.get(f"{BASE_URL}{path}", headers=HEADERS, params=params, timeout=30) response.raise_for_status() return response.json() def parse(timestamp): return datetime.fromisoformat(timestamp.replace("Z", "+00:00")) def load_cursor(): if CURSOR_FILE.exists(): return json.loads(CURSOR_FILE.read_text()) return {"created_at": None, "processed_ids": []} def collect_new_documents(cursor): documents = [] page = 1 while True: inbox = api( "/api/inbox/", sort_by="created_at", sort_order="desc", page=page, page_size=100, ) for doc in inbox["items"]: if cursor["created_at"] and parse(doc["created_at"]) < parse(cursor["created_at"]): return documents if doc["id"] not in cursor["processed_ids"]: documents.append(doc) if not inbox["has_next_page"]: return documents page += 1 def process_document(doc): ubl = api(f"/api/documents/{doc['id']}/ubl") download = requests.get(ubl["signed_url"], timeout=60) download.raise_for_status() Path(ubl["file_name"]).write_bytes(download.content) print(f"Processed {doc['id']} ({doc['document_type']} {doc.get('invoice_id')})") cursor = load_cursor() # Oldest first, so that the cursor only moves forward. for doc in reversed(collect_new_documents(cursor)): process_document(doc) if cursor["created_at"] == doc["created_at"]: cursor["processed_ids"].append(doc["id"]) else: cursor["processed_ids"] = [doc["id"]] cursor["created_at"] = doc["created_at"] CURSOR_FILE.write_text(json.dumps(cursor)) ``` ## Get the document `GET /api/documents/{document_id}` returns the data of the document as JSON. This is the same endpoint and the same schema as for the documents that you send. See the [Document schema](/api-reference/schemas/document). ```bash cURL theme={null} curl -X GET "https://api.e-invoice.be/api/documents/doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```javascript Node.js theme={null} const documentId = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"; const response = await fetch(`https://api.e-invoice.be/api/documents/${documentId}`, { headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }, }); if (!response.ok) { throw new Error(`Get document failed: ${response.status}`); } const doc = await response.json(); console.log(doc.document_type, doc.vendor_name, doc.invoice_total); ``` ```python Python theme={null} import os import requests document_id = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d" response = requests.get( f"https://api.e-invoice.be/api/documents/{document_id}", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, timeout=30, ) response.raise_for_status() doc = response.json() print(doc["document_type"], doc.get("vendor_name"), doc.get("invoice_total")) ``` ```php PHP theme={null} true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('E_INVOICE_API_KEY')], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 200) { throw new RuntimeException("Get document failed: {$status}"); } $doc = json_decode($body, true); echo $doc['document_type'] . ' ' . $doc['vendor_name'] . PHP_EOL; ``` ```csharp C# theme={null} using System.Net.Http.Headers; using System.Text.Json; var documentId = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"; using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var response = await client.GetAsync($"/api/documents/{documentId}"); response.EnsureSuccessStatusCode(); using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); Console.WriteLine(doc.RootElement.GetProperty("document_type").GetString()); ``` ```json theme={null} { "id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "created_at": "2026-10-01T09:15:42.118Z", "document_type": "INVOICE", "state": "RECEIVED", "direction": "INBOUND", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "purchase_order": "PO-12345", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email": "billing@e-invoice.be", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "subtotal": "1000.00", "total_tax": "210.00", "invoice_total": "1210.00", "amount_due": "1210.00", "items": [ { "description": "Professional services", "quantity": "10", "unit": "C62", "unit_price": "100.00", "amount": "1000.00", "tax_rate": "21.00", "tax": "210.00" } ], "tax_details": [ { "rate": "21.00", "amount": "210.00" } ], "payment_details": [ { "iban": "BE68539007547034", "payment_reference": "INV-2026-001" } ], "attachments": [ { "id": "doc-att-7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d", "file_name": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d.pdf", "file_type": "application/pdf", "file_size": 48213 } ] } ``` The response does not contain fields that have no value. Read optional fields defensively. These fields are the most important for a received document: | Field | Value for a received document | | - | - | | `direction` | `INBOUND` | | `state` | `RECEIVED` | | `document_type` | `INVOICE`, `CREDIT_NOTE`, `SELFBILLING_INVOICE` or `SELFBILLING_CREDIT_NOTE` | | `vendor_name`, `vendor_tax_id`, `vendor_company_id`, `vendor_email`, `vendor_address` | The supplier that issued the document | | `customer_name`, `customer_tax_id`, `customer_company_id` | The buyer on the document | | `invoice_id`, `invoice_date`, `due_date` | The number and the dates that the issuer gave to the document | | `subtotal`, `total_tax`, `invoice_total`, `amount_due` | The totals of the document, as strings | | `payment_details` | The bank account and the payment reference for the payment | | `attachments` | The ID, name, type and size of each attachment. This list has no download URL; see [Download the attachments and the PDF](#download-the-attachments-and-the-pdf) | | `created_at` | The time at which e-invoice.be created the document record. Use this field for your cursor | A received self-billing document has the type `SELFBILLING_INVOICE` or `SELFBILLING_CREDIT_NOTE`. In a self-billing document, the buyer issues the invoice in the name of the supplier, thus your company is the vendor on the document and not the customer. See [Self-billing and debit notes](/guides/self-billing). ## Download the original UBL The JSON data is a conversion of the UBL. Keep the original UBL XML for your archive: it is the legal document that the sender transmitted. `GET /api/documents/{document_id}/ubl` returns the metadata of the UBL file and a signed URL for the download. ```bash cURL theme={null} # 1. Get the signed URL curl -X GET "https://api.e-invoice.be/api/documents/doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d/ubl" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" # 2. Download the file from the signed_url of the response curl -o invoice.xml "SIGNED_URL" ``` ```javascript Node.js theme={null} import { writeFile } from "node:fs/promises"; const documentId = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"; const response = await fetch(`https://api.e-invoice.be/api/documents/${documentId}/ubl`, { headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }, }); if (!response.ok) { throw new Error(`Get UBL failed: ${response.status}`); } const ubl = await response.json(); const download = await fetch(ubl.signed_url); if (!download.ok) { throw new Error(`UBL download failed: ${download.status}`); } await writeFile(ubl.file_name, Buffer.from(await download.arrayBuffer())); ``` ```python Python theme={null} import os from pathlib import Path import requests document_id = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d" response = requests.get( f"https://api.e-invoice.be/api/documents/{document_id}/ubl", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, timeout=30, ) response.raise_for_status() ubl = response.json() download = requests.get(ubl["signed_url"], timeout=60) download.raise_for_status() Path(ubl["file_name"]).write_bytes(download.content) ``` ```php PHP theme={null} true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('E_INVOICE_API_KEY')], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 200) { throw new RuntimeException("Get UBL failed: {$status}"); } $ubl = json_decode($body, true); $download = curl_init($ubl['signed_url']); curl_setopt_array($download, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, ]); $xml = curl_exec($download); curl_close($download); file_put_contents($ubl['file_name'], $xml); ``` ```csharp C# theme={null} using System.Net.Http.Headers; using System.Text.Json; var documentId = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"; using var client = new HttpClient(); using var request = new HttpRequestMessage( HttpMethod.Get, $"https://api.e-invoice.be/api/documents/{documentId}/ubl"); request.Headers.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); using var ubl = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); var signedUrl = ubl.RootElement.GetProperty("signed_url").GetString(); var fileName = ubl.RootElement.GetProperty("file_name").GetString(); // The signed URL needs no Authorization header. var xml = await client.GetByteArrayAsync(signedUrl); await File.WriteAllBytesAsync(fileName!, xml); ``` ```json theme={null} { "id": "ubl-4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a", "file_name": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d.xml", "file_size": 12840, "signed_url": "https://storage.example.com/ubl/doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d.xml?X-Amz-Expires=3600&X-Amz-Signature=...", "sender_peppol_scheme": "0208", "sender_peppol_id": "1018265814", "receiver_peppol_scheme": "0208", "receiver_peppol_id": "0848934496", "validated_at": "2026-10-01T09:15:42.118Z" } ``` `validated_at` is present when the UBL was validated. The `signed_url` is valid for 1 hour. For the description of each field, see [Download the UBL](/guides/attachments#download-the-ubl). Do not store `signed_url`. It stops working after 1 hour. Store the `document_id`, and request a new URL each time that you need the file. The URL contains its own signature, thus do not add your API key to the download request. ## Download the attachments and the PDF A received document can have attachments: the PDF of the invoice and the other files that the sender embedded in the UBL (for example a timesheet or a delivery note). If the sender embedded no PDF, the list contains the PDF that e-invoice.be generated, with the file name `{document_id}.pdf`. ### List the attachments `GET /api/documents/{document_id}/attachments` returns all attachments of the document. Each item has a `file_url` that you can download immediately. ```bash cURL theme={null} curl -X GET "https://api.e-invoice.be/api/documents/doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d/attachments" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```javascript Node.js theme={null} import { writeFile } from "node:fs/promises"; const documentId = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"; const response = await fetch( `https://api.e-invoice.be/api/documents/${documentId}/attachments`, { headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` } } ); // 404 means that the document has no attachments. const attachments = response.status === 404 ? [] : await response.json(); for (const attachment of attachments) { const download = await fetch(attachment.file_url); if (!download.ok) { throw new Error(`Attachment download failed: ${download.status}`); } await writeFile(attachment.file_name, Buffer.from(await download.arrayBuffer())); } ``` ```python Python theme={null} import os from pathlib import Path import requests document_id = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d" response = requests.get( f"https://api.e-invoice.be/api/documents/{document_id}/attachments", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, timeout=30, ) # 404 means that the document has no attachments. if response.status_code == 404: attachments = [] else: response.raise_for_status() attachments = response.json() for attachment in attachments: download = requests.get(attachment["file_url"], timeout=60) download.raise_for_status() Path(attachment["file_name"]).write_bytes(download.content) ``` ```php PHP theme={null} true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('E_INVOICE_API_KEY')], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); // 404 means that the document has no attachments. $attachments = $status === 404 ? [] : json_decode($body, true); foreach ($attachments as $attachment) { $download = curl_init($attachment['file_url']); curl_setopt_array($download, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, ]); file_put_contents($attachment['file_name'], curl_exec($download)); curl_close($download); } ``` ```csharp C# theme={null} using System.Net; using System.Net.Http.Headers; using System.Text.Json; var documentId = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"; using var client = new HttpClient(); using var request = new HttpRequestMessage( HttpMethod.Get, $"https://api.e-invoice.be/api/documents/{documentId}/attachments"); request.Headers.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var response = await client.SendAsync(request); // 404 means that the document has no attachments. if (response.StatusCode != HttpStatusCode.NotFound) { response.EnsureSuccessStatusCode(); using var attachments = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); foreach (var attachment in attachments.RootElement.EnumerateArray()) { var fileUrl = attachment.GetProperty("file_url").GetString(); var fileName = attachment.GetProperty("file_name").GetString(); await File.WriteAllBytesAsync(fileName!, await client.GetByteArrayAsync(fileUrl)); } } ``` ```json theme={null} [ { "id": "doc-att-7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d", "file_name": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d.pdf", "file_type": "application/pdf", "file_size": 48213, "file_url": "https://storage.example.com/attachments/doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d.pdf?X-Amz-Expires=3600&X-Amz-Signature=..." }, { "id": "doc-att-0f1e2d3c4b5a69788796a5b4c3d2e1f0", "file_name": "timesheet-october.xlsx", "file_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "file_size": 20417, "file_url": "https://storage.example.com/attachments/timesheet-october.xlsx?X-Amz-Expires=3600&X-Amz-Signature=..." } ] ``` This endpoint returns `404 Not Found` with `"detail": "Document attachments not found"` when the document has no attachments. Treat this response as an empty list. ### Get one attachment `GET /api/documents/{document_id}/attachments/{attachment_id}` returns one attachment with a new `file_url`. Use this call when you stored the attachment ID and you need the file again later. ```bash cURL theme={null} curl -X GET "https://api.e-invoice.be/api/documents/doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d/attachments/doc-att-7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```javascript Node.js theme={null} const documentId = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"; const attachmentId = "doc-att-7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d"; const response = await fetch( `https://api.e-invoice.be/api/documents/${documentId}/attachments/${attachmentId}`, { headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` } }, ); if (!response.ok) { throw new Error(`Get attachment failed: ${response.status}`); } const attachment = await response.json(); console.log(attachment.file_url); ``` ```python Python theme={null} import os import requests document_id = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d" attachment_id = "doc-att-7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d" response = requests.get( f"https://api.e-invoice.be/api/documents/{document_id}/attachments/{attachment_id}", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, timeout=30, ) response.raise_for_status() attachment = response.json() print(attachment["file_url"]) ``` ```php PHP theme={null} true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('E_INVOICE_API_KEY')], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 200) { throw new RuntimeException("Get attachment failed: {$status}"); } $attachment = json_decode($body, true); echo $attachment['file_url'], PHP_EOL; ``` ```csharp C# theme={null} using System.Net.Http.Headers; using System.Text.Json; var documentId = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"; var attachmentId = "doc-att-7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d"; using var client = new HttpClient(); using var request = new HttpRequestMessage( HttpMethod.Get, $"https://api.e-invoice.be/api/documents/{documentId}/attachments/{attachmentId}"); request.Headers.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); using var attachment = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); Console.WriteLine(attachment.RootElement.GetProperty("file_url").GetString()); ``` ```json theme={null} { "id": "doc-att-7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d", "file_name": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d.pdf", "file_type": "application/pdf", "file_size": 48213, "file_url": "https://storage.example.com/attachments/doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d.pdf?X-Amz-Expires=3600&X-Amz-Signature=..." } ``` If the attachment does not exist, the endpoint returns `404 Not Found` with `"detail": "Document attachment not found"`. See also [Download one attachment](/guides/attachments#download-one-attachment). `file_url` is valid for 1 hour. Do not store it and do not show it to your users as a permanent link. Store the `document_id` and the attachment `id`, and request a new URL when you need the file. To find the PDF of the invoice, select the attachment with `file_type` equal to `application/pdf`. A sender can embed more than one PDF; in that case use `file_name` to identify the correct file. See [Attachments and PDF](/guides/attachments) for the permitted file types and for the procedure to add attachments to the documents that you send. ## Filter the inbox `GET /api/inbox/` returns the documents with `direction: "INBOUND"` and `state: "RECEIVED"`. It accepts the parameters in this table. All parameters are optional, and you can combine them. | Parameter | Type | Default | Description | | - | - | - | - | | `page` | integer | `1` | The page number. The minimum is 1. | | `page_size` | integer | `20` | The number of documents on each page. The minimum is 1 and the maximum is 100. | | `type` | string | All types | One document type: `INVOICE`, `CREDIT_NOTE`, `DEBIT_NOTE`, `SELFBILLING_INVOICE` or `SELFBILLING_CREDIT_NOTE`. | | `sender` | string | No filter | A text in `vendor_name`, `vendor_email`, `vendor_tax_id` or `vendor_company_id`. | | `date_from` | date-time | No filter | The first issue date. | | `date_to` | date-time | No filter | The last issue date. | | `search` | string | No filter | A text in the invoice number, the vendor name, the customer name or the note. | | `sort_by` | string | `created_at` | The sort field: `created_at`, `invoice_date`, `due_date`, `invoice_total`, `customer_name`, `vendor_name` or `invoice_id`. | | `sort_order` | string | `desc` | The sort direction: `asc` or `desc`. | See [List, filter and manage documents](/guides/managing-documents#filters) for the full description of each parameter. These rules are important for received documents: * `date_from` and `date_to` filter on the issue date of the invoice (`invoice_date`), not on the time of receipt. * `sender` finds a text in `vendor_name`, `vendor_email`, `vendor_tax_id` or `vendor_company_id`. * Received documents do not get the type `DEBIT_NOTE`, thus `type=DEBIT_NOTE` returns an empty list. Example: all credit notes from E-INVOICE BV with an issue date in October 2026, the oldest first. ```bash cURL theme={null} curl -G "https://api.e-invoice.be/api/inbox/" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ --data-urlencode "type=CREDIT_NOTE" \ --data-urlencode "sender=BE1018265814" \ --data-urlencode "date_from=2026-10-01T00:00:00" \ --data-urlencode "date_to=2026-10-31T00:00:00" \ --data-urlencode "sort_by=invoice_date" \ --data-urlencode "sort_order=asc" ``` The response has the same paginated shape as the response in [Polling](#polling). ### Shortcut endpoints Two endpoints return one document type only. They accept `page`, `page_size`, `sort_by` and `sort_order`, and return the same paginated response. They do not accept `sender`, `date_from`, `date_to` or `search`; use `GET /api/inbox/` with the `type` parameter when you need these filters. | Endpoint | Returns | | - | - | | `GET /api/inbox/invoices` | Received documents of the type `INVOICE` | | `GET /api/inbox/credit-notes` | Received documents of the type `CREDIT_NOTE` | ## Test with a sandbox company A sandbox company gets no documents from the Peppol network. Use **Simulate inbound** to put a document in its inbox: In [app.e-invoice.be](https://app.e-invoice.be), select your sandbox company and open **Inbox**. Click **Simulate inbound**. Select the built-in sample invoice, or upload your own UBL XML file. The document is in the `RECEIVED` state, `GET /api/inbox/` lists it, and the `document.received` webhooks of the company are sent. All samples on this page operate on this document with the API key of the sandbox company. A document that you send to your own company does not come into the inbox. In a sandbox company, a send goes to the contact email address of the company and not to Peppol, thus the document gets the `SENT` state and stays in the outbox. `POST /api/documents/` ignores a `direction` value of `INBOUND` and always makes an outbound draft. Simulate inbound is the only method to fill the inbox of a sandbox company. [Testing received documents](/environments#testing-received-documents) gives the full description. Resellers can do the same step through the Admin API: see [Simulate an inbound document](/admin-api#simulate-an-inbound-document). ## Frequently asked questions No. The API has no such endpoint and no such field. Keep the processing status in your own system, with the document `id` as the key. See [Prevent double processing](#prevent-double-processing). It is not mandatory, but it is good practice. A poll at a long interval (for example one time each hour) finds the documents for which your endpoint missed the webhook. Because you record the processed document IDs, the poll does not process a document twice. `signed_url` (UBL) and `file_url` (attachments) are valid for 1 hour. Each call to the endpoint returns a new URL. For a production company: make sure that the company is registered as a receiver on Peppol with [Look up Peppol participants](/guides/lookup-participants). For a sandbox company: the inbox stays empty until you use Simulate inbound. A received document gets one of four types: `INVOICE`, `CREDIT_NOTE`, `SELFBILLING_INVOICE` or `SELFBILLING_CREDIT_NOTE`. The type comes from the UBL document (Invoice or CreditNote) and its self-billing marker. Use the `type` parameter of `GET /api/inbox/` to get one type. ## Next Steps Get an event for each received document and verify its signature. Use the filters, the sort options and the pagination of the inbox. Use a sandbox company and Simulate inbound to test your integration. Read the timeline of a received document. # Self-billing and debit notes Source: https://docs.e-invoice.be/guides/self-billing Create self-billing invoices, self-billing credit notes and debit notes, and understand which party is the vendor and the customer in each document type. ## Overview In a [self-billing](/glossary#self-billing) arrangement, the buyer issues the invoice in the name of the supplier. This guide shows how to create and receive self-billing invoices and self-billing credit notes, and how to create debit notes. ## Document types The `document_type` field accepts five values. The guides for [invoices](/guides/creating-invoices) and [credit notes](/guides/credit-notes) cover the first two. This guide covers the other three. | `document_type` | Generated UBL | Type code | Your company is | | - | - | - | - | | `INVOICE` | `Invoice` | `380` | the vendor | | `CREDIT_NOTE` | `CreditNote` | `381` | the vendor | | `DEBIT_NOTE` | `Invoice` | `383` | the vendor | | `SELFBILLING_INVOICE` | `Invoice` | `389` | the customer | | `SELFBILLING_CREDIT_NOTE` | `CreditNote` | `261` | the customer | ## What self-billing is In a self-billing arrangement, the buyer issues the invoice on behalf of the supplier. The supplier and the buyer agree on this arrangement before the first document is issued. The roles in the document do not change. The supplier is still the seller, and the buyer is still the party that pays. Only the issuer changes. | JSON fields | Party | In a self-billing document | | - | - | - | | `vendor_name`, `vendor_tax_id`, `vendor_address`, ... | Supplier (seller) | Your trading partner | | `customer_name`, `customer_tax_id`, `customer_address`, ... | Buyer | Your company, which issues the document | Do not swap the parties to make the document look like a normal invoice. In a self-billing document, your company is in the `customer_*` fields and the supplier is in the `vendor_*` fields. ## Ownership rule The API derives two Peppol IDs from each document: one from the `vendor_*` identifiers and one from the `customer_*` identifiers. The party that issues the document is the Peppol sender, and the other party is the Peppol receiver. The API checks that the sender Peppol ID is one of the Peppol IDs of your company. The Peppol IDs of your company are in the `peppol_ids` field of `GET /api/me/`. | Document type | Peppol sender (must be in your `peppol_ids`) | Peppol receiver | | - | - | - | | `INVOICE`, `CREDIT_NOTE`, `DEBIT_NOTE` | The ID derived from `vendor_tax_id` or `vendor_company_id` | The ID derived from the `customer_*` fields | | `SELFBILLING_INVOICE`, `SELFBILLING_CREDIT_NOTE` | The ID derived from `customer_peppol_id`, `customer_tax_id` or `customer_company_id` | The ID derived from the `vendor_*` fields | The comparison ignores letter case and surrounding spaces. If the check fails, `POST /api/documents/` returns `406 Not Acceptable`. For a self-billing document, the ID in the message comes from the `customer_*` fields: ```json theme={null} { "detail": "Derived sender '0208:1018265814' is not in tenant peppol_ids" } ``` `POST /api/documents/{document_id}/send` does the same check and returns `409 Conflict` with the same `detail` text. The word "sender" in this message refers to the Peppol sender, not to the vendor. In a self-billing document, the Peppol sender is the party in the `customer_*` fields. If the ID in the message is the ID of your supplier, the vendor and the customer are in the usual invoice order. Put your company in the `customer_*` fields. ## Create a self-billing invoice In this example, E-INVOICE BV (`BE1018265814`) is the buyer and issues the invoice. OpenPeppol VZW (`BE0848934496`) is the supplier. Save the payload as `self-billing-invoice.json`: ```json self-billing-invoice.json theme={null} { "document_type": "SELFBILLING_INVOICE", "invoice_id": "SB-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "purchase_order": "PO-12345", "vendor_name": "OpenPeppol VZW", "vendor_tax_id": "BE0848934496", "vendor_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "customer_name": "E-INVOICE BV", "customer_tax_id": "BE1018265814", "customer_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "items": [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "payment_term": "Payment due within 30 days", "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "SB-2026-001" } ] } ``` The payment details in a self-billing invoice are those of the supplier, because the supplier receives the payment. Validation is not a separate mandatory call. `POST /api/documents/` rejects a payload that does not pass the same rules. Use `POST /api/validate/json` while you develop, because it returns all rule failures and the generated UBL. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/validate/json" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -H "Content-Type: application/json" \ -d @self-billing-invoice.json ``` ```javascript Node.js theme={null} import { readFile } from "node:fs/promises"; const payload = await readFile("self-billing-invoice.json", "utf8"); const response = await fetch("https://api.e-invoice.be/api/validate/json", { method: "POST", headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}`, "Content-Type": "application/json", }, body: payload, }); const result = await response.json(); console.log(result.is_valid, result.issues); ``` ```python Python theme={null} import json import os import requests with open("self-billing-invoice.json", encoding="utf-8") as f: payload = json.load(f) response = requests.post( "https://api.e-invoice.be/api/validate/json", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, json=payload, timeout=30, ) result = response.json() print(result["is_valid"], result["issues"]) ``` ```php PHP theme={null} true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('E_INVOICE_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => $payload, ]); $result = json_decode(curl_exec($ch), true); curl_close($ch); var_dump($result['is_valid'], $result['issues']); ``` ```csharp C# theme={null} using System.Net.Http.Headers; using System.Text; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var payload = await File.ReadAllTextAsync("self-billing-invoice.json"); var content = new StringContent(payload, Encoding.UTF8, "application/json"); var response = await client.PostAsync( "https://api.e-invoice.be/api/validate/json", content); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` The API returns `201 Created`: ```json theme={null} { "id": "9eec0b03-4649-4ae4-9c4c-323ebb6d53e8", "file_name": "9eec0b03-4649-4ae4-9c4c-323ebb6d53e8.xml", "is_valid": true, "issues": [], "ubl_document": "\n The `customer_tax_id` must resolve to a Peppol ID of the company that owns the API key. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/documents/" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -H "Content-Type: application/json" \ -d @self-billing-invoice.json ``` Response (`201 Created`, shortened): ```json theme={null} { "id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "document_type": "SELFBILLING_INVOICE", "state": "DRAFT", "direction": "OUTBOUND", "invoice_id": "SB-2026-001", "vendor_name": "OpenPeppol VZW", "vendor_tax_id": "BE0848934496", "customer_name": "E-INVOICE BV", "customer_tax_id": "BE1018265814" } ``` ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/documents/doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d/send" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` Response (`200 OK`, shortened): ```json theme={null} { "id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "document_type": "SELFBILLING_INVOICE", "state": "TRANSIT", "direction": "OUTBOUND" } ``` Develop and test with a sandbox company. A sandbox company runs in test mode: the API sends each document as UBL XML to the contact email address of the company, and nothing goes to the Peppol network. The API host and the endpoints are the same as for a production company. See [Test mode and sandbox companies](/environments). For a self-billing document, the API sends from your company (the `customer_*` fields) to the supplier (the `vendor_*` fields). In the query parameters of the send request, `sender_peppol_*` is the Peppol ID of your company and `receiver_peppol_*` is the Peppol ID of the supplier. See [Peppol ID routing](/guides/creating-invoices#peppol-id-routing). ### Use the example in a sandbox company The example uses the identity of E-INVOICE BV as the customer. In your own company, the create call returns `406` until the customer is your company. Replace the `customer_*` fields with the details of your company: ```bash cURL theme={null} curl "https://api.e-invoice.be/api/me/" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` Response (shortened): ```json theme={null} { "name": "My sandbox company", "company_tax_id": "BE1018265814", "company_email": "dev@example.com", "peppol_ids": ["0208:1018265814"] } ``` Set `customer_tax_id` to a tax number that resolves to one of the `peppol_ids` values. For a Belgian company, the Peppol ID `0208:1018265814` corresponds to the tax number `BE1018265814`. See [Test mode and sandbox companies](/environments). ### Generated UBL identifiers For both self-billing types, the generated UBL contains these identifiers: | UBL element | Value | | - | - | | `cbc:CustomizationID` | `urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:selfbilling:3.0` | | `cbc:ProfileID` | `urn:fdc:peppol.eu:2017:poacc:selfbilling:01:1.0` | | `cbc:InvoiceTypeCode` (invoice) | `389` | | `cbc:CreditNoteTypeCode` (credit note) | `261` | The supplier stays in `cac:AccountingSupplierParty` and your company stays in `cac:AccountingCustomerParty`. You can also upload a self-billing UBL file with `POST /api/documents/ubl`. The API reads the `CustomizationID` of the file. A file with the self-billing `CustomizationID` gets the type `SELFBILLING_INVOICE` or `SELFBILLING_CREDIT_NOTE`. A file with a `CustomizationID` that the API does not know gets `422`. ## Check that the supplier can receive self-billing documents A Peppol participant registers each document type that it can receive. Registration for Peppol BIS Billing 3.0 invoices does not include self-billing documents. Before you send, check the supplier with `GET /api/validate/peppol-id`: ```bash cURL theme={null} curl "https://api.e-invoice.be/api/validate/peppol-id?peppol_id=0208:0848934496" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` Response: ```json theme={null} { "is_valid": true, "dns_valid": true, "business_card_valid": true, "supported_document_types": [ "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", "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:selfbilling:3.0::2.1", "urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2::CreditNote##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:selfbilling:3.0::2.1" ], "business_card": { "name": "OpenPeppol VZW", "country_code": "BE", "registration_date": "2021-06-15" } } ``` Look in `supported_document_types` for these entries: | To send | Required entry contains | | - | - | | `SELFBILLING_INVOICE` | `Invoice-2::Invoice##` and `poacc:selfbilling:3.0` | | `SELFBILLING_CREDIT_NOTE` | `CreditNote-2::CreditNote##` and `poacc:selfbilling:3.0` | If the entries are absent, the supplier cannot receive self-billing documents through Peppol. Ask the supplier to have the self-billing document types registered by its access point. The response above is an illustration of the response shape. The document types of a real participant can be different. ## Self-billing credit notes Use `SELFBILLING_CREDIT_NOTE` to correct or cancel a self-billing invoice that your company issued. The parties are the same as in the self-billing invoice: the supplier is the vendor and your company is the customer. The ownership rule is also the same. The differences from the self-billing invoice example are: | Field | Self-billing credit note | | - | - | | `document_type` | `"SELFBILLING_CREDIT_NOTE"` | | `invoice_id` | The credit note number, for example `SB-CN-2026-001` | | `note` | The reason for the credit note (recommended) | ```json theme={null} { "document_type": "SELFBILLING_CREDIT_NOTE", "invoice_id": "SB-CN-2026-001", "invoice_date": "2026-10-05", "due_date": "2026-11-04", "currency": "EUR", "purchase_order": "PO-12345", "note": "Correction of self-billing invoice SB-2026-001: 2 units not delivered", "vendor_name": "OpenPeppol VZW", "vendor_tax_id": "BE0848934496", "vendor_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "customer_name": "E-INVOICE BV", "customer_tax_id": "BE1018265814", "customer_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "items": [ { "description": "Professional services", "quantity": 2, "unit": "C62", "unit_price": 100.00, "amount": 200.00, "tax_rate": "21.00" } ] } ``` Amounts are positive, as in a standard credit note. See [Create credit notes](/guides/credit-notes) for the general rules. ## Receive self-billing documents When your company is the supplier, your buyer can send you self-billing documents. A company that is registered on Peppol through e-invoice.be is registered for the self-billing invoice and the self-billing credit note document types, together with the standard billing document types. The API reads the `CustomizationID` of each inbound UBL document. If the identifier is a self-billing identifier, the document gets the type `SELFBILLING_INVOICE` or `SELFBILLING_CREDIT_NOTE`. In an inbound self-billing document, your company is in the `vendor_*` fields and the buyer that issued the document is in the `customer_*` fields. Use the `type` filter on the inbox to list these documents: ```bash cURL theme={null} curl "https://api.e-invoice.be/api/inbox/?type=SELFBILLING_INVOICE" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` Response (shortened): ```json theme={null} { "items": [ { "id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "document_type": "SELFBILLING_INVOICE", "state": "RECEIVED", "direction": "INBOUND", "invoice_id": "SB-2026-001", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496" } ], "total": 1, "page": 1, "page_size": 20, "pages": 1, "has_next_page": false } ``` The `type` filter on `GET /api/inbox/`, `GET /api/outbox/` and `GET /api/drafts/` accepts all five document types. In a sandbox company, Simulate inbound applies the same detection: a UBL file with a self-billing `CustomizationID` appears in the inbox with a self-billing type. See [Receive documents](/guides/receiving-documents) for the complete inbound flow. ## Debit notes Set `document_type` to `DEBIT_NOTE` to create a debit note. The API handles a debit note in the same way as an invoice, with one difference in the generated UBL: | Aspect | `DEBIT_NOTE` | | - | - | | UBL document | `Invoice` | | `cbc:InvoiceTypeCode` | `383` (an invoice has `380`) | | `cbc:CustomizationID` and `cbc:ProfileID` | Same as an invoice (Peppol BIS Billing 3.0) | | Validation rule set | Same as an invoice | | Vendor and customer | Same as an invoice: your company is the vendor | | Ownership rule | Same as an invoice: the vendor must be your company | All JSON fields, the totals calculation and the create and send calls are the same as for an invoice. A debit note increases the amount that the customer owes, so the amounts are positive. ```json theme={null} { "document_type": "DEBIT_NOTE", "invoice_id": "DN-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "purchase_order": "PO-12345", "note": "Additional transport cost for invoice INV-2026-001", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Additional transport cost", "quantity": 1, "unit": "C62", "unit_price": 150.00, "amount": 150.00, "tax_rate": "21.00" } ], "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "DN-2026-001" } ] } ``` Before you create this debit note, replace the vendor fields with the data of your company. Because a debit note is a UBL `Invoice` in the standard billing profile, a participant that can receive Peppol BIS Billing 3.0 invoices needs no additional registration to receive it. An inbound UBL invoice with type code `383` appears in the inbox as `INVOICE`. The API does not derive the `DEBIT_NOTE` type from the type code of an inbound UBL document. ## Frequently asked questions In a self-billing document, the Peppol sender is the party in the `customer_*` fields, and that party is not your company. Put your company in the `customer_*` fields and the supplier in the `vendor_*` fields. Compare the ID in the message with the `peppol_ids` field of `GET /api/me/`. `POST /api/validate/json` checks the document content only. The ownership rule applies when you create and when you send the document. Yes. `POST /api/documents/ubl` reads the `CustomizationID` of the file and stores a self-billing file as `SELFBILLING_INVOICE` or `SELFBILLING_CREDIT_NOTE`. Your company. The buyer issued the document, but your company is the supplier, and the supplier is always in the `vendor_*` fields. ## Next Steps Correct or cancel a standard invoice. Process inbound documents, self-billing documents included. Read validation issues and correct your JSON. Check the document types that a participant can receive. # Send UBL documents Source: https://docs.e-invoice.be/guides/ubl-documents Validate, create and send an invoice or credit note from a UBL XML file that your system generates. ## Overview If your system generates [UBL](/glossary#ubl) XML files, you can send them through e-invoice.be without a conversion to JSON. Use this method when: * Your ERP system generates UBL XML. * You migrate from a different Peppol Access Point. * You have existing UBL files to send. * You want full control of the UBL structure. If your ERP can only send invoices by email, see [Send invoices by email (Mailbox)](/guides/mailbox). ## Prerequisites Your UBL XML must: * Be in the Peppol BIS Billing 3.0 format. * Be an invoice (`Invoice`) or a [credit note](/guides/credit-notes) (`CreditNote`). * Have your company as the supplier. The send request returns `409` if the sender Peppol ID is not one of the `peppol_ids` of your company. The three steps are: 1. Validate the UBL XML (recommended while you develop). 2. Create the document from the UBL file. 3. Send the document. `POST /api/validate/ubl` and `POST /api/documents/ubl` accept `multipart/form-data` with one form field with the name `file`. A raw XML body (`Content-Type: application/xml`) gives `422` with the message `Field required` for `body.file`. ## Validate the UBL XML Validation does not create a document. `POST /api/validate/ubl` accepts only a file part with the content type `application/xml` or `text/xml`. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/validate/ubl" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -F "file=@invoice.xml;type=application/xml" ``` The [validation guide](/guides/validation#validate-a-ubl-xml-file) has this request in Node.js, Python, PHP and C#. Response for a valid file (`201`): ```json theme={null} { "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab", "file_name": "invoice.xml", "is_valid": true, "issues": [] } ``` Response for a file that is not valid (`201`, `is_valid` is `false`): ```json theme={null} { "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab", "file_name": "invoice.xml", "is_valid": false, "issues": [ { "message": "Belgian enterprise number MUST be stated in the correct format.", "type": "error", "rule_id": "PEPPOL-COMMON-R043", "flag": "fatal", "schematron": "PEPPOL-EN16931-UBL" } ] } ``` ## Create the document from UBL `POST /api/documents/ubl` validates the file again and creates the document in state `DRAFT`. If the file has a fatal validation issue, the request returns `415` and no issue list. Thus use `POST /api/validate/ubl` to find the cause. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/documents/ubl" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -F "file=@invoice.xml;type=application/xml" ``` ```javascript Node.js theme={null} import { readFile } from "node:fs/promises"; const form = new FormData(); form.append( "file", new Blob([await readFile("invoice.xml")], { type: "application/xml" }), "invoice.xml", ); // Do not set the Content-Type header. fetch adds it with the multipart boundary. const response = await fetch("https://api.e-invoice.be/api/documents/ubl", { method: "POST", headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }, body: form, }); const document = await response.json(); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${JSON.stringify(document)}`); } console.log(document.id, document.state); ``` ```python Python theme={null} import os import requests # Do not set the Content-Type header. requests adds it with the multipart boundary. with open("invoice.xml", "rb") as f: response = requests.post( "https://api.e-invoice.be/api/documents/ubl", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, files={"file": ("invoice.xml", f, "application/xml")}, timeout=60, ) response.raise_for_status() document = response.json() print(document["id"], document["state"]) ``` ```php PHP theme={null} true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("E_INVOICE_API_KEY")], // An array as body makes the request multipart/form-data. CURLOPT_POSTFIELDS => [ "file" => new CURLFile("invoice.xml", "application/xml", "invoice.xml"), ], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 201) { throw new RuntimeException("HTTP $status: $body"); } $document = json_decode($body, true); echo $document["id"] . " " . $document["state"] . PHP_EOL; ``` ```csharp C# theme={null} using System.Net.Http.Headers; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var file = new ByteArrayContent(await File.ReadAllBytesAsync("invoice.xml")); file.Headers.ContentType = new MediaTypeHeaderValue("application/xml"); using var form = new MultipartFormDataContent(); form.Add(file, "file", "invoice.xml"); var response = await client.PostAsync("https://api.e-invoice.be/api/documents/ubl", form); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {body}"); } Console.WriteLine(body); ``` Response (`201`, shortened; the full response also has the line items, the tax details and the attachments): ```json theme={null} { "id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "created_at": "2026-10-01T12:00:00Z", "document_type": "INVOICE", "state": "DRAFT", "direction": "OUTBOUND", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "subtotal": "1000.00", "total_tax": "210.00", "invoice_total": "1210.00" } ``` Files that are embedded in the UBL become attachments of the document. See [Attachments and PDF](/guides/attachments). ## Send the document Send the document with the `id` from the create response. The samples read the ID from the environment variable `DOCUMENT_ID`. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/documents/$DOCUMENT_ID/send?sender_peppol_scheme=0208&sender_peppol_id=1018265814&receiver_peppol_scheme=0208&receiver_peppol_id=0848934496" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```javascript Node.js theme={null} const documentId = process.env.DOCUMENT_ID; // the "id" from the create response const query = new URLSearchParams({ sender_peppol_scheme: "0208", sender_peppol_id: "1018265814", receiver_peppol_scheme: "0208", receiver_peppol_id: "0848934496", }); const response = await fetch( `https://api.e-invoice.be/api/documents/${documentId}/send?${query}`, { method: "POST", headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }, }, ); const result = await response.json(); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${JSON.stringify(result)}`); } console.log(result.state); ``` ```python Python theme={null} import os import requests document_id = os.environ["DOCUMENT_ID"] # the "id" from the create response response = requests.post( f"https://api.e-invoice.be/api/documents/{document_id}/send", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, params={ "sender_peppol_scheme": "0208", "sender_peppol_id": "1018265814", "receiver_peppol_scheme": "0208", "receiver_peppol_id": "0848934496", }, timeout=30, ) if not response.ok: raise SystemExit(f"HTTP {response.status_code}: {response.text}") print(response.json()["state"]) ``` ```php PHP theme={null} "0208", "sender_peppol_id" => "1018265814", "receiver_peppol_scheme" => "0208", "receiver_peppol_id" => "0848934496", ]); $ch = curl_init("https://api.e-invoice.be/api/documents/$documentId/send?$query"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("E_INVOICE_API_KEY"), ], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status >= 400) { throw new RuntimeException("HTTP $status: $body"); } echo json_decode($body, true)["state"], PHP_EOL; ``` ```csharp C# theme={null} using System.Net.Http.Headers; var documentId = Environment.GetEnvironmentVariable("DOCUMENT_ID"); // the "id" from the create response using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var query = "sender_peppol_scheme=0208&sender_peppol_id=1018265814&receiver_peppol_scheme=0208&receiver_peppol_id=0848934496"; var response = await client.PostAsync($"/api/documents/{documentId}/send?{query}", null); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {body}"); } Console.WriteLine(body); ``` Response (`200`, shortened): ```json theme={null} { "id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "created_at": "2026-10-01T12:00:00Z", "document_type": "INVOICE", "state": "TRANSIT", "direction": "OUTBOUND", "invoice_id": "INV-2026-001" } ``` To follow the document after the send, see [Document lifecycle and delivery tracking](/guides/document-lifecycle). ### Peppol ID routing When you create a document from UBL, the API stores the `cbc:EndpointID` values of the supplier and the customer with the document. The send request uses these values if you do not give query parameters. When you send a document, the API finds each of the four Peppol values (sender scheme and identifier, receiver scheme and identifier) in this order: 1. The query parameter of the send request. 2. The Peppol ID that the API stored with the UBL of the document when you created it. For a document that you created from UBL, this is the `cbc:EndpointID` of the party. 3. The derivation from the identifiers in the document: `customer_peppol_id` first for the receiver, then `vendor_company_id` / `customer_company_id` and `vendor_tax_id` / `customer_tax_id`. For `SELFBILLING_INVOICE` and `SELFBILLING_CREDIT_NOTE`, the direction is reversed: the API derives the sender from the `customer_*` fields and the receiver from the `vendor_*` fields. For a Belgian party, the API derives scheme `0208` and the enterprise number: the tax ID `BE1018265814` gives the Peppol ID `0208:1018265814`. If a tax ID has no country prefix, the API uses the country code at the end of the address (for example `1980 Zemst, BE`). Always give the VAT number with its country prefix, because rule `BR-CO-09` rejects a VAT number without it. For the scheme that the API derives in other countries, see [Peppol ID schemes](/guides/lookup-participants#peppol-id-schemes). When the API sends the document on the Peppol network, it writes the final sender and receiver values in the `cbc:EndpointID` elements of the UBL that it transmits. The other content of the UBL does not change. The derived scheme can be different from the scheme that the receiver registered. Thus the best practice is to give all four query parameters: ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/documents/$DOCUMENT_ID/send?sender_peppol_scheme=0208&sender_peppol_id=1018265814&receiver_peppol_scheme=0208&receiver_peppol_id=0848934496" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` Scheme of the sender Peppol ID. Example: `0208`. Identifier of the sender, without the scheme. Example: `1018265814`. Scheme of the receiver Peppol ID. Example: `0208`. Identifier of the receiver, without the scheme. Example: `0848934496`. The API returns `400` if it cannot find a complete sender and receiver Peppol ID. It returns `409` if the sender Peppol ID is not one of the `peppol_ids` of your company. Before you send, make sure that the receiver is registered on the Peppol network with the Peppol ID that you use. See [Look up Peppol participants](/guides/lookup-participants). ## Example UBL invoice This is a minimal invoice in the Peppol BIS Billing 3.0 format. Replace the supplier data with the data of your company. ```xml theme={null} urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 INV-2026-001 2026-10-01 2026-10-31 380 EUR PO-12345 1018265814 E-INVOICE BV Brusselsesteenweg 119/A Zemst 1980 BE BE1018265814 VAT E-INVOICE BV 1018265814 0848934496 OpenPeppol VZW Robert Schumanplein 6 bus 5 Brussel 1040 BE BE0848934496 VAT OpenPeppol VZW 0848934496 210.00 1000.00 210.00 S 21 VAT 1000.00 1000.00 1210.00 1210.00 1 10 1000.00 Professional services S 21 VAT 100.00 ``` ## UBL credit notes A credit note uses the same three steps. The root element is `CreditNote`, and the type code element is `cbc:CreditNoteTypeCode`: ```xml theme={null} urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 CN-2026-001 2026-10-15 381 EUR INV-2026-001 ``` ## Complete example The example validates a UBL file, creates the document and sends it. It needs only the environment variable `E_INVOICE_API_KEY`. ```javascript Node.js theme={null} import { readFile } from "node:fs/promises"; import { basename } from "node:path"; const BASE_URL = "https://api.e-invoice.be"; const headers = { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }; async function call(path, options = {}) { const response = await fetch(`${BASE_URL}${path}`, { ...options, headers }); const body = await response.json(); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${JSON.stringify(body)}`); } return body; } // Make a new FormData object for each request. async function ublForm(filePath) { const form = new FormData(); form.append( "file", new Blob([await readFile(filePath)], { type: "application/xml" }), basename(filePath), ); return form; } async function sendUblInvoice(filePath) { // 1. Validate the UBL const validation = await call("/api/validate/ubl", { method: "POST", body: await ublForm(filePath), }); if (!validation.is_valid) { console.error("UBL validation failed:", validation.issues); return null; } console.log("OK: UBL is valid"); // 2. Create the document const document = await call("/api/documents/ubl", { method: "POST", body: await ublForm(filePath), }); console.log("OK: document created:", document.id); // 3. Send the document const sent = await call(`/api/documents/${document.id}/send`, { method: "POST" }); console.log("OK: document sent, state:", sent.state); return sent; } await sendUblInvoice("./invoices/invoice.xml"); ``` ```python Python theme={null} import os import requests BASE_URL = "https://api.e-invoice.be" HEADERS = {"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"} def upload(path, ubl_file_path): # requests makes the multipart body. Do not set the Content-Type header. with open(ubl_file_path, "rb") as f: response = requests.post( f"{BASE_URL}{path}", headers=HEADERS, files={"file": (os.path.basename(ubl_file_path), f, "application/xml")}, timeout=60, ) response.raise_for_status() return response.json() def send_ubl_invoice(ubl_file_path): # 1. Validate the UBL validation = upload("/api/validate/ubl", ubl_file_path) if not validation["is_valid"]: print("UBL validation failed:", validation["issues"]) return None print("OK: UBL is valid") # 2. Create the document document = upload("/api/documents/ubl", ubl_file_path) print("OK: document created:", document["id"]) # 3. Send the document response = requests.post( f"{BASE_URL}/api/documents/{document['id']}/send", headers=HEADERS, timeout=60, ) response.raise_for_status() sent = response.json() print("OK: document sent, state:", sent["state"]) return sent send_ubl_invoice("./invoices/invoice.xml") ``` ## Get the UBL of a document `GET /api/documents/{document_id}/ubl` gives the UBL of a document, also for a document that you created from JSON. The response is JSON with a `signed_url` for the download of the XML file, not the XML itself. ```bash cURL theme={null} curl "https://api.e-invoice.be/api/documents/$DOCUMENT_ID/ubl" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` For the response and the lifetime of the URL, see [Attachments and PDF](/guides/attachments#download-the-ubl). ## Errors and frequent validation issues | Status | `detail` | Cause | | - | - | - | | `422` | `Field required` (`body.file`) | The request is not `multipart/form-data` with a `file` field. | | `422` | `Invalid file type` | `POST /api/validate/ubl`: the content type of the file part is not `application/xml` or `text/xml`. | | `415` | `UBL file is empty` | `POST /api/documents/ubl`: the file has no content. | | `415` | `UBL file is not valid and contains errors` | `POST /api/documents/ubl`: the file has a fatal validation issue. Use `POST /api/validate/ubl` to get the issues. | | `422` | `CustomizationID '...' matches no registered format profile` | `POST /api/documents/ubl`: the `cbc:CustomizationID` is not one of the two values that the API accepts (see below). The API does not use a different profile in its place. | | `422` | `UBL File could not be parsed` | `POST /api/documents/ubl`: the API could not read the document data from the file. | | `500` | `Failed to validate UBL document` | The validation did not complete. Send the request again. | Frequent causes of validation issues: * **Profile ID**: `cbc:ProfileID` must be `urn:fdc:peppol.eu:2017:poacc:billing:01:1.0` (`urn:fdc:peppol.eu:2017:poacc:selfbilling:01:1.0` for self-billing). * **Customisation ID**: `cbc:CustomizationID` must be `urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0`. For a self-billing document, it must be `urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:selfbilling:3.0` (see [Self-billing and debit notes](/guides/self-billing)). * **Endpoint ID**: the supplier party and the customer party each need a `cbc:EndpointID` with a `schemeID` attribute, for example `1018265814`. * **Tax category**: `cac:TaxCategory/cbc:ID` must be a code from the UNCL5305 list (`S`, `E`, `Z`, `AE` and others). * **Namespaces**: the root element must declare the three UBL namespaces that the example shows. For all status codes and error formats, see [Errors and troubleshooting](/guides/errors). ## Best practices * Validate with `POST /api/validate/ubl` while you develop. The create request rejects a file that is not valid, but it does not return the issues. * Give the four Peppol ID query parameters on the send request, and make sure that the receiver is registered before you send. See [Look up Peppol participants](/guides/lookup-participants). * Develop and test with a sandbox company. See [Test mode and sandbox companies](/environments). * Keep a copy of the original UBL file for your audit records. ## Resources * [Peppol BIS Billing 3.0 specification](https://docs.peppol.eu/poacc/billing/3.0/) * [UBL 2.1 technical documentation](http://docs.oasis-open.org/ubl/UBL-2.1.html) * [EN 16931 standard](https://ec.europa.eu/digital-building-blocks/sites/spaces/DIGITAL/pages/467108950/EN+16931+compliance) ## Next Steps Extract the invoice data from a PDF file Validate JSON and UBL while you develop Make sure that the receiver is registered on Peppol Status codes, error formats and retry rules # Usage statistics and credits Source: https://docs.e-invoice.be/guides/usage-statistics Read the number of sent and received documents per day, week or month, and compare the usage with the credit balance of your company. ## Overview The API reports how many documents your company sent and received in a period. Use these numbers for cost control, for internal reports, or to re-invoice usage to your own customers. The same response contains an estimate of the number of days that the credit balance covers. Two endpoints give the data: * `GET /api/stats` returns the usage counts and the estimate. * `GET /api/me/` returns the plan and the credit balance of the company. Both endpoints use the API key of the company. Each API key belongs to one company, so the numbers are always those of one company. ## What counts as usage One sent document is one action. One received document is one action. The response reports the two types of action separately: | Action | Counted documents | | - | - | | `DOCUMENT_SENT` | Outbound documents in the `SENT` state | | `DOCUMENT_RECEIVED` | Inbound documents in the `RECEIVED` state | These rules apply to the counts: * Documents in the `DRAFT`, `TRANSIT` or `FAILED` state are not counted. A send that fails is not usage. * Deleted documents are not counted. If you delete a document, the counts of earlier periods can become lower. * A document belongs to the day on which it was created in e-invoice.be, not to its `invoice_date`. An invoice with an invoice date in a previous month is counted in the period in which you created it. The API keeps each response for 5 minutes. A document that you sent a moment ago can be absent from the counts until that time has passed. ## Get usage statistics Send a `GET` request to `/api/stats`. All query parameters are optional. First day of the period, in `yyyy-mm-dd` format. If you omit it, the period starts on the day of the first counted document. Last day of the period, in `yyyy-mm-dd` format. The full day is included. If you omit it, the period ends on the day of the last counted document. Size of the periods in `actions`: `DAY`, `WEEK` or `MONTH`. ### Usage per day ```bash cURL theme={null} curl -G "https://api.e-invoice.be/api/stats" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -d "start_date=2026-09-01" \ -d "end_date=2026-09-07" \ -d "aggregation=DAY" ``` ```javascript Node.js theme={null} const params = new URLSearchParams({ start_date: "2026-09-01", end_date: "2026-09-07", aggregation: "DAY", }); const response = await fetch(`https://api.e-invoice.be/api/stats?${params}`, { headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }, }); if (!response.ok) { throw new Error(`Request failed: ${response.status} ${await response.text()}`); } const stats = await response.json(); console.log(stats); ``` ```python Python theme={null} import os import requests response = requests.get( "https://api.e-invoice.be/api/stats", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, params={ "start_date": "2026-09-01", "end_date": "2026-09-07", "aggregation": "DAY", }, timeout=30, ) response.raise_for_status() stats = response.json() print(stats) ``` ```php PHP theme={null} '2026-09-01', 'end_date' => '2026-09-07', 'aggregation' => 'DAY', ]); $ch = curl_init("https://api.e-invoice.be/api/stats?$query"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('E_INVOICE_API_KEY'), ], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 200) { throw new RuntimeException("Request failed: $status $body"); } $stats = json_decode($body, true); print_r($stats); ``` ```csharp C# theme={null} using System.Net.Http.Headers; using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var response = await client.GetAsync( "/api/stats?start_date=2026-09-01&end_date=2026-09-07&aggregation=DAY"); response.EnsureSuccessStatusCode(); var stats = await response.Content.ReadAsStringAsync(); Console.WriteLine(stats); ``` ```json Response theme={null} { "tenant_id": "ten-9k2m4p7q3w5x8r1t", "period_start": "2026-09-01", "period_end": "2026-09-07", "aggregation": "DAY", "actions": [ { "action": "DOCUMENT_RECEIVED", "stat_date": "2026-09-01", "count": 3 }, { "action": "DOCUMENT_SENT", "stat_date": "2026-09-01", "count": 12 }, { "action": "DOCUMENT_SENT", "stat_date": "2026-09-02", "count": 9 }, { "action": "DOCUMENT_RECEIVED", "stat_date": "2026-09-03", "count": 5 }, { "action": "DOCUMENT_SENT", "stat_date": "2026-09-03", "count": 14 }, { "action": "DOCUMENT_RECEIVED", "stat_date": "2026-09-04", "count": 2 }, { "action": "DOCUMENT_SENT", "stat_date": "2026-09-04", "count": 8 }, { "action": "DOCUMENT_SENT", "stat_date": "2026-09-07", "count": 10 } ], "total_days": 7, "average_daily_usage": 9.0, "budget_estimation_days": 555.6 } ``` A day without documents has no entry in `actions`. In this example there are no entries for 5 and 6 September, and no `DOCUMENT_RECEIVED` entry for 2 September. The company in the examples on this page has a credit balance of 5000. ### Usage per week ```bash cURL theme={null} curl -G "https://api.e-invoice.be/api/stats" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -d "start_date=2026-09-01" \ -d "end_date=2026-09-30" \ -d "aggregation=WEEK" ``` ```json Response theme={null} { "tenant_id": "ten-9k2m4p7q3w5x8r1t", "period_start": "2026-09-01", "period_end": "2026-09-30", "aggregation": "WEEK", "actions": [ { "action": "DOCUMENT_RECEIVED", "stat_date": "2026-08-31", "count": 10 }, { "action": "DOCUMENT_SENT", "stat_date": "2026-08-31", "count": 43 }, { "action": "DOCUMENT_RECEIVED", "stat_date": "2026-09-07", "count": 12 }, { "action": "DOCUMENT_SENT", "stat_date": "2026-09-07", "count": 51 }, { "action": "DOCUMENT_RECEIVED", "stat_date": "2026-09-14", "count": 8 }, { "action": "DOCUMENT_SENT", "stat_date": "2026-09-14", "count": 47 }, { "action": "DOCUMENT_RECEIVED", "stat_date": "2026-09-21", "count": 15 }, { "action": "DOCUMENT_SENT", "stat_date": "2026-09-21", "count": 55 }, { "action": "DOCUMENT_RECEIVED", "stat_date": "2026-09-28", "count": 4 }, { "action": "DOCUMENT_SENT", "stat_date": "2026-09-28", "count": 25 } ], "total_days": 30, "average_daily_usage": 9.0, "budget_estimation_days": 555.6 } ``` The first `stat_date` is `2026-08-31`, the Monday of the week that contains 1 September. The count of that week contains only the documents from 1 September, because documents before `start_date` are not in the period. ### Usage per month ```bash cURL theme={null} curl -G "https://api.e-invoice.be/api/stats" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -d "start_date=2026-07-01" \ -d "end_date=2026-09-30" \ -d "aggregation=MONTH" ``` ```json Response theme={null} { "tenant_id": "ten-9k2m4p7q3w5x8r1t", "period_start": "2026-07-01", "period_end": "2026-09-30", "aggregation": "MONTH", "actions": [ { "action": "DOCUMENT_RECEIVED", "stat_date": "2026-07-01", "count": 35 }, { "action": "DOCUMENT_SENT", "stat_date": "2026-07-01", "count": 190 }, { "action": "DOCUMENT_RECEIVED", "stat_date": "2026-08-01", "count": 28 }, { "action": "DOCUMENT_SENT", "stat_date": "2026-08-01", "count": 160 }, { "action": "DOCUMENT_RECEIVED", "stat_date": "2026-09-01", "count": 49 }, { "action": "DOCUMENT_SENT", "stat_date": "2026-09-01", "count": 221 } ], "total_days": 92, "average_daily_usage": 7.4, "budget_estimation_days": 675.7 } ``` The [command-line tool](/cli) gives the same data with `peppol stats --from 2026-07-01 --to 2026-09-30 --aggregation MONTH`. Add `--json` to get the response as JSON. ## Response fields Identifier of the company that owns the API key. First day of the period (`yyyy-mm-dd`). Equal to `start_date` when you gave it. Otherwise the day of the first counted document. Last day of the period (`yyyy-mm-dd`). Equal to `end_date` when you gave it. Otherwise the day of the last counted document. The aggregation that the API used: `DAY`, `WEEK` or `MONTH`. One entry for each combination of period and action that has a minimum of one document. The entries are sorted by `stat_date`, then by `action`. The list is empty when the period has no counted documents. `DOCUMENT_SENT` or `DOCUMENT_RECEIVED`. The date that identifies the period (`yyyy-mm-dd`). See the table below. Number of documents for this action in this period. Number of days from `period_start` to `period_end`, both days included. The minimum is `1`. Sum of all counts divided by `total_days`, rounded to one decimal. Estimated number of days that the credit balance covers. See [Read the budget estimate](#read-the-budget-estimate). ### How `stat_date` relates to the aggregation | Aggregation | Value of `stat_date` | | - | - | | `DAY` | The day itself | | `WEEK` | The Monday of the week | | `MONTH` | The first day of the month | With `WEEK` or `MONTH`, the first `stat_date` can be earlier than `period_start`. This occurs when `start_date` is not a Monday or not the first day of a month. The count for that entry contains only the documents inside the period. For totals per calendar week or calendar month, set `start_date` to the first day and `end_date` to the last day of a full week or month. ## Read the budget estimate `budget_estimation_days` compares the credit balance with the usage of the period in the request. One action uses one credit. ```text theme={null} budget_estimation_days = credit_balance / average_daily_usage ``` The result is rounded to one decimal. In the month example above, the balance is 5000 and `average_daily_usage` is 7.4, so the estimate is 5000 / 7.4 = 675.7 days. The estimate changes with the period that you request, because the average changes. A short, busy period gives a lower estimate than a long period that contains quiet months. For a forecast, use a period that represents your normal volume, for example the last 30 or 90 days. A value of `0.0` means that there is no estimate. It does not mean that there are no credits. The API returns `0.0` when `average_daily_usage` is `0.0`. This includes a period with very low usage: an average below 0.05 documents per day is rounded to `0.0`. To know the balance, read `credit_balance` from `GET /api/me/`. ## Credit balance and plan `GET /api/me/` returns the account data of the company, with the `plan` and the `credit_balance`. ```bash cURL theme={null} curl "https://api.e-invoice.be/api/me/" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```json Response theme={null} { "name": "E-INVOICE BV", "description": null, "plan": "enterprise", "credit_balance": 5000, "peppol_ids": ["0208:1018265814"], "ibans": ["BE68539007547034"], "company_number": "1018265814", "company_tax_id": null, "company_name": "E-INVOICE BV", "company_address": "Brusselsesteenweg 119/A", "company_zip": "1980", "company_city": "Zemst", "company_country": "Belgium", "company_email": null, "smp_registration": false, "smp_registration_date": null, "bcc_recipient_email": null } ``` Plan of the company: `starter`, `pro` or `enterprise`. Credit balance of the company. This is the value that `budget_estimation_days` uses. The other fields of this response are in the [API reference](/api-reference). ## Usage per tenant for resellers In the [reseller programme](/reseller-programme), each customer is a tenant with its own API key. `GET /api/stats` always reports the tenant that owns the API key in the request. To get the usage of each tenant, call the endpoint one time for each tenant, with the API key of that tenant. The `tenant_id` in each response tells you to which tenant the numbers belong. You create and read the API keys of a tenant with the [Admin API](/admin-api). The Admin API also lets you set the `plan` and the `credit_balance` of a tenant. The samples below read a JSON object from the environment variable `E_INVOICE_TENANT_KEYS`. The object maps a name of your choice to the API key of a tenant, for example `{"customer-a": "", "customer-b": ""}`. The samples print the monthly totals of each tenant. ```javascript Node.js theme={null} const tenantKeys = JSON.parse(process.env.E_INVOICE_TENANT_KEYS); const params = new URLSearchParams({ start_date: "2026-09-01", end_date: "2026-09-30", aggregation: "MONTH", }); for (const [name, apiKey] of Object.entries(tenantKeys)) { const response = await fetch(`https://api.e-invoice.be/api/stats?${params}`, { headers: { Authorization: `Bearer ${apiKey}` }, }); if (!response.ok) { console.error(`${name}: request failed with status ${response.status}`); continue; } const stats = await response.json(); const totals = { DOCUMENT_SENT: 0, DOCUMENT_RECEIVED: 0 }; for (const entry of stats.actions) { totals[entry.action] += entry.count; } console.log( `${name} (${stats.tenant_id}): sent ${totals.DOCUMENT_SENT}, received ${totals.DOCUMENT_RECEIVED}` ); } ``` ```python Python theme={null} import json import os import requests tenant_keys = json.loads(os.environ["E_INVOICE_TENANT_KEYS"]) params = { "start_date": "2026-09-01", "end_date": "2026-09-30", "aggregation": "MONTH", } for name, api_key in tenant_keys.items(): response = requests.get( "https://api.e-invoice.be/api/stats", headers={"Authorization": f"Bearer {api_key}"}, params=params, timeout=30, ) if not response.ok: print(f"{name}: request failed with status {response.status_code}") continue stats = response.json() totals = {"DOCUMENT_SENT": 0, "DOCUMENT_RECEIVED": 0} for entry in stats["actions"]: totals[entry["action"]] += entry["count"] print( f"{name} ({stats['tenant_id']}): " f"sent {totals['DOCUMENT_SENT']}, received {totals['DOCUMENT_RECEIVED']}" ) ``` ```text Output theme={null} customer-a (ten-9k2m4p7q3w5x8r1t): sent 221, received 49 customer-b (ten-5x7y9z1a3b5c7d9e): sent 64, received 0 ``` Always send `start_date` and `end_date` in a loop such as this one. With the two dates, a tenant without documents gives an empty `actions` list. Without them, that tenant gives a `404` response. ## Sandbox companies A [sandbox company](/environments) has no credits and no billing. `GET /api/stats` operates the same for a sandbox company as for a production company: it counts the documents of that company that are in the `SENT` or `RECEIVED` state. Each company has its own API key, so the documents of a sandbox company are never in the counts of a production company. Do not use `credit_balance` or `budget_estimation_days` of a sandbox company for a forecast. ## Errors | Status | Cause | | - | - | | `400` | `end_date` is before `start_date`. The `detail` is `end_date must be greater than or equal to start_date`. | | `401` | The API key is missing, invalid or inactive. | | `404` | You omitted `start_date`, `end_date` or the two, and the company has no document in the `SENT` or `RECEIVED` state. The `detail` is `No documents found for this tenant`. | | `422` | A date is not in `yyyy-mm-dd` format, or `aggregation` is not `DAY`, `WEEK` or `MONTH`. | ```json 404 response theme={null} { "detail": "No documents found for this tenant" } ``` See [Errors and troubleshooting](/guides/errors) for the error body formats. ## Next Steps Manage tenants and re-invoice usage to your customers. Create tenants and API keys, and set the plan and credit balance. Move from a sandbox company to a production company. See which document states exist and when a document becomes `SENT` or `RECEIVED`. # Validation during development Source: https://docs.e-invoice.be/guides/validation Validate invoice JSON and UBL XML against the Peppol BIS Billing 3.0 rules before you create a document. ## Overview The API has two validation endpoints: * `POST /api/validate/json` converts your JSON to [UBL](/glossary#ubl) and examines the result against the rules of [EN 16931](/glossary#en-16931) and [Peppol BIS Billing 3.0](/glossary#bis-billing-30). * `POST /api/validate/ubl` examines a UBL XML file that you made yourself. The two endpoints do not create a document and do not send data to the Peppol network. Validation is not a separate mandatory call. `POST /api/documents/` rejects a payload that does not pass the same rules. Use `POST /api/validate/json` while you develop, because it returns all rule failures and the generated UBL. ## Why validate * **No document is created**: you can repeat the call as often as necessary. * **All rule failures in one response**: each issue has a rule ID and a message. * **The generated UBL**: a valid result contains the XML that the API makes from your JSON. * **Fewer rejected create requests**: you correct the payload before you call `POST /api/documents/`. ## Validate a JSON document Send the document JSON to `POST /api/validate/json`. The body is the same as the body of `POST /api/documents/`. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/validate/json" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "purchase_order": "PO-12345", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email": "billing@e-invoice.be", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "subtotal": 1000.00, "total_tax": 210.00, "invoice_total": 1210.00, "amount_due": 1210.00, "payment_term": "Payment within 30 days", "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "INV-2026-001" } ] }' ``` ```javascript Node.js theme={null} const invoice = { document_type: "INVOICE", invoice_id: "INV-2026-001", invoice_date: "2026-10-01", due_date: "2026-10-31", currency: "EUR", purchase_order: "PO-12345", vendor_name: "E-INVOICE BV", vendor_tax_id: "BE1018265814", vendor_address: "Brusselsesteenweg 119/A, 1980 Zemst, BE", vendor_email: "billing@e-invoice.be", customer_name: "OpenPeppol VZW", customer_tax_id: "BE0848934496", customer_address: "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", items: [ { description: "Professional services", quantity: 10, unit: "C62", unit_price: 100.00, amount: 1000.00, tax_rate: "21.00" } ], subtotal: 1000.00, total_tax: 210.00, invoice_total: 1210.00, amount_due: 1210.00, payment_term: "Payment within 30 days", payment_details: [ { iban: "BE68539007547034", swift: "GEBABEBB", payment_reference: "INV-2026-001" } ] }; const response = await fetch("https://api.e-invoice.be/api/validate/json", { method: "POST", headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(invoice), }); const result = await response.json(); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${JSON.stringify(result)}`); } console.log(result); ``` ```python Python theme={null} import os import requests invoice = { "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "purchase_order": "PO-12345", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email": "billing@e-invoice.be", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "subtotal": 1000.00, "total_tax": 210.00, "invoice_total": 1210.00, "amount_due": 1210.00, "payment_term": "Payment within 30 days", "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "INV-2026-001" } ] } response = requests.post( "https://api.e-invoice.be/api/validate/json", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, json=invoice, timeout=30, ) if not response.ok: raise SystemExit(f"HTTP {response.status_code}: {response.text}") print(response.json()) ``` ```php PHP theme={null} "INVOICE", "invoice_id" => "INV-2026-001", "invoice_date" => "2026-10-01", "due_date" => "2026-10-31", "currency" => "EUR", "purchase_order" => "PO-12345", "vendor_name" => "E-INVOICE BV", "vendor_tax_id" => "BE1018265814", "vendor_address" => "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email" => "billing@e-invoice.be", "customer_name" => "OpenPeppol VZW", "customer_tax_id" => "BE0848934496", "customer_address" => "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items" => [ [ "description" => "Professional services", "quantity" => 10, "unit" => "C62", "unit_price" => 100.00, "amount" => 1000.00, "tax_rate" => "21.00" ] ], "subtotal" => 1000.00, "total_tax" => 210.00, "invoice_total" => 1210.00, "amount_due" => 1210.00, "payment_term" => "Payment within 30 days", "payment_details" => [ [ "iban" => "BE68539007547034", "swift" => "GEBABEBB", "payment_reference" => "INV-2026-001" ] ] ]; $ch = curl_init("https://api.e-invoice.be/api/validate/json"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("E_INVOICE_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode($invoice), ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status >= 400) { throw new RuntimeException("HTTP $status: $body"); } print_r(json_decode($body, true)); ``` ```csharp C# theme={null} using System.Net.Http.Headers; using System.Text; var invoice = """ { "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "purchase_order": "PO-12345", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email": "billing@e-invoice.be", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "subtotal": 1000.00, "total_tax": 210.00, "invoice_total": 1210.00, "amount_due": 1210.00, "payment_term": "Payment within 30 days", "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "INV-2026-001" } ] } """; using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var response = await client.PostAsync( "/api/validate/json", new StringContent(invoice, Encoding.UTF8, "application/json")); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {body}"); } Console.WriteLine(body); ``` `POST /api/validate/json` does not compare the vendor with your company. `POST /api/documents/` does: before you create the document, replace the vendor fields with those of your own company. When the document is valid, the API returns `201 Created` with the result and the generated UBL: ```json 201 Created theme={null} { "id": "9eec0b03-4649-4ae4-9c4c-323ebb6d53e8", "file_name": "9eec0b03-4649-4ae4-9c4c-323ebb6d53e8.xml", "is_valid": true, "issues": [], "ubl_document": "\n The identifier of this validation. It is not a document ID. The name of the generated XML file (`.xml`). For `POST /api/validate/ubl`, the name of the file that you uploaded. `false` when one issue or more has the flag `fatal` or is a failure of the XML schema (`rule_id` is `xsd-validation`). The rule failures and warnings. Each issue has `message`, `type` (`error` or `warning`) and `schematron` (the rule set that found the issue). An issue can also have `rule_id`, `flag`, `location` and `test`. For the description of each field, see [Validation result](/guides/errors#validation-result). The generated UBL XML. `POST /api/validate/json` returns this field only when `is_valid` is `true`. `POST /api/documents/{document_id}/validate` always returns it. The `ubl_document` field shows the XML that the API makes from your JSON. Use it to see how each JSON field maps to UBL. ### A document that breaks rules A rule failure does not change the HTTP status code. The API returns `201 Created`, `is_valid` is `false`, and `issues` contains the failures. Thus your code must read `is_valid`, not only the status code. This payload has one fault: the vendor enterprise number `BE0123456789` does not pass the modulo 97 check. It is a failing example on purpose. ```json Request body with a fault theme={null} { "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE0123456789", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services", "quantity": "10", "unit_price": "100.00", "unit": "C62", "tax_rate": "21.00" } ] } ``` ```json 201 Created theme={null} { "id": "b55354b0-5c69-489b-a8f7-44be7d5bdd6b", "file_name": "b55354b0-5c69-489b-a8f7-44be7d5bdd6b.xml", "is_valid": false, "issues": [ { "message": "Belgian enterprise number MUST be stated in the correct format.", "type": "error", "location": "/Q{urn:oasis:names:specification:ubl:schema:xsd:Invoice-2}Invoice[1]/Q{urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2}AccountingSupplierParty[1]/Q{urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2}Party[1]/Q{urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2}EndpointID[1]", "rule_id": "PEPPOL-COMMON-R043", "flag": "fatal", "test": "matches(normalize-space(), '^[0-9]{10}$') and u:mod97-0208(normalize-space())", "schematron": "PEPPOL-EN16931-UBL" } ] } ``` The response has no `ubl_document`, because the document is not valid. The `location` is the XPath of the element in the generated UBL: here the `cbc:EndpointID` of the supplier, which the API derives from `vendor_tax_id`. To correct the payload, use an enterprise number that passes the modulo 97 check: `BE1018265814` in place of `BE0123456789`. The line in this payload has no `amount`. This is not a fault: if a line has no `amount`, the API calculates it as `quantity` × `unit_price`. For the cause and the correction of each frequent rule ID, see [Frequent validation rules](/guides/errors#frequent-validation-rules). ### Totals that are not consistent Before the API generates the UBL, it calculates the totals from the lines and compares them with `subtotal`, `total_tax`, `total_discount`, `invoice_total` and `amount_due` in your JSON. If a value is different from the calculated value by more than 1.00, or if `amount_due` is more than the calculated invoice total, the API stops. It returns `406 Not Acceptable` with one text message, and no `issues` list: ```json 406 Not Acceptable theme={null} { "detail": "Document is not valid: ['Subtotal mismatch: provided 100.0, calculated 110.00']" } ``` The message can also contain `Total tax mismatch`, `Total discount mismatch`, `Invoice total mismatch` or `Amount due (...) cannot be greater than invoice total (...)`. When more than one total is wrong, the list in the message has one entry for each total. `POST /api/documents/` returns the same `406` for the same payload. If you leave out a total field, the API calculates it. See [Invoice totals and calculations](/guides/invoice-totals). A request that does not agree with the schema (for example a date that is not `YYYY-MM-DD`, or a currency code that is not supported) returns `422` before the rules run. See [Request validation error (422)](/guides/errors#request-validation-error-422). ### Peppol IDs that the API cannot derive To generate the UBL, the API derives a Peppol ID for the vendor and for the customer from the tax ID, the company ID or `customer_peppol_id`. If it cannot derive a Peppol ID, it returns `422` with one text message and no `issues` list: ```json 422 Unprocessable Entity theme={null} { "detail": "Invalid vendor tax id" } ``` For the customer, the message is `Invalid customer tax id`. Frequent causes: * The tax ID has no country prefix, and the address has no country code at the end. * The API has no Peppol scheme for the country. Give the VAT number with its country prefix, for example `BE1018265814`. `POST /api/documents/` returns `406` with the detail `Could not derive Peppol sender or receiver information` for the same payload. For the derivation rules, see [Peppol ID routing](/guides/ubl-documents#peppol-id-routing). ## Validate a UBL XML file Use `POST /api/validate/ubl` to examine a UBL BIS Billing 3.0 XML file that you have. This endpoint accepts `multipart/form-data` with one form field, `file`. It does not accept the XML as the raw request body. See [Common mistake: raw XML body](#common-mistake-raw-xml-body). | Item | Value | | - | - | | Method | `POST` | | URL | `https://api.e-invoice.be/api/validate/ubl` | | Authorization | `Bearer` and your API key | | Content-Type | `multipart/form-data` (your HTTP client sets it when you attach a file) | | Body | Form field `file` with the UBL XML file. The content type of this part must be `application/xml` or `text/xml`. If it is not, the API returns `422` with the detail `Invalid file type`. | ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/validate/ubl" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -F "file=@invoice.xml;type=application/xml" ``` ```javascript Node.js theme={null} import { readFile } from "node:fs/promises"; const form = new FormData(); form.append( "file", new Blob([await readFile("invoice.xml")], { type: "application/xml" }), "invoice.xml" ); // Do not set Content-Type. fetch adds the multipart boundary. const response = await fetch("https://api.e-invoice.be/api/validate/ubl", { method: "POST", headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }, body: form, }); const result = await response.json(); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${JSON.stringify(result)}`); } console.log(result); ``` ```python Python theme={null} import os import requests with open('invoice.xml', 'rb') as f: response = requests.post( 'https://api.e-invoice.be/api/validate/ubl', headers={'Authorization': f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, files={'file': ('invoice.xml', f, 'application/xml')}, ) print(response.json()) ``` ```php PHP theme={null} true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('E_INVOICE_API_KEY'), ], CURLOPT_POSTFIELDS => [ 'file' => new CURLFile('invoice.xml', 'application/xml', 'invoice.xml'), ], ]); echo curl_exec($ch); curl_close($ch); ``` ```csharp C# theme={null} using System.Net.Http; using System.Net.Http.Headers; using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); using var form = new MultipartFormDataContent(); var fileBytes = await File.ReadAllBytesAsync("invoice.xml"); var fileContent = new ByteArrayContent(fileBytes); fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/xml"); // The form field must have the name "file". form.Add(fileContent, "file", "invoice.xml"); var response = await http.PostAsync( "https://api.e-invoice.be/api/validate/ubl", form ); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` The API returns `201 Created` with the validation result. A valid file gives: ```json 201 Created theme={null} { "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab", "file_name": "invoice.xml", "is_valid": true, "issues": [] } ``` A file that breaks a rule gives the same status code, with `is_valid` set to `false`: ```json 201 Created theme={null} { "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab", "file_name": "invoice.xml", "is_valid": false, "issues": [ { "message": "[BR-CO-15]-Invoice total amount with VAT (BT-112) = Invoice total amount without VAT (BT-109) + Invoice total VAT amount (BT-110).", "type": "error", "rule_id": "BR-CO-15", "flag": "fatal", "schematron": "CEN-EN16931-UBL" } ] } ``` The `schematron` field names the [Schematron](/glossary#schematron) rule set that found the issue: `CEN-EN16931-UBL`, `PEPPOL-EN16931-UBL`, `PEPPOL-EN16931-UBL-SB` (self-billing) or `XSD` (the XML schema). ### Common mistake: raw XML body If you set `Content-Type: application/xml` and send the XML as the raw request body (for example with `--data-binary @invoice.xml`), the API returns this `422` response: ```json 422 Unprocessable Entity theme={null} { "detail": [ { "type": "missing", "loc": ["body", "file"], "msg": "Field required", "input": null } ] } ``` The correction: use `multipart/form-data` with a `file` form field, as the examples above show. | Wrong | Right | | - | - | | `-H "Content-Type: application/xml" --data-binary @invoice.xml` | `-F "file=@invoice.xml;type=application/xml"` | | `fetch(url, { body: xml, headers: { 'Content-Type': 'application/xml' } })` | `fetch(url, { body: formData })` | | `requests.post(url, data=xml, headers={'Content-Type': 'application/xml'})` | `requests.post(url, files={'file': ('invoice.xml', f, 'application/xml')})` | | `new StringContent(xml, ..., "application/xml")` | `new MultipartFormDataContent { { fileContent, "file", "invoice.xml" } }` | ### When to use UBL validation Use `POST /api/validate/ubl` when you: * have UBL XML files from your ERP system and want to examine them before you send them * move from a different Peppol Access Point and have UBL documents that are already generated * get UBL files from an external source and want to examine them before you post them to `POST /api/documents/ubl` If you create invoices from JSON, use `POST /api/validate/json`. It applies the same UBL rules and returns the generated UBL, so a second call to `POST /api/validate/ubl` is not necessary. See also [Send UBL documents](/guides/ubl-documents). ## Common validation problems | Problem | What the API returns | Correction | | - | - | - | | A Belgian enterprise number is not correct | Issue `PEPPOL-COMMON-R043` | Use the real 10-digit number with the prefix `BE`, for example `BE1018265814`. | | The `amount` of a line is not `quantity` × `unit_price` | Issue `PEPPOL-EN16931-R120`, and frequently a total rule such as `BR-CO-16` | Correct `amount`, or leave it out: the API then calculates it. | | The API cannot derive the vendor or customer Peppol ID | `422` with `Invalid vendor tax id` or `Invalid customer tax id` (see [Peppol IDs that the API cannot derive](#peppol-ids-that-the-api-cannot-derive)) | Give the VAT number with its country prefix. | | The totals are not consistent with the lines | `406` with one message (see [Totals that are not consistent](#totals-that-are-not-consistent)) | Correct the totals, or leave them out. | | A date does not have the format `YYYY-MM-DD` | `422` with the field in `loc` | Send dates as `2026-10-01`. | | The currency code is not supported | `422` with the field in `loc` | Use a code from the list below. | | Data that the rules make mandatory is missing (for example a party name or address) | One issue or more in `issues` | Read the `rule_id` and the `message` of each issue. | The `currency` field accepts these ISO 4217 codes: `EUR`, `USD`, `GBP`, `JPY`, `CHF`, `CAD`, `AUD`, `NZD`, `CNY`, `INR`, `SEK`, `NOK`, `DKK`, `SGD`, `HKD` The default is `EUR`. The `tax_rate` of a line is a percentage from 0 to 100, for example `21.00`, `6.00` or `0.00`. You can send it as a number or as a string (`"21.00"`). If you do not give it, the rate is `0.00`. For the rule ID reference, all HTTP status codes and the error body formats, see [Errors and troubleshooting](/guides/errors). ## Development workflow Start from the example in [Validate a JSON document](#validate-a-json-document) and replace the values with your data. For the fields, see [Create e-invoices](/guides/creating-invoices) and the [document schema](/api-reference/schemas/document). Send the JSON to `POST /api/validate/json`. If the status code is `406` or `422`, or if `is_valid` is `false`, correct the JSON and send it again. When `is_valid` is `true`, send the same JSON to `POST /api/documents/`. The API creates the document in the state `DRAFT`. ### Complete example These programs validate a document and create it only when the result is valid. Put your document JSON in the file `invoice.json`. The vendor in the file must be your own company. ```javascript validate-and-create.mjs theme={null} import { readFile } from "node:fs/promises"; const BASE_URL = "https://api.e-invoice.be"; const headers = { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}`, "Content-Type": "application/json", }; async function post(path, payload) { const response = await fetch(`${BASE_URL}${path}`, { method: "POST", headers, body: JSON.stringify(payload), }); const body = await response.json(); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${JSON.stringify(body)}`); } return body; } const invoice = JSON.parse(await readFile("invoice.json", "utf8")); const validation = await post("/api/validate/json", invoice); if (!validation.is_valid) { console.error("Validation failed:"); for (const issue of validation.issues) { console.error(` ${issue.rule_id}: ${issue.message}`); } process.exit(1); } console.log("Validation OK"); const document = await post("/api/documents/", invoice); console.log("Document created:", document.id); ``` ```python validate_and_create.py theme={null} import json import os import sys import requests BASE_URL = "https://api.e-invoice.be" HEADERS = {"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"} def post(path, payload): response = requests.post(f"{BASE_URL}{path}", headers=HEADERS, json=payload, timeout=30) if not response.ok: raise SystemExit(f"HTTP {response.status_code}: {response.text}") return response.json() with open("invoice.json", encoding="utf-8") as f: invoice = json.load(f) validation = post("/api/validate/json", invoice) if not validation["is_valid"]: print("Validation failed:") for issue in validation["issues"]: print(f" {issue.get('rule_id')}: {issue['message']}") sys.exit(1) print("Validation OK") document = post("/api/documents/", invoice) print("Document created:", document["id"]) ``` ## Testing strategy ### During development Use `POST /api/validate/json` for each change of your payload: 1. **Edge cases**: validate unusual documents (zero amounts, more than one VAT rate, a currency other than EUR). 2. **All document types**: validate invoices, credit notes and debit notes. 3. **Short cycles**: correct the JSON and validate again. No document is created. 4. **Automated tests**: add validation calls to your test suite. Develop and test with a sandbox company. A sandbox company runs in test mode: the API sends each document as UBL XML to the contact email address of the company, and nothing goes to the Peppol network. The API host and the endpoints are the same as for a production company. See [Test mode and sandbox companies](/environments). ### Before production 1. Validate representative samples of all the invoice types that you send. 2. Make sure that each receiver is registered on Peppol. See [Look up Peppol participants](/guides/lookup-participants). 3. Validate all the tax categories and currency codes that you use. 4. Validate complex documents (allowances, charges, many lines). See [Advanced invoicing](/guides/advanced-invoicing). ## Best practices Do not wait until production: * Validate each new invoice template. * Validate again after a change in your mapping or in the schema. * Add validation to your CI/CD pipeline. The validation endpoints have a rate limit. See [Rate limits](/guides/errors#rate-limits). Three results are possible. Handle each of them: * `201` and `is_valid` is `true`: continue with `POST /api/documents/`. * `201` and `is_valid` is `false`: record the `rule_id` and the `message` of each issue, and do not create the document. * `406` or `422`: record the `detail` value, and do not create the document. The `detail` is a text for a totals error or a Peppol ID error, and an array for a schema error. If you generate invoices from a template, validate one document of each template when the template changes. The create request applies the rules again to each document, so a validation call for each invoice in production is not necessary. ## Next Steps Create and send validated invoices Find the cause of a status code or a rule ID Calculate totals that pass the checks Find the Peppol ID of a receiver # Introduction Source: https://docs.e-invoice.be/index Find out how to send and receive Peppol e-invoices with the e-invoice.be API from JSON, UBL XML or PDF. ## What is e-invoice.be? e-invoice.be is a **recognised [Peppol](/glossary#peppol) [Access Point](/glossary#access-point) and [SMP](/glossary#smp)** that operates under the Belgian Peppol Authority (BOSA). Companies in all countries can use it to send and receive electronic invoices on the Peppol network. With the API you can: * Send invoices and [credit notes](/guides/credit-notes) from **JSON** * Upload **UBL XML** that your ERP system generates * Extract the invoice data from **PDF files**, then create the document from the result * Receive documents from other Peppol participants * Give Peppol e-invoicing to your own customers through the **reseller programme** e-invoice.be is a member of the Peppol Security Committee. e-invoice.be as Peppol Access Point ## Who uses e-invoice.be * **Individual companies** in many industries (manufacturing, services, retail) * **Public and private listed companies** * **SaaS companies**, through the [reseller programme](/reseller-programme) * **Public and government agencies** * **Companies with or without an ERP system** Peppol is an international network for electronic invoicing. E-invoices that you send through Peppol comply with the European standard EN 16931 and are recognised in many countries outside Europe. ## How Peppol works The Peppol network uses a **4-corner model** for the exchange of e-invoices between companies: Peppol 4-corner model 1. **Corner 1**: Your company (the sender) 2. **Corner 2**: Your Access Point (e-invoice.be) 3. **Corner 3**: The Access Point of the receiver 4. **Corner 4**: The company of the receiver e-invoice.be does the Peppol transmission. Your system sends a document to the API, and the API converts it to UBL BIS Billing 3.0 and sends it to the Access Point of the receiver. ## Get started Sign up at [app.e-invoice.be](https://app.e-invoice.be). A sandbox company runs in test mode. Nothing goes to the Peppol network. Call `GET /api/me/` with the API key of the sandbox company. Send the invoice JSON to `POST /api/validate/json`. Send the same JSON to `POST /api/documents/`. Call `POST /api/documents/{document_id}/send`. Do each step with complete requests and responses ## Core features ### Create e-invoices Send invoices and credit notes through Peppol from a JSON payload: ```json Invoice theme={null} { "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "purchase_order": "PO-12345", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "vendor_email": "billing@e-invoice.be", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00" } ], "subtotal": 1000.00, "total_tax": 210.00, "invoice_total": 1210.00, "amount_due": 1210.00, "payment_term": "Payment within 30 days", "payment_details": [ { "iban": "BE68539007547034", "swift": "GEBABEBB", "payment_reference": "INV-2026-001" } ] } ``` Replace the vendor fields with the data of your company. The API rejects a document if the Peppol ID of the vendor is not one of the `peppol_ids` of your company. Create and send an e-invoice ### Validate before you send `POST /api/validate/json` examines the invoice JSON and does not create a document. Validation is not a separate mandatory call. `POST /api/documents/` rejects a payload that does not pass the same rules. Use `POST /api/validate/json` while you develop, because it returns all rule failures and the generated UBL. Validate invoices while you develop ### Look up Peppol participants Make sure that a customer can receive e-invoices before you send. This endpoint does not require an API key. ```bash theme={null} curl "https://api.e-invoice.be/api/validate/peppol-id?peppol_id=0208:0848934496" ``` ```json theme={null} { "is_valid": true, "dns_valid": true, "business_card_valid": true, "supported_document_types": [ "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" ], "business_card": { "name": "OpenPeppol VZW", "country_code": "BE", "registration_date": null } } ``` Find and examine the Peppol IDs of your customers ### Receive documents Documents that other Peppol participants send to your company go into the inbox. Find received documents and download the UBL, the PDF and the attachments ### Webhooks The API sends a notification to your server for each of these events: * `document.received` * `document.received.failed` * `document.sent` * `document.sent.failed` Set up webhook notifications ## Developer tools Overview of the host, the authentication, the endpoint groups, pagination and errors Typed clients for TypeScript, Python, Ruby, PHP and Java Validate, create and send documents from the command line Status codes, error formats and retry rules ## For resellers SaaS companies and software vendors can give Peppol e-invoicing to their customers. Conditions and integration for SaaS platforms and service providers ## Support Send an email to the support team See the open-source projects ## Next Steps Send a first invoice with a sandbox company Test without traffic on the Peppol network Change from a sandbox company to a production company Changes to the API and to this documentation # Quickstart Source: https://docs.e-invoice.be/quickstart Create a sandbox company, then validate, create and send your first invoice with the API in about five minutes. This page takes you from a new account to a sent invoice. You use a **sandbox company**, which runs in test mode: the API does not transmit the document on the Peppol network, but sends the generated UBL XML to you by email. You need: * An account at [app.e-invoice.be](https://app.e-invoice.be). * A terminal with `curl` (or Node.js 18+, Python with `requests`, PHP with the cURL extension, or .NET 6+). A sandbox company needs no VAT verification, no contact verification and no payment. It is free to use. See [Test mode and sandbox companies](/environments) for the details of test mode. Sign up at [app.e-invoice.be](https://app.e-invoice.be) and log in. 1. Open **Companies** and select **Create sandbox company**. 2. Keep the country **Belgium** and select **Suggest valid VAT** to get a synthetic Belgian VAT number. 3. Make sure that **Contact email** is an address that you can read. The API sends the test document to this address. In the API, this address is the `company_email` field. 4. Create the company, open **API Settings** and copy the API key of the sandbox company. Each company has its own API key. Use the key of the sandbox company for all commands on this page. ```bash theme={null} export E_INVOICE_API_KEY="paste-the-sandbox-api-key-here" ``` Request the details of the company that owns the key. ```bash cURL theme={null} curl "https://api.e-invoice.be/api/me/" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```javascript Node.js theme={null} const response = await fetch("https://api.e-invoice.be/api/me/", { headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }, }); console.log(response.status, await response.json()); ``` ```python Python theme={null} import os import requests response = requests.get( "https://api.e-invoice.be/api/me/", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, ) print(response.status_code, response.json()) ``` ```php PHP theme={null} true, CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("E_INVOICE_API_KEY")], ]); echo curl_exec($ch); curl_close($ch); ``` ```csharp C# theme={null} using System.Net.Http.Headers; using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var response = await client.GetAsync("/api/me/"); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` The API returns `200 OK`: ```json theme={null} { "name": "Sandbox Demo BV", "description": null, "plan": "starter", "credit_balance": 10, "peppol_ids": ["0208:0999123457"], "ibans": null, "company_number": "0999123457", "company_tax_id": null, "company_name": "Sandbox Demo BV", "company_address": "Teststraat 1", "company_zip": "1000", "company_city": "Brussel", "company_country": "Belgium", "company_email": "you@example.com", "smp_registration": false, "smp_registration_date": null, "bcc_recipient_email": null } ``` The vendor of each document that you create must be a Peppol ID of your company. For a Belgian company, the Peppol ID is `0208:` followed by the VAT number without the `BE` prefix. Take the number after `0208:` in `peppol_ids`, put `BE` in front of it and keep it in a second environment variable: ```bash theme={null} export E_INVOICE_VENDOR_VAT="BE0999123457" ``` If the vendor VAT number does not match a Peppol ID of your company, `POST /api/documents/` returns `406` with the message `Derived sender '0208:...' is not in tenant peppol_ids`. Save the invoice as `invoice.json`. The command puts your vendor VAT number in the file. The customer is OpenPeppol VZW (`BE0848934496`). ```bash theme={null} cat > invoice.json < Validation is not a separate mandatory call. `POST /api/documents/` rejects a payload that does not pass the same rules. Use `POST /api/validate/json` while you develop, because it returns all rule failures and the generated UBL. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/validate/json" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -H "Content-Type: application/json" \ --data @invoice.json ``` ```javascript Node.js theme={null} import { readFile } from "node:fs/promises"; const response = await fetch("https://api.e-invoice.be/api/validate/json", { method: "POST", headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}`, "Content-Type": "application/json", }, body: await readFile("invoice.json"), }); const result = await response.json(); console.log(response.status, result.is_valid, result.issues); ``` ```python Python theme={null} import json import os import requests with open("invoice.json") as f: invoice = json.load(f) response = requests.post( "https://api.e-invoice.be/api/validate/json", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, json=invoice, ) result = response.json() print(response.status_code, result["is_valid"], result["issues"]) ``` ```php PHP theme={null} true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("E_INVOICE_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => file_get_contents("invoice.json"), ]); echo curl_exec($ch); curl_close($ch); ``` ```csharp C# theme={null} using System.Net.Http.Headers; using System.Text; using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var body = new StringContent( await File.ReadAllTextAsync("invoice.json"), Encoding.UTF8, "application/json"); var response = await client.PostAsync("/api/validate/json", body); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` The API returns `201 Created`. This call does not store a document. The `ubl_document` field contains the full UBL XML; it is shortened here. ```json theme={null} { "id": "3f2b8c1e-5d4a-4e6f-9a7b-1c2d3e4f5a6b", "file_name": "3f2b8c1e-5d4a-4e6f-9a7b-1c2d3e4f5a6b.xml", "is_valid": true, "issues": [], "ubl_document": "..." } ``` If `is_valid` is `false`, read the `issues` array, correct `invoice.json` and validate again. See [Validation during development](/guides/validation). Send the same file to `POST /api/documents/`. The API stores the document in the state `DRAFT`. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/documents/" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" \ -H "Content-Type: application/json" \ --data @invoice.json ``` ```javascript Node.js theme={null} import { readFile } from "node:fs/promises"; const response = await fetch("https://api.e-invoice.be/api/documents/", { method: "POST", headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}`, "Content-Type": "application/json", }, body: await readFile("invoice.json"), }); const document = await response.json(); console.log(response.status, document.id, document.state); ``` ```python Python theme={null} import json import os import requests with open("invoice.json") as f: invoice = json.load(f) response = requests.post( "https://api.e-invoice.be/api/documents/", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, json=invoice, ) document = response.json() print(response.status_code, document["id"], document["state"]) ``` ```php PHP theme={null} true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("E_INVOICE_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => file_get_contents("invoice.json"), ]); $document = json_decode(curl_exec($ch), true); curl_close($ch); echo $document["id"] . " " . $document["state"] . PHP_EOL; ``` ```csharp C# theme={null} using System.Net.Http.Headers; using System.Text; using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var body = new StringContent( await File.ReadAllTextAsync("invoice.json"), Encoding.UTF8, "application/json"); var response = await client.PostAsync("/api/documents/", body); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` The API returns `201 Created` with the complete document. The response is shortened here. ```json theme={null} { "id": "doc-4f8a2c6e9b1d3a5f7c0e2b4d6f8a1c3e", "created_at": "2026-10-01T09:15:23.482913Z", "document_type": "INVOICE", "state": "DRAFT", "direction": "OUTBOUND", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE0999123457", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "subtotal": "1000.00", "total_tax": "210.00", "invoice_total": "1210.00", "amount_due": "1210.00" } ``` Keep the `id` for the next step: ```bash theme={null} export E_INVOICE_DOCUMENT_ID="doc-4f8a2c6e9b1d3a5f7c0e2b4d6f8a1c3e" ``` Send the draft. The request has no body. ```bash cURL theme={null} curl -X POST "https://api.e-invoice.be/api/documents/$E_INVOICE_DOCUMENT_ID/send" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```javascript Node.js theme={null} const id = process.env.E_INVOICE_DOCUMENT_ID; const response = await fetch(`https://api.e-invoice.be/api/documents/${id}/send`, { method: "POST", headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` }, }); const document = await response.json(); console.log(response.status, document.id, document.state); ``` ```python Python theme={null} import os import requests document_id = os.environ["E_INVOICE_DOCUMENT_ID"] response = requests.post( f"https://api.e-invoice.be/api/documents/{document_id}/send", headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}, ) document = response.json() print(response.status_code, document["id"], document["state"]) ``` ```php PHP theme={null} true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("E_INVOICE_API_KEY")], CURLOPT_POSTFIELDS => "", ]); echo curl_exec($ch); curl_close($ch); ``` ```csharp C# theme={null} using System.Net.Http.Headers; using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY")); var id = Environment.GetEnvironmentVariable("E_INVOICE_DOCUMENT_ID"); var response = await client.PostAsync($"/api/documents/{id}/send", null); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` The API returns `200 OK` with the document. The state is now `TRANSIT`, because delivery runs in the background. The response is shortened here. ```json theme={null} { "id": "doc-4f8a2c6e9b1d3a5f7c0e2b4d6f8a1c3e", "created_at": "2026-10-01T09:15:23.482913Z", "document_type": "INVOICE", "state": "TRANSIT", "direction": "OUTBOUND", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "invoice_total": "1210.00" } ``` If `company_email` is empty, this call returns `400` with the message `Tenant is in test mode but no company email is set`. The state of the document is then `SENT`, but the API sends no email. Add a contact email address in the company settings, then create and send a new document. Open the mailbox of the contact email address of the sandbox company. You get an email with the subject `Document sent via Peppol // e-invoice.be [TEST-MODE]` and the UBL XML as an attachment. This is the file that a production company transmits on the Peppol network. When the email is sent, the state of the document changes to `SENT`: ```bash theme={null} curl "https://api.e-invoice.be/api/documents/$E_INVOICE_DOCUMENT_ID" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` ```json theme={null} { "id": "doc-4f8a2c6e9b1d3a5f7c0e2b4d6f8a1c3e", "document_type": "INVOICE", "state": "SENT", "direction": "OUTBOUND", "invoice_id": "INV-2026-001" } ``` The API also sends a `document.sent` event to each webhook that subscribes to it. See [Webhooks](/essentials/webhooks). ## Go live Test mode is fixed when a company is created. To send documents on the Peppol network, create a production company and use its API key. The API host and your code stay the same. Follow the [go-live checklist](/going-live) before you send the first production document. If you prefer a typed client, see [SDKs](/sdks). If you prefer a terminal, see the [peppol CLI](/cli). ## Next Steps Move from a sandbox company to a production company Learn how test mode and sandbox companies work Use all invoice fields, payment details and tax options Get a notification for each sent and received document # Reseller programme Source: https://docs.e-invoice.be/reseller-programme Offer Peppol e-invoicing to your customers through one integration with e-invoice.be. ## Overview The e-invoice.be **reseller programme** is for SaaS companies, software vendors and service providers that want to add Peppol e-invoicing to their products and services. A reseller gets an [organisation API key](/glossary#organisation-api-key) for the [Admin API](/admin-api) and manages one [tenant](/glossary#tenant) for each customer. A tenant is a company that a reseller manages. ## Who the programme is for * **SaaS platforms** (accounting, ERP, CRM and invoicing software) * **Software vendors** that sell business management products * **Managed service providers** that serve many clients * **Industry platforms** (for example construction, healthcare and logistics) * **Accounting and bookkeeping firms** that manage the invoicing of their clients ## Benefits Your customers use your interface. Your platform calls the e-invoice.be API in the background. Manage all your customers through one integration, with one tenant and one API key for each customer. Technical support during the integration and after it. Pricing that depends on the volume, for resellers and service providers. ## How it works Send an email to [support@e-invoice.be](mailto:support@e-invoice.be) to discuss: * your use case and your customers * the integration requirements * the expected transaction volumes * pricing and commercial terms Integrate the API into your product: * **API access**: all e-invoice.be API endpoints, with the API key of each tenant. * **Admin API**: create and manage tenants and their API keys with your organisation API key. See the [Admin API](/admin-api). * **Webhooks**: each tenant has its own webhooks. Create them with the API key of the tenant. See [Webhooks](/essentials/webhooks). * **Testing**: use test-mode tenants. Their sends go to the `company_email` of the tenant and they cause no Peppol traffic. Set `company_email` on each test-mode tenant: without it, a send returns `400`. See [Test mode and sandbox companies](/environments). Start e-invoicing for your customers: * **Customer onboarding**: create the tenant, the API key and the Peppol registration through the Admin API. * **Billing**: consolidated billing or billing for each customer, as agreed in your contract. To read the usage of each tenant for re-invoicing, see [Usage statistics and credits](/guides/usage-statistics). * **Support**: e-invoice.be handles technical Peppol problems; you give support to your customers. ## Use cases ### SaaS accounting platform A cloud accounting platform adds Peppol e-invoicing for its customers. For each new customer, the platform creates a tenant and an API key with the organisation API key (`E_INVOICE_ORG_API_KEY`): ```bash theme={null} # 1. Create a tenant for the new customer (Admin API, organisation API key) curl -X POST "https://api.e-invoice.be/api/admin/tenants" \ -H "Authorization: Bearer $E_INVOICE_ORG_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "e-invoice-bv", "description": "E-INVOICE BV", "company_number": "1018265814", "company_tax_id": "BE1018265814", "company_name": "E-INVOICE BV", "company_country": "BE", "company_email": "info@e-invoice.be", "peppol_ids": ["0208:1018265814"] }' ``` ```json theme={null} { "id": "ten-9k2m4p7q3w5x8r1t", "name": "e-invoice-bv", "description": "E-INVOICE BV", "company_number": "1018265814", "company_tax_id": "BE1018265814", "company_name": "E-INVOICE BV", "company_country": "BE", "company_email": "info@e-invoice.be", "peppol_ids": ["0208:1018265814"], "plan": "enterprise", "credit_balance": 999999999, "created_at": "2026-04-16T10:00:00.000000Z", "updated_at": "2026-04-16T10:00:00.000000Z" } ``` The response is shortened; see [Create a tenant](/admin-api#create-a-tenant) for all fields. Only `name` is required. `company_number` has no country prefix and `company_tax_id` has the country prefix. `company_email` is the contact email address of the tenant. A test-mode tenant cannot send without it. ```bash theme={null} # 2. Create an API key for the tenant (use the tenant id from step 1) curl -X POST "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r1t/api-keys" \ -H "Authorization: Bearer $E_INVOICE_ORG_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "production-key"}' ``` ```json theme={null} { "id": "api-3h8f5j2k9l4m7n6p1q5r8s2t4v6w9x3y", "tenant_id": "ten-9k2m4p7q3w5x8r1t", "name": "production-key", "description": null, "created_at": "2026-04-16T10:00:00.000000Z", "updated_at": "2026-04-16T10:00:00.000000Z", "is_deleted": false } ``` The `id` of the API key is the bearer token of the tenant. The platform stores it and uses it for all invoice operations of this customer. The API finds the tenant from the key, so the request body does not contain a tenant ID. Set `E_INVOICE_API_KEY` to the `id` of the API key of the tenant. For example, to list the documents that the customer sent: ```bash theme={null} # 3. Call the standard API with the tenant API key curl -X GET "https://api.e-invoice.be/api/outbox/" \ -H "Authorization: Bearer $E_INVOICE_API_KEY" ``` To create and send an invoice with this key, see [Create e-invoices](/guides/creating-invoices). ### ERP system integration An ERP vendor builds e-invoicing into the product: * Users send invoices from the ERP interface. * The ERP calls the e-invoice.be API in the background. * The ERP gets status changes through webhooks. * Users do not need a second login. ### Managed service provider A managed service provider handles the invoicing of 50 or more clients: * One API integration for all clients. * One tenant, with its own Peppol ID, for each client. * One API key for each client, which the provider can rotate and revoke through the Admin API. ## Technical requirements ### What your system must do * Send authenticated HTTPS requests. * Parse and generate JSON. * Host a webhook endpoint to receive notifications. * Handle errors and repeat failed requests. ### Security * Store API keys in a secure location. * Keep a separate credential for each tenant. * Verify the signature of each webhook. * Keep an audit log. ## Pricing Reseller pricing depends on: * **Volume**: the number of invoices for each month * **Number of customers**: the number of tenants * **Support level** * **Contract term** Send an email to [support@e-invoice.be](mailto:support@e-invoice.be) for a quotation. ## How to become a reseller Send an email to [support@e-invoice.be](mailto:support@e-invoice.be) with: * your company background * a description of your use case * the expected volumes * your schedule In a call, e-invoice.be and you discuss: * your requirements * the pricing options * the integration approach After the agreement: * you receive an organisation API key for the Admin API * you read the [Admin API](/admin-api) page * you build and test the integration with test-mode tenants Go live with your integration: * create production tenants * onboard your customers * get continued support ## Support A reseller gets: * **Technical support**: help with the API integration and with troubleshooting, through [support@e-invoice.be](mailto:support@e-invoice.be) * **Admin API access**: an organisation API key to manage tenants. See the [Admin API](/admin-api). ## Frequently asked questions Yes. Your end users do not see the API integration. They use your interface, and your platform calls the e-invoice.be API in the background. The billing model is part of your contract. Possible models are: * consolidated billing to you (you bill your customers) * billing for each tenant (e-invoice.be bills each customer) * a mix of the two You give first-line support to your customers. e-invoice.be gives: * technical support to you for API problems * communication about Peppol network status and incidents * documentation that you can share Yes. Peppol is an interoperable network. Your customers can exchange invoices with all participants on the Peppol network, whatever their access point provider. e-invoice.be handles: * Peppol compliance and certification * validation against Peppol BIS Billing 3.0 * the connection to the network You handle: * customer onboarding (know-your-customer checks, if applicable) * data privacy (GDPR compliance) * the security of your application The agreement depends on your situation. Send an email to [support@e-invoice.be](mailto:support@e-invoice.be) to discuss the options for your business model. ## Contact To become a reseller, send an email to [support@e-invoice.be](mailto:support@e-invoice.be). ## Next Steps Create tenants, issue API keys and register tenants on Peppol. Create and send invoices with the API key of a tenant. Receive status changes for the documents of each tenant. Test your integration with no Peppol traffic. # SDKs Source: https://docs.e-invoice.be/sdks Install an official e-invoice.be SDK for TypeScript, Python, Ruby, PHP or Java and validate, create and send a document with typed calls. e-invoice.be publishes SDKs for five languages. Each SDK is generated from the OpenAPI specification of the API and gives you typed requests, typed responses, automatic retries and error classes. You do not have to write HTTP calls. All SDKs call the single API host `https://api.e-invoice.be`. The API key selects the mode: a key of a sandbox company makes the SDK run in test mode, and a key of a production company sends documents on the Peppol network. See [Test mode and sandbox companies](/environments). ## Available SDKs | Language | Repository | Package | Minimum runtime | Release status | | - | - | - | - | - | | TypeScript and JavaScript | [e-invoice-ts](https://github.com/e-invoice-be/e-invoice-ts) | `e-invoice-api` (npm) | Node.js 20 LTS, TypeScript 4.9 | Version 1.x | | Python | [e-invoice-py](https://github.com/e-invoice-be/e-invoice-py) | `e-invoice-api` (PyPI) | Python 3.9 | Version 1.x | | Ruby | [e-invoice-rb](https://github.com/e-invoice-be/e-invoice-rb) | `e-invoice-api` (RubyGems) | Ruby 3.2.0 | Version 0.x, the interface can change | | PHP | [e-invoice-php](https://github.com/e-invoice-be/e-invoice-php) | `e-invoice-be/e-invoice` (Composer, from the repository) | PHP 8.1.0 | Version 0.x, the interface can change | | Java | [e-invoice-java](https://github.com/e-invoice-be/e-invoice-java) | `com.e_invoice.api:e-invoice-java` | Java 8 | Alpha | The TypeScript SDK also runs in Deno 1.28.0 and later, Bun 1.0 and later, Cloudflare Workers and the Vercel Edge Runtime. The README of the repository has the full list. ## Install an SDK ```bash TypeScript theme={null} npm install e-invoice-api ``` ```bash Python theme={null} pip install e-invoice-api ``` ```ruby Ruby theme={null} # Gemfile gem "e-invoice-api", "~> 0.33.0" ``` ```json PHP theme={null} { "repositories": [ { "type": "vcs", "url": "git@github.com:e-invoice-be/e-invoice-php.git" } ], "require": { "e-invoice-be/e-invoice": "dev-main" } } ``` ```xml Java theme={null} com.e_invoice.api e-invoice-java 0.1.0-alpha.12 ``` The Ruby and Java version numbers above are the versions in each README at the time of writing. Use the README of the repository to find the current version. The PHP SDK is not on Packagist. Composer installs it from the GitHub repository, as the `repositories` entry in the sample shows. The Java coordinates above are the coordinates in the README of the Java SDK. At the time of writing (2026-10-01), the artifact `com.e_invoice.api:e-invoice-java` is not yet on Maven Central: `repo1.maven.org` returns `404` for it. If your build cannot find the artifact, see the README of the [repository](https://github.com/e-invoice-be/e-invoice-java) for the current status. ## Authenticate Each SDK reads the API key from the environment variable `E_INVOICE_API_KEY`. Do not put the key in your source code. ```bash theme={null} export E_INVOICE_API_KEY="your-api-key" ``` To get an API key, see [Authentication](/authentication). ## Validate, create and send a document The samples do the same operations in each language: 1. Create a client and read the account of the API key (`GET /api/me/`). 2. Validate the invoice JSON (`POST /api/validate/json`). 3. Create the document (`POST /api/documents/`). The document is in the `DRAFT` state. 4. Send the document (`POST /api/documents/{document_id}/send`). Validation is not a separate mandatory call. `POST /api/documents/` rejects a payload that does not pass the same rules. Use `POST /api/validate/json` while you develop, because it returns all rule failures and the generated UBL. The samples use the vendor E-INVOICE BV (`BE1018265814`). Replace `vendor_name`, `vendor_tax_id` and `vendor_address` with the data of your own company. A sandbox company has an assigned VAT number, which you can read from the account response. ```typescript TypeScript theme={null} import EInvoice from 'e-invoice-api'; // Reads the key from E_INVOICE_API_KEY const client = new EInvoice(); const account = await client.me.retrieve(); console.log(account.name); const invoice: EInvoice.DocumentCreateParams = { document_type: 'INVOICE', invoice_id: 'INV-2026-001', invoice_date: '2026-10-01', due_date: '2026-10-31', currency: 'EUR', vendor_name: 'E-INVOICE BV', vendor_tax_id: 'BE1018265814', vendor_address: 'Brusselsesteenweg 119/A, 1980 Zemst, Belgium', customer_name: 'OpenPeppol VZW', customer_tax_id: 'BE0848934496', customer_address: 'Robert Schumanplein 6 bus 5, 1040 Brussel, Belgium', items: [ { description: 'Professional services', quantity: 10, unit: 'C62', unit_price: 100.0, amount: 1000.0, tax_rate: '21.00', }, ], }; const validation = await client.validate.validateJson(invoice); if (!validation.is_valid) { console.error(validation.issues); process.exit(1); } const document = await client.documents.create(invoice); console.log(document.id, document.state); const sent = await client.documents.send(document.id); console.log(sent.state); ``` ```python Python theme={null} from e_invoice_api import EInvoice # Reads the key from E_INVOICE_API_KEY client = EInvoice() account = client.me.retrieve() print(account.name) invoice = { "document_type": "INVOICE", "invoice_id": "INV-2026-001", "invoice_date": "2026-10-01", "due_date": "2026-10-31", "currency": "EUR", "vendor_name": "E-INVOICE BV", "vendor_tax_id": "BE1018265814", "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE", "customer_name": "OpenPeppol VZW", "customer_tax_id": "BE0848934496", "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", "items": [ { "description": "Professional services", "quantity": 10, "unit": "C62", "unit_price": 100.00, "amount": 1000.00, "tax_rate": "21.00", } ], } validation = client.validate.validate_json(**invoice) if not validation.is_valid: print(validation.issues) raise SystemExit(1) document = client.documents.create(**invoice) print(document.id, document.state) sent = client.documents.send(document.id) print(sent.state) ``` ```ruby Ruby theme={null} require "bundler/setup" require "date" require "e_invoice_api" # Reads the key from E_INVOICE_API_KEY e_invoice = EInvoiceAPI::Client.new account = e_invoice.me.retrieve puts(account.name) invoice = { document_type: :INVOICE, invoice_id: "INV-2026-001", invoice_date: Date.new(2026, 10, 1), due_date: Date.new(2026, 10, 31), currency: :EUR, vendor_name: "E-INVOICE BV", vendor_tax_id: "BE1018265814", vendor_address: "Brusselsesteenweg 119/A, 1980 Zemst, BE", customer_name: "OpenPeppol VZW", customer_tax_id: "BE0848934496", customer_address: "Robert Schumanplein 6 bus 5, 1040 Brussel, BE", items: [ { description: "Professional services", quantity: 10, unit: :C62, unit_price: 100.00, amount: 1000.00, tax_rate: "21.00" } ] } validation = e_invoice.validate.validate_json(**invoice) unless validation.is_valid puts(validation.issues) exit(1) end document = e_invoice.documents.create(**invoice) puts(document.id, document.state) sent = e_invoice.documents.send_(document.id) puts(sent.state) ``` ```php PHP theme={null} me->retrieve(); echo $account->name, PHP_EOL; $invoice = [ 'document_type' => 'INVOICE', 'invoice_id' => 'INV-2026-001', 'invoice_date' => '2026-10-01', 'due_date' => '2026-10-31', 'currency' => 'EUR', 'vendor_name' => 'E-INVOICE BV', 'vendor_tax_id' => 'BE1018265814', 'vendor_address' => 'Brusselsesteenweg 119/A, 1980 Zemst, Belgium', 'customer_name' => 'OpenPeppol VZW', 'customer_tax_id' => 'BE0848934496', 'customer_address' => 'Robert Schumanplein 6 bus 5, 1040 Brussel, Belgium', 'items' => [ [ 'description' => 'Professional services', 'quantity' => 10, 'unit' => 'C62', 'unit_price' => 100.00, 'amount' => 1000.00, 'tax_rate' => '21.00', ], ], ]; $validation = $client->validate->validateJson($invoice); if (!$validation->is_valid) { var_dump($validation->issues); exit(1); } $document = $client->documents->create($invoice); echo $document->id, PHP_EOL; $sent = $client->documents->send($document->id, []); var_dump($sent->state); ``` ```java Java theme={null} import com.e_invoice.api.client.EInvoiceClient; import com.e_invoice.api.client.okhttp.EInvoiceOkHttpClient; import com.e_invoice.api.models.documents.CurrencyCode; import com.e_invoice.api.models.documents.DocumentCreate; import com.e_invoice.api.models.documents.DocumentResponse; import com.e_invoice.api.models.documents.DocumentType; import com.e_invoice.api.models.documents.UnitOfMeasureCode; import com.e_invoice.api.models.validate.UblDocumentValidation; import java.time.LocalDate; public class SendInvoice { public static void main(String[] args) { // Reads the key from E_INVOICE_API_KEY EInvoiceClient client = EInvoiceOkHttpClient.fromEnv(); DocumentCreate invoice = DocumentCreate.builder() .documentType(DocumentType.INVOICE) .invoiceId("INV-2026-001") .invoiceDate(LocalDate.of(2026, 10, 1)) .dueDate(LocalDate.of(2026, 10, 31)) .currency(CurrencyCode.EUR) .vendorName("E-INVOICE BV") .vendorTaxId("BE1018265814") .vendorAddress("Brusselsesteenweg 119/A, 1980 Zemst, BE") .customerName("OpenPeppol VZW") .customerTaxId("BE0848934496") .customerAddress("Robert Schumanplein 6 bus 5, 1040 Brussel, BE") .addItem(DocumentCreate.Item.builder() .description("Professional services") .quantity(10.0) .unit(UnitOfMeasureCode.C62) .unitPrice(100.00) .amount(1000.00) .taxRate("21.00") .build()) .build(); UblDocumentValidation validation = client.validate().validateJson(invoice); if (!validation.isValid()) { System.err.println(validation.issues()); System.exit(1); } DocumentResponse document = client.documents().create(invoice); System.out.println(document.id()); DocumentResponse sent = client.documents().send(document.id()); System.out.println(sent.state()); } } ``` The Java SDK is an alpha release. It does not have the account resource (`GET /api/me/`), thus the Java sample starts with the validation step. The send call accepts the optional routing parameters `sender_peppol_scheme`, `sender_peppol_id`, `receiver_peppol_scheme`, `receiver_peppol_id` and `email`. In Java, the names are in camel case in `DocumentSendParams`. See [Create e-invoices](/guides/creating-invoices) for when to set them. With a sandbox company, the send call does not use the Peppol network. See [Test mode and sandbox companies](/environments). ## Method names for each SDK | Operation | TypeScript | Python | Ruby | PHP | Java | | - | - | - | - | - | - | | Get account | `client.me.retrieve()` | `client.me.retrieve()` | `me.retrieve` | `$client->me->retrieve()` | Not available | | Validate JSON | `client.validate.validateJson()` | `client.validate.validate_json()` | `validate.validate_json` | `$client->validate->validateJson()` | `client.validate().validateJson()` | | Create document | `client.documents.create()` | `client.documents.create()` | `documents.create` | `$client->documents->create()` | `client.documents().create()` | | Send document | `client.documents.send()` | `client.documents.send()` | `documents.send_` | `$client->documents->send()` | `client.documents().send()` | In Ruby, the send method has the name `send_` with an underscore at the end, because `send` is a method of each Ruby object. The TypeScript and Python repositories have a file `api.md` with the full list of methods. ## Versions and the OpenAPI specification The API has one OpenAPI specification, which is version 1.1.0 at this time. Each SDK is generated from this specification, but each SDK has its own version number. An SDK version number does not agree with the version number of the specification. * The TypeScript, Python and Java SDKs follow semantic versioning, with the exceptions that each README gives. * The Ruby and PHP SDKs have major version `0`. Their README states that the interface can change at any time. * A new endpoint can be in the API before it is in an SDK. The [API reference](/api-reference) always shows the current API. Each README shows how to call an endpoint that the SDK does not have yet. ## Report a problem Report a problem with an SDK in the issues of its repository: * [e-invoice-ts issues](https://github.com/e-invoice-be/e-invoice-ts/issues) * [e-invoice-py issues](https://github.com/e-invoice-be/e-invoice-py/issues) * [e-invoice-rb issues](https://github.com/e-invoice-be/e-invoice-rb/issues) * [e-invoice-php issues](https://github.com/e-invoice-be/e-invoice-php/issues) * [e-invoice-java issues](https://github.com/e-invoice-be/e-invoice-java/issues) ## Next Steps Validate, create and send documents from the command line Learn the fields of an invoice Handle the errors that the API returns See all endpoints and schemas