# CLAUDE
Source: https://docs.e-invoice.be/CLAUDE
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a Mintlify-based documentation site for e-invoice.be, an international Peppol Access Point and SMP operating under the Belgian Peppol Authority (BOSA). The documentation covers API integration, webhooks, and usage guides for electronic invoicing via Peppol.
e-invoice.be serves:
* Individual companies across multiple industries worldwide
* Public and private listed companies
* SaaS companies (via reseller programme)
* Public and government agencies
* Companies with or without ERP systems
## Tech Stack
* **Documentation framework**: Mintlify (v4.2.177)
* **Content format**: MDX (Markdown + JSX components)
* **Package manager**: Bun (bun.lock present)
* **Runtime**: Node.js
## Development Workflow
### Local Development
To run the documentation site locally with live preview:
```bash theme={null}
npx mint dev
```
This starts a local development server with hot-reloading for real-time preview of changes.
### Building
Mintlify handles the build process automatically when deployed. For local validation:
```bash theme={null}
npx mint build
```
## Project Structure
```
/
├── docs.json # Mintlify configuration (navigation, theme, colors)
├── index.mdx # Homepage - overview of e-invoice.be
├── quickstart.mdx # Getting started guide
├── authentication.mdx # API authentication guide
├── environments.mdx # API host and test mode
├── api-reference.mdx # API reference overview
├── admin-api.mdx # Admin API documentation
├── reseller-programme.mdx # Partner/reseller information
├── guides/
│ ├── creating-invoices.mdx # How to create and send e-invoices
│ ├── credit-notes.mdx # Creating credit notes
│ ├── advanced-invoicing.mdx # Allowances & charges (document + line level)
│ ├── invoice-totals.mdx # Calculating invoice totals
│ ├── validation.mdx # JSON validation during development
│ ├── lookup-participants.mdx # Peppol participant lookup
│ ├── ubl-documents.mdx # Sending pre-generated UBL XML
│ └── pdf-documents.mdx # Creating from PDF files
├── api-reference/
│ └── schemas/
│ ├── document.mdx # Document schema documentation
│ └── line-item.mdx # Line item schema documentation
├── essentials/
│ └── webhooks.mdx # Webhook integration guide
├── images/ # Static assets (logos, screenshots)
└── package.json
```
### Configuration (`docs.json`)
The `docs.json` file is the central configuration:
* **Navigation structure**: Defines tabs (Guides, API reference, Example invoices) and page organization
* **Theme**: Uses "willow" theme with blue color scheme (#0071b7 primary)
* **API integration**: References OpenAPI spec at `https://api.e-invoice.be/api/openapi.json`
* **Branding**: IBM Plex Sans font, custom logo at `/images/logo.svg`
Navigation structure:
* **Getting started**: index, quickstart, authentication, environments
* **Core guides**: creating-invoices, credit-notes, advanced-invoicing, invoice-totals, validation, lookup-participants
* **Alternative formats**: ubl-documents, pdf-documents
* **Integration**: webhooks
* **Partners**: reseller-programme
* **API reference**: Auto-generated from OpenAPI, plus overview page and custom schema documentation
* **Example invoices**: Sample invoice templates
## Content Guidelines
### File Organization
* **Getting started guides**: Root directory (quickstart.mdx, authentication.mdx, api-reference.mdx)
* **Core guides**: `/guides/` directory (creating-invoices, credit-notes, advanced-invoicing, invoice-totals, validation, lookup-participants, ubl-documents, pdf-documents)
* **Integration guides**: `/essentials/` directory (webhooks)
* **API schemas**: `/api-reference/schemas/` directory (document.mdx, line-item.mdx)
* **Images**: `/images/` directory for static assets
* **ALWAYS update `docs.json` navigation after creating new pages**
* Use `.mdx` extension for all content files
### MDX Components
Mintlify provides built-in components used throughout the docs:
```mdx theme={null}
Description
...
...
Important warning message
Additional context or information
Helpful tip or best practice
```
### Front Matter
Every MDX file requires YAML front matter:
```yaml theme={null}
---
title: "Page Title"
description: "Page description for SEO"
---
```
## API Documentation
### OpenAPI Specification
The API reference tab auto-generates from the OpenAPI specification at:
```
https://api.e-invoice.be/api/openapi.json
```
**Current OpenAPI version**: 1.1.0
### Updating API Documentation
The API documentation in Mintlify is automatically generated from the OpenAPI spec. To update the API reference:
1. **Backend changes**: Modify the OpenAPI spec on the backend API server
* The spec is served at `https://api.e-invoice.be/api/openapi.json`
* Changes to the backend automatically update this endpoint
2. **Mintlify sync**: Mintlify automatically pulls the latest OpenAPI spec
* The `docs.json` file references the OpenAPI URL via the `openapi` field
* Mintlify periodically refreshes the spec to keep docs up to date
* No manual intervention needed in the docs repository
3. **Custom schema documentation**: Some schemas have custom MDX documentation:
* `/api-reference/schemas/document.mdx` - Extended document schema explanation
* `/api-reference/schemas/line-item.mdx` - Line item details and examples
* Update these files manually when schema behavior changes
### OpenAPI Spec Structure Overview
The OpenAPI spec includes:
**Endpoints**:
* Document management (create, retrieve, validate, send, attachments)
* Inbox/Outbox operations (received/sent documents, drafts)
* Conversion & validation (PDF, UBL, JSON validation)
* Peppol network operations (participant lookup, ID validation)
* Account & integration (tenant info, webhooks, usage stats)
**Schemas**:
* Document types: Invoice, Credit Note, Debit Note, Self-Billing variants
* Document states: Draft, Transit, Failed, Sent, Received
* Tax handling: Category codes, VAT rates, exemption reasons
* Attachments: Binary file support with metadata
* Line items: Quantity, pricing, tax calculation
**Authentication**: HTTP Bearer token authentication
To update API docs, modify the OpenAPI spec on the backend - changes propagate automatically to Mintlify.
## Webhook System Architecture
The platform implements webhook notifications for document events:
### Event Types
* `document.received` - Document successfully received via Peppol
* `document.sent` - Document successfully sent
* `document.sent.failed` - Send failure
* `document.received.failed` - Receive failure
### Security Model
* Webhooks use HMAC-SHA256 signatures in `X-Signature` header
* Format: `sha256={hex_digest}`
* Payload is JSON-sorted and UTF-8 encoded before signing
* Verification required for production implementations
### Payload Structure
```json theme={null}
{
"id": "evt_...",
"tenant_id": "ten_...",
"created_at": 1729468923,
"type": "document.sent",
"data": {
"document_id": "doc_..."
}
}
```
## Deployment
Mintlify handles deployment automatically via their platform. Changes pushed to the main branch trigger automatic deployments.
The site is hosted at: `https://docs.e-invoice.be`
## API Architecture
Base URL (single host — there is no separate staging/development host):
* `https://api.e-invoice.be`
To test without touching the Peppol network, use a **sandbox company** (a workspace in **test mode**): outbound sends are diverted to email, inbound documents are seeded via Simulate inbound, and no Peppol traffic occurs. Test-ness is fixed at company creation. See `environments.mdx`.
Authentication: Bearer token in `Authorization` header
### Test Mode & Sandbox Companies
Testing is controlled by **test mode**, which is delivered through a **sandbox company** - a dedicated company (created self-serve in app.e-invoice.be via "Create sandbox company") that runs in test mode. It is determined by the company you authenticate as, not by a separate host. Each company (sandbox or regular) has its own API key; test status is set at creation and is immutable.
**Regular company (production):**
* Documents are sent via the actual Peppol network
* Recipients must be registered on Peppol
* Real business transactions only
**Sandbox company (development & testing):**
* Documents are sent via email as UBL XML to the `company_email` address
* No actual Peppol transmission occurs
* `company_email` field is REQUIRED for email delivery
* A synthetic Belgian VAT number is auto-assigned; no KBO/VIES or contact verification
* No credits/billing; free to use
* Everything else (validation, webhooks, API endpoints) works identically
**Important**: Always develop and test with a sandbox company. To go live, create a separate regular company. See `environments.mdx` for complete details.
### Core Workflow
The e-invoice.be API follows this workflow:
1. **Validate JSON** (`POST /api/validate/json`) - Test invoice JSON before creating documents
* CRITICAL: Validation must happen BEFORE document creation
* Only valid JSON that can convert to UBL BIS Billing 3.0 is accepted
* Does not create any documents - safe for testing
2. **Create Document** (`POST /api/documents/`) - Create invoice/credit note
* Requires valid JSON (pre-validated)
* Creates document in DRAFT state
* Returns document ID
3. **Send via Peppol** (`POST /api/documents/{id}/send`) - Transmit to recipient
* Changes state from DRAFT to TRANSIT
* Delivers via Peppol network
* Webhook notifications on success/failure
### Key Endpoint Categories
**Documents**:
* `POST /api/documents/` - Create invoice/credit note
* `GET /api/documents/{id}` - Get document details
* `POST /api/documents/{id}/send` - Send via Peppol
* `GET /api/inbox/` - List received documents
* `GET /api/outbox/` - List sent documents
**Validation** (Critical for development):
* `POST /api/validate/json` - Validate JSON structure (use extensively!)
* `POST /api/validate/ubl` - Validate UBL XML
* `GET /api/validate/peppol-id` - Check if participant is registered
**Participant Lookup**:
* `GET /api/lookup` - Get participant details
* `GET /api/lookup/participants` - Search by name/identifier
**Webhooks**:
* `POST /api/webhooks/` - Create webhook
* `GET /api/webhooks/` - List webhooks
* `PUT /api/webhooks/{id}` - Update webhook
* `DELETE /api/webhooks/{id}` - Delete webhook
* `POST /api/webhooks/{id}/test` - Test webhook
**Account**:
* `GET /api/me/` - Get account information
## Important Concepts
### Peppol IDs vs Company Identifiers
* **Company identifiers in API**: Documents can contain both tax IDs and company IDs
* `vendor_tax_id` / `customer_tax_id`: VAT/tax number (e.g., `BE1018265814`)
* `vendor_id` / `customer_id`: Company registration number (e.g., CBE number)
* **Peppol ID format**: `scheme:identifier` (e.g., `0208:0123456789`)
* Belgian companies use scheme `0208` with CBE number (VAT number without 'BE' prefix)
* Example: VAT number BE0123456789 → Peppol ID `0208:0123456789`
* **IMPORTANT**: The API automatically derives Peppol IDs from company identifiers when sending documents via `/api/documents/{id}/send`
* This happens regardless of any endpoint IDs in UBL documents
* For Belgian companies, `0208` scheme is mandatory (Belgian government requirement)
* **BEST PRACTICE**: Always explicitly specify Peppol IDs using query parameters: `sender_peppol_scheme`, `sender_peppol_id`, `receiver_peppol_scheme`, `receiver_peppol_id`
* Automatic derivation works in most cases but explicit routing prevents delivery failures
* Must be registered on Peppol network to receive e-invoices
* Always validate with `/api/validate/peppol-id` before sending
### Document States
* `DRAFT` - Created but not sent
* `TRANSIT` - Being transmitted via Peppol
* `SENT` - Successfully delivered
* `FAILED` - Transmission failed
* `RECEIVED` - Received from another party
### UBL BIS Billing 3.0
* European standard for e-invoicing (EN 16931)
* The API converts JSON to UBL XML automatically
* All invoices must comply with this standard
* Use `/api/validate/json` to ensure compliance
## Documentation Style Guidelines
When creating or updating documentation:
* Use professional, concise language - avoid casual phrases
* Include MDX front matter with `title` and `description` on all pages
* Use Mintlify components (``, ``, ``, ``) for emphasis
* Add "Next Steps" section with `` navigation cards at the end of guides
* Use real company examples: E-INVOICE BV (BE1018265814) as vendor, OpenPeppol VZW (BE0848934496) as customer
* Include `amount` field in line item examples (calculated as quantity × unit\_price)
* Keep examples consistent across all documentation
## Important Notes
* This is a documentation-only repository - no backend code
* The actual e-invoice.be API and webhook service are separate systems
* OpenAPI spec is maintained externally and referenced by URL at `https://api.e-invoice.be/api/openapi.json`
* **Always update `docs.json` navigation when adding pages** - Mintlify won't auto-discover new pages
* Validation is REQUIRED before document creation - the API rejects invalid invoices
# Admin API
Source: https://docs.e-invoice.be/admin-api
Organization and tenant management API for resellers
The Admin API is only available to resellers with an **organization API key**. This is a privileged API for managing multiple customer tenants. If you're looking for standard invoice operations, see the [regular API documentation](/guides/creating-invoices).
## Overview
The Admin API enables resellers to programmatically manage customer organizations (tenants), provision API credentials, and handle Peppol network registration on behalf of customers.
This is separate from the standard e-invoice.be API and uses different authentication.
### Key Capabilities
* **Tenant Management** - Create and manage customer organizations
* **API Key Provisioning** - Generate and manage API keys for customers
* **Peppol Registration** - Register customers on the Peppol network
* **Credential Rotation** - Update and revoke API keys without customer involvement
## Authentication
The Admin API requires an **organization API key** (also called admin API key), which is different from standard tenant API keys.
```bash theme={null}
curl -X GET "https://api.e-invoice.be/api/admin/tenants" \
-H "Authorization: Bearer YOUR_ORGANIZATION_API_KEY"
```
Organization API keys are only provided to approved resellers. Contact [support@e-invoice.be](mailto:support@e-invoice.be) to discuss the reseller programme.
## Tenant Management
### List All Tenants
Retrieve all customer organizations under your reseller account:
```bash theme={null}
curl -X GET "https://api.e-invoice.be/api/admin/tenants?skip=0&limit=100" \
-H "Authorization: Bearer YOUR_ORGANIZATION_API_KEY"
```
**Query Parameters:**
* `skip` - Number of records to skip (default: 0)
* `limit` - Maximum records to return (default: 100)
**Response:**
```json theme={null}
{
"tenants": [
{
"id": "ten-9k2m4p7q3w5x8r",
"name": "customer-company-bvba",
"description": "Customer Company BVBA",
"created_at": "2026-04-16T10:00:00.000000Z",
"updated_at": "2026-04-16T10:00:00.000000Z",
"is_deleted": false
}
],
"total": 1
}
```
### Create a Tenant
Create a new customer organization:
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/admin/tenants" \
-H "Authorization: Bearer YOUR_ORGANIZATION_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "customer-company-bvba",
"description": "Customer Company BVBA - Accounting Software Integration",
"company_number": "0123456789",
"company_tax_id": "BE0123456789",
"peppol_ids": ["0208:0123456789"]
}'
```
**Request Body:**
* `name` (required) - Unique identifier for the tenant (lowercase, no spaces)
* `description` (optional) - Human-readable description
* `company_number` (optional) - Company registration number (e.g., CBE/KBO for Belgium, KvK for Netherlands, RCS for Luxembourg)
* `company_tax_id` (optional) - Tax identification number (e.g., VAT number like BE0123456789)
* `peppol_ids` (recommended) - Array containing the tenant's Peppol ID
**Response (201 Created):**
```json theme={null}
{
"id": "ten-9k2m4p7q3w5x8r",
"name": "customer-company-bvba",
"description": "Customer Company BVBA - Accounting Software Integration",
"company_number": "0123456789",
"company_tax_id": "BE0123456789",
"peppol_ids": ["0208:0123456789"],
"created_at": "2026-04-16T10:00:00.000000Z",
"updated_at": "2026-04-16T10:00:00.000000Z",
"is_deleted": false
}
```
**You are responsible for determining and setting the correct Peppol ID for each tenant.** Although `peppol_ids` is an array, the platform currently only supports a single Peppol ID per tenant. Setting the Peppol ID at tenant creation is important for proper Peppol registration later.
#### Company Number vs Tax ID
The tenant schema now separates company registration identifiers:
* **`company_number`** - The official company registration number from the national business register:
* Belgium: CBE/KBO number (e.g., `0123456789`)
* Netherlands: KvK number (Chamber of Commerce)
* Luxembourg: RCS number (Registre de Commerce et des Sociétés)
* Other countries: Equivalent business registration number
* **`company_tax_id`** - The actual VAT or tax identification number:
* Format includes country prefix (e.g., `BE0123456789`, `NL123456789B01`)
* Used for tax purposes and invoicing
* May be different from the company number in some jurisdictions
For Belgian companies, the CBE number (without "BE" prefix) is typically used in the Peppol ID as `0208:`, while the full VAT number (with "BE" prefix) goes in `company_tax_id`.
#### Peppol ID Format
The Peppol ID follows the format `scheme:identifier`:
**Belgium (most common):**
```
Format: 0208:
Example: 0208:0123456789
```
For Belgian companies, use scheme `0208` with the CBE/KBO number (enterprise number).
Example relationship:
* `company_number`: `0123456789` (CBE number)
* `company_tax_id`: `BE0123456789` (VAT number)
* `peppol_ids`: `["0208:0123456789"]` (scheme 0208 + CBE number)
**Other schemes:**
* `9999` - DUNS number (international)
* `0088` - Global Location Number (GLN)
* `0184` - Dutch KVK number
* `9956` - Belgian company number (alternative)
#### Why Set Peppol ID During Creation?
The Peppol ID is used when:
1. **Registering on the SMP** (Service Metadata Publisher) - Maps the Peppol ID to e-invoice.be's access point
2. **Writing to the Peppol Directory** - Creates a searchable entry for participant lookup
3. **Routing documents** - Ensures incoming invoices reach the correct tenant
Every tenant must have exactly one associated Peppol ID. Setting it during tenant creation ensures the tenant is properly configured before Peppol registration.
Use a consistent naming convention for tenant names, such as `customer-slug` or `company-id`. This makes it easier to manage multiple customers.
### Get a Tenant
Retrieve details for a specific tenant:
```bash theme={null}
curl -X GET "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r" \
-H "Authorization: Bearer YOUR_ORGANIZATION_API_KEY"
```
### Update a Tenant
Update tenant information:
```bash theme={null}
curl -X PUT "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r" \
-H "Authorization: Bearer YOUR_ORGANIZATION_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "customer-company-bvba",
"description": "Updated description",
"company_number": "0123456789",
"company_tax_id": "BE0123456789",
"peppol_ids": ["0208:0123456789"]
}'
```
You can update the `company_number`, `company_tax_id`, and `peppol_ids` fields after tenant creation if needed.
### Delete a Tenant
Soft-delete a tenant (marks as deleted, doesn't remove data):
```bash theme={null}
curl -X DELETE "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r" \
-H "Authorization: Bearer YOUR_ORGANIZATION_API_KEY"
```
## API Key Management
Provision and manage API keys for customer tenants.
### Create an API Key
Generate a new API key for a tenant:
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r/api-keys" \
-H "Authorization: Bearer YOUR_ORGANIZATION_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "production-key",
"description": "Production API key for Customer Company"
}'
```
**Request Body:**
* `name` (required) - Identifier for the API key
* `description` (optional) - Purpose or environment
**Response:**
```json theme={null}
{
"id": "api-3h8f5j2k9l4m7n6p1q5r8s2t4v6w9x3y",
"tenant_id": "ten-9k2m4p7q3w5x8r",
"name": "production-key",
"description": "Production API key for Customer Company",
"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 — the value returned in `id` (e.g. `api-3h8f5j2k9l4m7n6p1q5r8s2t4v6w9x3y`) is the bearer token your customer must include in the `Authorization` header of every API request:
```
Authorization: Bearer api-3h8f5j2k9l4m7n6p1q5r8s2t4v6w9x3y
```
Treat the `id` as a secret. Capture it on creation, store it securely (e.g. encrypted at rest), and deliver it to your customer over a secure channel. Never log it, commit it to version control, or expose it in client-side code.
### List API Keys
Get all API keys for a tenant:
```bash theme={null}
curl -X GET "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r/api-keys?limit=100" \
-H "Authorization: Bearer YOUR_ORGANIZATION_API_KEY"
```
**Response:**
```json theme={null}
{
"api_keys": [
{
"id": "api-3h8f5j2k9l4m7n6p1q5r8s2t4v6w9x3y",
"tenant_id": "ten-9k2m4p7q3w5x8r",
"name": "production-key",
"description": "Production API key",
"created_at": "2026-04-16T10:00:00.000000Z",
"updated_at": "2026-04-16T10:00:00.000000Z",
"is_deleted": false
}
],
"total": 1
}
```
Because the `id` is itself the bearer token, listing API keys returns the live credentials for this tenant. Treat the response as sensitive credential material: restrict who can call this endpoint, avoid logging the response body, and never return it to end users.
### Get Latest API Key
Retrieve the most recently created API key for a tenant:
```bash theme={null}
curl -X GET "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r/api-keys/latest" \
-H "Authorization: Bearer YOUR_ORGANIZATION_API_KEY"
```
### Update an API Key
Update API key metadata (name/description):
```bash theme={null}
curl -X PUT "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r/api-keys/api-3h8f5j2k9l4m7n6p1q5r8s2t4v6w9x3y" \
-H "Authorization: Bearer YOUR_ORGANIZATION_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "production-key-v2",
"description": "Updated production key"
}'
```
### Revoke an API Key
Delete (revoke) an API key:
```bash theme={null}
curl -X DELETE "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r/api-keys/api-3h8f5j2k9l4m7n6p1q5r8s2t4v6w9x3y" \
-H "Authorization: Bearer YOUR_ORGANIZATION_API_KEY"
```
For credential rotation, create a new API key, update your customer's configuration, then revoke the old key.
## Peppol Registration
Manage Peppol network registration for customer tenants.
### Check Registration Status
Check if a tenant is registered on Peppol:
```bash theme={null}
curl -X GET "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r/peppol/" \
-H "Authorization: Bearer YOUR_ORGANIZATION_API_KEY"
```
**Response:**
```json theme={null}
{
"peppol_id": "0208:0123456789",
"state": "e-invoice",
"smp": {
"service_group": {
"participant_identifier": "0123456789",
"participant_scheme": "0208",
"service_metadata_references": []
},
"business_card": {
"participant_identifier": "0123456789",
"participant_scheme": "0208",
"business_entity": { "name": "Your Company BV", "country_code": "BE" }
},
"document_types": [
{
"document_type_code": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice",
"process_identifier": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0"
}
]
},
"last_checked_at": "2026-04-16T10:00:00.000000Z"
}
```
`state` reflects the SMP registration status: `not_registered`, `e-invoice` (registered on our SMP), or `other`. `smp` is a structured object describing the SMP service group, business card, and supported document types (`null` when not registered).
### Register on Peppol
Register a tenant on the Peppol network:
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r/peppol/register" \
-H "Authorization: Bearer YOUR_ORGANIZATION_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"peppol_id": "0208:0123456789",
"company_name": "Customer Company BVBA"
}'
```
**For Belgian companies**, the system can automatically fetch company data from KBO (Crossroads Bank for Enterprises):
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r/peppol/register" \
-H "Authorization: Bearer YOUR_ORGANIZATION_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"peppol_id": "0208:0123456789"
}'
```
The system will:
1. Fetch company details from KBO using the CBE number
2. Register with the SMP (Service Metadata Publisher)
3. Create a business card on the Peppol network
Peppol operations are not available for organizations in test mode. Calling the Peppol endpoints for a test-mode organization returns `403 Forbidden`. Ensure your organization is in production mode before attempting registration.
### Update Business Card
Update the business card information on Peppol:
```bash theme={null}
curl -X PUT "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r/peppol/business-card" \
-H "Authorization: Bearer YOUR_ORGANIZATION_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"company_name": "Updated Company Name BVBA"
}'
```
Currently, only company name updates are supported for business cards.
### Unregister from Peppol
Remove a tenant from the Peppol network:
```bash theme={null}
curl -X DELETE "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r/peppol/unregister" \
-H "Authorization: Bearer YOUR_ORGANIZATION_API_KEY"
```
This will:
* Remove SMP registration
* Delete the business card
* Mark the tenant as unregistered
## Testing
### Simulate an Inbound Document
Inject a UBL document into a tenant's inbox as if it had been received over Peppol. This is the supported way to test a customer's receive-side integration: the document is created in the `RECEIVED` state, appears in `GET /api/inbox/`, and any `document.received` webhooks configured on the tenant fire normally.
Simulation is only available for **test-mode tenants**. Calling this on a tenant that is not in test mode returns `400 Bad Request`.
The request is `multipart/form-data` with the UBL XML supplied as a `ubl_file` field (**not** a raw XML request body). The receiver identifiers in the UBL should match the tenant you are simulating for.
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/admin/tenants/ten-9k2m4p7q3w5x8r/simulate-inbound" \
-H "Authorization: Bearer YOUR_ORGANIZATION_API_KEY" \
-F "ubl_file=@invoice.xml"
```
**Form fields:**
* `ubl_file` - The UBL Invoice or Credit Note XML file (required, max 25 MB)
**Response** (`201 Created`):
```json theme={null}
{
"document_id": "doc-7h3k9m2p4q6r8t",
"state": "RECEIVED"
}
```
The sender and receiver Peppol identifiers, document type (invoice vs. credit note), and line-item details are extracted from the uploaded UBL. Use the returned `document_id` with the standard document endpoints (for example `GET /api/documents/{document_id}`) to inspect the result.
This endpoint powers the **Simulate inbound** button shown in a test workspace's inbox in the e-invoice.be app. See [Testing Received Documents](/environments#testing-received-documents) for the app-based workflow.
## Complete Workflow Example
Here's a complete Node.js example showing the typical reseller workflow:
```javascript theme={null}
const axios = require('axios');
const adminApi = axios.create({
baseURL: 'https://api.e-invoice.be',
headers: {
'Authorization': `Bearer ${process.env.ORGANIZATION_API_KEY}`,
'Content-Type': 'application/json'
}
});
async function onboardNewCustomer(customerData) {
try {
// 1. Create tenant with company details and Peppol ID
console.log('Creating tenant...');
const tenant = await adminApi.post('/api/admin/tenants', {
name: customerData.slug,
description: customerData.companyName,
company_number: customerData.companyNumber,
company_tax_id: customerData.vatNumber,
peppol_ids: [customerData.peppolId]
});
console.log('✓ Tenant created:', tenant.data.id);
console.log('✓ Company number:', customerData.companyNumber);
console.log('✓ Tax ID:', customerData.vatNumber);
console.log('✓ Peppol ID set:', customerData.peppolId);
// 2. Create API key for customer
console.log('Generating API key...');
const apiKey = await adminApi.post(
`/api/admin/tenants/${tenant.data.id}/api-keys`,
{
name: 'production-key',
description: 'Production API key'
}
);
// IMPORTANT: The id IS the bearer token — capture it now and store it securely.
// There is no separate `key` field, and you cannot retrieve the value again later.
const customerApiKey = apiKey.data.id;
console.log('✓ API key created (id IS the bearer token):', customerApiKey);
// 3. Register on Peppol (for Belgian companies)
console.log('Registering on Peppol...');
const registration = await adminApi.post(
`/api/admin/tenants/${tenant.data.id}/peppol/register`,
{
peppol_id: customerData.peppolId
}
);
console.log('✓ Peppol registration complete');
// 4. Return customer credentials
return {
tenantId: tenant.data.id,
apiKey: customerApiKey,
peppolId: customerData.peppolId,
status: 'active'
};
} catch (error) {
console.error('Onboarding failed:', error.response?.data || error.message);
throw error;
}
}
// Usage
const customerData = {
slug: 'customer-company-bvba',
companyName: 'Customer Company BVBA',
companyNumber: '0123456789', // CBE/KBO number
vatNumber: 'BE0123456789', // Full VAT number
peppolId: '0208:0123456789' // Peppol ID (scheme 0208 + CBE number)
};
onboardNewCustomer(customerData)
.then(credentials => {
console.log('Customer onboarded successfully:');
console.log('- Tenant ID:', credentials.tenantId);
console.log('- API Key:', credentials.apiKey);
console.log('- Peppol ID:', credentials.peppolId);
});
```
## Key Rotation Example
Rotate API keys without customer downtime:
```javascript theme={null}
async function rotateApiKey(tenantId, oldKeyId) {
try {
// 1. Create new API key
console.log('Creating new API key...');
const newKey = await adminApi.post(
`/api/admin/tenants/${tenantId}/api-keys`,
{
name: 'production-key-v2',
description: 'Rotated production key'
}
);
// The id IS the bearer token — capture it now; it cannot be retrieved later.
const newApiKey = newKey.data.id;
console.log('✓ New key created:', newApiKey);
// 2. Provide new key to customer
// (Send via secure channel, update their configuration)
// 3. Wait for customer to switch over
console.log('Waiting for customer to update configuration...');
await new Promise(resolve => setTimeout(resolve, 3600000)); // 1 hour
// 4. Revoke old key
console.log('Revoking old API key...');
await adminApi.delete(
`/api/admin/tenants/${tenantId}/api-keys/${oldKeyId}`
);
console.log('✓ Old key revoked');
return newKey.data;
} catch (error) {
console.error('Key rotation failed:', error.response?.data || error.message);
throw error;
}
}
```
## Error Handling
Common error responses:
### 401 Unauthorized
```json theme={null}
{
"detail": "Invalid authentication credentials"
}
```
**Solution**: Verify your organization API key is correct and active.
### 404 Not Found
```json theme={null}
{
"detail": "Tenant not found"
}
```
**Solution**: Check the tenant ID is correct and the tenant exists.
### 409 Conflict
```json theme={null}
{
"detail": "A tenant with similar name already exists"
}
```
**Solution**: Choose a different tenant name.
### 422 Validation Error
```json theme={null}
{
"detail": [
{
"loc": ["body", "name"],
"msg": "field required",
"type": "value_error.missing"
}
]
}
```
**Solution**: Ensure all required fields are provided.
## Best Practices
* Remember: the `id` returned from the create-key endpoint IS the bearer token. Treat it as a secret from the moment you receive it.
* Never log or display organization API keys
* Store customer API keys securely (encrypted database)
* Provide keys to customers via secure channels only
* Implement key rotation policies (e.g., every 90 days)
```javascript theme={null}
// ✓ Good: Secure handling — the id IS the bearer token
const encryptedKey = encrypt(apiKey.data.id);
await database.storeCustomerKey(customerId, encryptedKey);
// ✗ Bad: Insecure handling
console.log('API Key:', apiKey.data.id);
localStorage.setItem('key', apiKey.data.id);
```
Use consistent, predictable tenant names:
```javascript theme={null}
// ✓ Good patterns
const tenantName = 'customer-company-bvba';
const tenantName = `cust-${customerId}`;
const tenantName = companyName.toLowerCase().replace(/\s+/g, '-');
// ✗ Bad patterns
const tenantName = 'Customer 1';
const tenantName = Math.random().toString();
```
Implement idempotent operations and error recovery:
```javascript theme={null}
async function createTenantSafely(name, description) {
try {
return await adminApi.post('/api/admin/tenants', { name, description });
} catch (error) {
if (error.response?.status === 409) {
// Tenant already exists, fetch it instead
console.log('Tenant exists, fetching...');
const tenants = await adminApi.get('/api/admin/tenants');
return tenants.data.tenants.find(t => t.name === name);
}
throw error;
}
}
```
Log all admin operations for compliance:
```javascript theme={null}
async function auditedTenantCreate(tenantData) {
const result = await adminApi.post('/api/admin/tenants', tenantData);
await auditLog.create({
action: 'TENANT_CREATE',
tenant_id: result.data.id,
admin_user: currentUser.id,
timestamp: new Date(),
details: { name: tenantData.name }
});
return result.data;
}
```
Always verify registration prerequisites:
```javascript theme={null}
async function registerOnPeppol(tenantId, peppolId) {
// 1. Verify Peppol ID format
if (!/^\d{4}:\d+$/.test(peppolId)) {
throw new Error('Invalid Peppol ID format');
}
// 2. Register. Peppol operations are gated at the organization
// level: a test-mode organization gets 403 Forbidden here.
return await adminApi.post(
`/api/admin/tenants/${tenantId}/peppol/register`,
{ peppol_id: peppolId }
);
}
```
## Environment
The Admin API is available on the single API host:
| Base URL |
| -------------------------- |
| `https://api.e-invoice.be` |
To test your integration before going live, create tenants in **test mode** (the API's test Organization) and use their API keys. Test-mode tenants divert sends to email, support [Simulate an Inbound Document](#simulate-an-inbound-document), and never touch the Peppol network. See [Environments](/environments) for details.
## Rate Limiting
Rate limits are applied **per API key** on write and validation endpoints (see [API reference — Rate Limiting](/api-reference#rate-limiting)). When a limit is exceeded, the API returns `429 Too Many Requests` with a `Retry-After` header. Honor `Retry-After` and use exponential backoff when driving bulk tenant operations.
## Support
For Admin API access or technical questions:
* Email: [support@e-invoice.be](mailto:support@e-invoice.be)
* Reseller programme: [Learn more](/reseller-programme)
## Related Documentation
* [Reseller Programme](/reseller-programme)
* [Authentication](/authentication)
* [Creating Invoices](/guides/creating-invoices)
* [API Reference](/api-reference)
# API Reference
Source: https://docs.e-invoice.be/api-reference
Complete reference for the e-invoice.be API
## Overview
The e-invoice.be API enables you to create, send, and manage Peppol-compliant e-invoices and credit notes. All invoices are automatically converted to UBL BIS Billing 3.0 format and transmitted via the Peppol network.
## Base URL
e-invoice.be runs on a single API host:
```
https://api.e-invoice.be
```
**Testing without Peppol**: There is no separate staging or development host. To test without touching the Peppol network, use a **sandbox company** (a workspace in test mode). In test mode, documents are delivered as UBL XML attachments to the `company_email` address instead of being transmitted over Peppol, and inbound documents are simulated — perfect for testing without affecting real recipients.
Learn about test mode and sandbox companies
## Authentication
All API requests require authentication using an API key in the `Authorization` header:
```bash theme={null}
curl -X GET "https://api.e-invoice.be/api/documents/" \
-H "Authorization: Bearer YOUR_API_KEY"
```
See the [Authentication guide](/authentication) for details on obtaining and using API keys.
## API Structure
The e-invoice.be API is organized into several main sections:
### Documents
Create, manage, and send invoices and credit notes via Peppol.
**Key Endpoints:**
* `POST /api/documents/` - Create a new document
* `GET /api/documents/{id}` - Get document details
* `POST /api/documents/{id}/send` - Send document via Peppol
* `DELETE /api/documents/{id}` - Delete a document
To list documents, use `GET /api/outbox/` (sent documents) and `GET /api/inbox/` (received documents).
**Schemas:**
* [Document](/api-reference/schemas/document) - Invoice and credit note structure
* [LineItem](/api-reference/schemas/line-item) - Line item structure
**Related Guides:**
* [Creating Invoices](/guides/creating-invoices)
* [Credit Notes](/guides/credit-notes)
* [Advanced Invoicing](/guides/advanced-invoicing)
### Validation
Validate invoice JSON or UBL XML before creating documents.
**Key Endpoints:**
* `POST /api/validate/json` - Validate JSON invoice data
* `POST /api/validate/ubl` - Validate UBL XML
* `GET /api/validate/peppol-id` - Verify Peppol participant ID
**Related Guides:**
* [Validation Guide](/guides/validation)
* [Lookup Participants](/guides/lookup-participants)
### Inbox
Receive and manage incoming invoices from other Peppol participants.
**Key Endpoints:**
* `GET /api/inbox/` - List received documents
* `GET /api/inbox/invoices` - List received invoices
* `GET /api/inbox/credit-notes` - List received credit notes
Retrieve the full details of any received document with `GET /api/documents/{id}`.
### Lookup
Search for Peppol participants and verify their registration status.
**Key Endpoints:**
* `GET /api/lookup/participants` - Search for participants
* `GET /api/lookup` - Get participant details
**Related Guides:**
* [Lookup Participants](/guides/lookup-participants)
### Alternative Formats
Create documents from UBL XML or PDF files.
**Key Endpoints:**
* `POST /api/documents/ubl` - Create from UBL XML
* `POST /api/documents/pdf` - Create from PDF (with metadata)
**Related Guides:**
* [UBL Documents](/guides/ubl-documents)
* [PDF Documents](/guides/pdf-documents)
### Webhooks
Configure webhook endpoints to receive real-time notifications about document events.
**Key Endpoints:**
* `GET /api/webhooks/` - List webhook subscriptions
* `POST /api/webhooks/` - Create webhook subscription
* `DELETE /api/webhooks/{id}` - Delete webhook subscription
**Related Guides:**
* [Webhooks](/essentials/webhooks)
## Admin API (Resellers Only)
For resellers and service providers managing multiple customer organizations, we provide a separate **Admin API** with organization-level capabilities:
* **Tenant Management** - Create and manage customer organizations
* **API Key Provisioning** - Generate API keys for customers
* **Peppol Registration** - Register customers on the Peppol network
* **Credential Rotation** - Manage API keys without customer involvement
The Admin API requires an **organization API key** and is only available to approved reseller partners.
Complete guide to the Admin API for managing customer tenants (resellers only)
Learn more about our [Reseller Programme](/reseller-programme).
## Request & Response Format
### Request Format
All requests use JSON format:
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/documents/" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"document_type": "INVOICE",
"invoice_id": "INV-2024-001",
"invoice_date": "2024-10-24",
"currency": "EUR",
"vendor_name": "Your Company BVBA",
"vendor_tax_id": "BE0123456789",
"customer_name": "Customer Company NV",
"customer_tax_id": "BE0987654321",
"items": [
{
"description": "Professional Services",
"quantity": 10,
"unit": "C62",
"unit_price": 100.00,
"tax_rate": "21.00"
}
]
}'
```
### Response Format
Successful responses return JSON with a 2xx status code:
```json theme={null}
{
"id": "doc_abc123",
"document_type": "INVOICE",
"state": "DRAFT",
"invoice_id": "INV-2024-001",
"created_at": "2024-10-24T10:00:00Z",
"updated_at": "2024-10-24T10:00:00Z"
}
```
### Error Responses
Errors return JSON with an appropriate HTTP status code:
```json theme={null}
{
"detail": "Invalid authentication credentials"
}
```
Common status codes:
* `400 Bad Request` - Invalid request data
* `401 Unauthorized` - Missing or invalid API key
* `404 Not Found` - Resource not found
* `422 Unprocessable Entity` - Validation error
* `429 Too Many Requests` - Rate limit exceeded
* `500 Internal Server Error` - Server error
## Pagination
Document list endpoints (inbox and outbox) support pagination with `page` and `page_size` parameters:
```bash theme={null}
GET /api/outbox/?page=1&page_size=20
```
**Parameters:**
* `page` - Page number (default: 1)
* `page_size` - Number of items per page (default: 20, max: 100)
**Response:**
```json theme={null}
{
"items": [...],
"total": 250,
"page": 1,
"page_size": 20,
"pages": 13,
"has_next_page": true
}
```
The Admin API list endpoints (`/api/admin/tenants` and its API-key routes) use `skip` and `limit` parameters instead. See the [Admin API documentation](/admin-api).
## Rate Limiting
Write and validation endpoints are rate-limited **per API key** to ensure service quality. The limit applies to:
* `POST /api/validate/json` and `POST /api/validate/ubl`
* `POST /api/documents/` and `POST /api/documents/ubl`
These endpoints allow up to **60 requests per 60 seconds** per API key. PDF conversion is subject to a tighter limit (a few requests per minute) because it is more resource-intensive. Read endpoints (such as `GET /api/documents/{id}`, inbox, and outbox) are not rate-limited.
Exceeding a limit returns `429 Too Many Requests` with a `Retry-After` header indicating the number of seconds to wait before retrying. Clients should honor `Retry-After` and use exponential backoff.
## Document States
Documents progress through different states:
| State | Description |
| ---------- | ---------------------------- |
| `DRAFT` | Created but not sent |
| `TRANSIT` | Being transmitted via Peppol |
| `SENT` | Successfully delivered |
| `FAILED` | Transmission failed |
| `RECEIVED` | Received from another party |
### Outbound Document Flow
Documents you create and send follow this state progression:
**Automatic Retry Strategy**: Documents in `TRANSIT` state use an exponential backoff strategy with 10 retry attempts before transitioning to `FAILED`. Retry delays are: 1, 2, 4, 8, 16, 32, 64, 128, 256, and 360 minutes (final retry capped at 6 hours). This ensures maximum delivery success even during temporary network issues or recipient downtime.
**Manual Retry**: Documents in `FAILED` state can be retried by calling `POST /api/documents/{id}/send` again, which will transition them back to `TRANSIT` for another delivery attempt.
**Coming soon**: Detailed transmission attempt history will be available in a future release, allowing you to view all retry attempts, timestamps, and failure reasons for each transmission.
### Inbound Document Flow
Documents received from other Peppol participants:
Track state changes via [webhooks](/essentials/webhooks) or by polling the document endpoint.
## Supported Currencies
The API supports the following ISO 4217 currency codes:
`EUR`, `USD`, `GBP`, `JPY`, `CHF`, `CAD`, `AUD`, `NZD`, `CNY`, `INR`, `SEK`, `NOK`, `DKK`, `SGD`, `HKD`
Default: `EUR`
## Peppol Participant IDs
Peppol IDs use the format `scheme:identifier`:
**Common schemes:**
* Belgium: `0208:0123456789` (CBE number)
* Netherlands: `0106:12345678` (KVK number)
* Germany: `0204:DE123456789` (VAT number)
* France: `0009:12345678901234` (SIRET)
See the complete [Peppol ID schemes list](https://docs.peppol.eu/edelivery/codelists/v9.4/Peppol%20Code%20Lists%20-%20Participant%20identifier%20schemes%20v9.4.html).
## Getting Started
Get started in 5 minutes
Set up API authentication
Learn how to create e-invoices
Validate before sending
## OpenAPI Specification
The complete OpenAPI specification is available at:
```
https://api.e-invoice.be/api/openapi.json
```
You can use this specification to:
* Generate client libraries in your programming language
* Import into API testing tools (Postman, Insomnia, etc.)
* Validate request/response structures
**Prefer using the API from an AI assistant?** Connect Claude, ChatGPT, Cursor, or VS Code directly to your e-invoice.be account via MCP. See [MCP setup →](/essentials/mcp)
## Need Help?
Contact our support team
View our open-source projects
# 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 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
Invoice and credit note document structure
## Overview
The `Document` schema defines the structure for creating invoices, credit notes, and debit notes via the e-invoice.be API. This is the primary schema used when calling `POST /api/documents/`.
Only the `items` array is strictly required. All other fields are optional but recommended for complete, Peppol-compliant e-invoices.
## Document Metadata
Type of document to create
* `INVOICE` - Standard invoice
* `CREDIT_NOTE` - Credit note (refund/adjustment)
* `DEBIT_NOTE` - Debit note
Document state
* `DRAFT` - Created but not sent
* `TRANSIT` - Being transmitted
* `SENT` - Successfully delivered
* `FAILED` - Transmission failed
* `RECEIVED` - Received from another party
**State Flow Diagrams:**
Outbound documents (direction: `OUTBOUND`):
Documents in `TRANSIT` state use an exponential backoff retry strategy with 10 retry attempts before transitioning to `FAILED`. Retry delays are: 1, 2, 4, 8, 16, 32, 64, 128, 256, and 360 minutes (final retry capped at 6 hours).
Documents in `FAILED` state can be retried by calling `POST /api/documents/{id}/send` again, which will transition them back to `TRANSIT` for another delivery attempt.
**Coming soon**: Detailed transmission attempt history will be available in a future release, allowing you to view all retry attempts, timestamps, and failure reasons for each transmission.
Inbound documents (direction: `INBOUND`):
Document direction
* `OUTBOUND` - Sending to customer
* `INBOUND` - Received from supplier
## Vendor (Supplier) Information
Your company name
Example: `"Your Company BVBA"`
Your VAT number, including the country prefix. For Belgium this is the VAT number.
Example: `"BE1018265814"`
The API automatically derives the Peppol participant ID from this value when transmitting. To set the recipient's Peppol ID explicitly, use `customer_peppol_id` (format `scheme:identifier`, e.g. `"0208:0123456789"`).
Your company address (full address as single string)
Example: `"Main Street 123, 1000 Brussels, Belgium"`
Department or person at vendor address
Example: `"Accounts Department"`
Your contact email address
Example: `"billing@yourcompany.com"`
## Customer (Buyer) Information
Customer company name
Example: `"Customer Company NV"`
Customer VAT number, including the country prefix. For Belgium this is the VAT number.
Example: `"BE0848934496"`
Customer Peppol participant ID in format `scheme:identifier`
Example: `"0208:0848934496"`
Internal customer reference/ID
Example: `"CUST-12345"`
Customer address (full address as single string)
Example: `"Customer Lane 456, 2000 Antwerp, Belgium"`
Department or person at customer address
Example: `"Accounts Payable"`
Customer contact email
Example: `"ap@customer.com"`
## Invoice Details
Unique invoice number
Example: `"INV-2024-001"`
Invoice issue date in ISO 8601 format (`YYYY-MM-DD`)
Example: `"2024-10-24"`
Payment due date in ISO 8601 format (`YYYY-MM-DD`)
Example: `"2024-11-24"`
Customer purchase order reference. For credit notes, use this to reference the original invoice.
Example: `"PO-12345"` or `"INV-2024-001"` (for credit notes)
Free-text note or description
Example: `"Thank you for your business"` or `"Full refund - goods returned"`
Payment terms description
Example: `"Payment due within 30 days"` or `"Net 30"`
## Financial Fields
Currency code (ISO 4217)
Supported: `EUR`, `USD`, `GBP`, `JPY`, `CHF`, `CAD`, `AUD`, `NZD`, `CNY`, `INR`, `SEK`, `NOK`, `DKK`, `SGD`, `HKD`
Example: `"EUR"`
Taxable base amount (after document-level allowances and charges, before tax)
Corresponds to UBL `cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount`
Example: `1000.00`
Total document-level allowances (discounts only, not charges)
Corresponds to UBL `cac:LegalMonetaryTotal/cbc:AllowanceTotalAmount`
Example: `50.00`
Total VAT/tax amount
Corresponds to UBL `cac:TaxTotal/cbc:TaxAmount`
Example: `210.00`
Total invoice amount including 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 (if any). Hidden/internal field: it is excluded from the transmitted document and does not appear in 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
VAT exemption reason code (when tax\_code is E, AE, K, G, O, L, M, or B)
Example: `"VATEX-EU-132"` for intra-community supply
VAT exemption explanation
Example: `"Reverse charge applies - Art. 196 EU VAT Directive"`
## Service Period
Service period start date (ISO 8601: `YYYY-MM-DD`)
Example: `"2024-10-01"`
Service period end date (ISO 8601: `YYYY-MM-DD`)
Example: `"2024-10-31"`
## Additional Addresses
Billing address (if different from customer address)
Example: `"Billing Street 1, 1000 Brussels, Belgium"`
Recipient at billing address
Example: `"Accounts Payable Department"`
Delivery/shipping address
Example: `"Warehouse 5, Industrial Park, 3000 Leuven, Belgium"`
Recipient at shipping address
Example: `"Warehouse Manager"`
Service location address
Example: `"Service Location 3, 3000 Leuven, Belgium"`
Recipient at service address
Remittance/payment address
Example: `"Payment Processing Center, 1000 Brussels, Belgium"`
Recipient at remittance address
## Line Items
Array of line items (minimum 1 required)
See [LineItem schema](/api-reference/schemas/line-item) for details.
Example:
```json theme={null}
[
{
"description": "Professional Services",
"quantity": 10,
"unit": "C62",
"unit_price": 100.00,
"tax_rate": "21.00"
}
]
```
## Payment Details
Array of payment method details
Example:
```json theme={null}
[
{
"iban": "BE68539007547034",
"swift": "GEBABEBB",
"payment_reference": "INV-2024-001"
}
]
```
## Allowances and Charges
Document-level allowances (discounts)
Example:
```json theme={null}
[
{
"amount": 50.00,
"reason": "Early payment discount (5%)",
"tax_code": "S",
"tax_rate": "21.00"
}
]
```
Document-level charges (fees)
Example:
```json theme={null}
[
{
"amount": 25.00,
"reason": "Shipping and handling",
"tax_code": "S",
"tax_rate": "21.00"
}
]
```
## Tax Details
Detailed tax breakdown by category/rate
Automatically calculated if not provided.
## Attachments
Document attachments (supporting files)
Example:
```json theme={null}
[
{
"file_name": "supporting_document.pdf",
"file_type": "application/pdf",
"file_data": ""
}
]
```
## Example
Complete invoice example:
```json theme={null}
{
"document_type": "INVOICE",
"invoice_id": "INV-2024-001",
"invoice_date": "2024-10-24",
"due_date": "2024-11-24",
"currency": "EUR",
"purchase_order": "PO-12345",
"vendor_name": "Your Company BVBA",
"vendor_tax_id": "BE1018265814",
"vendor_address": "Main Street 123, 1000 Brussels, Belgium",
"vendor_email": "billing@yourcompany.com",
"customer_name": "Customer Company NV",
"customer_tax_id": "BE0848934496",
"customer_address": "Customer Lane 456, 2000 Antwerp, Belgium",
"items": [
{
"description": "Professional Services",
"quantity": 10,
"unit": "C62",
"unit_price": 100.00,
"tax_rate": "21.00"
}
],
"payment_term": "Net 30 days",
"payment_details": [
{
"iban": "BE68539007547034",
"swift": "GEBABEBB",
"payment_reference": "INV-2024-001"
}
]
}
```
## Related
* [LineItem Schema](/api-reference/schemas/line-item)
* [Creating Invoices Guide](/guides/creating-invoices)
* [Validation Guide](/guides/validation)
# LineItem
Source: https://docs.e-invoice.be/api-reference/schemas/line-item
Invoice line item structure
## Overview
The `LineItem` schema defines individual line items within an invoice or credit note. Each line item represents a product, service, or other billable item.
All fields are optional, but providing `description`, `quantity`, `unit_price`, and `tax_rate` creates complete, clear line items.
## Basic Fields
Description of the product or service
This is the primary field for identifying what is being sold.
Example: `"Professional Services"`, `"Product A - Premium Edition"`, `"Consulting hours - October 2024"`
Product code or SKU
Internal reference code for the product/service.
Example: `"PROD-001"`, `"SKU-PREMIUM-A"`
## Quantity and Unit
Quantity of items (max 4 decimal places)
Example: `10`, `2.5`, `100.0`
Unit of measure code (UN/ECE Recommendation 20)
Common values:
* `C62` - Units/pieces (most common)
* `HUR` - Hours
* `DAY` - Days
* `MTR` - Meters
* `KGM` - Kilograms
* `LTR` - Liters
* `MTK` - Square meters
* `MTQ` - Cubic meters
* `KWH` - Kilowatt hours
Example: `"C62"`, `"HUR"`, `"DAY"`
## Pricing
Price per unit (max 4 decimal places), **excluding VAT**
This is the price for a single unit before any allowances or charges.
Example: `100.00`, `50.99`, `1250.00`
Total line amount (max 2 decimal places), **excluding VAT**
Total amount for this line item after subtracting allowances and adding charges. This is the invoice line net amount (BT-131), exclusive of VAT.
**Calculation**: `(quantity × unit_price) - allowances + charges`
Provide this value: it is used directly as the UBL line extension amount and drives the document totals. Compute it yourself from quantity, unit\_price, allowances, and charges. Can be negative for credit notes or corrections.
Example: `1000.00`, `950.00` (after €50 discount)
## Tax
VAT/tax rate as a percentage (0–100), with 2 decimal places. Sent as 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`, `6.00`, `0.00`
Total VAT/tax amount for this line item (max 2 decimal places)
**Calculation**: `amount × (tax_rate / 100)`
If not provided, automatically calculated from `amount` and `tax_rate`.
Example: `210.00` (21% of €1000), `126.00` (21% of €600)
## Allowances (Discounts)
Line-level allowances (discounts) applied to this specific item
Use for product-specific discounts (bulk discounts, promotions, etc.)
Example:
```json theme={null}
[
{
"amount": 100.00,
"reason": "Bulk discount (10%)",
"tax_code": "S",
"tax_rate": "21.00"
}
]
```
See [Advanced Invoicing guide](/guides/advanced-invoicing) for details.
## Charges (Fees)
Line-level charges (fees) applied to this specific item
Use for product-specific fees (special handling, customization fees, etc.)
Example:
```json theme={null}
[
{
"amount": 50.00,
"reason": "Special handling - fragile item",
"tax_code": "S",
"tax_rate": "21.00"
}
]
```
See [Advanced Invoicing guide](/guides/advanced-invoicing) for details.
## Examples
### Simple Line Item
Basic product line:
```json theme={null}
{
"description": "Product A",
"quantity": 10,
"unit": "C62",
"unit_price": 100.00,
"tax_rate": "21.00"
}
```
**Calculation**:
* Amount: 10 × €100 = €1,000.00
* Tax: €1,000 × 21% = €210.00
* **Total**: €1,210.00
### Service Line Item
Hourly services:
```json theme={null}
{
"description": "Consulting Services - October 2024",
"quantity": 40,
"unit": "HUR",
"unit_price": 150.00,
"tax_rate": "21.00"
}
```
**Calculation**:
* Amount: 40 hours × €150 = €6,000.00
* Tax: €6,000 × 21% = €1,260.00
* **Total**: €7,260.00
### Line Item with Discount
Product with bulk discount:
```json theme={null}
{
"description": "Premium Product B",
"quantity": 100,
"unit": "C62",
"unit_price": 50.00,
"tax_rate": "21.00",
"allowances": [
{
"amount": 500.00,
"reason": "Bulk discount (10%)",
"tax_code": "S",
"tax_rate": "21.00"
}
]
}
```
**Calculation**:
* Base: 100 × €50 = €5,000.00
* Discount: -€500.00
* Amount: €4,500.00
* Tax: €4,500 × 21% = €945.00
* **Total**: €5,445.00
### Line Item with Charge
Product with special handling fee:
```json theme={null}
{
"description": "Fragile Equipment",
"quantity": 1,
"unit": "C62",
"unit_price": 500.00,
"tax_rate": "21.00",
"charges": [
{
"amount": 50.00,
"reason": "Special handling - fragile item",
"tax_code": "S",
"tax_rate": "21.00"
}
]
}
```
**Calculation**:
* Base: 1 × €500 = €500.00
* Handling: +€50.00
* Amount: €550.00
* Tax: €550 × 21% = €115.50
* **Total**: €665.50
### Mixed Products Invoice
Multiple line items with different rates:
```json theme={null}
{
"items": [
{
"description": "Standard Product",
"quantity": 10,
"unit": "C62",
"unit_price": 100.00,
"tax_rate": "21.00"
},
{
"description": "Reduced Rate Product (Books)",
"quantity": 5,
"unit": "C62",
"unit_price": 20.00,
"tax_rate": "6.00"
},
{
"description": "Export Item (Zero-rated)",
"quantity": 3,
"unit": "C62",
"unit_price": 200.00,
"tax_rate": "0.00"
}
]
}
```
**Calculation**:
* Line 1: €1,000 + €210 (21% tax) = €1,210.00
* Line 2: €100 + €6 (6% tax) = €106.00
* Line 3: €600 + €0 (0% tax) = €600.00
* **Total**: €1,916.00
## Calculation Flow
Understanding how amounts are calculated:
1. **Base Amount**: `quantity × unit_price`
2. **Apply Allowances**: Subtract line-level allowances
3. **Apply Charges**: Add line-level charges
4. **Line Amount**: `base - allowances + charges`
5. **Tax**: `line_amount × (tax_rate / 100)`
6. **Line Total**: `line_amount + tax`
Example with all modifiers:
```json theme={null}
{
"description": "Complex Product",
"quantity": 20,
"unit": "C62",
"unit_price": 100.00,
"tax_rate": "21.00",
"allowances": [
{
"amount": 200.00,
"reason": "Volume discount"
}
],
"charges": [
{
"amount": 50.00,
"reason": "Customization fee"
}
]
}
```
**Calculation**:
1. Base: 20 × €100 = €2,000.00
2. * Allowance: -€200.00
3. * Charge: +€50.00
4. \= Amount: €1,850.00
5. Tax (21%): €388.50
6. **Total**: €2,238.50
## Best Practices
Clear descriptions help customers understand what they're paying for:
✓ Good:
```json theme={null}
{
"description": "Premium Consulting Services - Project XYZ - October 2024"
}
```
✗ Poor:
```json theme={null}
{
"description": "Services"
}
```
Match the unit to what you're selling:
* Products/goods: `"C62"` (pieces)
* Services by time: `"HUR"` (hours) or `"DAY"` (days)
* Materials by weight: `"KGM"` (kilograms)
* Materials by volume: `"LTR"` (liters)
`tax_rate` is a number (0–100). A string is also accepted for backward compatibility, but sending a number with 2 decimal places is preferred:
✓ Preferred:
```json theme={null}
{
"tax_rate": 21.00
}
```
✓ Also accepted:
```json theme={null}
{
"tax_rate": "21.00"
}
```
Use consistent decimal places:
* Prices: up to 4 decimals (€100.0000)
* Quantities: up to 4 decimals (10.5000)
* Tax rates: 2 decimals (21.00)
## Related
* [Document Schema](/api-reference/schemas/document)
* [Creating Invoices Guide](/guides/creating-invoices)
* [Advanced Invoicing Guide](/guides/advanced-invoicing)
# 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
How to authenticate with the e-invoice.be API
## Overview
The e-invoice.be API uses **Bearer Token Authentication** for all endpoints. You'll need to include your API key in the `Authorization` header of every request.
## Quick Start for New Users
If you're just getting started:
1. **Use the production API**: `https://api.e-invoice.be`
2. **Create a sandbox company**: In [app.e-invoice.be](https://app.e-invoice.be), click **Create sandbox company** to get a test-mode company and its own API key
3. **Start developing**: A sandbox company prevents real Peppol transmission while you build your integration
**For most users**, you only need to know about `api.e-invoice.be` and test mode.
## Understanding the Setup
### What API Should I Use?
**Answer: Use `https://api.e-invoice.be`** - This single API works for both development and live transactions.
### How Do I Test Without Sending Real Invoices?
**Answer: Create a sandbox company.** A sandbox company runs in test mode: documents are emailed instead of being sent via Peppol. This lets you:
* Test your integration safely
* Verify invoice data and UBL generation
* See exactly what would be sent via Peppol
Create one from [app.e-invoice.be](https://app.e-invoice.be) — see [Test Mode](/environments) for the full walkthrough.
## API Host
### Production API: `api.e-invoice.be`
**Base URL:** `https://api.e-invoice.be`
This is the only API host you need:
* ✅ **For development**: With test mode enabled
* ✅ **For production**: With test mode disabled
* ✅ **Stable features**: Well-tested, production-ready
* ✅ **Standard rate limits**: Suitable for most applications
Use `https://api.e-invoice.be` for everything. Test mode determines whether documents go out over Peppol, not the base URL.
## Test Mode Explained
Test mode is delivered through a **sandbox company** — a dedicated company that changes how documents are transmitted:
| Company | What Happens When You Send a Document |
| ----------------------------- | ------------------------------------------------ |
| **Sandbox** (for development) | Email sent with UBL XML - no Peppol transmission |
| **Regular** (for production) | Document sent via Peppol network to recipient |
### Key Points About Test Mode
* It's determined by the company you authenticate as, not by which base URL you use
* A sandbox company has its **own API key**, separate from your live company's key
* A sandbox company cannot be converted to a live one — create a separate regular company to go live
* Everything else works the same (validation, webhooks, API endpoints, etc.)
Create a sandbox company from [app.e-invoice.be](https://app.e-invoice.be). See [Test Mode](/environments) for details.
## Getting Your API Key
If you haven't already obtained your API key:
1. Log in to [app.e-invoice.be](https://app.e-invoice.be)
2. Go to **Settings** → **API Keys**
3. Click **Create API Key**
4. Copy and securely store your key
Your API key is sensitive. Never share it publicly, commit it to version control, or expose it in client-side code.
## Making Authenticated Requests
Include your API key in the `Authorization` header with the `Bearer` prefix:
```bash theme={null}
curl -X GET "https://api.e-invoice.be/api/me/" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### Request Format
```
Authorization: Bearer YOUR_API_KEY
```
Replace `YOUR_API_KEY` with your actual API key.
The same API key works whether or not test mode is enabled on your account.
## Code Examples
```javascript Node.js theme={null}
const axios = require('axios');
// Most users should use the production API
const BASE_URL = 'https://api.e-invoice.be';
const api = axios.create({
baseURL: BASE_URL,
headers: {
'Authorization': `Bearer ${process.env.E_INVOICE_API_KEY}`,
'Content-Type': 'application/json'
}
});
// Example: Get account info
async function getAccountInfo() {
try {
const response = await api.get('/api/me/');
console.log(response.data);
} catch (error) {
console.error('Error:', error.response?.data);
}
}
```
```python Python theme={null}
import os
import requests
API_KEY = os.environ.get('E_INVOICE_API_KEY')
# Most users should use the production API
BASE_URL = 'https://api.e-invoice.be'
headers = {
'Authorization': f'Bearer {API_KEY}',
'Content-Type': 'application/json'
}
# Example: Get account info
response = requests.get(f'{BASE_URL}/api/me/', headers=headers)
print(response.json())
```
```php PHP theme={null}
```
```go Go theme={null}
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
apiKey := os.Getenv("E_INVOICE_API_KEY")
// Most users should use the production API
baseURL := "https://api.e-invoice.be"
client := &http.Client{}
req, _ := http.NewRequest("GET", baseURL+"/api/me/", nil)
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
```
## Best Practices
### Store API Keys Securely
Use environment variables or secure credential management systems:
```bash theme={null}
# .env file (add to .gitignore!)
E_INVOICE_API_KEY=your_api_key_here
```
```javascript theme={null}
// Load from environment
require('dotenv').config();
const apiKey = process.env.E_INVOICE_API_KEY;
```
### Never Hardcode Keys
❌ **Don't do this:**
```javascript theme={null}
const apiKey = 'sk_live_abc123...'; // Never hardcode!
const baseUrl = 'https://api.e-invoice.be'; // Don't hardcode this either!
```
✅ **Do this instead:**
```javascript theme={null}
const apiKey = process.env.E_INVOICE_API_KEY;
const baseUrl = process.env.E_INVOICE_BASE_URL || 'https://api.e-invoice.be';
```
### Development Workflow
1. **Create a sandbox company** in [app.e-invoice.be](https://app.e-invoice.be) and use its API key
2. **Use the production API** (`api.e-invoice.be`) for development
3. **Switch to your live company's API key** when you're ready to go live
```bash theme={null}
# .env (same base URL; swap the API key per environment)
E_INVOICE_API_KEY=your_api_key
E_INVOICE_BASE_URL=https://api.e-invoice.be
```
You don't need to change your base URL or code when switching from development to production - just use your live company's API key and documents will be sent via Peppol instead of email. [Learn more about test mode →](/environments)
### Rotate Keys Regularly
For security best practices:
1. Generate a new API key in your dashboard
2. Update your applications to use the new key
3. Delete the old key once migration is complete
### Use Multiple API Keys
Create separate API keys for:
* Sandbox companies (test mode) and real companies (production)
* Different applications or services
* Different team members or departments
* Programmatic access vs. manual testing
This allows you to rotate or revoke keys without affecting all systems.
## Error Responses
### 401 Unauthorized
If authentication fails, you'll receive a 401 error:
```json theme={null}
{
"detail": "Invalid authentication"
}
```
**Common causes:**
* Missing `Authorization` header
* Invalid API key format
* Expired or revoked API key
* Typo in API key
* Using a sandbox company's API key for production sends (or vice versa)
### Troubleshooting
1. **Verify the header format**: Ensure you're using `Bearer YOUR_API_KEY`
2. **Check for whitespace**: Trim any extra spaces from your API key
3. **Confirm the workspace**: Make sure you're using the correct API key (sandbox company vs. real company)
4. **Test with curl**: Verify your key works with a simple curl command
```bash theme={null}
curl -X GET "https://api.e-invoice.be/api/me/" \
-H "Authorization: Bearer YOUR_API_KEY" \
-v
```
## Testing Your Authentication
Use the `/api/me/` endpoint to verify your authentication is working:
```bash theme={null}
curl -X GET "https://api.e-invoice.be/api/me/" \
-H "Authorization: Bearer YOUR_API_KEY"
```
**Successful response:**
```json theme={null}
{
"name": "Your Company",
"company_name": "Your Company BV",
"company_number": "0123456789",
"company_country": "Belgium",
"peppol_ids": ["0208:0123456789"],
"plan": "starter",
"credit_balance": 100
}
```
If you see this response, your authentication is working correctly!
## Next Steps
Learn how to create and send e-invoices
Test your invoice data before sending
# Environments
Source: https://docs.e-invoice.be/environments
Understanding the API host, test mode, and sandbox companies
## Overview
e-invoice.be runs on a single API host, `https://api.e-invoice.be`. There is no separate staging or development host — you develop, test, and run in production against the same URL. What changes is the **mode** your workspace operates in:
* **Production** — documents are transmitted over the real Peppol network.
* **Test mode** — documents are never sent over Peppol; sends are diverted to email and inbound documents are simulated, so you can build and verify your integration safely.
The easiest way to get a test-mode workspace is to create a **sandbox company** (see below).
## API Host: `api.e-invoice.be`
**Base URL:** `https://api.e-invoice.be`
This is the only API host. Use it for everything:
* Real Peppol network transmission (for production companies)
* Development and testing (for sandbox companies / test mode)
* Stable, released features
Configure your integration with a single base URL and switch behaviour by using a production or a sandbox company's API key — not by changing hosts.
```bash theme={null}
# .env
E_INVOICE_API_KEY=your_api_key
E_INVOICE_BASE_URL=https://api.e-invoice.be
```
## Test Mode
Test mode is an operating mode for a workspace (tenant) that prevents any real interaction with the Peppol network. When a workspace is in test mode:
* Documents are **not** sent via Peppol. Instead, an email containing the UBL XML that would have been transmitted is sent to the workspace's contact email.
* Inbound documents are never received from the real network, but can be injected with the **Simulate inbound** feature (see [Testing Received Documents](#testing-received-documents)).
* Peppol registration actions (Admin API) are simulated rather than performed on the live network.
* Everything else works exactly as in production: document creation, validation, webhooks, inbox/outbox, and all API endpoints.
Test mode is fixed for the lifetime of a workspace — a company is either a real (production) company or a sandbox company, decided when it is created. There is no switch to convert one into the other; instead, create the type of company you need.
## Sandbox Companies
A **sandbox company** is a company whose workspace runs in test mode. It behaves like any other company in the app and the API — company switcher, settings, documents, webhooks, API key — but nothing it does touches the Peppol network. Sandbox companies are ideal for building an integration, running a hackathon, or evaluating the platform.
Key characteristics:
* **Test mode always on** — outbound sends go to email, inbound is simulated, no Peppol traffic. This is immutable.
* **Synthetic identifiers** — a placeholder Belgian VAT number is assigned automatically (you can edit it). KBO/VIES, email, phone, and payment verification are skipped.
* **Full webhook machinery** — `document.sent` and `document.received` webhooks fire exactly as they do in production, so you can exercise your complete integration.
* **No billing** — sandbox companies don't consume credits and have no plan; credit, top-up, and usage screens are hidden.
* **Own API key** — each sandbox company has its own API key, just like a real company. Use it as the `Authorization: Bearer` token against `https://api.e-invoice.be`.
### Creating a Sandbox Company
1. Sign in to the app at [app.e-invoice.be](https://app.e-invoice.be).
2. Open the **Companies** view and choose **Create sandbox company**.
3. Fill in the details. The suggested country and VAT number are safe placeholders and can be edited; click **Suggest valid VAT** to generate a synthetic Belgian VAT number.
4. Open the new sandbox workspace and copy its **API key** from settings.
You can now point your integration at `https://api.e-invoice.be` using the sandbox API key and develop against the full API without sending anything over Peppol.
### How Sends Behave in Test Mode
When you send a document from a sandbox company (or any test-mode workspace):
1. **Document creation**: Works exactly the same as production.
2. **Document sending** (`POST /api/documents/{id}/send`):
* The document is validated and converted to UBL XML.
* State transitions: `DRAFT` → `TRANSIT` → `SENT`.
* The UBL XML is attached to an email sent to the workspace's contact email.
* No Peppol transmission occurs.
3. **Webhooks and everything else**: Operate normally.
## Testing Received Documents
To test incoming documents (inbox functionality), use the **Simulate inbound** feature. It injects a UBL document into your sandbox company's inbox exactly as if it had been received over Peppol — the document lands in `GET /api/inbox/` in the `RECEIVED` state, and any `document.received` webhooks you have configured fire normally.
You **cannot** put a document in your own inbox by sending one to yourself. In test mode, sending is diverted to email rather than transmitted over Peppol, so a self-addressed document reaches `SENT` but never loops back into the inbox. The `direction` field on `POST /api/documents/` is also ignored — that endpoint always creates `OUTBOUND` drafts. **Simulate inbound is the only way to populate the inbox without a real Peppol receipt.**
### Using the app
In a sandbox workspace, open **Inbox** and click **Simulate inbound**. You can either:
* Inject a built-in **sample invoice** with one click, or
* **Upload your own UBL** XML — the receiver identifiers are automatically substituted with your company's, so the document is addressed to you.
The simulated document then appears in your inbox, and you can exercise your complete receive-side integration (listing, webhooks, downstream processing) without needing a second Peppol participant.
If you don't see the **Simulate inbound** option, confirm you are in a sandbox company, or contact [support@e-invoice.be](mailto:support@e-invoice.be).
### Using the Admin API (resellers)
Resellers with an organization API key can drive the same behaviour programmatically for any of their sandbox tenants via [Simulate an Inbound Document](/admin-api#simulate-an-inbound-document) (`POST /api/admin/tenants/{tenant_id}/simulate-inbound`).
## Recommended Development Workflow
1. **Create a sandbox company** and use its API key against `https://api.e-invoice.be`.
2. **Validate thoroughly** — use `POST /api/validate/json` extensively; test multiple invoice scenarios and verify calculations, tax rates, and totals.
3. **Test end-to-end** — create and send documents (verify the UBL email in test mode), use **Simulate inbound** to test receiving, and confirm your webhook handling.
4. **Go to production** — create (or use) a real company and switch your integration to that company's API key. No code changes are needed beyond the API key; real documents are then transmitted over Peppol.
## Frequently Asked Questions
### How do I test my integration?
Create a **sandbox company** in the app and use its API key against `https://api.e-invoice.be`. It runs in test mode, so you can build and verify everything without sending real documents over Peppol.
### Which API host should I use?
There is only one: `https://api.e-invoice.be`. Use it for both testing (with a sandbox company) and production (with a real company).
### Can I convert a sandbox company into a real one?
No. Whether a company is a sandbox (test mode) or a real (production) company is fixed at creation. When you're ready for production, use a real company and switch to its API key.
### How do I move from testing to production?
Point your integration at a **real company's** API key instead of the sandbox one. The base URL and your code stay the same; documents are then sent via Peppol instead of email.
### Can I test webhooks in a sandbox company?
Yes. Webhooks work identically in test mode and production. You'll receive events for document state changes, including when documents are "sent" (via email in test mode, via Peppol in production) and received (via **Simulate inbound** in test mode).
### Can I use different API keys for different applications?
Yes. Each company has its own API key, and you can create additional keys to isolate different applications.
## Next Steps
Get started with your first API call
Learn how to validate invoices during development
Create and send your first e-invoice
Set up webhook notifications
# Model Context Protocol (MCP)
Source: https://docs.e-invoice.be/essentials/mcp
Connect AI assistants to your e-invoice.be account via MCP
The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) is an open standard that lets AI assistants interact with external services. Think of it as a USB-C port for AI — one protocol to connect any AI tool to e-invoice.be.
The e-invoice.be MCP server gives AI agents access to your invoices, credit notes, Peppol network data, and usage statistics — all through natural language.
All MCP tools are **read-only**. No data can be created, modified, or deleted through MCP. To create or send documents, use the [REST API](/api-reference).
## Prerequisites
Before setting up MCP, you need:
1. An e-invoice.be account — [sign up at app.e-invoice.be](https://app.e-invoice.be)
2. An API key — go to **Settings → API Keys** in your dashboard ([learn more](/authentication))
**Server URL:** `https://api.e-invoice.be/mcp`
The endpoint is the same across all clients. Transport is streamable HTTP, authenticated with your API key as a Bearer token.
## Setup
### Claude Code
[Claude Code](https://docs.anthropic.com/en/docs/claude-code) supports remote MCP servers natively via streamable HTTP.
**CLI (recommended):**
```bash theme={null}
claude mcp add --transport http e-invoice https://api.e-invoice.be/mcp \
--header "Authorization: Bearer YOUR_API_KEY"
```
This registers the server at user scope. To add it at project scope, append `--scope project`.
**Manual JSON** — add to `~/.claude.json` (user scope) or `.claude/settings.local.json` (project scope):
```json theme={null}
{
"mcpServers": {
"e-invoice": {
"type": "http",
"url": "https://api.e-invoice.be/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
```
After adding, restart Claude Code or run `/mcp` to verify the server appears in the list.
Official documentation for MCP in Claude Code
### Claude Desktop
Claude Desktop does not yet support remote streamable HTTP MCP servers directly. You need [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) as a bridge (requires Node.js ≥ v18).
**Step 1** — Open the config file from **Claude → Settings → Developer → Edit Config**:
| OS | Config path |
| ------- | ----------------------------------------------------------------- |
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
| Linux | `~/.config/Claude/claude_desktop_config.json` |
**Step 2** — Add (or merge into) the `mcpServers` key:
```json theme={null}
{
"mcpServers": {
"e-invoice": {
"command": "npx",
"args": [
"mcp-remote@latest",
"https://api.e-invoice.be/mcp",
"--header",
"Authorization: Bearer YOUR_API_KEY"
]
}
}
}
```
**Step 3** — Restart Claude Desktop. The e-invoice tools appear under the hammer (🔨) icon in the composer.
Official documentation for connecting MCP servers to Claude Desktop
### ChatGPT
ChatGPT supports remote MCP servers as custom connectors (available on Plus, Pro, Business, and Enterprise plans).
Custom connectors via developer mode are currently **not available** for all account types in the EEA, GB, and CH regions. Since most e-invoice.be users are in the EU, verify that your ChatGPT plan supports this feature before proceeding.
**Setup flow:**
1. Open [chatgpt.com](https://chatgpt.com) → **Settings → Connectors → Advanced → Developer mode** (toggle on)
2. Click **Create → Add custom connector**
3. Fill in the connector details:
* **Name:** `e-invoice`
* **Description:** Send, validate, and look up Peppol invoices
* **MCP server URL:** `https://api.e-invoice.be/mcp`
* **Authentication:** Custom header
* Header name: `Authorization`
* Header value: `Bearer YOUR_API_KEY`
4. Save and accept the trust prompt
5. Enable the connector inside any chat via the **+ → Connectors** menu
Learn about connectors in ChatGPT
Enable custom MCP connectors
### Cursor
Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (per-project):
```json theme={null}
{
"mcpServers": {
"e-invoice": {
"url": "https://api.e-invoice.be/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
```
After saving, restart Cursor or open a new chat session. The tools will appear in the MCP tools list.
Official documentation for MCP in Cursor
### VS Code (GitHub Copilot)
Add to `.vscode/mcp.json` in your project root:
```json theme={null}
{
"servers": {
"e-invoice": {
"type": "http",
"url": "https://api.e-invoice.be/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
```
Alternatively, add to your user or workspace `settings.json` under the `mcp.servers` key.
Official documentation for MCP in VS Code
### Windsurf
Add 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"
}
}
}
}
```
Restart Windsurf after saving the configuration.
Don't have an API key yet? See the [Authentication](/authentication) guide to create one.
## Available Tools
### Documents
| Tool | Description |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `get_document` | Retrieve a single invoice or credit note by ID, including line items, tax details, payment info, and attachments |
| `get_document_attachments` | List all attachments for a document |
| `get_document_attachment` | Get a specific attachment with a signed download URL (valid for 1 hour) |
| `get_document_timeline` | Get the chronological event history for a document (creation, validation, transmission, delivery) |
### Inbox
| Tool | Description |
| ------------------------ | -------------------------------------------------------------------------------------------- |
| `get_inbox` | List all received documents with filtering by type, sender, date range, and full-text search |
| `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 with filtering by receiver, date range, and full-text search |
### Drafts
| Tool | Description |
| ------------ | ------------------------------------------------------------------------------------------- |
| `get_drafts` | List draft documents with filtering by state (DRAFT, TRANSIT, FAILED), type, and date range |
### Peppol Lookup
These tools are **public** and do not require authentication.
| Tool | Description |
| ------------------------- | ------------------------------------------------------------------------------------------------------- |
| `get_lookup_peppol_id` | Verify if a participant is registered on the Peppol network and retrieve their supported document types |
| `get_lookup_participants` | Search for Peppol participants by name, with optional country filter |
### Statistics
| Tool | Description |
| ----------- | ------------------------------------------------------------------------- |
| `get_stats` | Retrieve sending and receiving statistics, grouped by day, week, or month |
## Example Prompts
Once connected, you can interact with your e-invoice.be account using natural language:
* *"Show me all invoices received this month"*
* *"Get the details and timeline of document doc\_abc123"*
* *"Is the company with VAT number BE0848934496 registered on Peppol?"*
* *"Search for Peppol participants named 'OpenPeppol' in Belgium"*
* *"Show my sending and receiving stats for the last 3 months, aggregated by month"*
* *"List all failed documents in my drafts"*
* *"What attachments are on invoice doc\_xyz789?"*
## Troubleshooting
### Tools not appearing after setup
1. **Restart the application** — most MCP clients only load server configurations on startup.
2. **Verify your API key** — run a quick test with curl to confirm it works:
```bash theme={null}
curl -s https://api.e-invoice.be/api/me/ \
-H "Authorization: Bearer YOUR_API_KEY"
```
3. **Check the server URL** — make sure you're using `https://api.e-invoice.be/mcp` (no trailing slash).
### Claude Desktop: tools not showing
* Check the log file for errors:
* **macOS:** `tail -n 50 ~/Library/Logs/Claude/mcp.log`
* **Windows:** open `%APPDATA%\Claude\logs\mcp.log`
* Verify Node.js is installed and on your PATH: `node --version` (v18+ required).
* Test `mcp-remote` manually to surface auth errors:
```bash theme={null}
npx mcp-remote@latest https://api.e-invoice.be/mcp \
--header "Authorization: Bearer YOUR_API_KEY"
```
* Ensure your JSON is valid — a trailing comma or missing quote will silently prevent loading.
### ChatGPT: connector not available
* Custom connectors require **Developer mode** to be enabled under Settings → Connectors → Advanced.
* Developer mode is not available on all plans or in all regions (EEA/GB/CH restrictions may apply).
* After adding the connector, you must explicitly enable it in each new chat via the **+ → Connectors** menu.
### Authentication errors
* API keys are prefixed with your tenant — make sure you copied the full key.
* Keys created for the [Admin API](/admin-api) (reseller keys) will not work with MCP.
* If you recently rotated your key, update the configuration file and restart the client.
## Security
Your API key gives MCP access to all read-only data within your tenant. Treat it with the same care as any API credential.
* **Read-only access** — MCP tools cannot create, modify, or delete any data
* **Tenant-scoped** — your API key determines which tenant's data is accessible
* **Lookup tools are public** — `get_lookup_peppol_id` and `get_lookup_participants` work without authentication
* **Best practice** — consider creating a dedicated API key for MCP access, separate from your application's key
## Next Steps
Get your API key and learn about authentication
Full REST API for creating and sending documents
Receive real-time notifications for document events
# Webhooks
Source: https://docs.e-invoice.be/essentials/webhooks
How to set up webhooks
Webhooks allow you to receive real-time notifications about events in your e-invoice.be account. This guide explains how to set up and use webhooks effectively.
## Overview
Webhooks are HTTP callbacks that notify your application when specific events occur, such as:
* Document received
* Document sent
* Document send failed
* Document receive failed
## Setting Up Webhooks
### 1. Create a Webhook
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/webhooks" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-domain.com/webhook",
"events": ["document.received", "document.sent", "document.sent.failed"],
"enabled": true
}'
```
### 2. Configure Your Endpoint
Your webhook endpoint should:
* Accept POST requests
* Use HTTPS with a valid SSL/TLS certificate (recommended; self-signed certificates are not supported)
* Support HTTP Basic Authentication via URL format: `https://username:password@host.com/endpoint` (optional)
* Return a 200 OK response quickly
* Handle retries gracefully
* Verify webhook signatures
**Endpoint URL Requirements:**
* **Protocol**: HTTP and HTTPS are both supported (HTTPS strongly recommended for production)
* **Certificate**: If using HTTPS, SSL/TLS certificate must be valid and issued by a trusted Certificate Authority
* **Self-signed certificates**: Not supported - webhook delivery will fail with HTTPS
* **Redirects**: Not followed. The configured URL must be the final endpoint - any 3xx redirect response (301, 302, 307, 308, etc.) is treated as a delivery failure and will be retried. This is a deliberate security measure to prevent SSRF and redirect-based attacks.
* **Authentication**: You can include HTTP Basic Authentication credentials directly in the URL:
```
https://username:password@your-domain.com/webhook
http://username:password@your-domain.com/webhook
```
* **URL format**: Standard URL format with optional path, query parameters, and authentication
Configure your webhook URL to point directly at the final endpoint. If your URL responds with a redirect (e.g., `http://` → `https://`, or apex → `www`), delivery will fail. Update the registered webhook URL to match the destination your server actually serves the handler from.
### 3. Delivery and Retries
Each event is delivered as a single POST request and must receive a `2xx` response within **10 seconds**. If your endpoint returns a non-`2xx` status, times out, or is unreachable, delivery is retried up to **3 attempts** in total, with exponential backoff between attempts (roughly 1 second, then 2 seconds). After the final attempt fails, the event is not delivered again.
Because a delivery may be retried, design your handler to be **idempotent** (use the event `id` to de-duplicate) and to return quickly — do any heavy processing asynchronously after acknowledging the request.
## Webhook Security
### Signature Verification
Webhook requests are signed using HMAC-SHA256 to ensure authenticity and integrity. The signature is included in the `X-Signature` header of each request.
#### How the Signature is Computed
The signature is computed over the **entire webhook event payload** (not just the `data` field). This ensures the integrity of all webhook data including the event ID, timestamp, type, and content.
1. The complete webhook event is serialized to JSON with sorted keys
2. The JSON string is encoded to UTF-8 bytes
3. An HMAC is created using your webhook secret and the SHA-256 algorithm
4. The resulting signature is formatted as `sha256={hexadecimal_signature}`
#### Example
Given this complete webhook event payload:
```json theme={null}
{
"id": "evt-e7wyc7gtpqx4z73x2wqmwhebhbb3r8n3ovfhcsdbulxr3s2awf49de76yglrnri3",
"tenant_id": "ten-abc123",
"created_at": 1762780249,
"type": "document.sent",
"data": {
"document_id": "doc-1"
},
"text": "⚡️ New webhook event: document.sent\n\n{\n \"document_id\": \"doc-1\"\n}"
}
```
When serialized to JSON with sorted keys, this becomes the following string (before UTF-8 encoding):
```json theme={null}
{"created_at": 1762780249, "data": {"document_id": "doc-1"}, "id": "evt-e7wyc7gtpqx4z73x2wqmwhebhbb3r8n3ovfhcsdbulxr3s2awf49de76yglrnri3", "tenant_id": "ten-abc123", "text": "\u26a1\ufe0f New webhook event: document.sent\n\n{\n \"document_id\": \"doc-1\"\n}", "type": "document.sent"}
```
**Important**: Notice how special characters are properly escaped in the JSON serialization:
* Newlines are represented as `\n` (literal backslash-n, not actual line breaks)
* Unicode characters like emojis are escaped as `\u26a1\ufe0f` (for ⚡️)
* Quotes inside the `text` field are escaped as `\"`
This is critical for signature verification. The JSON must be serialized with:
* **Sorted keys** (`sort_keys=True` in Python's `json.dumps()`)
* **No extra whitespace** (compact format, not pretty-printed)
* **Proper escaping** of special characters (standard JSON encoding)
Your JSON library should handle this automatically when using `json.dumps(payload, sort_keys=True)` in Python, `JSON.stringify()` in JavaScript, or equivalent functions in other languages.
With the webhook secret `"secret"`, this produces the signature:
```
sha256=2f8ec8fab5adedd8f82a2b4064f559c40b14aed0c6da27ed394e51176b10bd1b
```
#### Computing the Signature
All webhook requests from e-invoice.be include a cryptographic signature that allows you to verify the request's authenticity. This prevents unauthorized parties from sending fake webhook events to your endpoint.
Here's the exact code used by e-invoice.be to compute webhook signatures:
```python theme={null}
import hmac
import hashlib
import json
def compute_signature(webhook_event_json: str, secret: str) -> str:
"""
Compute HMAC-SHA256 signature for the entire webhook event payload.
Args:
webhook_event_json: JSON string of the complete webhook event
(serialized with sort_keys=True, exclude_none=True)
secret: Your webhook secret
Returns:
Signature in format "sha256={hex_digest}"
"""
payload_bytes = webhook_event_json.encode("utf-8")
signature = hmac.new(
secret.encode("utf-8"), payload_bytes, hashlib.sha256
).hexdigest()
return f"sha256={signature}"
```
**How it works:**
1. **Serialize the entire webhook event**: The complete webhook event (including `id`, `tenant_id`, `created_at`, `type`, `data`, and `text` fields) is serialized to JSON with `sort_keys=True` to ensure consistent key ordering. This is critical because `{"a": 1, "b": 2}` and `{"b": 2, "a": 1}` are semantically identical but would produce different signatures without key sorting.
2. **Convert to bytes**: The JSON string is encoded to UTF-8 bytes, which is required for the HMAC operation.
3. **Generate HMAC**: Using your webhook secret as the key, an HMAC (Hash-based Message Authentication Code) is computed using the SHA-256 hashing algorithm. This creates a cryptographic signature that only someone with your secret can reproduce.
4. **Format the result**: The hexadecimal digest is prefixed with `sha256=` to indicate the algorithm used, matching the format in the `X-Signature` header.
**Why HMAC?** HMAC is preferred over simple hashing because it requires a secret key. Even if an attacker knows the payload and hashing algorithm, they cannot generate a valid signature without your webhook secret.
**Important**: The signature covers the **entire webhook event payload** you receive in the POST request body, not just the `data` field. This ensures the integrity and authenticity of all event information including the event ID, timestamp, type, and all data.
#### Verifying the Signature
To verify the signature in your webhook handler:
1. Extract the signature from the `X-Signature` header (format: `sha256={hex_digest}`)
2. **Parse the request body as JSON**, then re-serialize it with **sorted keys** and no extra whitespace (e.g. `json.dumps(payload, sort_keys=True)` in Python, or the equivalent in your language)
3. Encode that canonical JSON string to UTF-8 bytes
4. Compute the HMAC-SHA256 over those bytes using your webhook secret
5. Compare the computed signature with the one in the header using a constant-time comparison
The signature is computed over the JSON serialized with `sort_keys=True`, but the body delivered over the wire is **not guaranteed** to use that same key order. You **must** parse the JSON and re-serialize it with sorted keys before computing the HMAC — verifying directly against the raw request body will fail whenever the transport-level key order differs from the canonical (sorted) order.
**Important Notes:**
* Always verify the signature **before** processing the webhook
* Use constant-time comparison to prevent timing attacks
* The signature covers the **entire webhook event** (all fields: `id`, `tenant_id`, `created_at`, `type`, `data`, `text`) — not just the `data` field
* Re-serialize with `sort_keys=True` and no extra whitespace; do not pretty-print
* If the signatures match, the webhook request is authentic and hasn't been tampered with
## Available Events
The following webhook events are supported:
* **`document.received`**: A new document is successfully received via Peppol or other channels
* **`document.received.failed`**: A document failed to be received/processed
* **`document.sent`**: A document is successfully sent via Peppol or other channels
* **`document.sent.failed`**: A document failed to send (e.g., validation error, network error, Peppol transmission failure)
## Webhook Payload
When a webhook is triggered, it sends a POST request to your configured URL with the following structure:
```json theme={null}
{
"id": "evt-3k8d9f2h4j6m8n0p",
"tenant_id": "ten-5x7y9z1a3b5c7d9e",
"created_at": 1729468923,
"type": "document.sent",
"data": {
"document_id": "doc-2f4h6j8k0m2n4p6q"
},
"text": "⚡️ New webhook event: document.sent\n\n{\n \"document_id\": \"doc-2f4h6j8k0m2n4p6q\"\n}"
}
```
**Note**: Currently, the `data` object only contains the `document_id`. You can use this ID to fetch additional document details via the API (`GET /api/documents/{document_id}`).
### Headers
Each webhook request includes the following headers:
* **`X-Signature`**: HMAC-SHA256 signature for verifying authenticity (format: `sha256=...`)
* **`X-Event-Type`**: The event type (e.g., `document.sent`)
* **`Content-Type`**: `application/json`
* **`User-Agent`**: `e-invoice-be-webhook-service`
### Event Data Structure
The `data` object contains event-specific information:
* For all document events (`document.received`, `document.sent`, `document.sent.failed`, `document.received.failed`):
```json theme={null}
{
"document_id": "doc-2f4h6j8k0m2n4p6q"
}
```
The `document_id` can be used to retrieve full document details:
```bash theme={null}
curl -X GET "https://api.e-invoice.be/api/documents/{document_id}" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### Complete Example Requests
#### Success Event Example
Here's what a successful `document.sent` webhook HTTP request looks like:
```http theme={null}
POST /webhook HTTP/1.1
Host: your-domain.com
Content-Type: application/json
User-Agent: e-invoice-be-webhook-service
X-Signature: sha256=a3f8d9e2c1b5a7f9e3d2c1b5a7f9e3d2c1b5a7f9e3d2c1b5a7f9e3d2c1b5
X-Event-Type: document.sent
{
"id": "evt-3k8d9f2h4j6m8n0p",
"tenant_id": "ten-5x7y9z1a3b5c7d9e",
"created_at": 1729468923,
"type": "document.sent",
"data": {
"document_id": "doc-2f4h6j8k0m2n4p6q"
},
"text": "⚡️ New webhook event: document.sent\n\n{\n \"document_id\": \"doc-2f4h6j8k0m2n4p6q\"\n}"
}
```
#### Failure Event Example
Here's what a failed `document.sent.failed` webhook HTTP request looks like:
```http theme={null}
POST /webhook HTTP/1.1
Host: your-domain.com
Content-Type: application/json
User-Agent: e-invoice-be-webhook-service
X-Signature: sha256=7e9c3a1d5f8b2e6a4c9d7f3b1e8a6c2d9f7e3a1c5b8d6f2a4e9c7b3d1f8a5c
X-Event-Type: document.sent.failed
{
"id": "evt-9m2p4r6t8v0x2z4b",
"tenant_id": "ten-5x7y9z1a3b5c7d9e",
"created_at": 1729469845,
"type": "document.sent.failed",
"data": {
"document_id": "doc-8h3j5k7m9n1p3q5r"
},
"text": "⚡️ New webhook event: document.sent.failed\n\n{\n \"document_id\": \"doc-8h3j5k7m9n1p3q5r\"\n}"
}
```
**Note**: For failure events, you'll need to retrieve the document details via the API to understand what went wrong. The document state will typically be set to `FAILED` and may contain error information. You can retry sending a failed document by calling `POST /api/documents/{document_id}/send` again.
## Webhook Management
### List Webhooks
```bash theme={null}
curl -X GET "https://api.e-invoice.be/api/webhooks" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### Update Webhook
```bash theme={null}
curl -X PUT "https://api.e-invoice.be/api/webhooks/{webhook_id}" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"enabled": false
}'
```
### Delete Webhook
```bash theme={null}
curl -X DELETE "https://api.e-invoice.be/api/webhooks/{webhook_id}" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### Test Webhook
Send a test event to verify your webhook is working correctly:
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/webhooks/{webhook_id}/test" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"event_type": "document.sent",
"data": {
"document_id": "test_doc_123"
}
}'
```
# Advanced Invoicing
Source: https://docs.e-invoice.be/guides/advanced-invoicing
Learn how to use allowances and charges at document and line item levels
## Overview
Beyond basic invoicing, you may need to apply:
* **Allowances** (discounts) - Reduce the invoice amount
* **Charges** (fees) - Increase the invoice amount
These can be applied at two levels:
1. **Document level** - Apply to the entire invoice (e.g., early payment discount, handling fees)
2. **Line item level** - Apply to specific products/services (e.g., bulk discount on one product)
## Document-Level Allowances
Document-level allowances reduce the total invoice amount **after** calculating line items.
### Early Payment Discount Example
```json theme={null}
{
"document_type": "INVOICE",
"invoice_id": "INV-2024-001",
"invoice_date": "2024-10-24",
"currency": "EUR",
"vendor_name": "Your Company BVBA",
"vendor_tax_id": "BE1018265814",
"vendor_address": "Main Street 123, 1000 Brussels, Belgium",
"customer_name": "Customer Company NV",
"customer_tax_id": "BE0848934496",
"customer_address": "Customer Lane 456, 2000 Antwerp, Belgium",
"items": [
{
"description": "Product A",
"quantity": 10,
"unit": "C62",
"unit_price": 100.00,
"tax_rate": "21.00"
}
],
"allowances": [
{
"amount": 50.00,
"reason": "Early payment discount (5%)",
"tax_code": "S",
"tax_rate": "21.00"
}
]
}
```
**Calculation**:
* Line items subtotal: €1,000.00
* Allowance (discount): -€50.00
* Net amount: €950.00
* VAT (21%): €199.50
* **Total**: €1,149.50
### Multiple Document-Level Allowances
You can apply multiple allowances:
```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**:
* Line items subtotal: €1,000.00
* Volume discount: -€50.00
* Promotional discount: -€25.00
* Net amount: €925.00
* VAT (21%): €194.25
* **Total**: €1,119.25
### Percentage-Based Allowances
While the API accepts fixed amounts, you can calculate percentages in your code:
```javascript theme={null}
const lineItemsTotal = 1000.00;
const discountPercent = 5;
const discountAmount = lineItemsTotal * (discountPercent / 100);
const invoiceData = {
// ... other fields
allowances: [
{
amount: discountAmount, // 50.00
reason: `Early payment discount (${discountPercent}%)`,
tax_code: 'S',
tax_rate: '21.00'
}
]
};
```
## Document-Level Charges
Document-level charges increase the total invoice amount.
### Shipping Fee Example
```json theme={null}
{
"document_type": "INVOICE",
"invoice_id": "INV-2024-002",
"invoice_date": "2024-10-24",
"currency": "EUR",
"vendor_name": "Your Company BVBA",
"vendor_tax_id": "BE1018265814",
"vendor_address": "Main Street 123, 1000 Brussels, Belgium",
"customer_name": "Customer Company NV",
"customer_tax_id": "BE0848934496",
"customer_address": "Customer Lane 456, 2000 Antwerp, Belgium",
"items": [
{
"description": "Product B",
"quantity": 5,
"unit": "C62",
"unit_price": 200.00,
"tax_rate": "21.00"
}
],
"charges": [
{
"amount": 25.00,
"reason": "Shipping and handling",
"tax_code": "S",
"tax_rate": "21.00"
}
]
}
```
**Calculation**:
* Line items subtotal: €1,000.00
* Shipping charge: +€25.00
* Net amount: €1,025.00
* VAT (21%): €215.25
* **Total**: €1,240.25
### Financial Charges
```json theme={null}
{
"charges": [
{
"amount": 30.00,
"reason": "Payment processing fee",
"tax_code": "S",
"tax_rate": "21.00"
}
]
}
```
### Multiple Charges
```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"
}
]
}
```
## Combining Allowances and Charges
You can use both allowances and charges on the same invoice:
```json theme={null}
{
"document_type": "INVOICE",
"invoice_id": "INV-2024-003",
"invoice_date": "2024-10-24",
"currency": "EUR",
"vendor_name": "Your Company BVBA",
"vendor_tax_id": "BE1018265814",
"vendor_address": "Main Street 123, 1000 Brussels, Belgium",
"customer_name": "Customer Company NV",
"customer_tax_id": "BE0848934496",
"customer_address": "Customer Lane 456, 2000 Antwerp, Belgium",
"items": [
{
"description": "Product C",
"quantity": 10,
"unit": "C62",
"unit_price": 100.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 subtotal: €1,000.00
* Bulk discount: -€100.00
* Express delivery: +€30.00
* Net amount: €930.00
* VAT (21%): €195.30
* **Total**: €1,125.30
## Line Item Level Allowances
Line item allowances apply to specific products/services.
### Product-Specific Discount
```json theme={null}
{
"items": [
{
"description": "Product D",
"quantity": 10,
"unit": "C62",
"unit_price": 100.00,
"tax_rate": "21.00",
"allowances": [
{
"amount": 100.00,
"reason": "10% discount on this product",
"tax_code": "S",
"tax_rate": "21.00"
}
]
},
{
"description": "Product E",
"quantity": 5,
"unit": "C62",
"unit_price": 50.00,
"tax_rate": "21.00"
}
]
}
```
**Calculation**:
* Line 1: (10 × €100) - €100 = €900.00
* Line 2: 5 × €50 = €250.00
* Subtotal: €1,150.00
* VAT (21%): €241.50
* **Total**: €1,391.50
### Multiple Allowances on One Line Item
```json theme={null}
{
"items": [
{
"description": "Product F",
"quantity": 20,
"unit": "C62",
"unit_price": 100.00,
"tax_rate": "21.00",
"allowances": [
{
"amount": 100.00,
"reason": "Volume discount (5%)",
"tax_code": "S",
"tax_rate": "21.00"
},
{
"amount": 50.00,
"reason": "Loyalty program discount",
"tax_code": "S",
"tax_rate": "21.00"
}
]
}
]
}
```
**Line 1 Calculation**:
* Base: 20 × €100 = €2,000.00
* Volume discount: -€100.00
* Loyalty discount: -€50.00
* Line total: €1,850.00
## Line Item Level Charges
Apply charges to specific line items:
### Special Handling Fee
```json theme={null}
{
"items": [
{
"description": "Fragile Equipment",
"quantity": 1,
"unit": "C62",
"unit_price": 500.00,
"tax_rate": "21.00",
"charges": [
{
"amount": 50.00,
"reason": "Special handling - fragile item",
"tax_code": "S",
"tax_rate": "21.00"
}
]
}
]
}
```
**Line 1 Calculation**:
* Base: 1 × €500 = €500.00
* Special handling: +€50.00
* Line total: €550.00
## Complex Invoice Example
Here's a complete invoice using allowances and charges at both levels:
```json theme={null}
{
"document_type": "INVOICE",
"invoice_id": "INV-2024-100",
"invoice_date": "2024-10-24",
"due_date": "2024-11-24",
"currency": "EUR",
"payment_term": "Net 30 days - 2% discount if paid within 10 days",
"vendor_name": "Your Company BVBA",
"vendor_tax_id": "BE1018265814",
"vendor_address": "Main Street 123, 1000 Brussels, Belgium",
"customer_name": "Customer Company NV",
"customer_tax_id": "BE0848934496",
"customer_address": "Customer Lane 456, 2000 Antwerp, Belgium",
"items": [
{
"description": "Premium Product A",
"quantity": 20,
"unit": "C62",
"unit_price": 100.00,
"tax_rate": "21.00",
"allowances": [
{
"amount": 200.00,
"reason": "Bulk discount (10%)",
"tax_code": "S",
"tax_rate": "21.00"
}
]
},
{
"description": "Standard Product B",
"quantity": 10,
"unit": "C62",
"unit_price": 50.00,
"tax_rate": "21.00"
},
{
"description": "Heavy Equipment",
"quantity": 1,
"unit": "C62",
"unit_price": 1000.00,
"tax_rate": "21.00",
"charges": [
{
"amount": 100.00,
"reason": "Oversized item handling",
"tax_code": "S",
"tax_rate": "21.00"
}
]
}
],
"allowances": [
{
"amount": 150.00,
"reason": "Early payment discount (5%)",
"tax_code": "S",
"tax_rate": "21.00"
}
],
"charges": [
{
"amount": 50.00,
"reason": "Delivery service",
"tax_code": "S",
"tax_rate": "21.00"
}
]
}
```
**Detailed Calculation**:
**Line Items**:
* Line 1: (20 × €100) - €200 (bulk) = €1,800.00
* Line 2: 10 × €50 = €500.00
* Line 3: €1,000 + €100 (handling) = €1,100.00
* Line items subtotal: €3,400.00
**Document Level**:
* Early payment allowance: -€150.00
* Delivery charge: +€50.00
* Net amount: €3,300.00
**Tax**:
* VAT (21% on €3,300): €693.00
**Total**: €3,993.00
## Complete Code Example
```javascript theme={null}
const axios = require('axios');
const api = axios.create({
baseURL: 'https://api.e-invoice.be',
headers: {
'Authorization': `Bearer ${process.env.E_INVOICE_API_KEY}`,
'Content-Type': 'application/json'
}
});
async function createAdvancedInvoice() {
const invoiceData = {
document_type: 'INVOICE',
invoice_id: 'INV-2024-100',
invoice_date: '2024-10-24',
due_date: '2024-11-24',
currency: 'EUR',
payment_term: 'Net 30 days - 2% discount if paid within 10 days',
vendor_name: 'Your Company BVBA',
vendor_tax_id: 'BE1018265814',
vendor_address: 'Main Street 123, 1000 Brussels, Belgium',
customer_name: 'Customer Company NV',
customer_tax_id: 'BE0848934496',
customer_address: 'Customer Lane 456, 2000 Antwerp, Belgium',
items: [
{
description: 'Premium Product A',
quantity: 20,
unit: 'C62',
unit_price: 100.00,
tax_rate: '21.00',
allowances: [
{
amount: 200.00,
reason: 'Bulk discount (10%)',
tax_code: 'S',
tax_rate: '21.00'
}
]
},
{
description: 'Standard Product B',
quantity: 10,
unit: 'C62',
unit_price: 50.00,
tax_rate: '21.00'
}
],
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'
}
]
};
try {
// 1. Validate
console.log('Validating invoice...');
const validation = await api.post('/api/validate/json', invoiceData);
if (!validation.data.is_valid) {
console.error('Validation failed:', validation.data.issues);
return;
}
console.log('✓ Invoice validated');
// 2. Create
console.log('Creating invoice...');
const invoice = await api.post('/api/documents/', invoiceData);
console.log('✓ Invoice created:', invoice.data.id);
// 3. Send
console.log('Sending invoice...');
const result = await api.post(`/api/documents/${invoice.data.id}/send`);
console.log('✓ Invoice sent:', result.data.state);
} catch (error) {
console.error('Error:', error.response?.data || error.message);
}
}
createAdvancedInvoice();
```
## Tax Considerations
### Same Tax Rate
Allowances and charges typically use the same tax rate as the line items:
```json theme={null}
{
"items": [
{
"tax_rate": "21.00"
}
],
"allowances": [
{
"tax_rate": "21.00", // Match line items
"tax_code": "S"
}
]
}
```
### Different Tax Rates
If your invoice has multiple tax rates, specify the appropriate rate for each allowance/charge:
```json theme={null}
{
"items": [
{
"description": "Standard rate item",
"unit_price": 100.00,
"tax_rate": "21.00"
},
{
"description": "Reduced rate item",
"unit_price": 50.00,
"tax_rate": "6.00"
}
],
"allowances": [
{
"amount": 50.00,
"reason": "General discount on standard rate items",
"tax_rate": "21.00",
"tax_code": "S"
}
]
}
```
### Tax-Exempt Allowances/Charges
For tax-exempt items:
```json theme={null}
{
"allowances": [
{
"amount": 100.00,
"reason": "Discount on exempt items",
"tax_rate": "0.00",
"tax_code": "E"
}
]
}
```
## Context-Specific UBL Requirements
UBL (Universal Business Language) compliance depends on the context of your invoice. Depending on who the vendor is and the jurisdiction, UBL requires different fields to be present for the document to be valid. This is why **validation is critical before document creation**.
### Why Validation Matters
Consider two similar invoice structures:
**Example 1: Valid UBL Document**
```xml theme={null}
```
This document passes validation because all required fields for this particular vendor and jurisdiction are present.
**Example 2: Invalid UBL Document**
```xml theme={null}
```
This document fails validation because certain fields required by UBL BIS Billing 3.0 for this vendor context are missing.
### Context-Dependent Fields
What makes an invoice "valid" depends on:
* **Vendor jurisdiction** - Different countries have different requirements
* **Vendor business type** - Public sector, private company, non-profit, etc.
* **Invoice type** - Regular invoice, credit note, etc.
* **Customer type** - B2B, B2G, B2C, etc.
* **Tax obligations** - VAT, reverse charge, exemptions, etc.
The e-invoice.be API automatically converts your JSON to UBL BIS Billing 3.0 XML. **Always validate before creating documents** to ensure your JSON structure will convert to a valid UBL document for your specific context.
### Best Practice: Validate First
```javascript theme={null}
// Step 1: Always validate BEFORE creating documents
const validationResult = await api.post('/api/validate/json', invoiceData);
if (!validationResult.data.is_valid) {
// Check which fields are missing or incorrect
console.error('Validation errors:', validationResult.data.issues);
// Fix the data based on validation feedback
return;
}
// Step 2: Only after validation passes, create the document
const invoiceResult = await api.post('/api/documents/', invoiceData);
```
The validation endpoint checks:
* ✓ JSON structure matches API schema
* ✓ All required fields for your vendor context are present
* ✓ The JSON can be converted to valid UBL BIS Billing 3.0
* ✓ Tax codes and rates are compatible
* ✓ Amounts and calculations are correct
**Never skip validation.** It catches issues early and prevents failed document transmissions.
## Best Practices
Always provide clear reasons for allowances and charges:
✓ Good:
```json theme={null}
{
"reason": "Early payment discount (2% if paid within 10 days)"
}
```
✗ Poor:
```json theme={null}
{
"reason": "Discount"
}
```
**Document level**: When the allowance/charge applies to the entire order
* Early payment discounts
* Shipping for the entire order
* Order-wide promotional discounts
**Line item level**: When it's specific to a product
* Bulk discount on one specific product
* Special handling for fragile items
* Product-specific promotions
When calculating percentage-based discounts, ensure accuracy:
```javascript theme={null}
// Calculate discount based on line items total
const lineItemsTotal = calculateLineItemsTotal(invoice.items);
const discountPercent = 5;
const discountAmount = Math.round(
lineItemsTotal * (discountPercent / 100) * 100
) / 100; // Round to 2 decimal places
```
Ensure allowances and charges use appropriate tax categories that match the items they apply to.
When offering early payment discounts, document the terms clearly:
```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"
}
]
}
```
## Common Use Cases
### 1. Volume Discounts
```json theme={null}
{
"items": [
{
"description": "Product X",
"quantity": 100,
"unit": "C62",
"unit_price": 10.00,
"tax_rate": "21.00",
"allowances": [
{
"amount": 100.00,
"reason": "Volume discount - 100+ units (10%)",
"tax_code": "S",
"tax_rate": "21.00"
}
]
}
]
}
```
### 2. Shipping and Handling
```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"
}
]
}
```
### 3. Loyalty Program Discounts
```json theme={null}
{
"allowances": [
{
"amount": 25.00,
"reason": "Gold member discount (5%)",
"tax_code": "S",
"tax_rate": "21.00"
}
]
}
```
### 4. Financial Charges
```json theme={null}
{
"charges": [
{
"amount": 2.50,
"reason": "Credit card processing fee (2.5%)",
"tax_code": "S",
"tax_rate": "21.00"
}
]
}
```
## Next Steps
Learn about credit notes with allowances
Review basic invoice creation
Test complex invoices during development
Explore all endpoints
# Creating E-Invoices
Source: https://docs.e-invoice.be/guides/creating-invoices
Learn how to create and send e-invoices via the Peppol network
## Overview
The e-invoice.be API allows you to create invoices and credit notes that comply with the European e-invoicing standard (EN 16931) and transmit them via the Peppol network.
## Before You Start
**Use a sandbox company for development and testing.** A sandbox company runs in test mode, so documents are emailed instead of being sent via Peppol, allowing you to safely test your integration. Create one from [app.e-invoice.be](https://app.e-invoice.be) and use its API key. [Learn more about test mode →](/environments)
## Workflow
Creating and sending an e-invoice involves these steps:
1. **Validate** your JSON invoice data (required during development)
2. **Create** the document - only valid invoices can be created
3. **Send** the document via Peppol (or email if test mode is enabled)
You must validate your invoice JSON **before** creating a document. The API only accepts invoices that can be converted into valid UBL BIS Billing 3.0 format. Use `/api/validate/json` during development to test your payload.
## Step 1: Validate Your Invoice Data (Required)
Before creating an invoice, validate your JSON payload using `POST /api/validate/json`.
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/validate/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"document_type": "INVOICE",
"invoice_id": "INV-2024-001",
"invoice_date": "2024-10-24",
"due_date": "2024-11-24",
"currency": "EUR",
"purchase_order": "PO-12345",
"vendor_name": "E-INVOICE BV",
"vendor_tax_id": "BE1018265814",
"vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, Belgium",
"vendor_email": "billing@e-invoice.be",
"customer_name": "OpenPeppol VZW",
"customer_tax_id": "BE0848934496",
"customer_address": "Robert Schumainplein 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"
}
],
"payment_term": "Payment due within 30 days",
"payment_details": [
{
"iban": "BE68539007547034",
"swift": "GEBABEBB",
"payment_reference": "INV-2024-001"
}
]
}'
```
### Validation Response
**Success - Ready to create:**
```json theme={null}
{
"id": "val_...",
"is_valid": true,
"issues": []
}
```
**Validation errors:**
```json theme={null}
{
"id": "val_...",
"is_valid": false,
"issues": [
{
"message": "Invalid tax ID format. Expected format: country code + number (e.g., BE1018265814 for Belgium)",
"type": "error",
"location": "vendor_tax_id"
},
{
"message": "Invalid tax rate format. Must be a percentage string (e.g., '21.00')",
"type": "error",
"location": "items[0].tax_rate"
}
]
}
```
Use `/api/validate/json` extensively during development. This endpoint does not create any documents - it only validates that your JSON can be converted to valid UBL format. See the [Validation Guide](/guides/validation) for more details.
## Step 2: Create the Invoice
Once validation passes, use the **exact same JSON payload** with `POST /api/documents/`.
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/documents/" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"document_type": "INVOICE",
"invoice_id": "INV-2024-001",
"invoice_date": "2024-10-24",
"due_date": "2024-11-24",
"currency": "EUR",
"purchase_order": "PO-12345",
"vendor_name": "E-INVOICE BV",
"vendor_tax_id": "BE1018265814",
"vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, Belgium",
"vendor_email": "billing@e-invoice.be",
"customer_name": "OpenPeppol VZW",
"customer_tax_id": "BE0848934496",
"customer_address": "Robert Schumainplein 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"
}
],
"payment_term": "Payment due within 30 days",
"payment_details": [
{
"iban": "BE68539007547034",
"swift": "GEBABEBB",
"payment_reference": "INV-2024-001"
}
]
}'
```
### Response
```json theme={null}
{
"id": "doc_abc123",
"document_type": "INVOICE",
"state": "DRAFT",
"invoice_id": "INV-2024-001",
"invoice_date": "2024-10-24",
"invoice_total": 1210.00,
"total_tax": 210.00,
"created_at": 1729468923,
"updated_at": 1729468923
}
```
## Understanding the Invoice Structure
### Required Fields
Only the `items` array is strictly required. All other fields are optional but recommended for complete invoices:
| Field | Description | Required |
| --------------- | -------------------------------------------------------- | --------------------- |
| `items` | Array of invoice line items | **Yes** (min 1) |
| `document_type` | Document type: `INVOICE`, `CREDIT_NOTE`, or `DEBIT_NOTE` | No (default: INVOICE) |
| `invoice_id` | Your unique invoice number | Recommended |
| `invoice_date` | Invoice date (ISO 8601: `YYYY-MM-DD`) | Recommended |
| `currency` | Currency code (e.g., `EUR`, `USD`, `GBP`) | No (default: EUR) |
### Vendor (Supplier) Fields
Information about your company (the seller):
| Field | Description | Example |
| ---------------- | ------------------ | ------------------------------------------------ |
| `vendor_name` | Your company name | `"E-INVOICE BV"` |
| `vendor_tax_id` | Your VAT/Tax ID | `"BE1018265814"` |
| `vendor_address` | Your full address | `"Brusselsesteenweg 119/A, 1980 Zemst, Belgium"` |
| `vendor_email` | Your contact email | `"billing@e-invoice.be"` |
The `vendor_tax_id` is your company's VAT or tax identification number (not the Peppol ID).
* For Belgian companies: Use the full VAT number including 'BE' prefix (e.g., `BE1018265814`)
* The API will automatically convert this to the appropriate Peppol ID format when transmitting via Peppol
### Customer Fields
Information about your customer (the buyer):
| Field | Description | Example |
| ------------------ | ---------------------- | ------------------------------------------------------- |
| `customer_name` | Customer company name | `"OpenPeppol VZW"` |
| `customer_tax_id` | Customer VAT/Tax ID | `"BE0848934496"` |
| `customer_address` | Customer full address | `"Robert Schumainplein 6 bus 5, 1040 Brussel, Belgium"` |
| `customer_email` | Customer contact email | `"info@openpeppol.org"` |
### Line Items
Each line item in the `items` array represents a product or service:
```json theme={null}
{
"description": "Product/Service description", // What is being sold
"quantity": 10, // Quantity
"unit": "C62", // Unit of measure (C62 = pieces/units)
"unit_price": 100.00, // Price per unit (excluding VAT)
"amount": 1000.00, // Line total (quantity × unit_price)
"tax_rate": "21.00" // VAT percentage as string
}
```
**Required item fields:**
* At least `description` or `unit_price` should be provided for meaningful invoices
**Common unit codes:**
* `C62` - Units/pieces
* `HUR` - Hours
* `DAY` - Days
* `MTR` - Meters
* `KGM` - Kilograms
**Tax rate format:**
* Must be a string representing a percentage
* Examples: `"21.00"`, `"6.00"`, `"0.00"`
* Standard Belgian VAT rates: `"21.00"` (standard), `"6.00"` (reduced), `"0.00"` (zero-rated)
### Optional Fields
```json theme={null}
{
"payment_term": "Net 30 days",
"payment_details": [
{
"iban": "BE68539007547034",
"swift": "GEBABEBB",
"payment_reference": "INV-2024-001"
}
]
}
```
```json theme={null}
{
"billing_address": "Billing Street 1, 1000 Brussels, Belgium",
"billing_address_recipient": "Accounts Payable Department",
"shipping_address": "Delivery Street 2, 2000 Antwerp, Belgium",
"shipping_address_recipient": "Warehouse Manager",
"service_address": "Service Location 3, 3000 Leuven, Belgium"
}
```
```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 the [Advanced Invoicing Guide](/guides/advanced-invoicing) for more details.
```json theme={null}
{
"purchase_order": "PO-12345",
"note": "Thank you for your business"
}
```
## Step 3: Send the Document
Once your invoice is created, send it to the recipient using `POST /api/documents/{document_id}/send`.
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/documents/doc_abc123/send" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### Response
```json theme={null}
{
"id": "doc_abc123",
"state": "TRANSIT",
"message": "Document queued for transmission"
}
```
### Peppol ID Routing
By default, the sender and receiver Peppol IDs are **automatically derived from the company identifiers** in your document:
* Derived from `vendor_tax_id` / `customer_tax_id` (VAT/tax number) OR `vendor_id` / `customer_id` (company registration number)
* For Belgian companies: Tax ID `BE1018265814` → Peppol ID `0208:1018265814`
* The `0208` scheme is mandatory for Belgian companies (required by Belgian government)
* This automatic conversion happens regardless of any endpoint IDs in UBL documents
**Belgian Peppol Requirement**: All Belgian companies must use the `0208` scheme with their enterprise number (CBE). This is automatically handled when you provide a Belgian tax ID starting with `BE`.
### Explicitly Specifying Peppol IDs (Recommended)
**Best Practice**: Always explicitly specify sender and receiver Peppol IDs using query parameters to ensure documents are routed to the correct endpoints. While automatic derivation works in most cases, explicit routing prevents delivery failures and ensures full control over transmission.
To send to a specific Peppol endpoint, explicitly set the Peppol IDs using query parameters:
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/documents/doc_abc123/send?sender_peppol_scheme=0208&sender_peppol_id=1018265814&receiver_peppol_scheme=0208&receiver_peppol_id=0848934496" \
-H "Authorization: Bearer YOUR_API_KEY"
```
**Available query parameters**:
| Parameter | Description | Example |
| ------------------------ | ---------------------------- | --------------- |
| `sender_peppol_scheme` | Sender's Peppol scheme ID | `0208` |
| `sender_peppol_id` | Sender's Peppol identifier | `1018265814` |
| `receiver_peppol_scheme` | Receiver's Peppol scheme ID | `0088` |
| `receiver_peppol_id` | Receiver's Peppol identifier | `1234567890123` |
**Common Peppol schemes**:
* `0208` - Belgian enterprise number (BE)
* `0088` - Global Location Number (GLN)
* `0106` - Dutch KVK
Always verify the recipient is registered at the specified Peppol endpoint before sending. Use `/api/validate/peppol-id?peppol_id=scheme:identifier` to confirm registration and prevent delivery failures.
### What Happens Next?
The behavior depends on whether test mode is enabled:
**Test mode disabled (production):**
* Document is transmitted via the Peppol network
* Recipient receives it in their official e-invoicing system
* Delivery confirmed via Access Point acknowledgments
**Test mode enabled (testing):**
* Document is converted to UBL XML
* UBL XML is sent via email to the address configured for your account
* No actual Peppol transmission occurs
* Perfect for testing without affecting real recipients
Test mode is delivered through a sandbox company. Create one from [app.e-invoice.be](https://app.e-invoice.be) for development, and use a separate regular company when you're ready to go live.
### Document States
| State | Description |
| ---------- | ----------------------------------- |
| `DRAFT` | Created but not sent |
| `TRANSIT` | Being transmitted (Peppol or email) |
| `SENT` | Successfully delivered to recipient |
| `FAILED` | Transmission failed |
| `RECEIVED` | Received from another party |
**Automatic Retries**: Documents in `TRANSIT` use an exponential backoff strategy with 10 retry attempts before transitioning to `FAILED`. Retry delays are: 1, 2, 4, 8, 16, 32, 64, 128, 256, and 360 minutes (final retry capped at 6 hours). This maximizes delivery success during temporary network issues.
**Manual Retry**: If a document reaches `FAILED` state, you can retry delivery by calling `POST /api/documents/{document_id}/send` again, which will transition it back to `TRANSIT` for another delivery attempt.
Track delivery status using webhooks or by polling `GET /api/documents/{document_id}`
**Coming soon**: Detailed transmission attempt history will be available in a future release, showing all retry attempts with timestamps and failure reasons.
## Complete Example
Here's a complete Node.js example:
```javascript theme={null}
const axios = require('axios');
// Most users should use the production API
const BASE_URL = 'https://api.e-invoice.be';
const api = axios.create({
baseURL: BASE_URL,
headers: {
'Authorization': `Bearer ${process.env.E_INVOICE_API_KEY}`,
'Content-Type': 'application/json'
}
});
async function createAndSendInvoice() {
const invoiceData = {
document_type: 'INVOICE',
invoice_id: 'INV-2024-001',
invoice_date: '2024-10-24',
due_date: '2024-11-24',
currency: 'EUR',
vendor_name: 'E-INVOICE BV',
vendor_tax_id: 'BE1018265814',
vendor_address: 'Brusselsesteenweg 119/A, 1980 Zemst, Belgium',
vendor_email: 'billing@e-invoice.be',
customer_name: 'OpenPeppol VZW',
customer_tax_id: 'BE0848934496',
customer_address: 'Robert Schumainplein 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'
}
],
payment_term: 'Net 30 days',
payment_details: [
{
iban: 'BE68539007547034',
swift: 'GEBABEBB',
payment_reference: 'INV-2024-001'
}
]
};
try {
// 1. Validate JSON (always do this during development)
const validation = await api.post('/api/validate/json', invoiceData);
if (!validation.data.is_valid) {
console.error('Validation failed:', validation.data.issues);
return;
}
console.log('✓ Invoice JSON is valid');
// 2. Create invoice (uses same payload)
const invoice = await api.post('/api/documents/', invoiceData);
console.log('✓ Invoice created:', invoice.data.id);
// 3. Send (via email if test mode enabled, Peppol if disabled)
const result = await api.post(`/api/documents/${invoice.data.id}/send`);
console.log('✓ Invoice sent:', result.data.state);
} catch (error) {
console.error('Error:', error.response?.data || error.message);
}
}
createAndSendInvoice();
```
If test mode is enabled on your account, this will email the UBL XML. If test mode is disabled, it will send via Peppol. Your code doesn't need to change - just the test mode setting on your account.
## Working with Credit Notes
Credit notes follow the same structure, but set `document_type: "CREDIT_NOTE"`:
```json theme={null}
{
"document_type": "CREDIT_NOTE",
"invoice_id": "CN-2024-001",
"purchase_order": "INV-2024-001", // Original invoice reference
"invoice_date": "2024-10-24",
"currency": "EUR",
"vendor_name": "Your Company",
"vendor_tax_id": "BE1018265814",
"customer_name": "Customer Company",
"customer_tax_id": "BE0848934496",
"items": [ ... ]
// ... rest of the structure same as invoices
}
```
## Next Steps
Test invoices during development
Find customer Peppol IDs
Get delivery notifications
Explore all endpoints
# Creating Credit Notes
Source: https://docs.e-invoice.be/guides/credit-notes
Learn how to create and send credit notes via Peppol
## Overview
Credit notes are used to reduce or cancel an invoice that has already been issued. Common scenarios include:
* **Partial or full refunds** - Customer returns goods or cancels services
* **Invoice corrections** - Price errors, wrong quantities, or incorrect amounts
* **Discounts after invoicing** - Retroactive discounts or price adjustments
* **Cancellations** - Voiding an incorrect invoice
Like invoices, credit notes are sent via Peppol and follow the UBL BIS Billing 3.0 standard.
## Workflow
Creating and sending a credit note follows the same workflow as invoices:
1. **Validate** your JSON credit note data (required)
2. **Create** the credit note document
3. **Send** via Peppol
Always validate your credit note JSON using `/api/validate/json` before creating the document. Only valid JSON that converts to UBL BIS Billing 3.0 is accepted.
## Basic Credit Note
Here's a simple credit note that refunds a full invoice:
### Step 1: Validate the Credit Note
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/validate/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"document_type": "CREDIT_NOTE",
"invoice_id": "CN-2024-001",
"invoice_date": "2024-10-24",
"currency": "EUR",
"purchase_order": "INV-2024-001",
"note": "Full refund - goods returned",
"vendor_name": "Your Company BVBA",
"vendor_tax_id": "BE0123456789",
"vendor_address": "Main Street 123, 1000 Brussels, Belgium",
"customer_name": "Customer Company NV",
"customer_tax_id": "BE0987654321",
"customer_address": "Customer Lane 456, 2000 Antwerp, Belgium",
"items": [
{
"description": "Product A - Returned",
"quantity": 10,
"unit": "C62",
"unit_price": 100.00,
"tax_rate": "21.00"
}
]
}'
```
### Key Differences from Invoices
| Field | Credit Note | Invoice |
| ---------------- | ----------------------------------------- | ------------------------ |
| `document_type` | `"CREDIT_NOTE"` | `"INVOICE"` |
| `invoice_id` | Credit note number (e.g., CN-2024-001) | Invoice number |
| `purchase_order` | **Recommended** - Original invoice number | Purchase order reference |
| `note` | **Recommended** - Reason for credit note | Optional |
Use the `purchase_order` field to reference the original invoice number. This creates a clear audit trail linking the credit note to the original invoice.
### Step 2: Create the Credit Note
Once validation passes, create the document:
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/documents/" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"document_type": "CREDIT_NOTE",
"invoice_id": "CN-2024-001",
"invoice_date": "2024-10-24",
"currency": "EUR",
"purchase_order": "INV-2024-001",
"note": "Full refund - goods returned",
"vendor_name": "Your Company BVBA",
"vendor_tax_id": "BE0123456789",
"vendor_address": "Main Street 123, 1000 Brussels, Belgium",
"customer_name": "Customer Company NV",
"customer_tax_id": "BE0987654321",
"customer_address": "Customer Lane 456, 2000 Antwerp, Belgium",
"items": [
{
"description": "Product A - Returned",
"quantity": 10,
"unit": "C62",
"unit_price": 100.00,
"tax_rate": "21.00"
}
]
}'
```
### Step 3: Send via Peppol
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/documents/{document_id}/send" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Partial Credit Notes
To credit only part of an invoice, adjust the quantities or prices:
```json theme={null}
{
"document_type": "CREDIT_NOTE",
"invoice_id": "CN-2024-002",
"invoice_date": "2024-10-24",
"currency": "EUR",
"purchase_order": "INV-2024-001",
"note": "Partial refund - 5 of 10 items returned",
"vendor_name": "Your Company BVBA",
"vendor_tax_id": "BE0123456789",
"vendor_address": "Main Street 123, 1000 Brussels, Belgium",
"customer_name": "Customer Company NV",
"customer_tax_id": "BE0987654321",
"customer_address": "Customer Lane 456, 2000 Antwerp, Belgium",
"items": [
{
"description": "Product A - Partial return",
"quantity": 5,
"unit": "C62",
"unit_price": 100.00,
"tax_rate": "21.00"
}
]
}
```
**Result**: Credit note for €605.00 (5 × €100 + 21% VAT)
## Price Correction Credit Notes
To correct a pricing error, credit the difference:
```json theme={null}
{
"document_type": "CREDIT_NOTE",
"invoice_id": "CN-2024-003",
"invoice_date": "2024-10-24",
"currency": "EUR",
"purchase_order": "INV-2024-001",
"note": "Price correction - overcharged by €10 per unit",
"vendor_name": "Your Company BVBA",
"vendor_tax_id": "BE0123456789",
"vendor_address": "Main Street 123, 1000 Brussels, Belgium",
"customer_name": "Customer Company NV",
"customer_tax_id": "BE0987654321",
"customer_address": "Customer Lane 456, 2000 Antwerp, Belgium",
"items": [
{
"description": "Product A - Price adjustment",
"quantity": 10,
"unit": "C62",
"unit_price": 10.00,
"tax_rate": "21.00"
}
]
}
```
**Result**: Credit note for €121.00 (10 × €10 difference + 21% VAT)
## Credit Note with Multiple Line Items
Credit notes can include multiple line items:
```json theme={null}
{
"document_type": "CREDIT_NOTE",
"invoice_id": "CN-2024-004",
"invoice_date": "2024-10-24",
"currency": "EUR",
"purchase_order": "INV-2024-001",
"note": "Mixed return - multiple products",
"vendor_name": "Your Company BVBA",
"vendor_tax_id": "BE0123456789",
"vendor_address": "Main Street 123, 1000 Brussels, Belgium",
"customer_name": "Customer Company NV",
"customer_tax_id": "BE0987654321",
"customer_address": "Customer Lane 456, 2000 Antwerp, Belgium",
"items": [
{
"description": "Product A - Returned",
"quantity": 5,
"unit": "C62",
"unit_price": 100.00,
"tax_rate": "21.00"
},
{
"description": "Product B - Damaged",
"quantity": 3,
"unit": "C62",
"unit_price": 50.00,
"tax_rate": "21.00"
}
]
}
```
## Credit Notes with Allowances
You can include allowances (discounts) on credit notes:
```json theme={null}
{
"document_type": "CREDIT_NOTE",
"invoice_id": "CN-2024-005",
"invoice_date": "2024-10-24",
"currency": "EUR",
"purchase_order": "INV-2024-001",
"note": "Return with goodwill discount",
"vendor_name": "Your Company BVBA",
"vendor_tax_id": "BE0123456789",
"vendor_address": "Main Street 123, 1000 Brussels, Belgium",
"customer_name": "Customer Company NV",
"customer_tax_id": "BE0987654321",
"customer_address": "Customer Lane 456, 2000 Antwerp, Belgium",
"items": [
{
"description": "Product A",
"quantity": 10,
"unit": "C62",
"unit_price": 100.00,
"tax_rate": "21.00"
}
],
"allowances": [
{
"amount": 100.00,
"reason": "Goodwill gesture",
"tax_code": "S",
"tax_rate": "21.00"
}
]
}
```
**Calculation**:
* Line items: €1,000.00
* Allowance: -€100.00
* Subtotal: €900.00
* VAT (21%): €189.00
* **Total credit: €1,089.00**
## Payment Terms
Credit notes can include payment details if you're issuing a refund:
```json theme={null}
{
"document_type": "CREDIT_NOTE",
"invoice_id": "CN-2024-006",
"invoice_date": "2024-10-24",
"currency": "EUR",
"purchase_order": "INV-2024-001",
"note": "Refund to be processed",
"payment_term": "Refund will be processed within 14 days",
"vendor_name": "Your Company BVBA",
"vendor_tax_id": "BE0123456789",
"vendor_address": "Main Street 123, 1000 Brussels, Belgium",
"customer_name": "Customer Company NV",
"customer_tax_id": "BE0987654321",
"customer_address": "Customer Lane 456, 2000 Antwerp, Belgium",
"payment_details": [
{
"iban": "BE68539007547034",
"swift": "GEBABEBB",
"payment_reference": "CN-2024-006"
}
],
"items": [
{
"description": "Product A",
"quantity": 10,
"unit": "C62",
"unit_price": 100.00,
"tax_rate": "21.00"
}
]
}
```
## Complete Example
Here's a complete Node.js example for creating and sending a credit note:
```javascript theme={null}
const axios = require('axios');
const api = axios.create({
baseURL: 'https://api.e-invoice.be',
headers: {
'Authorization': `Bearer ${process.env.E_INVOICE_API_KEY}`,
'Content-Type': 'application/json'
}
});
async function createAndSendCreditNote() {
const creditNoteData = {
document_type: 'CREDIT_NOTE',
invoice_id: 'CN-2024-001',
invoice_date: '2024-10-24',
currency: 'EUR',
purchase_order: 'INV-2024-001',
note: 'Full refund - goods returned',
vendor_name: 'Your Company BVBA',
vendor_tax_id: 'BE0123456789',
vendor_address: 'Main Street 123, 1000 Brussels, Belgium',
customer_name: 'Customer Company NV',
customer_tax_id: 'BE0987654321',
customer_address: 'Customer Lane 456, 2000 Antwerp, Belgium',
items: [
{
description: 'Product A - Returned',
quantity: 10,
unit: 'C62',
unit_price: 100.00,
tax_rate: '21.00'
}
]
};
try {
// 1. Validate JSON
console.log('Validating credit note...');
const validation = await api.post('/api/validate/json', creditNoteData);
if (!validation.data.is_valid) {
console.error('Validation failed:', validation.data.issues);
return;
}
console.log('✓ Credit note JSON is valid');
// 2. Create credit note
console.log('Creating credit note...');
const creditNote = await api.post('/api/documents/', creditNoteData);
console.log('✓ Credit note created:', creditNote.data.id);
// 3. Send via Peppol
console.log('Sending credit note...');
const result = await api.post(`/api/documents/${creditNote.data.id}/send`);
console.log('✓ Credit note sent:', result.data.state);
} catch (error) {
console.error('Error:', error.response?.data || error.message);
}
}
createAndSendCreditNote();
```
## Best Practices
Use the `purchase_order` field to link to the original invoice:
```json theme={null}
{
"purchase_order": "INV-2024-001"
}
```
This maintains a clear audit trail and helps customers match credits to invoices.
Use the `note` field to explain why the credit note is being issued:
```json theme={null}
{
"note": "Full refund - goods returned damaged"
}
```
Also use descriptive line item descriptions:
```json theme={null}
{
"description": "Product A - Returned (damaged in transit)"
}
```
If crediting specific line items from the original invoice, use the same descriptions for consistency:
```json theme={null}
// Original invoice line item
{
"description": "Professional Services",
"unit_price": 100.00
}
// Credit note line item (same structure)
{
"description": "Professional Services - Credit",
"unit_price": 100.00
}
```
Ensure tax rates on the credit note match the original invoice, even if tax rates have changed since then. The credit note should use the rates from the original transaction date.
Credit note amounts should be **positive** values, not negative. The system interprets the entire credit note as a reduction:
❌ Wrong:
```json theme={null}
{
"unit_price": -100.00 // Don't use negative
}
```
✓ Correct:
```json theme={null}
{
"unit_price": 100.00 // Use positive amounts
}
```
Track credit notes the same way as invoices:
* `DRAFT` - Created but not sent
* `TRANSIT` - Being transmitted
* `SENT` - Successfully delivered
* `FAILED` - Transmission failed
* `RECEIVED` - Received from another party
Documents in `TRANSIT` use automatic retry with exponential backoff (10 attempts with delays: 1, 2, 4, 8, 16, 32, 64, 128, 256, and 360 minutes) before transitioning to `FAILED`.
If a document reaches `FAILED` state, you can retry by calling `POST /api/documents/{document_id}/send` again.
Use webhooks to monitor delivery status.
## Common Use Cases
### 1. Full Invoice Cancellation
```json theme={null}
{
"document_type": "CREDIT_NOTE",
"invoice_id": "CN-2024-007",
"invoice_date": "2024-10-24",
"currency": "EUR",
"purchase_order": "INV-2024-001",
"note": "Invoice cancelled - issued in error",
"vendor_name": "Your Company BVBA",
"vendor_tax_id": "BE0123456789",
"vendor_address": "Main Street 123, 1000 Brussels, Belgium",
"customer_name": "Customer Company NV",
"customer_tax_id": "BE0987654321",
"customer_address": "Customer Lane 456, 2000 Antwerp, Belgium",
"items": [
// Include all line items from original invoice with same quantities
]
}
```
### 2. Product Returns
```json theme={null}
{
"document_type": "CREDIT_NOTE",
"invoice_id": "CN-2024-008",
"invoice_date": "2024-10-24",
"currency": "EUR",
"purchase_order": "INV-2024-001",
"note": "Product return - not as described",
"vendor_name": "Your Company BVBA",
"vendor_tax_id": "BE0123456789",
"vendor_address": "Main Street 123, 1000 Brussels, Belgium",
"customer_name": "Customer Company NV",
"customer_tax_id": "BE0987654321",
"customer_address": "Customer Lane 456, 2000 Antwerp, Belgium",
"items": [
{
"description": "Product A - Returned",
"quantity": 3,
"unit": "C62",
"unit_price": 100.00,
"tax_rate": "21.00"
}
]
}
```
### 3. Price Adjustments
```json theme={null}
{
"document_type": "CREDIT_NOTE",
"invoice_id": "CN-2024-009",
"invoice_date": "2024-10-24",
"currency": "EUR",
"purchase_order": "INV-2024-001",
"note": "Price adjustment - volume discount applied retroactively",
"vendor_name": "Your Company BVBA",
"vendor_tax_id": "BE0123456789",
"vendor_address": "Main Street 123, 1000 Brussels, Belgium",
"customer_name": "Customer Company NV",
"customer_tax_id": "BE0987654321",
"customer_address": "Customer Lane 456, 2000 Antwerp, Belgium",
"items": [
{
"description": "Volume discount adjustment",
"quantity": 10,
"unit": "C62",
"unit_price": 5.00,
"tax_rate": "21.00"
}
]
}
```
### 4. Service Cancellation
```json theme={null}
{
"document_type": "CREDIT_NOTE",
"invoice_id": "CN-2024-010",
"invoice_date": "2024-10-24",
"currency": "EUR",
"purchase_order": "INV-2024-001",
"note": "Service cancellation - pro-rata refund",
"vendor_name": "Your Company BVBA",
"vendor_tax_id": "BE0123456789",
"vendor_address": "Main Street 123, 1000 Brussels, Belgium",
"customer_name": "Customer Company NV",
"customer_tax_id": "BE0987654321",
"customer_address": "Customer Lane 456, 2000 Antwerp, Belgium",
"items": [
{
"description": "Consulting services - unused portion",
"quantity": 1,
"unit": "DAY",
"unit_price": 500.00,
"tax_rate": "21.00"
}
]
}
```
## Troubleshooting
### Credit Note Rejected
**Error**: `purchase_order is required for credit notes`
**Solution**: Always include the `purchase_order` field with the original invoice number to create a clear audit trail.
### Amount Mismatch Concerns
**Question**: "My credit note total is higher than the original invoice"
**Solution**: This can be valid if you're crediting the invoice amount plus adding a goodwill gesture or covering return shipping costs. Ensure your accounting system can handle this scenario.
### Customer Didn't Receive Credit Note
**Check**:
1. Verify the customer's Peppol ID is correct
2. Check document state: `GET /api/documents/{document_id}`
3. Review transmission report: `GET /api/documents/{document_id}/transmission-report`
4. Check webhook notifications for delivery status
## Next Steps
Learn about allowances and charges
Review invoice creation guide
Monitor credit note delivery
Explore all endpoints
# Invoice Totals and Calculations
Source: https://docs.e-invoice.be/guides/invoice-totals
Understanding how invoice totals are calculated in the e-invoice.be API
This guide explains how invoice totals are calculated and what each field represents in our e-invoicing system.
## Overview of Total Fields
Our API uses the following fields to represent invoice amounts:
| Field | Description | Can be negative? |
| ------------------------- | -------------------------------------------------------------------------------- | ---------------- |
| `subtotal` | The taxable base (amount subject to VAT) after document-level allowances/charges | No |
| `total_discount` | Total document-level allowances (discounts) | No |
| `total_tax` | The total VAT/tax amount | No |
| `invoice_total` | The final invoice amount including tax | No (usually) |
| `amount_due` | The amount the customer needs to pay after prepayments | No |
| `previous_unpaid_balance` | Any outstanding balance from previous invoices | No |
## The Basic Formula
```
invoice_total = subtotal + total_tax
amount_due = invoice_total - prepaid_amount
```
Note: The `subtotal` is calculated after applying document-level allowances and charges, so `total_discount` is already factored into the subtotal.
## Understanding Each Field
### 1. Subtotal (Taxable Base)
The **subtotal** represents the taxable base - the amount on which VAT is calculated.
**Calculation:**
```
subtotal = sum of line items
- document-level allowances with VAT
+ document-level charges with VAT
```
**Key points:**
* This is always positive
* Only includes allowances and charges that have VAT applied to them
* This is the base amount used for tax calculation
**Example:**
```
Line items: €1,000.00
Commercial discount (21% VAT): -€100.00
Shipping charge (21% VAT): +€50.00
→ Subtotal: €950.00
```
### 2. Total Tax
The **total\_tax** is the total amount of VAT calculated on the subtotal.
**Calculation:**
* VAT is calculated on the subtotal for each applicable tax rate
* Multiple tax rates are grouped and calculated separately
* The results are summed to get the total tax
**Example:**
```
Subtotal at 21% VAT: €950.00
→ Total tax: €199.50 (950.00 × 0.21)
```
### 3. Total Discount
The **total\_discount** field represents the total amount of document-level allowances (discounts) applied to the invoice.
**Calculation:**
```
total_discount = sum of all document-level allowances
```
**Key points:**
* This field is always positive (or zero)
* Represents the total value of discounts given at the document level
* These allowances are already factored into the `subtotal`
* Examples: Early payment discounts, volume discounts, promotional discounts
**Example:**
```
Line items: €1,000.00
Early payment discount (21% VAT): -€50.00
→ Total discount: €50.00
→ Subtotal (after discount): €950.00
→ Tax (21%): €199.50
→ Invoice total: €1,149.50
```
### 4. Invoice Total
The **invoice\_total** is the final amount of the invoice including VAT.
**Calculation:**
```
invoice_total = subtotal + total_tax
```
This is the amount before any prepayments are deducted. The subtotal already includes the effect of document-level allowances and charges.
### 5. Amount Due
The **amount\_due** is what the customer actually needs to pay.
**Calculation:**
```
amount_due = invoice_total - prepaid_amount
```
**Example with prepayment:**
```
Invoice total: €1,199.50
Prepaid amount: €200.00
→ Amount due: €999.50
```
### 6. Previous Unpaid Balance
The **previous\_unpaid\_balance** represents any outstanding amounts from previous invoices. This is a custom field not part of standard UBL.
## Document-Level vs Line-Level Allowances and Charges
Understanding where to apply adjustments is crucial:
### Document-Level Allowances/Charges
* **Affect the subtotal** (taxable base)
* Applied to the entire invoice after line items are totaled
* VAT is calculated on the adjusted amount
* Contribute to the `total_discount` field (for allowances)
* Examples: Early payment discounts, shipping charges for the entire order, handling fees
### Line-Level Allowances/Charges
* **Affect individual line item amounts**
* Applied to specific products or services
* Do NOT appear in the document-level `total_discount` field
* Examples: Bulk discount on a specific product, special handling for fragile items
## Complete Example
Here's a complete invoice calculation:
```json theme={null}
{
"items": [
{
"description": "Product A",
"quantity": 10,
"unit_price": 100.00,
"amount": 1000.00,
"tax_rate": "21.00"
}
],
"allowances": [
{
"reason": "Commercial discount",
"amount": 200.00,
"tax_rate": "21.00"
},
{
"reason": "Early payment discount",
"amount": 50.00,
"tax_rate": "21.00"
}
],
"charges": [
{
"reason": "Shipping",
"amount": 50.00,
"tax_rate": "21.00"
}
]
}
```
**Calculation breakdown:**
1. **Line items total:** €1,000.00
2. **Apply document-level allowances:**
* Commercial discount: -€200.00
* Early payment discount: -€50.00
3. **Apply document-level charges:**
* Shipping: +€50.00
4. **Subtotal (taxable base):** €800.00
5. **Calculate tax:** €800.00 × 21% = €168.00
6. **Total discount:** €250.00 (sum of allowances only)
7. **Invoice total:** €800.00 + €168.00 = **€968.00**
8. **Amount due:** €968.00 (no prepayment)
## UBL Mapping
For reference, here's how our fields map to UBL (Universal Business Language) elements:
| Our 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` |
**Key points about UBL mapping:**
* `TaxExclusiveAmount` (subtotal) is the taxable base after applying document-level allowances and charges
* `AllowanceTotalAmount` (total\_discount) represents the sum of all document-level allowances
* `TaxInclusiveAmount` (invoice\_total) is the total including VAT
* `PayableAmount` (amount\_due) is the final amount to be paid after prepayments
## Validation Rules
When creating or updating invoices, the following validations are applied:
1. **Subtotal** must match the calculated taxable base after document-level adjustments (within €0.01 tolerance)
2. **Total tax** must match the calculated VAT amount (within €0.01 tolerance)
3. **Total discount** must match the sum of document-level allowances (within €0.01 tolerance)
4. **Invoice total** must equal `subtotal + total_tax` (within €0.01 tolerance)
5. **Amount due** must be between 0 and invoice\_total (inclusive)
## Common Questions
### What's the difference between document-level and line-level allowances?
* **Document-level allowances** apply to the entire invoice and are reflected in the `total_discount` field
* **Line-level allowances** apply to specific line items and are included in the line item calculations, not in `total_discount`
### Can invoice\_total be negative?
In theory yes, if you have large allowances, but this is unusual. Most invoices should have a positive invoice total. Credit notes are the proper way to issue refunds.
### What if I don't provide these fields?
If you don't provide `subtotal`, `total_tax`, `total_discount`, or `invoice_total`, the system will automatically calculate them based on your line items, allowances, and charges. The calculated values will be validated if you do provide them.
### How are document-level charges handled?
Document-level charges (like shipping fees) increase the subtotal before VAT is calculated. They are not reflected in the `total_discount` field, which only includes allowances (discounts).
## Need Help?
If you have questions about invoice calculations or need help structuring your invoice data, please refer to our [API documentation](/api-reference) or contact support.
# Looking Up Peppol Participants
Source: https://docs.e-invoice.be/guides/lookup-participants
Find and verify customer Peppol IDs before sending invoices
## Overview
Before sending an e-invoice via Peppol, you need to know your customer's **Peppol ID** and verify they can receive invoices. The e-invoice.be API provides two distinct lookup methods, each with different data sources and use cases:
1. **`/api/lookup`** - Direct SMP lookup (real-time, always accurate)
2. **`/api/lookup/participants`** - Peppol Directory search (broader search, may be incomplete)
Understanding the difference between these endpoints is crucial for reliable participant verification.
## What is a Peppol ID?
A Peppol ID is a unique identifier for organizations registered on the Peppol network. It consists of:
* **Scheme**: The identifier scheme (e.g., `0208` for Belgian companies)
* **Identifier**: The actual ID
Format: `scheme:identifier` → `0208:0123456789`
**Belgian Peppol IDs** use the CBE number (Crossroads Bank for Enterprises number), which is equivalent to the Belgian VAT number **without the 'BE' prefix**. For example, if the VAT number is BE0123456789, the Peppol ID is `0208:0123456789` (numbers only).
Organizations must be registered with a Peppol Access Point to receive e-invoices. If a customer is not registered, they cannot receive invoices via Peppol.
## `/api/lookup` - Direct SMP Lookup
### What It Does
Performs an **exact, real-time lookup** directly against the Service Metadata Publisher (SMP) using a specific identifier. This queries the authoritative source for participant registration information.
### Key Characteristics
* **Always accurate**: Queries the SMP in real-time (a-la-minute)
* **Exact match required**: Requires precise Peppol ID
* **Authoritative data**: Returns the current registration status directly from the SMP
* **100% reliable**: Always shows registered participants
### When to Use
* **Before sending invoices**: Verify a recipient can receive documents
* **Exact identifier known**: You have the CBE number or Peppol ID
* **Need certainty**: Must confirm current registration status
* **Production validation**: Pre-flight checks before document transmission
### Request
The endpoint requires a Peppol ID in the format `:`. For Belgian companies, use scheme `0208` followed by the 10-digit CBE/BTW number.
```bash theme={null}
curl -X GET "https://api.e-invoice.be/api/lookup?peppol_id=0208:1018265814" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### Response
**Registered participant** (`PeppolIdLookupResponse`, abbreviated):
```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": { "...": "supported document types and processes" },
"businessCard": {
"status": "success",
"entities": [
{ "name": "E-INVOICE BV", "countryCode": "BE" }
],
"queryTimeMs": 123.45
},
"certificates": [],
"executionTimeMs": 895.32
}
```
To decide whether a participant can receive invoices, inspect `dnsInfo.status` (DNS resolves to an SMP), whether a `businessCard` is present, and the entries under `serviceMetadata`. There is no single top-level `registered` boolean — use the [`/api/validate/peppol-id`](/guides/validation) endpoint if you want a consolidated `is_valid` result.
## `/api/lookup/participants` - Peppol Directory Search
### What It Does
Searches the **official Peppol Directory** database, which contains participant information that access points have voluntarily published. This is a proxy for the [public Peppol Directory](https://directory.peppol.eu).
### Key Characteristics
* **Search functionality**: Find participants by name or partial identifier
* **Directory-based**: Only shows participants whose access points publish to the directory
* **May be incomplete**: Not all registered participants appear in the directory
* **Discovery tool**: Useful for finding participants when exact ID is unknown
### Important Limitation
**Not all registered Peppol participants appear in the Directory**. Publishing to the Peppol Directory is optional, not mandatory. An access point may choose not to synchronize participant data with the directory, even though those participants are fully registered and can receive invoices.
### When to Use
* **Discovery**: Search for participants by company name
* **Browsing**: Explore registered participants in a country
* **Fuzzy search**: Find participants without knowing exact identifiers
* **Autocomplete features**: Suggest participants as users type
### When NOT to Use
* **Validation before sending**: Use `/api/lookup` instead for accurate verification
* **Confirming registration**: Directory absence doesn't mean unregistered
* **Production checks**: Not reliable for pre-send validation
### Request
```bash theme={null}
curl -X GET "https://api.e-invoice.be/api/lookup/participants?query=E-INVOICE&country_code=BE" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### Query Parameters
| Parameter | Required | Description | Example |
| -------------- | -------- | -------------------------------------- | ----------- |
| `query` | Yes | Search term (name or identifier) | `E-INVOICE` |
| `country_code` | No | Filter by country (ISO 3166-1 alpha-2) | `BE` |
### Response
```json theme={null}
{
"total_count": 2,
"used_count": 2,
"query_terms": "E-INVOICE",
"search_date": "2026-01-12T14:32:10.123456",
"participants": [
{
"peppol_id": "1018265814",
"peppol_scheme": "0208",
"entities": [
{
"name": "E-INVOICE BV",
"country_code": "BE",
"registration_date": "2023-05-14"
}
],
"document_types": []
},
{
"peppol_id": "0123456789",
"peppol_scheme": "0208",
"entities": [
{
"name": "Example Company BVBA",
"country_code": "BE",
"registration_date": "2022-11-02"
}
],
"document_types": []
}
]
}
```
## Comparison
| Feature | `/api/lookup` | `/api/lookup/participants` |
| ------------------ | ------------------------------------ | -------------------------------- |
| **Data source** | SMP (real-time) | Peppol Directory (database) |
| **Accuracy** | 100% accurate | May be incomplete |
| **Query type** | Exact match | Fuzzy search |
| **Use case** | Validation | Discovery |
| **Speed** | Fast (single lookup) | Moderate (database search) |
| **Reliability** | Always shows registered participants | May miss registered participants |
| **Required input** | Exact Peppol ID | Partial name or identifier |
## Common Peppol ID Schemes
Different countries use different identifier schemes:
| Country | Scheme Code | Identifier Type | Format Example |
| ------------- | ----------- | ----------------------------- | --------------------- |
| Belgium | `0208` | CBE number (VAT without 'BE') | `0208:0123456789` |
| Netherlands | `0106` | KVK number | `0106:12345678` |
| Germany | `0204` | VAT number | `0204:DE123456789` |
| France | `0009` | SIRET | `0009:12345678901234` |
| UK | `0088` | GLN | `0088:1234567890123` |
| Norway | `0192` | Organization number | `0192:123456789` |
| Sweden | `0007` | Organization number | `0007:1234567890` |
| Denmark | `0184` | CVR number | `0184:12345678` |
| International | `0088` | Global Location Number | `0088:1234567890123` |
For a complete list of Peppol identifier schemes, see the [official Peppol code list](https://docs.peppol.eu/edelivery/codelists/v9.5/Peppol%20Code%20Lists%20-%20Participant%20identifier%20schemes%20v9.5.html).
## Integration Examples
### Direct Lookup by CBE Number
```javascript theme={null}
const axios = require('axios');
const api = axios.create({
baseURL: 'https://api.e-invoice.be',
headers: {
'Authorization': `Bearer ${process.env.E_INVOICE_API_KEY}`,
'Content-Type': 'application/json'
}
});
async function lookupCustomer(cbeNumber) {
try {
// Construct Peppol ID (Belgian example)
// Note: For Belgium, use CBE number without 'BE' prefix
// If you have VAT number BE0123456789, use just 0123456789
const peppolId = `0208:${cbeNumber}`;
const response = await api.get('/api/lookup', {
params: { peppol_id: peppolId }
});
if (response.data.dnsInfo?.status === 'success') {
console.log('✓ Customer can receive e-invoices via Peppol');
console.log(' Name:', response.data.businessCard?.entities?.[0]?.name);
console.log(' Peppol ID:', peppolId);
return response.data;
}
// /api/lookup returns HTTP 200 even when not registered
console.log('✗ Customer is not registered on Peppol network');
console.log(' They cannot receive e-invoices electronically');
return null;
} catch (error) {
console.error('Error looking up customer:', error.response?.data || error.message);
return null;
}
}
// Example usage
await lookupCustomer('1018265814'); // CBE number without 'BE' prefix
```
### Discovery + Validation Pattern
Combine both endpoints for the best user experience:
```javascript theme={null}
async function findAndValidateCustomer(companyName, cbeNumber = null) {
// Step 1: If we have exact CBE number, use direct lookup
if (cbeNumber) {
console.log(`Looking up CBE ${cbeNumber} directly...`);
try {
const peppolId = `0208:${cbeNumber}`;
const response = await api.get('/api/lookup', {
params: { peppol_id: peppolId }
});
if (response.data.dnsInfo?.status === 'success') {
console.log('✓ Found via direct SMP lookup');
console.log(' Name:', response.data.businessCard?.entities?.[0]?.name);
return response.data;
}
// /api/lookup returns HTTP 200 even when not registered
console.log('✗ Not registered on Peppol network');
return null;
} catch (error) {
console.error('Direct lookup failed:', error.message);
return null;
}
}
// Step 2: Search directory by name
console.log(`Searching directory for "${companyName}"...`);
try {
const searchResponse = await api.get('/api/lookup/participants', {
params: {
query: companyName,
country_code: 'BE'
}
});
if (!searchResponse.data.participants || searchResponse.data.participants.length === 0) {
console.log('⚠ No results in Peppol Directory');
console.log(' Note: This does not mean the company is unregistered.');
console.log(' Some access points do not publish to the directory.');
console.log(' If you have the CBE number, use direct lookup instead.');
return null;
}
console.log(`Found ${searchResponse.data.participants.length} participant(s) in directory:`);
searchResponse.data.participants.forEach((p, i) => {
const peppolId = `${p.peppol_scheme}:${p.peppol_id}`;
const name = p.entities[0]?.name;
console.log(` ${i + 1}. ${name} (${peppolId})`);
});
// Step 3: Validate the first result via direct lookup
const firstResult = searchResponse.data.participants[0];
const firstPeppolId = `${firstResult.peppol_scheme}:${firstResult.peppol_id}`;
console.log(`\nValidating ${firstResult.entities[0]?.name} via direct SMP lookup...`);
const validateResponse = await api.get('/api/lookup', {
params: { peppol_id: firstPeppolId }
});
if (validateResponse.data.dnsInfo?.status === 'success') {
console.log('✓ Confirmed registration via SMP');
return validateResponse.data;
}
} catch (error) {
console.error('Search failed:', error.message);
return null;
}
}
// Example usage
// With exact CBE number (preferred)
await findAndValidateCustomer('E-INVOICE', '1018265814');
// With company name only (discovery)
await findAndValidateCustomer('E-INVOICE');
```
### Pre-Flight Check Workflow
Before creating an invoice, validate the recipient:
```javascript theme={null}
async function createInvoiceWithValidation(invoiceData) {
try {
// 1. Validate recipient is on Peppol network
const customerPeppolId = invoiceData.customer.party_legal_entity.company_id;
console.log('Checking if customer can receive e-invoices...');
const validation = await api.get('/api/lookup', {
params: { peppol_id: customerPeppolId }
});
if (validation.data.dnsInfo?.status !== 'success') {
console.error('❌ Customer cannot receive e-invoices via Peppol');
console.error(' Peppol ID:', customerPeppolId);
console.error(' Please use alternative delivery method (email, PDF)');
return null;
}
console.log('✓ Customer can receive e-invoices');
// 2. Validate invoice JSON
console.log('Validating invoice JSON...');
const jsonValidation = await api.post('/api/validate/json', invoiceData);
if (!jsonValidation.data.is_valid) {
console.error('❌ Invoice validation failed');
jsonValidation.data.issues.forEach(issue => {
console.error(` - ${issue.rule_id}: ${issue.message}`);
});
return null;
}
console.log('✓ Invoice JSON is valid');
// 3. Create the invoice
console.log('Creating invoice...');
const document = await api.post('/api/documents/', invoiceData);
console.log('✓ Invoice created:', document.data.id);
// 4. Send via Peppol
console.log('Sending via Peppol...');
const result = await api.post(`/api/documents/${document.data.id}/send`);
console.log('✓ Invoice sent:', result.data.state);
return document.data;
} catch (error) {
console.error('Error:', error.response?.data || error.message);
return null;
}
}
```
## Handling Unregistered Customers
If a customer is not on the Peppol network:
1. **Inform them**: Let them know about Peppol e-invoicing benefits
2. **Alternative delivery**: Send PDF invoices via email
3. **Register with e-invoice.be**: Customers can sign up at [e-invoice.be](https://app.e-invoice.be)
### Example: Fallback Logic
```javascript theme={null}
async function sendInvoiceToCustomer(invoiceData) {
const peppolId = invoiceData.customer.party_legal_entity.company_id;
try {
// Check if customer is on Peppol
const validation = await api.get('/api/lookup', {
params: { peppol_id: peppolId }
});
if (validation.data.dnsInfo?.status === 'success') {
// Send via Peppol
return await sendViaPeppol(invoiceData);
}
} catch (error) {
// Fallback to email with PDF
console.log('Customer not on Peppol, sending PDF via email');
return await sendPdfViaEmail(invoiceData);
}
}
```
## Best Practices
Before sending invoices, use `/api/lookup` with the exact Peppol ID:
```javascript theme={null}
// ✓ Correct: Direct lookup for validation
const peppolId = `0208:${cbeNumber}`;
try {
const validation = await api.get('/api/lookup', {
params: { peppol_id: peppolId }
});
if (validation.data.dnsInfo?.status === 'success') {
await createAndSendInvoice(invoiceData);
}
} catch (error) {
// Participant not found
console.log('Customer not registered on Peppol');
}
// ✗ Wrong: Using directory search for validation
const search = await api.get('/api/lookup/participants', {
params: { query: cbeNumber }
});
// May return no results even if registered!
```
The Peppol Directory is excellent for discovery, not validation:
```javascript theme={null}
// Use case: User types company name in autocomplete
async function autocompleteSearch(userInput) {
const results = await api.get('/api/lookup/participants', {
params: {
query: userInput,
country_code: 'BE'
}
});
// Display results as suggestions
return results.data.participants || [];
}
// When user selects a result, validate via direct lookup
async function onCustomerSelected(identifier) {
const peppolId = `${identifier.scheme}:${identifier.value}`;
try {
const participant = await api.get('/api/lookup', {
params: { peppol_id: peppolId }
});
return participant.data;
} catch (error) {
return null;
}
}
```
Cache participant lookups to reduce API calls:
```javascript theme={null}
const participantCache = new Map();
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
async function lookupWithCache(peppolId) {
const cached = participantCache.get(peppolId);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.data;
}
try {
const response = await api.get('/api/lookup', {
params: { peppol_id: peppolId }
});
const data = response.data;
participantCache.set(peppolId, {
data,
timestamp: Date.now()
});
return data;
} catch (error) {
return null;
}
}
```
Different countries use different schemes. Build a mapping:
```javascript theme={null}
const PEPPOL_SCHEMES = {
'BE': '0208', // Belgian CBE (numbers only, no 'BE' prefix)
'NL': '0106', // Dutch KVK
'DE': '0204', // German VAT
'FR': '0009', // French SIRET
'UK': '0088', // UK GLN
};
function buildPeppolId(countryCode, identifier) {
const scheme = PEPPOL_SCHEMES[countryCode];
if (!scheme) {
throw new Error(`Unknown country: ${countryCode}`);
}
return `${scheme}:${identifier}`;
}
```
Explain why directory search may not find participants:
```javascript theme={null}
const searchResults = await api.get('/api/lookup/participants', {
params: { query: companyName }
});
if (!searchResults.data.participants || searchResults.data.participants.length === 0) {
console.log(`
No results found in Peppol Directory for "${companyName}".
This does not necessarily mean they are unregistered.
Some Peppol access points do not publish participant data
to the directory, even though those participants can receive
invoices.
If you have the customer's CBE number or Peppol ID, use that
for a direct lookup instead.
`);
}
```
Don't assume a participant is unregistered if not in directory:
```javascript theme={null}
async function lookupCustomer(cbeNumber) {
const peppolId = `0208:${cbeNumber}`;
try {
// Try direct lookup
const response = await api.get('/api/lookup', {
params: { peppol_id: peppolId }
});
if (response.data.dnsInfo?.status === 'success') {
return {
found: true,
data: response.data,
source: 'SMP'
};
}
} catch (error) {
// Not registered or error
return {
found: false,
message: 'Customer not registered on Peppol network',
suggestion: 'Ask customer to register at https://app.e-invoice.be'
};
}
}
```
## Real-World Scenario
### Directory vs. SMP Lookup
Some Peppol access points register participants but do not publish all participant data to the Peppol Directory.
**What happens**:
* A participant is fully registered with their access point
* They can send and receive invoices via Peppol
* Their registration is in the SMP (authoritative source)
* But their access point has not synchronized data with the directory
**Results**:
**Via `/api/lookup` (Direct SMP)** — `dnsInfo.status` is `"success"` and a `businessCard` is present:
```json theme={null}
{
"status": "success",
"dnsInfo": { "status": "success", "smpHostname": "smp.example.com" },
"businessCard": {
"status": "success",
"entities": [{ "name": "Example Company BV", "countryCode": "BE" }]
},
"serviceMetadata": { "...": "supported document types" }
}
```
✓ **Found** - The participant is registered
**Via `/api/lookup/participants` (Peppol Directory)**:
```json theme={null}
{
"total_count": 0,
"used_count": 0,
"query_terms": "Example Company BV",
"search_date": "2026-01-12T14:32:10.123456",
"participants": []
}
```
✗ **Not found** - Not in the directory database
### The Takeaway
**The participant is fully registered and can receive invoices**, but does not appear in directory searches. This demonstrates why `/api/lookup` must be used for validation before sending invoices.
## Technical Details
### Why the Difference Exists
* **SMP registration** is required for Peppol participation
* **Directory publication** is optional for access points
* Some access points prioritize privacy and don't publish participant lists
* Others may have technical reasons for not synchronizing with the directory
### Data Freshness
| Endpoint | Data Age | Updates |
| -------------------------- | --------- | --------------------------------------- |
| `/api/lookup` | Real-time | Immediate (queries SMP directly) |
| `/api/lookup/participants` | Cached | Periodic (synced from Peppol Directory) |
## Integration Checklist
When implementing participant lookup in your application:
* [ ] Use `/api/lookup` for all validation before sending invoices
* [ ] Use `/api/lookup/participants` only for discovery and search features
* [ ] Never rely on directory search absence as proof of non-registration
* [ ] Always validate directory search results via direct SMP lookup
* [ ] Provide clear feedback when directory search returns no results
* [ ] Cache direct lookup results (with appropriate TTL)
* [ ] Handle cases where CBE number is known vs. only company name
* [ ] Test with known participants from different access points
## Next Steps
Create and send e-invoices
Test invoice JSON during development
Get notified about delivery status
Explore all endpoints
# Creating Documents from PDF
Source: https://docs.e-invoice.be/guides/pdf-documents
Learn how to create invoices from PDF files with optional auto-conversion to UBL
## Overview
The e-invoice.be API allows you to create documents directly from PDF files using AI-powered data extraction. This is useful when:
* You have existing PDF invoices you want to send via Peppol
* Your system generates PDF invoices but not structured data
* You want to digitize paper invoices
* You're migrating from traditional PDF invoicing to e-invoicing
## How It Works
Upload a PDF invoice and the API automatically extracts invoice data and attempts to generate a valid UBL document. If the `ubl_document` field is present in the response, sufficient details were extracted to create a Peppol-compliant document ready for sending.
The API provides two processing modes:
1. **Synchronous** - Immediate processing with instant results (recommended for most cases)
2. **Asynchronous** - Long-running conversions with task polling (for complex PDFs)
## Method 1: Synchronous PDF Upload
Upload a PDF file and receive immediate extraction results.
### Basic Upload
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/documents/pdf" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@invoice.pdf"
```
### Upload with Tax IDs (Recommended)
Providing tax IDs improves extraction accuracy:
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/documents/pdf?vendor_tax_id=BE1018265814&customer_tax_id=BE0848934496" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@invoice.pdf"
```
### Response
The response contains extracted invoice data and, if successful, a UBL document ready for sending:
```json theme={null}
{
"success": true,
"document_type": "INVOICE",
"state": "DRAFT",
"direction": "OUTBOUND",
"invoice_id": "INV-2024-001",
"invoice_date": "2024-10-24",
"invoice_total": "1210.00",
"currency": "EUR",
"vendor_name": "E-INVOICE BV",
"vendor_tax_id": "BE1018265814",
"customer_name": "OpenPeppol VZW",
"customer_tax_id": "BE0848934496",
"items": [
{
"description": "Professional Services",
"quantity": "10.0",
"unit_price": "100.00",
"tax_rate": "21.00",
"amount": "1000.00"
}
],
"ubl_document": "..."
}
```
If the `ubl_document` field is present in the response, the PDF contained sufficient information to generate a valid UBL document. You can then create a document from this UBL and send it via Peppol.
### Creating a Document from Extracted UBL
If the response contains a `ubl_document`, create and send it:
```bash theme={null}
# Step 1: Create document from the extracted UBL
# /api/documents/ubl takes multipart/form-data with a "file" field — not a raw XML body
curl -X POST "https://api.e-invoice.be/api/documents/ubl" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@extracted_invoice.xml"
# Step 2: Send via Peppol
curl -X POST "https://api.e-invoice.be/api/documents/doc_abc123/send" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Method 2: Asynchronous PDF Upload
For complex PDFs or when immediate processing isn't required, use the asynchronous endpoint.
### Step 1: Initiate Conversion
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/conversion/pdf" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@invoice.pdf"
```
Response:
```json theme={null}
{
"task_id": "task_abc123",
"status": "pending"
}
```
### Step 2: Check Status
```bash theme={null}
curl -X GET "https://api.e-invoice.be/api/conversion/pdf/task_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Response:
```json theme={null}
{
"task_id": "task_abc123",
"status": "completed"
}
```
Status values: `pending`, `completed`, `failed`
### Step 3: Retrieve Result
```bash theme={null}
curl -X GET "https://api.e-invoice.be/api/conversion/pdf/task_abc123/result" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Response contains the same structure as the synchronous endpoint.
## Code Examples
### Node.js - Synchronous Upload
```javascript theme={null}
const axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');
async function uploadPDFInvoice(pdfPath, vendorTaxId, customerTaxId) {
const formData = new FormData();
formData.append('file', fs.createReadStream(pdfPath));
// Build URL with optional tax IDs
let url = 'https://api.e-invoice.be/api/documents/pdf';
const params = new URLSearchParams();
if (vendorTaxId) params.append('vendor_tax_id', vendorTaxId);
if (customerTaxId) params.append('customer_tax_id', customerTaxId);
if (params.toString()) url += `?${params.toString()}`;
try {
const response = await axios.post(url, formData, {
headers: {
'Authorization': `Bearer ${process.env.E_INVOICE_API_KEY}`,
...formData.getHeaders()
}
});
console.log('PDF processed successfully');
console.log('Invoice ID:', response.data.invoice_id);
console.log('Invoice total:', response.data.invoice_total);
if (response.data.ubl_document) {
console.log('✓ UBL document generated - ready to send');
return response.data;
} else {
console.log('⚠ Could not generate UBL - manual review required');
console.log('Extracted data:', response.data);
return null;
}
} catch (error) {
console.error('Error:', error.response?.data || error.message);
}
}
// Usage
uploadPDFInvoice('./invoice.pdf', 'BE1018265814', 'BE0848934496');
```
### Python - Synchronous Upload
```python theme={null}
import requests
import os
API_KEY = os.environ.get('E_INVOICE_API_KEY')
BASE_URL = 'https://api.e-invoice.be'
def upload_pdf_invoice(pdf_path, vendor_tax_id=None, customer_tax_id=None):
headers = {
'Authorization': f'Bearer {API_KEY}'
}
files = {
'file': open(pdf_path, 'rb')
}
params = {}
if vendor_tax_id:
params['vendor_tax_id'] = vendor_tax_id
if customer_tax_id:
params['customer_tax_id'] = customer_tax_id
try:
response = requests.post(
f'{BASE_URL}/api/documents/pdf',
headers=headers,
files=files,
params=params
)
response.raise_for_status()
data = response.json()
print('PDF processed successfully')
print(f'Invoice ID: {data.get("invoice_id")}')
print(f'Invoice total: {data.get("invoice_total")}')
if data.get('ubl_document'):
print('✓ UBL document generated - ready to send')
return data
else:
print('⚠ Could not generate UBL - manual review required')
print(f'Extracted data: {data}')
return None
except Exception as error:
print(f'Error: {error}')
# Usage
upload_pdf_invoice('./invoice.pdf', 'BE1018265814', 'BE0848934496')
```
### Node.js - Asynchronous Upload with Polling
```javascript theme={null}
const axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');
const api = axios.create({
baseURL: 'https://api.e-invoice.be',
headers: {
'Authorization': `Bearer ${process.env.E_INVOICE_API_KEY}`
}
});
async function uploadPDFAsync(pdfPath) {
// Step 1: Initiate conversion
const formData = new FormData();
formData.append('file', fs.createReadStream(pdfPath));
const taskResponse = await api.post('/api/conversion/pdf', formData, {
headers: formData.getHeaders()
});
const taskId = taskResponse.data.task_id;
console.log('Conversion started:', taskId);
// Step 2: Poll for completion
while (true) {
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds
const statusResponse = await api.get(`/api/conversion/pdf/${taskId}`);
const status = statusResponse.data.status;
console.log('Status:', status);
if (status === 'completed') {
// Step 3: Get result
const resultResponse = await api.get(`/api/conversion/pdf/${taskId}/result`);
console.log('Conversion complete');
return resultResponse.data;
} else if (status === 'failed') {
throw new Error('Conversion failed');
}
}
}
// Usage
uploadPDFAsync('./invoice.pdf')
.then(result => console.log('Result:', result))
.catch(error => console.error('Error:', error));
```
## Complete Workflow Example
Here's a complete example that extracts data from a PDF and sends it via Peppol:
```javascript theme={null}
const axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');
const api = axios.create({
baseURL: 'https://api.e-invoice.be',
headers: {
'Authorization': `Bearer ${process.env.E_INVOICE_API_KEY}`
}
});
async function processAndSendPDFInvoice(pdfPath, vendorTaxId, customerTaxId) {
try {
// Step 1: Upload PDF and extract data
console.log('Processing PDF invoice...');
const formData = new FormData();
formData.append('file', fs.createReadStream(pdfPath));
const params = new URLSearchParams();
if (vendorTaxId) params.append('vendor_tax_id', vendorTaxId);
if (customerTaxId) params.append('customer_tax_id', customerTaxId);
const extractResponse = await api.post(
`/api/documents/pdf?${params.toString()}`,
formData,
{ headers: formData.getHeaders() }
);
const extracted = extractResponse.data;
console.log('✓ PDF processed');
console.log(` Invoice: ${extracted.invoice_id}`);
console.log(` Amount: ${extracted.currency} ${extracted.invoice_total}`);
// Step 2: Check if UBL was generated
if (!extracted.ubl_document) {
console.error('✗ Could not generate UBL document');
console.log('Extracted data:', extracted);
console.log('Please review and create document manually');
return null;
}
console.log('✓ UBL document generated');
// Step 3: Save UBL to file
const ublPath = './extracted_invoice.xml';
fs.writeFileSync(ublPath, extracted.ubl_document);
// Step 4: Create document from UBL
// /api/documents/ubl takes multipart/form-data with a "file" field
console.log('Creating document...');
const ublForm = new FormData();
ublForm.append('file', fs.createReadStream(ublPath));
const documentResponse = await api.post(
'/api/documents/ubl',
ublForm,
{ headers: ublForm.getHeaders() }
);
const documentId = documentResponse.data.id;
console.log('✓ Document created:', documentId);
// Step 5: Send via Peppol
console.log('Sending document...');
const sendResponse = await api.post(`/api/documents/${documentId}/send`);
console.log('✓ Document sent:', sendResponse.data.state);
return {
extracted,
documentId,
state: sendResponse.data.state
};
} catch (error) {
console.error('Error:', error.response?.data || error.message);
throw error;
}
}
// Usage
processAndSendPDFInvoice('./invoice.pdf', 'BE1018265814', 'BE0848934496')
.then(result => console.log('Complete:', result))
.catch(error => console.error('Failed:', error));
```
## PDF Requirements
### File Format
* **Format**: PDF (Portable Document Format)
* **Content**: Must be valid and non-empty
* **Text extraction**: PDFs with readable text produce better results
Text-based PDFs generated from software (not scanned documents) produce the most accurate extraction results. Providing vendor and customer tax IDs as query parameters significantly improves extraction accuracy.
### Best Practices for PDFs
1. **Use text-based PDFs**: Generated from software rather than scanned images
2. **Clear layout**: Structured format with clear sections for vendor, customer, line items
3. **Readable fonts**: Standard fonts with good contrast
4. **Complete information**: All required invoice fields clearly visible
5. **Single invoice per PDF**: Don't combine multiple invoices in one file
6. **Provide tax IDs**: Include `vendor_tax_id` and `customer_tax_id` query parameters
## Common Issues
### Invalid or Empty PDF
**Status Code**: `415 Unsupported Media Type`
**Solution**:
* Ensure the file is a valid PDF document
* Check the file is not corrupted
* Verify the file is not empty
### No UBL Document Generated
**Issue**: Response contains extracted data but no `ubl_document` field
**Cause**: The PDF didn't contain sufficient information to generate a complete UBL document
**Solution**:
* Review the extracted data in the response
* Provide `vendor_tax_id` and `customer_tax_id` query parameters
* Use a more detailed PDF with complete invoice information
* Manually create the document using the JSON API with the extracted data as a starting point
### Extraction Inaccuracies
**Issue**: Extracted amounts, dates, or tax IDs are incorrect
**Solution**:
* Always provide `vendor_tax_id` and `customer_tax_id` as query parameters
* Use text-based PDFs rather than scanned images
* Ensure the PDF has a clear, structured layout
* Review extracted data before creating documents
### Authentication Errors
**Status Code**: `401 Unauthorized`
**Solution**:
* Verify your API key is correct
* Ensure the `Authorization` header is properly formatted: `Bearer YOUR_API_KEY`
* Check your API key has not expired
## Best Practices
Include vendor and customer tax IDs as query parameters for better extraction:
```javascript theme={null}
// ✓ Good: Provide tax IDs
const url = `/api/documents/pdf?vendor_tax_id=BE1018265814&customer_tax_id=BE0848934496`;
await api.post(url, formData);
// ✗ Less accurate: No tax IDs
await api.post('/api/documents/pdf', formData);
```
Always check if a UBL document was generated before proceeding:
```javascript theme={null}
const result = await api.post('/api/documents/pdf', formData);
if (!result.data.ubl_document) {
console.warn('Manual review required - incomplete extraction');
// Handle manually or notify user
}
```
For processing multiple PDFs, use the async endpoint to avoid timeouts:
```javascript theme={null}
const tasks = await Promise.all(
pdfFiles.map(file => initiateConversion(file))
);
// Poll for results separately
const results = await waitForResults(tasks);
```
Store original PDFs for audit and compliance purposes:
```javascript theme={null}
const backup = `./backup/${invoiceNumber}.pdf`;
fs.copyFileSync(pdfPath, backup);
```
Review extracted data before sending, especially for critical fields:
```javascript theme={null}
const extracted = response.data;
// Check critical fields
if (!extracted.invoice_id || !extracted.invoice_total) {
console.error('Missing critical data');
return;
}
// Validate tax IDs are in correct format
if (extracted.vendor_tax_id && !extracted.vendor_tax_id.match(/^BE\d{10}$/)) {
console.warn('Invalid vendor tax ID format');
}
```
## Next Steps
Learn standard JSON invoice creation
Create from UBL XML files
Test invoice data
Explore all endpoints
# Sending UBL Documents
Source: https://docs.e-invoice.be/guides/ubl-documents
Learn how to send invoices from pre-generated UBL XML files
## Overview
If you already have UBL (Universal Business Language) XML files, you can send them directly via e-invoice.be without converting from JSON. This is useful when:
* You have an ERP system that generates UBL XML
* You're migrating from another Peppol Access Point
* You have existing UBL files to send
* You want full control over the UBL structure
## Prerequisites
Your UBL XML must:
* Be valid UBL BIS Billing 3.0 format
* Comply with Peppol specifications
* Be either an Invoice or Credit Note
Use the `/api/validate/ubl` endpoint to validate your UBL XML before creating documents.
## Workflow
1. **Validate** your UBL XML (recommended)
2. **Create** document from UBL
3. **Send** via Peppol
## Step 1: Validate UBL XML
Before creating a document, validate your UBL file. **Both `/api/validate/ubl` and `/api/documents/ubl` accept `multipart/form-data` with a `file` field — not a raw XML body.**
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/validate/ubl" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@invoice.xml"
```
Do not use `-H "Content-Type: application/xml" --data-binary @invoice.xml` — that returns `422 Field required: body.file`. See the [validation guide](/guides/validation#validating-ubl-xml) for the full request contract and copy-paste examples in Node.js, Python, C#, and PHP.
### Validation Response
**Valid UBL**:
```json theme={null}
{
"id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"file_name": "invoice.xml",
"is_valid": true,
"issues": []
}
```
**Invalid UBL**:
```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"
}
]
}
```
## Step 2: Create Document from UBL
Once validation passes, create the document. Same contract: `multipart/form-data` with a `file` field.
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/documents/ubl" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@invoice.xml"
```
### Response
```json theme={null}
{
"id": "doc_abc123",
"document_type": "INVOICE",
"state": "DRAFT",
"direction": "OUTBOUND",
"invoice_id": "INV-2024-001",
"invoice_date": "2024-10-24",
"invoice_total": "1210.00",
"total_tax": "210.00",
"created_at": "2024-10-24T12:00:00Z"
}
```
## Step 3: Send via Peppol
Send the document using the document ID:
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/documents/doc_abc123/send" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### Response
```json theme={null}
{
"id": "doc_abc123",
"document_type": "INVOICE",
"state": "TRANSIT",
"direction": "OUTBOUND"
}
```
### Peppol ID Routing
By default, sender and receiver Peppol IDs are **automatically derived from the company identifiers** in your document, **regardless of any endpoint IDs specified in the UBL XML**:
* Derived from company tax IDs (`vendor_tax_id` / `customer_tax_id`) OR company IDs (`vendor_id` / `customer_id`)
* For Belgian companies: `0208` scheme is always used (Belgian government requirement)
* Example: Tax ID `BE1018265814` → Peppol ID `0208:1018265814`
**UBL Endpoint IDs are ignored**: Even if your UBL document contains specific endpoint IDs (e.g., `1234567890123`), these are **not used for routing**. The API derives Peppol IDs from the company identifiers in the document metadata instead.
### Explicitly Specifying Peppol IDs (Recommended)
**Best Practice**: Always explicitly specify sender and receiver Peppol IDs using query parameters to ensure documents are routed to the correct endpoints. This is especially important when sending UBL documents, as endpoint IDs within the UBL XML are ignored.
To route to a specific Peppol endpoint, explicitly provide the Peppol IDs via query parameters:
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/documents/doc_abc123/send?sender_peppol_scheme=0208&sender_peppol_id=1018265814&receiver_peppol_scheme=0088&receiver_peppol_id=1234567890123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
**Available query parameters**:
| Parameter | Description | Example |
| ------------------------ | ---------------------------- | --------------- |
| `sender_peppol_scheme` | Sender's Peppol scheme ID | `0208` |
| `sender_peppol_id` | Sender's Peppol identifier | `1018265814` |
| `receiver_peppol_scheme` | Receiver's Peppol scheme ID | `0088` |
| `receiver_peppol_id` | Receiver's Peppol identifier | `1234567890123` |
**Common Peppol schemes**:
* `0208` - Belgian enterprise number (BE)
* `0088` - Global Location Number (GLN)
* `0106` - Dutch KVK
Always verify the recipient is registered at the specified Peppol endpoint before sending. Use `/api/validate/peppol-id?peppol_id=scheme:identifier` to confirm registration and prevent delivery failures.
## Example UBL Invoice
Here's a minimal valid UBL invoice:
```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-2024-001
2024-10-24
2024-11-24
380
EUR
0123456789
Your Company
Main Street
123
Brussels
1000
BE
Your Company BVBA
0123456789
0987654321
Customer Company
Customer Lane
456
Antwerp
2000
BE
Customer Company NV
0987654321
1000.00
1000.00
1210.00
1210.00
210.00
1000.00
210.00
S
21.0
VAT
1
10
1000.00
Professional Services
Professional Services
S
21.0
VAT
100.00
```
## Code Examples
### Node.js
```javascript theme={null}
const axios = require('axios');
const fs = require('fs');
const FormData = require('form-data');
const BASE_URL = 'https://api.e-invoice.be';
const authHeader = { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` };
// Both /api/validate/ubl and /api/documents/ubl take multipart/form-data
// with a single "file" field. Always create a fresh FormData per request —
// the underlying stream is consumed on send.
function uploadXml(path) {
const form = new FormData();
form.append('file', fs.createReadStream(path));
return form;
}
async function sendUBLInvoice(ublFilePath) {
try {
// 1. Validate UBL
console.log('Validating UBL...');
const validateForm = uploadXml(ublFilePath);
const validation = await axios.post(
`${BASE_URL}/api/validate/ubl`,
validateForm,
{ headers: { ...authHeader, ...validateForm.getHeaders() } }
);
if (!validation.data.is_valid) {
console.error('UBL validation failed:', validation.data.issues);
return;
}
console.log('✓ UBL is valid');
// 2. Create document from UBL
console.log('Creating document...');
const createForm = uploadXml(ublFilePath);
const document = await axios.post(
`${BASE_URL}/api/documents/ubl`,
createForm,
{ headers: { ...authHeader, ...createForm.getHeaders() } }
);
console.log('✓ Document created:', document.data.id);
// 3. Send via Peppol
console.log('Sending document...');
const result = await axios.post(
`${BASE_URL}/api/documents/${document.data.id}/send`,
null,
{ headers: authHeader }
);
console.log('✓ Document sent:', result.data.state);
} catch (error) {
console.error('Error:', error.response?.data || error.message);
}
}
// Usage
sendUBLInvoice('./invoices/invoice.xml');
```
### Python
```python theme={null}
import os
import requests
API_KEY = os.environ['E_INVOICE_API_KEY']
BASE_URL = 'https://api.e-invoice.be'
HEADERS = {'Authorization': f'Bearer {API_KEY}'}
# Both /api/validate/ubl and /api/documents/ubl take multipart/form-data
# with a single "file" field. requests builds the multipart envelope when
# you pass `files=`; do NOT also set Content-Type — requests sets it
# (with the boundary) for you.
def send_ubl_invoice(ubl_file_path):
try:
# 1. Validate UBL
print('Validating UBL...')
with open(ubl_file_path, 'rb') as f:
validation = requests.post(
f'{BASE_URL}/api/validate/ubl',
headers=HEADERS,
files={'file': (os.path.basename(ubl_file_path), f, 'application/xml')},
)
if not validation.json().get('is_valid'):
print('UBL validation failed:', validation.json().get('issues'))
return
print('✓ UBL is valid')
# 2. Create document
print('Creating document...')
with open(ubl_file_path, 'rb') as f:
document = requests.post(
f'{BASE_URL}/api/documents/ubl',
headers=HEADERS,
files={'file': (os.path.basename(ubl_file_path), f, 'application/xml')},
)
doc_id = document.json()['id']
print(f'✓ Document created: {doc_id}')
# 3. Send via Peppol
print('Sending document...')
result = requests.post(
f'{BASE_URL}/api/documents/{doc_id}/send',
headers=HEADERS,
)
print(f'✓ Document sent: {result.json()["state"]}')
except Exception as error:
print(f'Error: {error}')
# Usage
send_ubl_invoice('./invoices/invoice.xml')
```
## Retrieving UBL from Created Documents
If you created a document via JSON and want to retrieve the generated UBL:
```bash theme={null}
curl -X GET "https://api.e-invoice.be/api/documents/{document_id}/ubl" \
-H "Authorization: Bearer YOUR_API_KEY"
```
This returns the UBL XML that was generated from your JSON payload.
## Common UBL Validation Errors
### Missing Peppol Profile
**Error**:
```json theme={null}
{
"field": "cbc:ProfileID",
"message": "Missing or invalid ProfileID"
}
```
**Fix**: Add the correct profile ID:
```xml theme={null}
urn:fdc:peppol.eu:2017:poacc:billing:01:1.0
```
### Invalid Customization ID
**Error**:
```json theme={null}
{
"field": "cbc:CustomizationID",
"message": "Invalid CustomizationID for Peppol BIS Billing 3.0"
}
```
**Fix**: Use the correct customization ID:
```xml theme={null}
urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0
```
### Missing Endpoint ID
**Error**:
```json theme={null}
{
"field": "cac:AccountingSupplierParty/cac:Party/cbc:EndpointID",
"message": "Missing Peppol endpoint ID"
}
```
**Fix**: Add endpoint IDs for both parties:
```xml theme={null}
0123456789
```
### Invalid Tax Category
**Error**:
```json theme={null}
{
"field": "cac:TaxCategory/cbc:ID",
"message": "Invalid tax category code"
}
```
**Fix**: Use valid UNCL5305 codes (S, E, Z, etc.):
```xml theme={null}
S
21.0
```
## Best Practices
Use `/api/validate/ubl` to catch errors early. Both endpoints take `multipart/form-data` with a `file` field — use `-F` (curl) / `files=` (requests) / `FormData` (axios), not raw XML bodies.
```bash theme={null}
# Validate first
curl -X POST "https://api.e-invoice.be/api/validate/ubl" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@invoice.xml"
# Only create if validation passes
curl -X POST "https://api.e-invoice.be/api/documents/ubl" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@invoice.xml"
```
Ensure all UBL namespaces are correctly declared:
```xml theme={null}
```
Before sending, verify customer Peppol IDs:
```bash theme={null}
curl "https://api.e-invoice.be/api/validate/peppol-id?peppol_id=0208:0987654321"
```
For large UBL files:
* Use streaming when reading files
* Consider compressing before transmission
* Check file size limits in your HTTP client
Keep a copy of the original UBL for audit purposes:
```javascript theme={null}
// Before sending
const backup = `./backup/${invoiceNumber}.xml`;
fs.copyFileSync(ublFilePath, backup);
```
## UBL Credit Notes
Credit notes follow the same process but use the CreditNote element:
```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-2024-001
2024-10-24
381
EUR
INV-2024-001
```
## 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
Learn how to create documents from PDF files
Create invoices from JSON
Test invoice data
Explore all endpoints
# Validation During Development
Source: https://docs.e-invoice.be/guides/validation
Test your invoice JSON before creating documents
## Overview
The `/api/validate/json` endpoint is essential for development. It validates your invoice JSON and ensures it can be converted to valid UBL BIS Billing 3.0 format **before** you create any documents.
You cannot create documents with invalid JSON. The API will reject invoices that don't meet UBL BIS Billing 3.0 standards. Always validate during development to catch errors early.
## Why Validate?
* **No document creation**: Validation doesn't create any records - it's risk-free testing
* **Fast feedback**: Get instant validation results without creating documents
* **Detailed errors**: Receive specific field-level error messages
* **UBL compliance**: Ensures your JSON converts to valid UBL BIS Billing 3.0 XML
* **Save API calls**: Fix errors before attempting to create documents
## Basic Validation
Use `POST /api/validate/json` with your invoice data:
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/validate/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"document_type": "INVOICE",
"invoice_id": "INV-2024-001",
"invoice_date": "2024-10-24",
"due_date": "2024-11-23",
"currency": "EUR",
"vendor_name": "Your Company BVBA",
"vendor_tax_id": "BE0123456789",
"vendor_address": "Main Street 123, 1000 Brussels, Belgium",
"customer_name": "Customer Company NV",
"customer_tax_id": "BE0987654321",
"customer_address": "Customer Lane 456, 2000 Antwerp, Belgium",
"items": [
{
"description": "Professional Services - Consulting services for October 2024",
"quantity": "10",
"unit_price": "100.00",
"unit_code": "C62",
"tax_rate": "21.00"
}
]
}'
```
The example above will fail validation! This is intentional - it demonstrates why validation is critical. See below for the errors and how to fix them.
## Response Types
### Error Response (Real Example)
When validation fails, you'll receive detailed UBL compliance errors. Here's what the example above returns:
```json 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"
},
{
"message": "[BR-S-08]-For each different value of VAT category rate (BT-119) where the VAT category code (BT-118) is \"Standard rated\", the VAT category taxable amount (BT-116) in a VAT breakdown (BG-23) shall equal the sum of Invoice line net amounts (BT-131)...",
"type": "error",
"rule_id": "BR-S-08",
"flag": "fatal"
},
{
"message": "Invoice line net amount MUST equal (Invoiced quantity * (Item net price/item price base quantity) + Sum of invoice line charge amount - sum of invoice line allowance amount",
"type": "error",
"rule_id": "PEPPOL-EN16931-R120",
"flag": "fatal"
}
]
}
```
**This is exactly why the validation endpoint exists!** These UBL BIS Billing 3.0 compliance errors would prevent your invoice from being sent. Let's fix them.
### Fixed Example
Here's a corrected version that passes validation:
```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/validate/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"document_type": "INVOICE",
"invoice_id": "INV-2024-001",
"invoice_date": "2024-10-24",
"due_date": "2024-11-23",
"currency": "EUR",
"vendor_name": "Your Company BVBA",
"vendor_tax_id": "BE0897290877",
"vendor_address": "Main Street 123, 1000 Brussels, Belgium",
"customer_name": "Customer Company NV",
"customer_tax_id": "BE0817331995",
"customer_address": "Customer Lane 456, 2000 Antwerp, Belgium",
"items": [
{
"description": "Professional Services - Consulting services for October 2024",
"quantity": "10",
"unit_price": "100.00",
"unit_code": "C62",
"tax_rate": "21.00",
"amount": "1000.00"
}
]
}'
```
**What changed:**
1. **Valid Belgian VAT numbers** with correct mod97 checksums:
* `BE0897290877` (vendor) instead of `BE0123456789`
* `BE0817331995` (customer) instead of `BE0987654321`
2. **Added `amount` field** to line items: `"1000.00"` (quantity × unit\_price)
### Success Response
When your JSON is valid, you'll receive confirmation along with the generated UBL XML:
```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 `ubl_document` field shows you exactly what XML will be sent via Peppol. This is useful for debugging or understanding how your JSON maps to UBL BIS Billing 3.0.
## Validating UBL XML
Use `POST /api/validate/ubl` to validate an existing UBL BIS Billing 3.0 XML file.
**This endpoint expects `multipart/form-data` with a single `file` field — NOT a raw XML body.** The most common integration mistake is sending the XML as the request body with `Content-Type: application/xml`, which returns a 422 with `{"detail":[{"type":"missing","loc":["body","file"],"msg":"Field required"}]}`. See [common mistake](#common-mistake-raw-xml-body) below.
### Request Contract
| | |
| ----------------- | ------------------------------------------------------------------------------------ |
| **Method** | `POST` |
| **URL** | `https://api.e-invoice.be/api/validate/ubl` |
| **Authorization** | `Bearer YOUR_API_KEY` |
| **Content-Type** | `multipart/form-data` (set automatically by your HTTP client when you attach a file) |
| **Body** | Form field `file` containing the UBL XML file (binary upload) |
### Copy-Paste Examples
```bash cURL theme={null}
curl -X POST "https://api.e-invoice.be/api/validate/ubl" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@invoice.xml"
```
```javascript Node.js theme={null}
const fs = require('fs');
const FormData = require('form-data');
const axios = require('axios');
const form = new FormData();
form.append('file', fs.createReadStream('./invoice.xml'));
const response = await axios.post(
'https://api.e-invoice.be/api/validate/ubl',
form,
{
headers: {
...form.getHeaders(),
Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}`,
},
}
);
console.log(response.data);
```
```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())
```
```csharp C# / .NET 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");
// IMPORTANT: the form field MUST be named "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());
```
```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);
```
### Response
**Success (valid UBL):**
```json theme={null}
{
"id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"file_name": "invoice.xml",
"is_valid": true,
"issues": []
}
```
**Validation errors:**
```json theme={null}
{
"id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"file_name": "invoice.xml",
"is_valid": false,
"issues": [
{
"message": "Invoice total amount mismatch",
"type": "error",
"rule_id": "BR-CO-15",
"flag": "fatal",
"schematron": "PEPPOL-EN16931"
}
]
}
```
### Common mistake: raw XML body
If you set `Content-Type: application/xml` and send the XML as the raw request body (e.g. `--data-binary @invoice.xml`), you'll get this 422 response:
```json theme={null}
{
"detail": [
{
"type": "missing",
"loc": ["body", "file"],
"msg": "Field required",
"input": null
}
]
}
```
The fix: switch to `multipart/form-data` with a `file` form field (as shown in the examples above).
| Wrong | Right |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `-H "Content-Type: application/xml" --data-binary @invoice.xml` | `-F "file=@invoice.xml"` |
| `requests.post(url, data=xml, headers={'Content-Type': 'application/xml'})` | `requests.post(url, files={'file': open('invoice.xml', 'rb')})` |
| `axios.post(url, xml, { headers: { 'Content-Type': 'application/xml' } })` | `axios.post(url, formData, { headers: formData.getHeaders() })` |
| `new StringContent(xml, ..., "application/xml")` | `new MultipartFormDataContent { { fileContent, "file", "invoice.xml" } }` |
### When to Use UBL Validation
Use `POST /api/validate/ubl` when you:
* **Have existing UBL XML files** to verify before sending (e.g. generated by your ERP)
* **Are migrating** from another Peppol Access Point with pre-generated UBL documents
* **Need to validate** UBL files from external sources before posting them to `/api/documents/ubl`
* **Want to double-check** the XML produced by `/api/validate/json` (the `ubl_document` field in that response)
If you're creating invoices from JSON, prefer `/api/validate/json` — it validates the same UBL rules and returns the generated UBL alongside, so you don't need a separate UBL validation call.
## Common Validation Errors
### Invalid Peppol ID Format
```json theme={null}
{
"field": "vendor_tax_id",
"message": "Invalid Peppol ID format. Expected: scheme:identifier"
}
```
**Fix**: Use the correct format `scheme:identifier`
* Belgian companies: `0208:0123456789` (CBE number - VAT without 'BE' prefix)
* See [Peppol schemes](https://docs.peppol.eu/edelivery/codelists/v9.4/Peppol%20Code%20Lists%20-%20Participant%20identifier%20schemes%20v9.4.html)
### Invalid Tax Rate
```json theme={null}
{
"field": "items[0].tax_rate",
"message": "Invalid tax rate format. Must be a percentage string"
}
```
**Fix**: Use string format for tax rates:
* Standard rate: `"21.00"`
* Reduced rate: `"6.00"`
* Zero rated: `"0.00"`
### Missing Required Fields
```json theme={null}
{
"field": "vendor_address",
"message": "Field is required for UBL compliance"
}
```
**Fix**: Ensure all required fields are present:
* Vendor/customer name, tax ID, and address
* Invoice ID, invoice date, currency
* At least one item with description, quantity, unit price, and tax rate
### Invalid Date Format
```json theme={null}
{
"field": "invoice_date",
"message": "Invalid date format. Use ISO 8601: YYYY-MM-DD"
}
```
**Fix**: Use the format `YYYY-MM-DD`, e.g., `2024-10-24`
### Invalid Currency Code
```json theme={null}
{
"field": "currency",
"message": "Unsupported currency code"
}
```
**Fix**: Use supported ISO 4217 codes: EUR, USD, GBP, JPY, CHF, CAD, AUD, NZD, CNY, INR, SEK, NOK, DKK, SGD, HKD
## Development Workflow
### 1. Build Your Invoice JSON
Start with a template or build your invoice object:
```javascript theme={null}
const invoice = {
document_type: 'INVOICE',
invoice_id: 'INV-2024-001',
invoice_date: '2024-10-24',
currency: 'EUR',
// ... rest of invoice data
};
```
### 2. Validate First
Always validate before attempting to create:
```javascript theme={null}
const validation = await api.post('/api/validate/json', invoice);
if (!validation.data.is_valid) {
console.error('Validation errors:', validation.data.issues);
// Fix errors and try again
return;
}
```
### 3. Create Document
Only after validation passes, create the document:
```javascript theme={null}
const document = await api.post('/api/documents/', invoice);
```
## Complete Development Example
```javascript theme={null}
const axios = require('axios');
const api = axios.create({
baseURL: 'https://api.e-invoice.be',
headers: {
'Authorization': `Bearer ${process.env.E_INVOICE_API_KEY}`,
'Content-Type': 'application/json'
}
});
async function validateAndCreateInvoice(invoiceData) {
try {
// Step 1: Validate the JSON
console.log('Validating invoice JSON...');
const validation = await api.post('/api/validate/json', invoiceData);
if (!validation.data.is_valid) {
console.error('❌ Validation failed:');
validation.data.issues.forEach(issue => {
console.error(` - ${issue.rule_id}: ${issue.message}`);
});
return null;
}
console.log('✓ Invoice JSON is valid');
// Step 2: Create the document
console.log('Creating invoice...');
const document = await api.post('/api/documents/', invoiceData);
console.log('✓ Invoice created:', document.data.id);
return document.data;
} catch (error) {
console.error('Error:', error.response?.data || error.message);
return null;
}
}
// Example usage - using valid data
const invoiceData = {
document_type: 'INVOICE',
invoice_id: 'INV-2024-001',
invoice_date: '2024-10-24',
due_date: '2024-11-23',
currency: 'EUR',
vendor_name: 'Your Company BVBA',
vendor_tax_id: 'BE0897290877', // Valid Belgian VAT with mod97 checksum
vendor_address: 'Main Street 123, 1000 Brussels, Belgium',
customer_name: 'Customer Company NV',
customer_tax_id: 'BE0817331995', // Valid Belgian VAT with mod97 checksum
customer_address: 'Customer Lane 456, 2000 Antwerp, Belgium',
items: [
{
description: 'Professional Services - Consulting services for October 2024',
quantity: '10',
unit_price: '100.00',
unit_code: 'C62',
tax_rate: '21.00',
amount: '1000.00' // Required: quantity × unit_price
}
]
};
validateAndCreateInvoice(invoiceData);
```
## Testing Strategy
### During Development
Use `/api/validate/json` liberally:
1. **Test edge cases**: Validate unusual scenarios (zero amounts, multiple currencies, etc.)
2. **Test all document types**: Validate invoices, credit notes, and debit notes
3. **Iterate quickly**: Fix errors and re-validate without creating documents
4. **Build test suites**: Create automated validation tests
### Before Production
1. Validate representative samples of all invoice types
2. Test with real customer Peppol IDs
3. Verify all tax categories and currency codes you'll use
4. Test complex scenarios (allowances, charges, multiple line items)
## Best Practices
Don't wait until production. Validate during development to catch issues early:
* Test each new invoice template
* Validate after schema changes
* Include validation in CI/CD pipelines
```javascript theme={null}
if (!validation.data.is_valid) {
// Log detailed errors
logger.error('Invoice validation failed', {
invoice_id: invoiceData.invoice_id,
issues: validation.data.issues
});
// Notify developers/users
notifyError(validation.data.issues);
// Don't attempt to create the document
return;
}
```
If you're generating invoices from templates, validate the template once and cache the result:
```javascript theme={null}
const templateCache = new Map();
function validateTemplate(templateName, templateData) {
if (templateCache.has(templateName)) {
return templateCache.get(templateName);
}
const isValid = validateInvoiceJSON(templateData);
templateCache.set(templateName, isValid);
return isValid;
}
```
Test validation extensively with a sandbox company before going live:
* A sandbox company sends via email instead of Peppol, so testing is safe
* Use the production API: `https://api.e-invoice.be`
* Create a sandbox company from [app.e-invoice.be](https://app.e-invoice.be)
## Next Steps
Create and send validated invoices
Find valid Peppol IDs
Get notified about events
Explore all endpoints
# Introduction
Source: https://docs.e-invoice.be/index
Welcome to e-invoice.be - International Peppol Access Point
## What is e-invoice.be?
e-invoice.be is a **recognised Peppol Access Point and SMP** operating under the Belgian Peppol Authority (BOSA). We enable businesses worldwide to send and receive electronic invoices via the Peppol network.
### Switch to e-invoicing, keep your workflow
We've built comprehensive tools to make integrating e-invoicing as simple as possible - even for companies with legacy ERP systems. Our API lets you:
* Send invoices from **JSON** (simple integration)
* Upload **pre-generated UBL XML** (ERP compatibility)
* Create invoices from **PDF files** (legacy system support)
* Integrate via our **reseller programme** (for SaaS platforms)
As a member of the **Peppol Security Committee**, we're committed to maintaining the highest standards of security and compliance in e-invoicing.
## Who We Serve
We provide Peppol e-invoicing services to:
* **Individual companies** across many industries (manufacturing, services, retail, etc.)
* **Public and private listed companies**
* **SaaS companies** through our reseller programme ([contact us](mailto:support@e-invoice.be) for details)
* **Public and government agencies**
* **Companies with or without ERP systems**
Peppol is the international standard for electronic invoicing. E-invoices sent via Peppol are legally compliant with the EU's e-invoicing mandate (EN 16931) and recognized globally.
## How Peppol Works
The Peppol network uses a **4-corner model** to enable secure, standardized e-invoice exchange between businesses:
1. **Corner 1**: Your business (the sender)
2. **Corner 2**: Your Access Point (e-invoice.be)
3. **Corner 3**: Recipient's Access Point
4. **Corner 4**: Recipient's business
e-invoice.be handles all the technical complexity of Peppol transmission, so you can focus on your business.
## Why e-invoice.be?
Integrate seamlessly with your existing systems using our JSON-based API
Automatic conversion to UBL BIS Billing 3.0 format
Validate invoices before sending with our validation endpoint
E-invoices delivered instantly via the Peppol network
## Getting Started
Get up and running with e-invoice.be in minutes:
Sign up at [app.e-invoice.be](https://app.e-invoice.be) and get your API key
Use your API key to authenticate requests to the API
Test your invoice JSON with the validation endpoint
Create your first invoice and send it via Peppol
Follow our step-by-step quickstart guide
## Core Features
### Create E-Invoices
Send invoices and credit notes via Peppol using simple JSON payloads:
```json theme={null}
{
"document_type": "INVOICE",
"invoice_id": "INV-2024-001",
"invoice_date": "2024-10-24",
"due_date": "2024-11-24",
"currency": "EUR",
"vendor_name": "Your Company",
"vendor_tax_id": "BE1018265814",
"customer_name": "Customer Company",
"customer_tax_id": "BE0848934496",
"items": [
{
"description": "Professional Services",
"quantity": 10,
"unit_price": 100.00,
"tax_rate": "21.00"
}
]
}
```
Learn how to create and send e-invoices
### Validate Before Sending
Test your invoice JSON without creating documents:
```bash theme={null}
POST /api/validate/json
```
Ensures your invoice converts to valid UBL BIS Billing 3.0 format.
Test invoices during development
### Lookup Peppol Participants
Verify customers can receive e-invoices before sending:
```bash theme={null}
GET /api/validate/peppol-id?peppol_id=0208:0123456789
```
Find and verify customer Peppol IDs
### Webhooks
Get notified about invoice events in real-time:
* Document received
* Document sent
* Document failed
* And more...
Set up webhook notifications
## API Reference
Explore the complete API documentation with all endpoints, request/response schemas, and examples.
Auto-generated from our OpenAPI specification
## For Partners
Are you a SaaS company or software vendor interested in offering Peppol e-invoicing to your customers?
Learn about our partnership opportunities for SaaS platforms and service providers
## Need Help?
Contact our support team
View our open-source projects
# Quickstart
Source: https://docs.e-invoice.be/quickstart
Get started with e-invoice.be in minutes
## Getting an Account
To start using the e-invoice.be API, you need to create an account and obtain your API credentials.
### 1. Sign Up
Visit [app.e-invoice.be](https://app.e-invoice.be) and create your account. You'll need to provide:
* Your company information
* Company registration details (VAT number, business registration number, etc.)
* Contact details
**For SaaS companies**: Interested in our reseller programme? Contact us at [support@e-invoice.be](mailto:support@e-invoice.be) to learn about integration options and partnership opportunities.
### 2. Get Your API Key
Once your account is created:
1. Log in to your dashboard at [app.e-invoice.be](https://app.e-invoice.be)
2. Navigate to **Settings** → **API Keys**
3. Click **Create API Key**
4. Copy your API key immediately - it won't be shown again
5. Store it securely (never commit it to version control)
### 3. Create a Sandbox Company
For development and testing, create a **sandbox company** - a dedicated company that runs in test mode and comes with its own API key:
**A sandbox company is essential for safe development.** Documents are emailed instead of being sent via Peppol, so you can test your integration without affecting real recipients.
In [app.e-invoice.be](https://app.e-invoice.be), click **Create sandbox company**, then use that company's API key for your requests. [Learn more about test mode →](/environments)
### 4. Choose Your API
There is a single API host:
| API | Base URL | When to Use |
| ---------------- | -------------------------- | ---------------------------------------------------------------------------------- |
| **e-invoice.be** | `https://api.e-invoice.be` | Everything — testing (with a sandbox company) and production (with a real company) |
**Everything runs on `https://api.e-invoice.be`.** Use a sandbox company's API key to test in test mode, and a real company's API key to send over Peppol. You won't need to change your code or base URL when going live.
Understand test mode and when to use the development API
## Quick Test
Verify your API key works by checking your account information:
```bash theme={null}
curl -X GET "https://api.e-invoice.be/api/me/" \
-H "Authorization: Bearer YOUR_API_KEY"
```
You should receive a response with your account details:
```json theme={null}
{
"name": "Your Company Name",
"company_name": "Your Company BV",
"company_number": "0123456789",
"peppol_ids": ["0208:0123456789"]
}
```
Since both environments share the same database and credentials, the response will be identical regardless of which environment you query.
## What's Next?
Now that you have your account set up, you can:
Learn how to authenticate API requests
Send your first e-invoice via Peppol
Test your invoices before sending
Get notified about invoice events
Use the API from any AI assistant
# Reseller Programme
Source: https://docs.e-invoice.be/reseller-programme
Partner with e-invoice.be to offer Peppol e-invoicing to your customers
## Overview
e-invoice.be offers a **reseller programme** designed for SaaS companies, software vendors, and service providers who want to integrate Peppol e-invoicing capabilities into their products and services.
## Who Is This For?
Our reseller programme is ideal for:
* **SaaS platforms** (accounting, ERP, CRM, invoicing software)
* **Software vendors** offering business management solutions
* **Managed service providers** serving multiple clients
* **Industry-specific platforms** (construction, healthcare, logistics, etc.)
* **Accounting and bookkeeping firms** managing client invoicing
## Benefits
Integrate e-invoicing seamlessly into your platform under your brand
Manage multiple clients/customers through a single integration
Dedicated technical support during integration and beyond
Volume-based pricing for resellers and service providers
## How It Works
### 1. Partnership Setup
Contact us at [support@e-invoice.be](mailto:support@e-invoice.be) to discuss:
* Your use case and customer base
* Integration requirements
* Expected transaction volumes
* Pricing and commercial terms
### 2. Technical Integration
We'll work with you to integrate our API:
* **API access**: Full access to all e-invoice.be API endpoints
* **Admin API**: Organization-level API for managing customer tenants ([learn more](/admin-api))
* **Tenant management**: Create and manage customer accounts programmatically
* **Webhook configuration**: Real-time notifications for all customers
* **Sandbox environment**: Test thoroughly before going live
### 3. Go Live
Launch e-invoicing for your customers:
* **Customer onboarding**: Automated account creation via API
* **Billing**: Consolidated billing or per-customer invoicing
* **Support**: We handle Peppol technical issues, you handle customer support
## Use Cases
### SaaS Accounting Platform
A cloud accounting platform integrates e-invoice.be to offer Peppol e-invoicing:
```javascript theme={null}
// Create a tenant for a new customer (Admin API, organization key)
const tenant = await adminApi.post('/api/admin/tenants', {
name: 'customer-company',
description: 'Customer Company',
company_tax_id: 'BE0123456789',
peppol_ids: ['0208:0123456789']
});
// Provision an API key for that tenant
const apiKey = await adminApi.post(
`/api/admin/tenants/${tenant.data.id}/api-keys`,
{ name: 'production-key' }
);
// The customer authenticates with their own API key to send e-invoices.
// The tenant is derived from the key, so it is not passed in the body.
const invoice = await customerApi.post('/api/documents/', {
document_type: 'INVOICE',
// ... invoice data
});
```
### ERP System Integration
An ERP vendor builds e-invoicing directly into their product:
* Users send invoices from ERP interface
* ERP calls e-invoice.be API in the background
* Real-time status updates via webhooks
* Seamless user experience - no separate login needed
### Managed Service Provider
An MSP manages invoicing for 50+ clients:
* Single API integration
* Each client gets their own Peppol ID
* Centralized monitoring and reporting
* Bulk operations for efficiency
## Technical Requirements
### API Capabilities Needed
* Ability to make authenticated HTTP requests
* JSON parsing and generation
* Webhook endpoint hosting (for receiving notifications)
* Basic error handling and retry logic
### Security Considerations
* Secure storage of API keys
* Per-tenant credential management
* Webhook signature verification
* Audit logging for compliance
## Pricing Structure
Reseller pricing is customized based on:
* **Volume**: Number of invoices processed monthly
* **Customer count**: Number of end customers/tenants
* **Support level**: Standard or premium support
* **Contract term**: Annual commitments receive discounts
Contact us for a personalized quote.
## Getting Started
Ready to partner with e-invoice.be?
Email [support@e-invoice.be](mailto:support@e-invoice.be) with:
* Your company background
* Use case description
* Expected volumes
* Timeline
We'll schedule a call to discuss:
* Your requirements
* Pricing options
* Integration approach
Once partnered:
* Receive organization API key for Admin API access
* Access extended technical documentation
* Review [Admin API guide](/admin-api)
* Begin integration in sandbox environment
Go live with your integration:
* Production API access
* Customer onboarding
* Ongoing support
## Support
As a reseller partner, you'll receive:
* **Dedicated account manager**: Your main point of contact
* **Technical support**: Help with API integration and troubleshooting
* **Admin API access**: Organization-level API for tenant management ([documentation](/admin-api))
* **Extended documentation**: Including reseller-specific guides and best practices
* **Partner portal**: Manage customers, view analytics, access billing
## Frequently Asked Questions
Yes, the API integration is completely transparent to your end users. They interact with your interface, and you handle the e-invoice.be API calls in the background.
We offer flexible billing options:
* Consolidated billing to you (you bill your customers)
* Per-tenant billing (we bill each customer directly)
* Hybrid models
You provide first-line support to your customers. We provide:
* Technical support to you for API issues
* Peppol network status and incident communication
* Documentation and resources you can share
Yes! Peppol is an interoperable network. Your customers can exchange invoices with anyone on the Peppol network, regardless of their Access Point provider.
e-invoice.be handles:
* Peppol compliance and certification
* UBL BIS Billing 3.0 validation
* Network connectivity
You handle:
* Customer onboarding (KYC if applicable)
* Data privacy (GDPR compliance)
* Your application's security
We tailor agreements based on your situation. Contact us to discuss options that work for your business model.
## Technical Resources
Complete guide to the organization-level Admin API for managing customer tenants
Standard API documentation for creating and sending invoices
## Contact Us
Interested in becoming a reseller partner?
Email us at [support@e-invoice.be](mailto:support@e-invoice.be) to start the conversation
# Invoice Totals and Calculations
Source: https://docs.e-invoice.be/totals_docs
Understanding how invoice totals are calculated and what each field represents
## Overview
The e-invoice.be API calculates invoice totals based on line items, allowances, charges, and tax rates. This guide explains the calculation logic and what each total field represents.
## Total Fields
| Field | Description | Can be negative? |
| ------------------------- | -------------------------------------------- | ---------------- |
| `subtotal` | The taxable base (amount subject to VAT) | No |
| `total_discount` | Net financial adjustments not subject to VAT | Yes |
| `total_tax` | The total VAT/tax amount | No |
| `invoice_total` | The final invoice amount before prepayments | No (usually) |
| `amount_due` | The amount the customer needs to pay | No |
| `previous_unpaid_balance` | Outstanding balance from previous invoices | No |
## Calculation Formula
The basic formula for invoice calculations:
```
invoice_total = subtotal + total_tax + total_discount
amount_due = invoice_total - prepaid_amount
```
## Understanding Each Field
### Subtotal (Taxable Base)
The **subtotal** represents the taxable base - the amount on which VAT is calculated.
**Calculation:**
```
subtotal = sum of line items
- document-level allowances with VAT
+ document-level charges with VAT
```
**Key points:**
* Always positive
* Only includes allowances and charges that have VAT applied
* This is the base amount used for tax calculation
**Example:**
```
Line items: €1,000.00
Commercial discount (21% VAT): -€100.00
Shipping charge (21% VAT): +€50.00
→ Subtotal: €950.00
```
### Total Tax
The **total\_tax** is the total amount of VAT calculated on the subtotal.
**Calculation:**
* VAT is calculated on the subtotal for each applicable tax rate
* Multiple tax rates are grouped and calculated separately
* Results are summed to get the total tax
**Example:**
```
Subtotal at 21% VAT: €950.00
→ Total tax: €199.50 (950.00 × 0.21)
```
### Total Discount (Financial Adjustments)
The **total\_discount** field represents net financial adjustments that are **not subject to VAT**.
Despite its name, this field can represent both discounts AND charges. It is the net of non-VAT charges minus non-VAT allowances.
**Calculation:**
```
total_discount = non-VAT charges - non-VAT allowances
```
**This field can be:**
* **Positive**: When non-VAT charges exceed non-VAT allowances (adds to invoice total)
* **Negative**: When non-VAT allowances exceed non-VAT charges (reduces invoice total)
* **Zero**: When they balance out or don't exist
**Common use case - Early Payment Discount Pattern:**
Many invoices use a pattern where:
1. A discount with VAT is applied to reduce the taxable base
2. The same amount is added back as a charge without VAT
This allows customers to benefit from reduced VAT while maintaining standard payment terms.
**Example:**
```
Line items: €1,000.00
Early payment discount (21% VAT): -€50.00 (reduces tax base)
Early payment charge (0% VAT): +€50.00 (added after tax)
→ Subtotal: €950.00
→ Tax (21%): €199.50
→ Total discount: €50.00 (charge without VAT)
→ Invoice total: €1,199.50
Customer saves: €10.50 in VAT (€50.00 × 0.21)
```
### Invoice Total
The **invoice\_total** is the final amount of the invoice before prepayments.
**Calculation:**
```
invoice_total = subtotal + total_tax + total_discount
```
### Amount Due
The **amount\_due** is what the customer actually needs to pay.
**Calculation:**
```
amount_due = invoice_total - prepaid_amount
```
**Example with prepayment:**
```
Invoice total: €1,199.50
Prepaid amount: €200.00
→ Amount due: €999.50
```
### Previous Unpaid Balance
The **previous\_unpaid\_balance** represents any outstanding amounts from previous invoices. This is a custom field not part of standard UBL.
## VAT vs Non-VAT Allowances and Charges
Understanding the difference between VAT and non-VAT adjustments is critical for correct invoice calculations.
### Allowances/Charges with VAT (tax\_rate > 0)
* **Affect the subtotal** (taxable base)
* VAT is calculated on the adjusted amount
* Examples: Commercial discounts, volume discounts, shipping charges
### Allowances/Charges without VAT (tax\_rate = 0 or exempt)
* **Do NOT affect the subtotal**
* Applied AFTER tax calculation
* Affect the `total_discount` field
* Examples: Financial charges, administrative fees without VAT
Use VAT allowances for commercial discounts that should reduce both the base amount and VAT. Use non-VAT allowances for financial discounts that should not affect VAT calculation.
## Complete Example
Here's a complete invoice with both VAT and non-VAT adjustments:
```json theme={null}
{
"items": [
{
"description": "Product A",
"quantity": 10,
"unit_price": 100.00,
"amount": 1000.00,
"tax_rate": "21.00"
}
],
"allowances": [
{
"reason": "Commercial discount",
"amount": 200.00,
"tax_rate": "21.00"
},
{
"reason": "Early payment discount",
"amount": 50.00,
"tax_rate": "21.00"
}
],
"charges": [
{
"reason": "Early payment terms",
"amount": 50.00,
"tax_rate": "0.00"
}
]
}
```
**Calculation breakdown:**
1. **Line items total:** €1,000.00
2. **Apply VAT allowances:**
* Commercial discount: -€200.00
* Early payment discount: -€50.00
3. **Subtotal (taxable base):** €750.00
4. **Calculate tax:** €750.00 × 21% = €157.50
5. **Apply non-VAT adjustments:**
* Early payment charge (0% VAT): +€50.00
6. **Total discount:** €50.00
7. **Invoice total:** €750.00 + €157.50 + €50.00 = **€957.50**
8. **Amount due:** €957.50 (no prepayment)
## UBL Mapping
For reference, here's how the API fields map to UBL (Universal Business Language) elements:
| API Field | UBL Element |
| ---------------- | ---------------------------------------------------------------- |
| `subtotal` | TaxExclusiveAmount |
| `total_tax` | TaxAmount |
| `invoice_total` | TaxInclusiveAmount / PayableAmount |
| `amount_due` | PayableAmount (after prepayments) |
| `total_discount` | Calculated from ChargeTotal - AllowanceTotal (for non-VAT items) |
In UBL, `AllowanceTotalAmount` and `ChargeTotalAmount` include ALL allowances and charges (both VAT and non-VAT), whereas the API's `total_discount` only includes non-VAT adjustments.
## Validation Rules
When creating or updating invoices, the following validations are applied:
1. **Subtotal** must match the calculated taxable base (within €0.01 tolerance)
2. **Total tax** must match the calculated VAT amount (within €0.01 tolerance)
3. **Total discount** must match the net non-VAT adjustments (within €0.01 tolerance)
4. **Invoice total** must equal `subtotal + total_tax + total_discount` (within €0.01 tolerance)
5. **Amount due** must be between 0 and invoice\_total (inclusive)
## Common Questions
### Why can total\_discount be positive?
The field is named `total_discount` for historical reasons, but it actually represents the **net** of non-VAT charges minus non-VAT allowances. When non-VAT charges exceed non-VAT allowances, the value is positive and increases the invoice total.
### When should I use VAT vs non-VAT allowances?
* Use **VAT allowances** (tax\_rate > 0) for commercial discounts, volume discounts, etc. that should reduce both the base amount and VAT
* Use **non-VAT allowances** (tax\_rate = 0) for financial discounts or adjustments that should not affect VAT calculation
### Can invoice\_total be negative?
While theoretically possible with large negative adjustments, this is unusual. Most invoices should have a positive invoice total.
### What if I don't provide these fields?
If you don't provide `subtotal`, `total_tax`, `total_discount`, or `invoice_total`, the system will automatically calculate them based on your line items, allowances, and charges. The calculated values will be validated if you do provide them.
## Next Steps
Learn the basics of invoice creation
Work with allowances and charges
Test invoices during development
Explore all endpoints