> ## Documentation Index
> Fetch the complete documentation index at: https://docs.e-invoice.be/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors and troubleshooting

> Read the error formats and status codes of the e-invoice.be API, correct frequent validation failures and retry failed requests safely.

## Overview

This page is the reference for error handling. It shows the error body formats, the status codes of each endpoint, the frequent validation rules and the correct retry behaviour.

## Error body formats

The API uses three formats. Read the HTTP status code first, then parse the body.

### Error with a text message

Most errors return a JSON object with one field, `detail`, that contains a string.

```json 404 Not Found theme={null}
{
  "detail": "Document not found"
}
```

### Request validation error (422)

When a request does not match the schema of the endpoint (a missing field, a wrong type, a wrong date format), `detail` is an array. Each item identifies one field.

```json 422 Unprocessable Entity theme={null}
{
  "detail": [
    {
      "type": "missing",
      "loc": ["body", "file"],
      "msg": "Field required",
      "input": null
    }
  ]
}
```

<ResponseField name="loc" type="array">
  The path to the field. The first item is the part of the request (`body`, `query` or `path`).
</ResponseField>

<ResponseField name="msg" type="string">
  The description of the problem.
</ResponseField>

<ResponseField name="type" type="string">
  The machine-readable error type, for example `missing`.
</ResponseField>

<Note>
  Some endpoints also return `422` with a text message in `detail`, for example `"Invalid file type"`. Your code must accept a string and an array in `detail`.
</Note>

### Validation result

The validation endpoints (`POST /api/validate/json`, `POST /api/validate/ubl` and `POST /api/documents/{document_id}/validate`) return a success status when the document breaks a rule: `201 Created` for the two `/api/validate/` endpoints and `200 OK` for the validation of a stored document. The result is in the body: `is_valid` is `false` and `issues` contains the rule failures.

```json 201 Created theme={null}
{
  "id": "b55354b0-5c69-489b-a8f7-44be7d5bdd6b",
  "file_name": "b55354b0-5c69-489b-a8f7-44be7d5bdd6b.xml",
  "is_valid": false,
  "issues": [
    {
      "message": "Belgian enterprise number MUST be stated in the correct format.",
      "type": "error",
      "rule_id": "PEPPOL-COMMON-R043",
      "flag": "fatal"
    }
  ]
}
```

<ResponseField name="message" type="string" required>
  The text of the rule that failed.
</ResponseField>

<ResponseField name="type" type="string" required>
  The issue type, for example `error`.
</ResponseField>

<ResponseField name="rule_id" type="string">
  The identifier of the rule, for example `BR-CO-15` or `PEPPOL-EN16931-R120`. A failure of the XML schema has the value `xsd-validation`.
</ResponseField>

<ResponseField name="flag" type="string">
  The severity. An issue with the value `fatal` makes the document invalid.
</ResponseField>

<ResponseField name="location" type="string">
  The position in the generated UBL where the rule failed.
</ResponseField>

<ResponseField name="test" type="string">
  The test expression of the rule.
</ResponseField>

<ResponseField name="schematron" type="string" required>
  The rule set that found the issue.
</ResponseField>

## HTTP status codes

