> ## 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.

# Validation during development

> Validate invoice JSON and UBL XML against the Peppol BIS Billing 3.0 rules before you create a document.

## Overview

The API has two validation endpoints:

* `POST /api/validate/json` converts your JSON to [UBL](/glossary#ubl) and examines the result against the rules of [EN 16931](/glossary#en-16931) and [Peppol BIS Billing 3.0](/glossary#bis-billing-30).
* `POST /api/validate/ubl` examines a UBL XML file that you made yourself.

The two endpoints do not create a document and do not send data to the Peppol network.

<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>

## Why validate

* **No document is created**: you can repeat the call as often as necessary.
* **All rule failures in one response**: each issue has a rule ID and a message.
* **The generated UBL**: a valid result contains the XML that the API makes from your JSON.
* **Fewer rejected create requests**: you correct the payload before you call `POST /api/documents/`.

## Validate a JSON document

Send the document JSON to `POST /api/validate/json`. The body is the same as the body of `POST /api/documents/`.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.e-invoice.be/api/validate/json" \
    -H "Authorization: Bearer $E_INVOICE_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
    "document_type": "INVOICE",
    "invoice_id": "INV-2026-001",
    "invoice_date": "2026-10-01",
    "due_date": "2026-10-31",
    "currency": "EUR",
    "purchase_order": "PO-12345",
    "vendor_name": "E-INVOICE BV",
    "vendor_tax_id": "BE1018265814",
    "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE",
    "vendor_email": "billing@e-invoice.be",
    "customer_name": "OpenPeppol VZW",
    "customer_tax_id": "BE0848934496",
    "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE",
    "items": [
      {
        "description": "Professional services",
        "quantity": 10,
        "unit": "C62",
        "unit_price": 100.00,
        "amount": 1000.00,
        "tax_rate": "21.00"
      }
    ],
    "subtotal": 1000.00,
    "total_tax": 210.00,
    "invoice_total": 1210.00,
    "amount_due": 1210.00,
    "payment_term": "Payment within 30 days",
    "payment_details": [
      {
        "iban": "BE68539007547034",
        "swift": "GEBABEBB",
        "payment_reference": "INV-2026-001"
      }
    ]
  }'
  ```

  ```javascript Node.js theme={null}
  const invoice = {
    document_type: "INVOICE",
    invoice_id: "INV-2026-001",
    invoice_date: "2026-10-01",
    due_date: "2026-10-31",
    currency: "EUR",
    purchase_order: "PO-12345",
    vendor_name: "E-INVOICE BV",
    vendor_tax_id: "BE1018265814",
    vendor_address: "Brusselsesteenweg 119/A, 1980 Zemst, BE",
    vendor_email: "billing@e-invoice.be",
    customer_name: "OpenPeppol VZW",
    customer_tax_id: "BE0848934496",
    customer_address: "Robert Schumanplein 6 bus 5, 1040 Brussel, BE",
    items: [
      {
        description: "Professional services",
        quantity: 10,
        unit: "C62",
        unit_price: 100.00,
        amount: 1000.00,
        tax_rate: "21.00"
      }
    ],
    subtotal: 1000.00,
    total_tax: 210.00,
    invoice_total: 1210.00,
    amount_due: 1210.00,
    payment_term: "Payment within 30 days",
    payment_details: [
      {
        iban: "BE68539007547034",
        swift: "GEBABEBB",
        payment_reference: "INV-2026-001"
      }
    ]
  };

  const response = await fetch("https://api.e-invoice.be/api/validate/json", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(invoice),
  });

  const result = await response.json();
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${JSON.stringify(result)}`);
  }
  console.log(result);
  ```

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

  import requests

  invoice = {
    "document_type": "INVOICE",
    "invoice_id": "INV-2026-001",
    "invoice_date": "2026-10-01",
    "due_date": "2026-10-31",
    "currency": "EUR",
    "purchase_order": "PO-12345",
    "vendor_name": "E-INVOICE BV",
    "vendor_tax_id": "BE1018265814",
    "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE",
    "vendor_email": "billing@e-invoice.be",
    "customer_name": "OpenPeppol VZW",
    "customer_tax_id": "BE0848934496",
    "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE",
    "items": [
      {
        "description": "Professional services",
        "quantity": 10,
        "unit": "C62",
        "unit_price": 100.00,
        "amount": 1000.00,
        "tax_rate": "21.00"
      }
    ],
    "subtotal": 1000.00,
    "total_tax": 210.00,
    "invoice_total": 1210.00,
    "amount_due": 1210.00,
    "payment_term": "Payment within 30 days",
    "payment_details": [
      {
        "iban": "BE68539007547034",
        "swift": "GEBABEBB",
        "payment_reference": "INV-2026-001"
      }
    ]
  }

  response = requests.post(
      "https://api.e-invoice.be/api/validate/json",
      headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"},
      json=invoice,
      timeout=30,
  )
  if not response.ok:
      raise SystemExit(f"HTTP {response.status_code}: {response.text}")
  print(response.json())
  ```

  ```php PHP theme={null}
  <?php
  $invoice = [
    "document_type" => "INVOICE",
    "invoice_id" => "INV-2026-001",
    "invoice_date" => "2026-10-01",
    "due_date" => "2026-10-31",
    "currency" => "EUR",
    "purchase_order" => "PO-12345",
    "vendor_name" => "E-INVOICE BV",
    "vendor_tax_id" => "BE1018265814",
    "vendor_address" => "Brusselsesteenweg 119/A, 1980 Zemst, BE",
    "vendor_email" => "billing@e-invoice.be",
    "customer_name" => "OpenPeppol VZW",
    "customer_tax_id" => "BE0848934496",
    "customer_address" => "Robert Schumanplein 6 bus 5, 1040 Brussel, BE",
    "items" => [
      [
        "description" => "Professional services",
        "quantity" => 10,
        "unit" => "C62",
        "unit_price" => 100.00,
        "amount" => 1000.00,
        "tax_rate" => "21.00"
      ]
    ],
    "subtotal" => 1000.00,
    "total_tax" => 210.00,
    "invoice_total" => 1210.00,
    "amount_due" => 1210.00,
    "payment_term" => "Payment within 30 days",
    "payment_details" => [
      [
        "iban" => "BE68539007547034",
        "swift" => "GEBABEBB",
        "payment_reference" => "INV-2026-001"
      ]
    ]
  ];

  $ch = curl_init("https://api.e-invoice.be/api/validate/json");
  curl_setopt_array($ch, [
      CURLOPT_POST => true,
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER => [
          "Authorization: Bearer " . getenv("E_INVOICE_API_KEY"),
          "Content-Type: application/json",
      ],
      CURLOPT_POSTFIELDS => json_encode($invoice),
  ]);

  $body = curl_exec($ch);
  $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  curl_close($ch);

  if ($status >= 400) {
      throw new RuntimeException("HTTP $status: $body");
  }
  print_r(json_decode($body, true));
  ```

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

  var invoice = """
  {
    "document_type": "INVOICE",
    "invoice_id": "INV-2026-001",
    "invoice_date": "2026-10-01",
    "due_date": "2026-10-31",
    "currency": "EUR",
    "purchase_order": "PO-12345",
    "vendor_name": "E-INVOICE BV",
    "vendor_tax_id": "BE1018265814",
    "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE",
    "vendor_email": "billing@e-invoice.be",
    "customer_name": "OpenPeppol VZW",
    "customer_tax_id": "BE0848934496",
    "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE",
    "items": [
      {
        "description": "Professional services",
        "quantity": 10,
        "unit": "C62",
        "unit_price": 100.00,
        "amount": 1000.00,
        "tax_rate": "21.00"
      }
    ],
    "subtotal": 1000.00,
    "total_tax": 210.00,
    "invoice_total": 1210.00,
    "amount_due": 1210.00,
    "payment_term": "Payment within 30 days",
    "payment_details": [
      {
        "iban": "BE68539007547034",
        "swift": "GEBABEBB",
        "payment_reference": "INV-2026-001"
      }
    ]
  }
  """;

  using var client = new HttpClient { BaseAddress = new Uri("https://api.e-invoice.be") };
  client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
      "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY"));

  var response = await client.PostAsync(
      "/api/validate/json",
      new StringContent(invoice, Encoding.UTF8, "application/json"));

  var body = await response.Content.ReadAsStringAsync();
  if (!response.IsSuccessStatusCode)
  {
      throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {body}");
  }
  Console.WriteLine(body);
  ```
