# 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: Outbound document state flow **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: Inbound document state flow 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`): Outbound document state flow 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`): Inbound document state flow 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 | Outbound document state flow **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 Outbound document state flow 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. e-invoice.be as Peppol Access Point ## 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: Peppol 4-Corner Model 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