| Code | Endpoints | Typical `detail` | Action |
| - | - | - | - |
| `400` | Send document | `Could not derive Peppol sender/receiver information from document. Provide via API params or ensure document has valid tax/company IDs.` | Give the four Peppol routing parameters in the send call. |
| `400` | Send document (sandbox company) | `Tenant is in test mode but no company email is set` | Set the contact email address of the sandbox company. |
| `400` | Delete document, delete attachment | `Document is not in draft state` | Delete only documents in state `DRAFT` or `FAILED`. See [Delete a draft](/guides/managing-documents#delete-a-draft). |
| `400` | Usage statistics | `end_date must be greater than or equal to start_date` | Correct the date range. |
| `401` | All endpoints with authentication | `Authentication required`, `Invalid authentication` | Send a valid API key. See [Authentication](/authentication). |
| `403` | Admin API only | `This Peppol ID does not belong to this tenant.` | See [Admin API](/admin-api). The other endpoints do not return `403`. |
| `404` | Endpoints with an ID in the path | `Document not found`, `Webhook not found`, `Inbound email not found` | Make sure that the ID is correct and that it belongs to the company of the API key. |
| `404` | Usage statistics | `No documents found for this tenant` | Give `start_date` and `end_date`. See [Usage statistics and credits](/guides/usage-statistics#errors). |
| `404` | List attachments | `Document attachments not found` | The document has no attachments. Treat the response as an empty list. See [Attachments and PDF](/guides/attachments#list-the-attachments-of-a-document). |
| `405` | Send document | `Document is not in a valid state to send (DRAFT, FAILED)` | Do not send the document again. See [Send errors](#send-errors). |
| `406` | Create document, validate JSON | `Document is not valid: [...]` | Correct the payload. See [Create document errors](#create-document-errors-406). |
| `409` | Send document | `Derived sender '0208:0848934496' is not in tenant peppol_ids` | Send from a Peppol ID of your company. See [Ownership rule](/guides/self-billing#ownership-rule). |
| `409` | Reprocess inbound email | `Email was already processed successfully` | Do not reprocess. See [Send invoices by email (Mailbox)](/guides/mailbox). |
| `415` | Create document from UBL | `UBL file is empty`, `UBL file is not valid and contains errors` | Validate the file with `POST /api/validate/ubl` and correct it. |
| `422` | All endpoints with parameters or a body | Array with `loc`, `msg`, `type` | Correct the field in `loc`. |
| `422` | Create document from UBL or PDF, validate UBL, validate JSON, webhooks | `UBL File could not be parsed`, `PDF could not be converted to a document`, `Invalid file type`, `Invalid vendor tax id`, `Invalid webhook event` | Correct the input. |
| `429` | Create document, create document from UBL, validate JSON, validate UBL | `Too many requests, please try again later.` | Wait for the number of seconds in the `Retry-After` header. |
| `500` | All endpoints | `Internal server error` | Retry with a delay. If the error continues, contact support. |

<Note>
  The same status code can have different causes on different endpoints. A failure to derive the Peppol IDs gives `406` when you create a document and `400` when you send it. A Peppol ID that is not owned by your company gives `406` when you create a document and `409` when you send it.
</Note>

## Create document errors (406)

`POST /api/documents/` returns `406 Not Acceptable` for four causes. No document is created.

<AccordionGroup>
  <Accordion title="The Peppol sender or receiver cannot be derived">
    ```json theme={null}
    {
      "detail": "Could not derive Peppol sender or receiver information"
    }
    ```

    The API derives the Peppol IDs from the tax IDs and the company IDs of the vendor and the customer. Make sure that `vendor_tax_id` and `customer_tax_id` (or the company IDs) are present and correct.
  </Accordion>

  <Accordion title="The sender is not a Peppol ID of your company">
    ```json theme={null}
    {
      "detail": "Derived sender '0208:0848934496' is not in tenant peppol_ids"
    }
    ```

    The Peppol ID that the API derived from the vendor is not registered for the company of the API key. The comparison ignores upper and lower case and spaces at the start and the end. Use the vendor identifiers of your company. Use `GET /api/me/` to see the Peppol IDs of your company.

    For a self-billing document, the API examines the receiver, and the message starts with `Derived receiver`. See [Ownership rule](/guides/self-billing#ownership-rule).
  </Accordion>

  <Accordion title="The totals in the JSON are not correct">
    ```json theme={null}
    {
      "detail": "Document is not valid: ['Subtotal mismatch: provided 100.0, calculated 110.00']"
    }
    ```

    The API calculates the totals from the lines and compares them with your values. The message can also start with `Total tax mismatch`, `Total discount mismatch`, `Invoice total mismatch` or `Amount due`. See [Invoice totals and calculations](/guides/invoice-totals).
  </Accordion>

  <Accordion title="The generated UBL is not valid for Peppol BIS Billing 3.0">
    ```json theme={null}
    {
      "detail": "Provided document details lead to an invalid UBL document: {'Belgian enterprise number MUST be stated in the correct format.'}"
    }
    ```

    `detail` contains the messages of the rules that failed, without the rule identifiers. Send the same payload to `POST /api/validate/json` to get the full list of issues with `rule_id`. Then see [Frequent validation rules](#frequent-validation-rules).
  </Accordion>
</AccordionGroup>

<Note>
  Validation is not a separate mandatory call. `POST /api/documents/` rejects a payload that does not pass the same rules. Use `POST /api/validate/json` while you develop, because it returns all rule failures and the generated UBL.
</Note>

## Send errors

`POST /api/documents/{document_id}/send` does these checks in sequence.

| Code | `detail` | Cause | Correction |
| - | - | - | - |
| `404` | `Document not found` | The ID is wrong, or the document belongs to a different company. | Use the `id` from the create response. |
| `405` | `Document is not in a valid state to send (DRAFT, FAILED)` | The state is `TRANSIT`, `SENT` or `RECEIVED`. | Do not send again. Read the state with `GET /api/documents/{document_id}`. |
| `404` | `UBL document not found` | The document has no generated UBL. | Create the document again. |
| `400` | `Could not derive Peppol sender/receiver information from document. Provide via API params or ensure document has valid tax/company IDs.` | The API cannot find a full sender and receiver Peppol ID. | Give `sender_peppol_scheme`, `sender_peppol_id`, `receiver_peppol_scheme` and `receiver_peppol_id` as query parameters. |
| `409` | `Derived sender '0208:0848934496' is not in tenant peppol_ids` | The sender is not a Peppol ID of your company. For self-billing, the API examines the receiver. | Send from a Peppol ID of your company. |
| `400` | `Tenant is in test mode but no company email is set` | A sandbox company sends documents to the contact email address of the company, and the company has no such address. | Set the contact email address in app.e-invoice.be, then create and send the document again. |

### Peppol ID routing

When you send a document, the API finds the sender and receiver Peppol IDs in this order:

1. The query parameters of the send request.
2. The Peppol IDs that the API stored with the UBL of the document.
3. The identifiers in the document: `vendor_tax_id` or `vendor_company_id` for the sender, and `customer_peppol_id`, `customer_tax_id` or `customer_company_id` for the receiver.

For a Belgian party, the API derives scheme `0208` and the enterprise number: the tax ID `BE1018265814` gives the Peppol ID `0208:1018265814`. For other countries, the derived scheme can be different from the scheme that the receiver registered. Thus the best practice is to give all four query parameters:

```bash cURL theme={null}
curl -X POST "https://api.e-invoice.be/api/documents/$DOCUMENT_ID/send?sender_peppol_scheme=0208&sender_peppol_id=1018265814&receiver_peppol_scheme=0208&receiver_peppol_id=0848934496" \
  -H "Authorization: Bearer $E_INVOICE_API_KEY"
```

<ParamField query="sender_peppol_scheme" type="string">
  Scheme of the sender Peppol ID. Example: `0208`.
</ParamField>

<ParamField query="sender_peppol_id" type="string">
  Identifier of the sender, without the scheme. Example: `1018265814`.
</ParamField>

<ParamField query="receiver_peppol_scheme" type="string">
  Scheme of the receiver Peppol ID. Example: `0208`.
</ParamField>

<ParamField query="receiver_peppol_id" type="string">
  Identifier of the receiver, without the scheme. Example: `0848934496`.
</ParamField>

The API returns `400` if it cannot find a complete sender and receiver Peppol ID. It returns `409` if the sender Peppol ID is not one of the `peppol_ids` of your company.

<Tip>
  Before you send, make sure that the receiver is registered on the Peppol network with the Peppol ID that you use. See [Look up Peppol participants](/guides/lookup-participants).
</Tip>

## Frequent validation rules

| Rule | Cause | Correction in the JSON |
| - | - | - |
| `PEPPOL-COMMON-R043` | A Belgian enterprise number does not have the correct format or fails the modulo 97 check. | Use a real enterprise number of 10 digits in `vendor_tax_id` and `customer_tax_id`, for example `BE1018265814`. |
| `BR-S-08` | The taxable amount of a VAT rate is not equal to the sum of the line net amounts with that rate. | Give `amount` on each line, and make sure that `subtotal` is the sum of the line amounts. |
| `PEPPOL-EN16931-R120` | The net amount of a line is not equal to quantity × unit price, plus line charges, minus line allowances. | Set `amount` to `quantity` × `unit_price` on each line. For allowances and charges, see [Advanced invoicing](/guides/advanced-invoicing). |
| `BR-CO-15` | The total with VAT is not equal to the total without VAT plus the VAT amount. | Make sure that `invoice_total` = `subtotal` + `total_tax`. See [Invoice totals and calculations](/guides/invoice-totals). |
| `BR-CO-25` | The amount due is more than zero, and the document has no due date and no payment terms. | Add `due_date` or `payment_term`. |
| No rule (`422`) | A date does not have the format `YYYY-MM-DD`. The request fails before the rules run. | Send dates as `2026-10-01`. The array in `detail` gives the field in `loc`. |

For the full procedure, see [Validation during development](/guides/validation).

## A document is in state `FAILED`

After an accepted send call, the document is in state `TRANSIT`. The transmission occurs in the background. If all transmission attempts fail, the state becomes `FAILED` and the `document.sent.failed` webhook event is sent. See [Retry behaviour](/guides/document-lifecycle#retry-behaviour) for the number of attempts.

<Steps>
  <Step title="Read the timeline">
    `GET /api/documents/{document_id}/timeline` returns the events of the document. Each transmission attempt that failed is an event with `event_type` `send_failed`. The `details` object shows the sender and receiver Peppol IDs that the API used.

    ```bash cURL theme={null}
    curl "https://api.e-invoice.be/api/documents/doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d/timeline" \
      -H "Authorization: Bearer $E_INVOICE_API_KEY"
    ```

    ```json 200 OK theme={null}
    {
      "document_id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
      "events": [
        {
          "event_type": "document_created",
          "timestamp": "2026-10-01T09:00:00Z",
          "id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"
        },
        {
          "event_type": "send_failed",
          "timestamp": "2026-10-01T09:00:05Z",
          "id": "7c1d2a90-5b3e-4f6a-9d2c-8e1f0a4b6c3d",
          "details": {
            "sender_peppol_id": "0208:1018265814",
            "receiver_peppol_id": "0208:0848934496"
          }
        }
      ]
    }
    ```

    The values in this example are illustrative.
  </Step>

  <Step title="Examine the receiver">
    Make sure that the receiver Peppol ID in `details` is correct. Then call `GET /api/validate/peppol-id?peppol_id=0208:0848934496` to see if the receiver is registered on the Peppol network. See [Look up Peppol participants](/guides/lookup-participants).
  </Step>

  <Step title="Send again">
    Call `POST /api/documents/{document_id}/send` again. The API accepts a send call for a document in state `FAILED`. If the Peppol IDs were wrong, give the correct IDs as query parameters. See [Send a failed document again](/guides/managing-documents#send-a-failed-document-again).
  </Step>
</Steps>

<Note>
  The timeline does not contain the technical reason of a failed transmission. If the receiver is registered and the document fails again, contact [support@e-invoice.be](mailto:support@e-invoice.be) with the document ID.
</Note>

For all states and transitions, see [Document lifecycle and delivery tracking](/guides/document-lifecycle).

## Retries and idempotency

### Which errors to retry

| Response | Retry | How |
| - | - | - |
| `429` | Yes | Wait for the number of seconds in the `Retry-After` header. |
| `500` and network errors | Yes | Use a delay that increases with each attempt. Before you retry a create call, do the check below. |
| `400`, `401`, `404`, `405`, `406`, `409`, `415`, `422` | No | The same request gives the same error. Correct the request first. |

### Rate limits

The rate limit applies to each API key and only to these endpoints:

* `POST /api/documents/`
* `POST /api/documents/ubl`
* `POST /api/validate/json`
* `POST /api/validate/ubl`

When you exceed the limit, the API returns `429` with a `Retry-After` header that contains a number of seconds.

```json 429 Too Many Requests theme={null}
{
  "detail": "Too many requests, please try again later."
}
```

Do not write a fixed limit into your code. Read `Retry-After` and wait.

### No idempotency key

The API has no idempotency key. If you repeat `POST /api/documents/`, the API creates a second draft with the same `invoice_id`. A send call is different: a second send call for a document in state `TRANSIT` or `SENT` returns `405` and does not transmit the document again.

If a create call ends without a response (a timeout or a network error), find out if the document exists before you create it again. Search the drafts for your invoice number. This list contains documents in the states `DRAFT`, `TRANSIT` and `FAILED`. For documents that are sent, use `GET /api/outbox/` with the same `search` parameter. [Find duplicates before a retry](/guides/managing-documents#find-duplicates-before-a-retry) gives the full procedure.

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.e-invoice.be/api/drafts/?search=INV-2026-001" \
    -H "Authorization: Bearer $E_INVOICE_API_KEY"
  ```

  ```javascript Node.js theme={null}
  const params = new URLSearchParams({ search: "INV-2026-001" });

  const response = await fetch(
    `https://api.e-invoice.be/api/drafts/?${params}`,
    {
      headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` },
    }
  );

  const { items } = await response.json();
  const existing = items.find((doc) => doc.invoice_id === "INV-2026-001");
  console.log(existing ? existing.id : "Not found. It is safe to create the document.");
  ```

  ```python Python theme={null}
  import os

  import requests

  response = requests.get(
      "https://api.e-invoice.be/api/drafts/",
      params={"search": "INV-2026-001"},
      headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"},
  )
  response.raise_for_status()

  existing = next(
      (doc for doc in response.json()["items"] if doc["invoice_id"] == "INV-2026-001"),
      None,
  )
  print(existing["id"] if existing else "Not found. It is safe to create the document.")
  ```

  ```php PHP theme={null}
  <?php
  $url = 'https://api.e-invoice.be/api/drafts/?' . http_build_query(['search' => 'INV-2026-001']);

  $ch = curl_init($url);
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('E_INVOICE_API_KEY')],
  ]);
  $body = json_decode(curl_exec($ch), true);
  curl_close($ch);

  $existing = null;
  foreach ($body['items'] as $doc) {
      if ($doc['invoice_id'] === 'INV-2026-001') {
          $existing = $doc;
          break;
      }
  }
  echo $existing ? $existing['id'] : 'Not found. It is safe to create the document.';
  ```

  ```csharp C# theme={null}
  using System.Net.Http.Headers;
  using System.Text.Json;

  using var client = new HttpClient();
  client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
      "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY"));

  var json = await client.GetStringAsync(
      "https://api.e-invoice.be/api/drafts/?search=INV-2026-001");

  using var body = JsonDocument.Parse(json);
  string? existingId = null;
  foreach (var doc in body.RootElement.GetProperty("items").EnumerateArray())
  {
      if (doc.GetProperty("invoice_id").GetString() == "INV-2026-001")
      {
          existingId = doc.GetProperty("id").GetString();
          break;
      }
  }
  Console.WriteLine(existingId ?? "Not found. It is safe to create the document.");
  ```
</CodeGroup>

```json 200 OK theme={null}
{
  "items": [
    {
      "id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
      "state": "DRAFT",
      "document_type": "INVOICE",
      "invoice_id": "INV-2026-001",
      "vendor_name": "E-INVOICE BV",
      "customer_name": "OpenPeppol VZW"
    }
  ],
  "total": 1,
  "page": 1,
  "page_size": 20,
  "pages": 1,
  "has_next_page": false
}
```

The response is shortened. The `search` parameter finds a part of the invoice number, the vendor name, the customer name or the note, thus compare `invoice_id` for an exact match.

## Troubleshooting

<AccordionGroup>
  <Accordion title="422 with &#x22;Field required&#x22; and loc [&#x22;body&#x22;, &#x22;file&#x22;]">
    The endpoint expects a `multipart/form-data` upload with a part that has the name `file`. You sent the XML or the PDF as the raw request body. With cURL, use `-F "file=@invoice.xml"` and do not set the `Content-Type` header manually. See [Send UBL documents](/guides/ubl-documents) and [Create documents from PDF](/guides/pdf-documents).
  </Accordion>

  <Accordion title="401 on each request">
    `Authentication required` means that the request has no `Authorization: Bearer` header. `Invalid authentication` means that the API key is not known or is deleted. Make sure that the header has the format `Authorization: Bearer <key>` and that the key is the key of the company that you want to use. See [Authentication](/authentication).
  </Accordion>

  <Accordion title="429 during a bulk import">
    The create and validate endpoints have a rate limit for each API key. Read the `Retry-After` header, wait, then continue. Send the requests in sequence, not in parallel.
  </Accordion>

  <Accordion title="The customer did not receive the document">
    Read the state with `GET /api/documents/{document_id}`.

    * `DRAFT`: the document was not sent. Call the send endpoint.
    * `TRANSIT`: the transmission is in progress.
    * `FAILED`: see [A document is in state `FAILED`](#a-document-is-in-state-failed).
    * `SENT`: the access point of the receiver accepted the document. Ask the customer to look in the software that is connected to their Peppol ID. Read the timeline to see which receiver Peppol ID was used.

    If you use a sandbox company, the document is not sent on the Peppol network. See [Test mode and sandbox companies](/environments).
  </Accordion>

  <Accordion title="The test email did not arrive">
    A sandbox company sends each document as an email with the UBL file to the contact email address of your company, not to the customer. Make sure that the contact email address is set, and look in the spam folder. If the company has no contact email address, the send call returns `400` with `Tenant is in test mode but no company email is set`. See [Test mode and sandbox companies](/environments).
  </Accordion>

  <Accordion title="The webhook signature does not match">
    The signature is calculated on the full event as JSON with sorted keys, not on the `data` field only. Make sure that you use the secret of the correct webhook and that you serialise the payload as the [Webhooks](/essentials/webhooks) guide shows.
  </Accordion>

  <Accordion title="The sandbox inbox is empty">
    A sandbox company does not receive documents from the Peppol network, and a document that you send to your own company does not go into the inbox. Use **Simulate inbound** to put a document into the inbox. See [Receive documents](/guides/receiving-documents).
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Validation during development" icon="circle-check" href="/guides/validation">
    Find all rule failures of a payload before you create a document.
  </Card>

  <Card title="Document lifecycle and delivery tracking" icon="arrows-rotate" href="/guides/document-lifecycle">
    Learn the states of a document and the transitions between them.
  </Card>

  <Card title="Webhooks" icon="webhook" href="/essentials/webhooks">
    Receive an event when a document is sent or fails.
  </Card>

  <Card title="Invoice totals and calculations" icon="calculator" href="/guides/invoice-totals">
    Calculate totals that pass the validation rules.
  </Card>
</CardGroup>