</CodeGroup>

`POST /api/validate/json` does not compare the vendor with your company. `POST /api/documents/` does: before you create the document, replace the vendor fields with those of your own company.

When the document is valid, the API returns `201 Created` with the result and the generated UBL:

```json 201 Created theme={null}
{
  "id": "9eec0b03-4649-4ae4-9c4c-323ebb6d53e8",
  "file_name": "9eec0b03-4649-4ae4-9c4c-323ebb6d53e8.xml",
  "is_valid": true,
  "issues": [],
  "ubl_document": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Invoice xmlns=\"urn:oasis:names:specification:ubl:schema:xsd:Invoice-2\"..."
}
```

The `ubl_document` value is shortened in this example.

<ResponseField name="id" type="string" required>
  The identifier of this validation. It is not a document ID.
</ResponseField>

<ResponseField name="file_name" type="string" required>
  The name of the generated XML file (`<id>.xml`). For `POST /api/validate/ubl`, the name of the file that you uploaded.
</ResponseField>

<ResponseField name="is_valid" type="boolean" required>
  `false` when one issue or more has the flag `fatal` or is a failure of the XML schema (`rule_id` is `xsd-validation`).
</ResponseField>

<ResponseField name="issues" type="array" required>
  The rule failures and warnings. Each issue has `message`, `type` (`error` or `warning`) and `schematron` (the rule set that found the issue). An issue can also have `rule_id`, `flag`, `location` and `test`. For the description of each field, see [Validation result](/guides/errors#validation-result).
</ResponseField>

<ResponseField name="ubl_document" type="string">
  The generated UBL XML. `POST /api/validate/json` returns this field only when `is_valid` is `true`. `POST /api/documents/{document_id}/validate` always returns it.
</ResponseField>

<Tip>
  The `ubl_document` field shows the XML that the API makes from your JSON. Use it to see how each JSON field maps to UBL.
</Tip>

### A document that breaks rules

A rule failure does not change the HTTP status code. The API returns `201 Created`, `is_valid` is `false`, and `issues` contains the failures. Thus your code must read `is_valid`, not only the status code.

This payload has two faults: the vendor enterprise number is not a real number, and the line has no `amount`. It is a failing example on purpose.

```json Request body with faults theme={null}
{
  "document_type": "INVOICE",
  "invoice_id": "INV-2026-001",
  "invoice_date": "2026-10-01",
  "due_date": "2026-10-31",
  "currency": "EUR",
  "vendor_name": "E-INVOICE BV",
  "vendor_tax_id": "BE0123456789",
  "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE",
  "customer_name": "OpenPeppol VZW",
  "customer_tax_id": "BE0848934496",
  "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE",
  "items": [
    {
      "description": "Professional services",
      "quantity": "10",
      "unit_price": "100.00",
      "unit": "C62",
      "tax_rate": "21.00"
    }
  ]
}
```

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

The response has no `ubl_document`, because the document is not valid. To correct the payload:

1. Use an enterprise number that passes the modulo 97 check: `BE1018265814` in place of `BE0123456789`. This removes `PEPPOL-COMMON-R043`.
2. Add `amount` to the line: `"1000.00"` (`quantity` × `unit_price`). This removes `BR-S-08` and `PEPPOL-EN16931-R120`.

For the cause and the correction of each frequent rule ID, see [Frequent validation rules](/guides/errors#frequent-validation-rules).

### Totals that are not consistent

Before the API generates the UBL, it calculates the totals from the lines and compares them with `subtotal`, `total_tax`, `total_discount`, `invoice_total` and `amount_due` in your JSON. If a value is different from the calculated value by more than 1.00, or if `amount_due` is more than the calculated invoice total, the API stops. It returns `406 Not Acceptable` with one text message, and no `issues` list:

```json 406 Not Acceptable theme={null}
{
  "detail": "Document is not valid: ['Subtotal mismatch: provided 100.0, calculated 110.00']"
}
```

The message can also contain `Total tax mismatch`, `Total discount mismatch`, `Invoice total mismatch` or `Amount due (...) cannot be greater than invoice total (...)`. When more than one total is wrong, the list in the message has one entry for each total. `POST /api/documents/` returns the same `406` for the same payload.

<Tip>
  If you leave out a total field, the API calculates it. See [Invoice totals and calculations](/guides/invoice-totals).
</Tip>

A request that does not agree with the schema (for example a date that is not `YYYY-MM-DD`, or a currency code that is not supported) returns `422` before the rules run. See [Request validation error (422)](/guides/errors#request-validation-error-422).

## Validate a UBL XML file

Use `POST /api/validate/ubl` to examine a UBL BIS Billing 3.0 XML file that you have.

<Warning>
  This endpoint accepts `multipart/form-data` with one form field, `file`. It does not accept the XML as the raw request body. See [Common mistake: raw XML body](#common-mistake-raw-xml-body).
</Warning>

| Item | Value |
| - | - |
| Method | `POST` |
| URL | `https://api.e-invoice.be/api/validate/ubl` |
| Authorization | `Bearer` and your API key |
| Content-Type | `multipart/form-data` (your HTTP client sets it when you attach a file) |
| Body | Form field `file` with the UBL XML file. The content type of this part must be `application/xml` or `text/xml`. |

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.e-invoice.be/api/validate/ubl" \
    -H "Authorization: Bearer $E_INVOICE_API_KEY" \
    -F "file=@invoice.xml;type=application/xml"
  ```

  ```javascript Node.js theme={null}
  import { readFile } from "node:fs/promises";

  const form = new FormData();
  form.append(
    "file",
    new Blob([await readFile("invoice.xml")], { type: "application/xml" }),
    "invoice.xml"
  );

  // Do not set Content-Type. fetch adds the multipart boundary.
  const response = await fetch("https://api.e-invoice.be/api/validate/ubl", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` },
    body: form,
  });

  const result = await response.json();
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${JSON.stringify(result)}`);
  }
  console.log(result);
  ```

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

  with open('invoice.xml', 'rb') as f:
      response = requests.post(
          'https://api.e-invoice.be/api/validate/ubl',
          headers={'Authorization': f"Bearer {os.environ['E_INVOICE_API_KEY']}"},
          files={'file': ('invoice.xml', f, 'application/xml')},
      )

  print(response.json())
  ```

  ```php PHP theme={null}
  <?php
  $ch = curl_init('https://api.e-invoice.be/api/validate/ubl');
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_POST => true,
      CURLOPT_HTTPHEADER => [
          'Authorization: Bearer ' . getenv('E_INVOICE_API_KEY'),
      ],
      CURLOPT_POSTFIELDS => [
          'file' => new CURLFile('invoice.xml', 'application/xml', 'invoice.xml'),
      ],
  ]);

  echo curl_exec($ch);
  curl_close($ch);
  ```

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

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

  using var form = new MultipartFormDataContent();
  var fileBytes = await File.ReadAllBytesAsync("invoice.xml");
  var fileContent = new ByteArrayContent(fileBytes);
  fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/xml");
  // The form field must have the name "file".
  form.Add(fileContent, "file", "invoice.xml");

  var response = await http.PostAsync(
      "https://api.e-invoice.be/api/validate/ubl",
      form
  );

  Console.WriteLine(await response.Content.ReadAsStringAsync());
  ```
</CodeGroup>

The API returns `201 Created` with the validation result. A valid file gives:

```json 201 Created theme={null}
{
  "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
  "file_name": "invoice.xml",
  "is_valid": true,
  "issues": []
}
```

A file that breaks a rule gives the same status code, with `is_valid` set to `false`:

```json 201 Created theme={null}
{
  "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
  "file_name": "invoice.xml",
  "is_valid": false,
  "issues": [
    {
      "message": "[BR-CO-15]-Invoice total amount with VAT (BT-112) = Invoice total amount without VAT (BT-109) + Invoice total VAT amount (BT-110).",
      "type": "error",
      "rule_id": "BR-CO-15",
      "flag": "fatal",
      "schematron": "CEN-EN16931-UBL"
    }
  ]
}
```

The `schematron` field names the [Schematron](/glossary#schematron) rule set that found the issue: `CEN-EN16931-UBL`, `PEPPOL-EN16931-UBL`, `PEPPOL-EN16931-UBL-SB` (self-billing) or `XSD` (the XML schema).

### Common mistake: raw XML body

If you set `Content-Type: application/xml` and send the XML as the raw request body (for example with `--data-binary @invoice.xml`), the API returns this `422` response:

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

The correction: use `multipart/form-data` with a `file` form field, as the examples above show.

| Wrong | Right |
| - | - |
| `-H "Content-Type: application/xml" --data-binary @invoice.xml` | `-F "file=@invoice.xml;type=application/xml"` |
| `fetch(url, { body: xml, headers: { 'Content-Type': 'application/xml' } })` | `fetch(url, { body: formData })` |
| `requests.post(url, data=xml, headers={'Content-Type': 'application/xml'})` | `requests.post(url, files={'file': ('invoice.xml', f, 'application/xml')})` |
| `new StringContent(xml, ..., "application/xml")` | `new MultipartFormDataContent { { fileContent, "file", "invoice.xml" } }` |

### When to use UBL validation

Use `POST /api/validate/ubl` when you:

* have UBL XML files from your ERP system and want to examine them before you send them
* move from a different Peppol Access Point and have UBL documents that are already generated
* get UBL files from an external source and want to examine them before you post them to `POST /api/documents/ubl`

<Tip>
  If you create invoices from JSON, use `POST /api/validate/json`. It applies the same UBL rules and returns the generated UBL, so a second call to `POST /api/validate/ubl` is not necessary.
</Tip>

See also [Send UBL documents](/guides/ubl-documents).

## Common validation problems

| Problem | What the API returns | Correction |
| - | - | - |
| A Belgian enterprise number is not correct | Issue `PEPPOL-COMMON-R043` | Use the real 10-digit number with the prefix `BE`, for example `BE1018265814`. |
| A line has no `amount`, or `amount` is not `quantity` × `unit_price` | Issues `PEPPOL-EN16931-R120` and `BR-S-08` | Set `amount` on each line. |
| The totals are not consistent with the lines | `406` with one message (see [Totals that are not consistent](#totals-that-are-not-consistent)) | Correct the totals, or leave them out. |
| A date does not have the format `YYYY-MM-DD` | `422` with the field in `loc` | Send dates as `2026-10-01`. |
| The currency code is not supported | `422` with the field in `loc` | Use a code from the list below. |
| Data that the rules make mandatory is missing (for example a party name or address) | One issue or more in `issues` | Read the `rule_id` and the `message` of each issue. |

The `currency` field accepts these ISO 4217 codes:

`EUR`, `USD`, `GBP`, `JPY`, `CHF`, `CAD`, `AUD`, `NZD`, `CNY`, `INR`, `SEK`, `NOK`, `DKK`, `SGD`, `HKD`

The default is `EUR`.

The `tax_rate` of a line is a percentage with two decimals as a string, for example `"21.00"`, `"6.00"` or `"0.00"`.

For the rule ID reference, all HTTP status codes and the error body formats, see [Errors and troubleshooting](/guides/errors).

## Development workflow

<Steps>
  <Step title="Build the invoice JSON">
    Start from the example in [Validate a JSON document](#validate-a-json-document) and replace the values with your data. For the fields, see [Create e-invoices](/guides/creating-invoices) and the [document schema](/api-reference/schemas/document).
  </Step>

  <Step title="Validate the JSON">
    Send the JSON to `POST /api/validate/json`. If the status code is `406` or `422`, or if `is_valid` is `false`, correct the JSON and send it again.
  </Step>

  <Step title="Create the document">
    When `is_valid` is `true`, send the same JSON to `POST /api/documents/`. The API creates the document in the state `DRAFT`.
  </Step>
</Steps>

### Complete example

These programs validate a document and create it only when the result is valid. Put your document JSON in the file `invoice.json`. The vendor in the file must be your own company.

<AccordionGroup>
  <Accordion title="Node.js">
    ```javascript validate-and-create.mjs theme={null}
    import { readFile } from "node:fs/promises";

    const BASE_URL = "https://api.e-invoice.be";
    const headers = {
      Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}`,
      "Content-Type": "application/json",
    };

    async function post(path, payload) {
      const response = await fetch(`${BASE_URL}${path}`, {
        method: "POST",
        headers,
        body: JSON.stringify(payload),
      });
      const body = await response.json();
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${JSON.stringify(body)}`);
      }
      return body;
    }

    const invoice = JSON.parse(await readFile("invoice.json", "utf8"));

    const validation = await post("/api/validate/json", invoice);
    if (!validation.is_valid) {
      console.error("Validation failed:");
      for (const issue of validation.issues) {
        console.error(`  ${issue.rule_id}: ${issue.message}`);
      }
      process.exit(1);
    }
    console.log("Validation OK");

    const document = await post("/api/documents/", invoice);
    console.log("Document created:", document.id);
    ```
  </Accordion>

  <Accordion title="Python">
    ```python validate_and_create.py theme={null}
    import json
    import os
    import sys

    import requests

    BASE_URL = "https://api.e-invoice.be"
    HEADERS = {"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}


    def post(path, payload):
        response = requests.post(f"{BASE_URL}{path}", headers=HEADERS, json=payload, timeout=30)
        if not response.ok:
            raise SystemExit(f"HTTP {response.status_code}: {response.text}")
        return response.json()


    with open("invoice.json", encoding="utf-8") as f:
        invoice = json.load(f)

    validation = post("/api/validate/json", invoice)
    if not validation["is_valid"]:
        print("Validation failed:")
        for issue in validation["issues"]:
            print(f"  {issue.get('rule_id')}: {issue['message']}")
        sys.exit(1)
    print("Validation OK")

    document = post("/api/documents/", invoice)
    print("Document created:", document["id"])
    ```
  </Accordion>
</AccordionGroup>

## Testing strategy

### During development

Use `POST /api/validate/json` for each change of your payload:

1. **Edge cases**: validate unusual documents (zero amounts, more than one VAT rate, a currency other than EUR).
2. **All document types**: validate invoices, credit notes and debit notes.
3. **Short cycles**: correct the JSON and validate again. No document is created.
4. **Automated tests**: add validation calls to your test suite.

<Note>
  Develop and test with a sandbox company. A sandbox company runs in test mode: the API sends each document as UBL XML to the contact email address of the company, and nothing goes to the Peppol network. The API host and the endpoints are the same as for a production company. See [Test mode and sandbox companies](/environments).
</Note>

### Before production

1. Validate representative samples of all the invoice types that you send.
2. Make sure that each receiver is registered on Peppol. See [Look up Peppol participants](/guides/lookup-participants).
3. Validate all the tax categories and currency codes that you use.
4. Validate complex documents (allowances, charges, many lines). See [Advanced invoicing](/guides/advanced-invoicing).

## Best practices

<AccordionGroup>
  <Accordion title="Validate early and frequently">
    Do not wait until production:

    * Validate each new invoice template.
    * Validate again after a change in your mapping or in the schema.
    * Add validation to your CI/CD pipeline.
  </Accordion>

  <Accordion title="Handle validation results in your code">
    Three results are possible. Handle each of them:

    * `201` and `is_valid` is `true`: continue with `POST /api/documents/`.
    * `201` and `is_valid` is `false`: record the `rule_id` and the `message` of each issue, and do not create the document.
    * `406` or `422`: record the `detail` value, and do not create the document.
  </Accordion>

  <Accordion title="Validate templates one time">
    If you generate invoices from a template, validate one document of each template when the template changes. The create request applies the rules again to each document, so a validation call for each invoice in production is not necessary.
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Create e-invoices" icon="file-invoice" href="/guides/creating-invoices">
    Create and send validated invoices
  </Card>

  <Card title="Errors and troubleshooting" icon="triangle-exclamation" href="/guides/errors">
    Find the cause of a status code or a rule ID
  </Card>

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

  <Card title="Look up Peppol participants" icon="magnifying-glass" href="/guides/lookup-participants">
    Find the Peppol ID of a receiver
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.