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

# Attachments and PDF

> Add files to an invoice so that the receiver gets them in the UBL, let the platform generate a PDF, and download attachments and UBL files.

## Overview

A Peppol invoice is a UBL XML file. A file that the receiver must get (for example a PDF copy of the invoice or a timesheet) must be embedded in that UBL. There are three methods to add a file to a document. Only two of them put the file in the UBL.

| Method | Goes into the UBL | When to use |
| - | - | - |
| `attachments[]` in the body of `POST /api/documents/` | Yes | You have the file and the receiver must get it. |
| `construct_pdf=true` on `POST /api/documents/` | Yes | You have no PDF and you want the platform to generate a readable copy of the invoice. |
| `POST /api/documents/{document_id}/attachments` (deprecated) | No | Do not use for new integrations. The file is stored with the document only. |

<Note>
  This page is about files that go out with a document. To use a PDF as the input from which a document is created, see [Create documents from PDF](/guides/pdf-documents).
</Note>

## Add attachments when you create the document

Add an `attachments` array to the body of `POST /api/documents/`. The API embeds each file in the UBL as an `AdditionalDocumentReference` and stores it with the document.

<ParamField body="attachments[].file_name" type="string" required>
  File name with extension, for example `timesheet-2026-09.pdf`. The receiver sees this name.
</ParamField>

<ParamField body="attachments[].file_type" type="string" default="application/pdf">
  MIME type of the file. See the permitted types below.
</ParamField>

<ParamField body="attachments[].file_data" type="string">
  File content, base64 encoded. Always supply this field when you create a document.
</ParamField>

<ParamField body="attachments[].file_size" type="integer" default="0">
  Optional. The API calculates the stored size from the decoded `file_data`.
</ParamField>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.e-invoice.be/api/documents/" \
    -H "Authorization: Bearer $E_INVOICE_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "document_type": "INVOICE",
      "invoice_id": "INV-2026-001",
      "invoice_date": "2026-10-01",
      "due_date": "2026-10-31",
      "currency": "EUR",
      "vendor_name": "E-INVOICE BV",
      "vendor_tax_id": "BE1018265814",
      "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE",
      "customer_name": "OpenPeppol VZW",
      "customer_tax_id": "BE0848934496",
      "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE",
      "items": [
        {
          "description": "Consulting services",
          "quantity": 10,
          "unit_price": 100.00,
          "amount": 1000.00,
          "tax_rate": "21.00"
        }
      ],
      "attachments": [
        {
          "file_name": "timesheet-2026-09.pdf",
          "file_type": "application/pdf",
          "file_data": "JVBERi0xLjQKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFI..."
        }
      ]
    }'
  ```

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

  const fileData = (await readFile("timesheet-2026-09.pdf")).toString("base64");

  const response = await fetch("https://api.e-invoice.be/api/documents/", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      document_type: "INVOICE",
      invoice_id: "INV-2026-001",
      invoice_date: "2026-10-01",
      due_date: "2026-10-31",
      currency: "EUR",
      vendor_name: "E-INVOICE BV",
      vendor_tax_id: "BE1018265814",
      vendor_address: "Brusselsesteenweg 119/A, 1980 Zemst, BE",
      customer_name: "OpenPeppol VZW",
      customer_tax_id: "BE0848934496",
      customer_address: "Robert Schumanplein 6 bus 5, 1040 Brussel, BE",
      items: [
        {
          description: "Consulting services",
          quantity: 10,
          unit_price: 100.0,
          amount: 1000.0,
          tax_rate: "21.00",
        },
      ],
      attachments: [
        {
          file_name: "timesheet-2026-09.pdf",
          file_type: "application/pdf",
          file_data: fileData,
        },
      ],
    }),
  });

  console.log(response.status, await response.json());
  ```

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

  import requests

  with open("timesheet-2026-09.pdf", "rb") as f:
      file_data = base64.b64encode(f.read()).decode("ascii")

  response = requests.post(
      "https://api.e-invoice.be/api/documents/",
      headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"},
      json={
          "document_type": "INVOICE",
          "invoice_id": "INV-2026-001",
          "invoice_date": "2026-10-01",
          "due_date": "2026-10-31",
          "currency": "EUR",
          "vendor_name": "E-INVOICE BV",
          "vendor_tax_id": "BE1018265814",
          "vendor_address": "Brusselsesteenweg 119/A, 1980 Zemst, BE",
          "customer_name": "OpenPeppol VZW",
          "customer_tax_id": "BE0848934496",
          "customer_address": "Robert Schumanplein 6 bus 5, 1040 Brussel, BE",
          "items": [
              {
                  "description": "Consulting services",
                  "quantity": 10,
                  "unit_price": 100.00,
                  "amount": 1000.00,
                  "tax_rate": "21.00",
              }
          ],
          "attachments": [
              {
                  "file_name": "timesheet-2026-09.pdf",
                  "file_type": "application/pdf",
                  "file_data": file_data,
              }
          ],
      },
  )

  print(response.status_code, response.json())
  ```

  ```php PHP theme={null}
  <?php
  $payload = [
      'document_type' => 'INVOICE',
      'invoice_id' => 'INV-2026-001',
      'invoice_date' => '2026-10-01',
      'due_date' => '2026-10-31',
      'currency' => 'EUR',
      'vendor_name' => 'E-INVOICE BV',
      'vendor_tax_id' => 'BE1018265814',
      'vendor_address' => 'Brusselsesteenweg 119/A, 1980 Zemst, Belgium',
      'customer_name' => 'OpenPeppol VZW',
      'customer_tax_id' => 'BE0848934496',
      'customer_address' => 'Robert Schumanplein 6 bus 5, 1040 Brussel, Belgium',
      'items' => [[
          'description' => 'Consulting services',
          'quantity' => 10,
          'unit_price' => 100.00,
          'amount' => 1000.00,
          'tax_rate' => '21.00',
      ]],
      'attachments' => [[
          'file_name' => 'timesheet-2026-09.pdf',
          'file_type' => 'application/pdf',
          'file_data' => base64_encode(file_get_contents('timesheet-2026-09.pdf')),
      ]],
  ];

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

  $body = curl_exec($ch);
  echo curl_getinfo($ch, CURLINFO_HTTP_CODE) . PHP_EOL . $body . PHP_EOL;
  curl_close($ch);
  ```

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

  var fileData = Convert.ToBase64String(await File.ReadAllBytesAsync("timesheet-2026-09.pdf"));

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

  var payload = new
  {
      document_type = "INVOICE",
      invoice_id = "INV-2026-001",
      invoice_date = "2026-10-01",
      due_date = "2026-10-31",
      currency = "EUR",
      vendor_name = "E-INVOICE BV",
      vendor_tax_id = "BE1018265814",
      vendor_address = "Brusselsesteenweg 119/A, 1980 Zemst, BE",
      customer_name = "OpenPeppol VZW",
      customer_tax_id = "BE0848934496",
      customer_address = "Robert Schumanplein 6 bus 5, 1040 Brussel, BE",
      items = new[]
      {
          new
          {
              description = "Consulting services",
              quantity = 10,
              unit_price = 100.00,
              amount = 1000.00,
              tax_rate = "21.00"
          }
      },
      attachments = new[]
      {
          new
          {
              file_name = "timesheet-2026-09.pdf",
              file_type = "application/pdf",
              file_data = fileData
          }
      }
  };

  var response = await client.PostAsJsonAsync("/api/documents/", payload);
  Console.WriteLine((int)response.StatusCode);
  Console.WriteLine(await response.Content.ReadAsStringAsync());
  ```
</CodeGroup>

Before you run the sample, replace the vendor fields with the data of your company. The API rejects a document if the Peppol ID of the vendor is not one of the `peppol_ids` of your company.

The API returns `201 Created` with the document in the `DRAFT` state. The response below shows only the fields that are related to attachments.

```json theme={null}
{
  "id": "doc-8f3a2b1c9d4e5f60718293a4b5c6d7e8",
  "state": "DRAFT",
  "document_type": "INVOICE",
  "invoice_id": "INV-2026-001",
  "attachments": [
    {
      "id": "doc-att-1a2b3c4d5e6f708192a3b4c5d6e7f809",
      "file_name": "timesheet-2026-09.pdf",
      "file_type": "application/pdf",
      "file_size": 48213
    }
  ]
}
```

### Permitted file types

Peppol rule BR-CL-24 permits only these MIME types for a file that is embedded in the UBL:

| File | `file_type` |
| - | - |
| PDF | `application/pdf` |
| PNG image | `image/png` |
| JPEG image | `image/jpeg` |
| CSV | `text/csv` |
| Excel workbook (`.xlsx`) | `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` |
| OpenDocument spreadsheet (`.ods`) | `application/vnd.oasis.opendocument.spreadsheet` |

If `file_type` is not one of these values, the API uses the extension of `file_name` (`.pdf`, `.png`, `.jpg`, `.jpeg`, `.csv`, `.xlsx`, `.ods`) to find the MIME type that it writes to the UBL. Give each file a correct extension.

### File size

The API does not publish a size limit for the files in `attachments[]`. Base64 makes the request body approximately one third larger than the file, and the receiving Access Point must accept the full UBL. Keep attachments small, and send large supporting files through a different channel.

<Note>
  The limits of 5 MB for each file and 25 MB for each batch apply to the PDF conversion endpoint only. See [Create documents from PDF](/guides/pdf-documents).
</Note>

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

## Let the platform make the PDF

Add the query parameter `construct_pdf=true` to `POST /api/documents/`. The API then does these steps:

<Steps>
  <Step title="Generate the UBL">
    The API converts the JSON body to UBL and validates it.
  </Step>

  <Step title="Render the PDF">
    The API makes a PDF from that UBL.
  </Step>

  <Step title="Embed the PDF">
    The API adds the PDF to the attachments of the document, generates the UBL again with the PDF embedded, and validates the result.
  </Step>
</Steps>

The receiver gets a UBL that contains a readable PDF copy of the invoice. If the body also has an `attachments` array, the generated PDF is added after those files, and the UBL contains all of them.

<ParamField query="construct_pdf" type="boolean" default="false">
  If `true`, the API generates a PDF from the document, stores it as an attachment and embeds it in the UBL.
</ParamField>

The samples read the invoice JSON from a file `invoice.json`. Save this payload in that file:

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

Replace the vendor fields with the data of your company. The API rejects a document if the Peppol ID of the vendor is not one of the `peppol_ids` of your company.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.e-invoice.be/api/documents/?construct_pdf=true" \
    -H "Authorization: Bearer $E_INVOICE_API_KEY" \
    -H "Content-Type: application/json" \
    -d @invoice.json
  ```

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

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

  console.log(response.status, await response.json());
  ```

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

  import requests

  with open("invoice.json") as f:
      invoice = json.load(f)

  response = requests.post(
      "https://api.e-invoice.be/api/documents/",
      params={"construct_pdf": "true"},
      headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"},
      json=invoice,
  )

  print(response.status_code, response.json())
  ```

  ```php PHP theme={null}
  <?php
  $ch = curl_init('https://api.e-invoice.be/api/documents/?construct_pdf=true');
  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 => file_get_contents('invoice.json'),
  ]);

  $body = curl_exec($ch);
  echo curl_getinfo($ch, CURLINFO_HTTP_CODE) . PHP_EOL . $body . PHP_EOL;
  curl_close($ch);
  ```

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

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

  var content = new StringContent(
      await File.ReadAllTextAsync("invoice.json"), Encoding.UTF8, "application/json");

  var response = await client.PostAsync("/api/documents/?construct_pdf=true", content);
  Console.WriteLine((int)response.StatusCode);
  Console.WriteLine(await response.Content.ReadAsStringAsync());
  ```
</CodeGroup>

```json theme={null}
{
  "id": "doc-8f3a2b1c9d4e5f60718293a4b5c6d7e8",
  "state": "DRAFT",
  "document_type": "INVOICE",
  "invoice_id": "INV-2026-001",
  "attachments": [
    {
      "id": "doc-att-9e8d7c6b5a4f30211203f4a5b6c7d8e9",
      "file_name": "INV-2026-001-generated.pdf",
      "file_type": "application/pdf",
      "file_size": 31876
    }
  ]
}
```

### File name of the generated PDF

* The name is `<invoice_id>-generated.pdf`.
* Each character of `invoice_id` that is not a letter, a digit, `-`, `_` or `.` becomes `-`. For example, `INV/2026 001` gives `INV-2026-001-generated.pdf`.
* If the document has no `invoice_id`, the name is a random UUID followed by `-generated.pdf`.

### Errors

| Status | Cause |
| - | - |
| `406` | The UBL is not valid, before or after the PDF is embedded. The `detail` field gives the rule failures. |
| `500` | The API could not generate the PDF. The `detail` field is `Could not generate constructed PDF`. No document is created. |

<Tip>
  The [command-line tool](/cli) has the same option: `peppol document create json invoice.json --construct-pdf`.
</Tip>

## Deprecated upload endpoint

<Warning>
  `POST /api/documents/{document_id}/attachments` is deprecated. A file that you upload with this endpoint is stored with the document, but it is not added to the UBL. The receiver does not get it. To send a file to the receiver, put it in `attachments[]` when you create the document, or use `construct_pdf=true`.
</Warning>

Existing integrations that use this endpoint send a `multipart/form-data` request with one file in the form field `file`:

```bash cURL theme={null}
curl -X POST "https://api.e-invoice.be/api/documents/doc-8f3a2b1c9d4e5f60718293a4b5c6d7e8/attachments" \
  -H "Authorization: Bearer $E_INVOICE_API_KEY" \
  -F "file=@delivery-note.pdf;type=application/pdf"
```

```json theme={null}
{
  "id": "doc-att-4c5d6e7f8091a2b3c4d5e6f708192a3b",
  "file_name": "delivery-note.pdf",
  "file_type": "application/pdf",
  "file_size": 20480
}
```

## List, download and delete attachments

These calls work for documents that you created and for documents that you received.

### List the attachments of a document

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

```json theme={null}
[
  {
    "id": "doc-att-1a2b3c4d5e6f708192a3b4c5d6e7f809",
    "file_name": "timesheet-2026-09.pdf",
    "file_type": "application/pdf",
    "file_size": 48213,
    "file_url": "https://storage.example.com/timesheet-2026-09.pdf?X-Amz-Expires=3600&X-Amz-Signature=..."
  }
]
```

<Note>
  If the document has no attachments, this call returns `404` with the detail `Document attachments not found`. It does not return an empty array. Treat this response as an empty list.
</Note>

For a sample in five languages that lists and downloads all attachments, see [Download the attachments and the PDF](/guides/receiving-documents#download-the-attachments-and-the-pdf).

### Download one attachment

Get the attachment to receive a `file_url`. This is a signed URL that is valid for 1 hour. Download the file from that URL with a plain `GET` request, without the `Authorization` header.

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

```json theme={null}
{
  "id": "doc-att-1a2b3c4d5e6f708192a3b4c5d6e7f809",
  "file_name": "timesheet-2026-09.pdf",
  "file_type": "application/pdf",
  "file_size": 48213,
  "file_url": "https://storage.example.com/timesheet-2026-09.pdf?X-Amz-Expires=3600&X-Amz-Signature=..."
}
```

```bash cURL theme={null}
curl -o timesheet-2026-09.pdf "<file_url>"
```

Do not store `file_url`. Request a new URL each time that you need the file.

### Delete an attachment

You can delete an attachment only while the document is in the `DRAFT` or `FAILED` state.

```bash cURL theme={null}
curl -X DELETE "https://api.e-invoice.be/api/documents/doc-8f3a2b1c9d4e5f60718293a4b5c6d7e8/attachments/doc-att-1a2b3c4d5e6f708192a3b4c5d6e7f809" \
  -H "Authorization: Bearer $E_INVOICE_API_KEY"
```

```json theme={null}
{
  "is_deleted": true
}
```

| Status | Cause |
| - | - |
| `400` | The document is not in the `DRAFT` or `FAILED` state. The detail is `Document is not in draft state`. |
| `404` | The document or the attachment does not exist. |

<Warning>
  This call removes the stored attachment only. It does not generate the UBL again. To send a document without a file that is embedded in its UBL, delete the document and create it again without that file.
</Warning>

For the states of a document, see [Document lifecycle and delivery tracking](/guides/document-lifecycle).

## Download the UBL

`GET /api/documents/{document_id}/ubl` returns the metadata of the UBL file of a document and a signed URL from which you can download the XML. Use it to see what the receiver gets, or to archive the UBL of a received document.

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

```json theme={null}
{
  "id": "ubl-5d6e7f8091a2b3c4d5e6f708192a3b4c",
  "file_name": "doc-8f3a2b1c9d4e5f60718293a4b5c6d7e8.xml",
  "file_size": 73412,
  "signed_url": "https://storage.example.com/doc-8f3a2b1c9d4e5f60718293a4b5c6d7e8.xml?X-Amz-Expires=3600&X-Amz-Signature=...",
  "sender_peppol_scheme": "0208",
  "sender_peppol_id": "1018265814",
  "receiver_peppol_scheme": "0208",
  "receiver_peppol_id": "0848934496",
  "validated_at": "2026-10-01T09:15:42Z"
}
```

| Field | Description |
| - | - |
| `signed_url` | Signed URL of the XML file. It is valid for 1 hour. Download it without the `Authorization` header. |
| `file_name`, `file_size` | Name and size in bytes of the stored XML file. |
| `sender_peppol_scheme`, `sender_peppol_id`, `receiver_peppol_scheme`, `receiver_peppol_id` | The Peppol IDs that are stored with the UBL. |
| `validated_at` | Time at which the UBL passed validation. |

Fields that have no value are not in the response. The call returns `404` if the document has no UBL. Do not store `signed_url`. For a download sample in five languages, see [Download the original UBL](/guides/receiving-documents#download-the-original-ubl).

## Uploaded UBL and received documents

You do not add attachments separately in these two cases. The files are already in the UBL.

* **Documents that you create from UBL.** When you upload a UBL file with `POST /api/documents/ubl`, the API extracts each embedded file and stores it as an attachment of the document. The UBL is not changed. See [Send UBL documents](/guides/ubl-documents).
* **Received documents.** The API extracts each file that the sender embedded and stores it as an attachment. If the received UBL contains no PDF, the API generates a PDF from the UBL and stores it as an attachment with the name `<document_id>.pdf`. Thus a received document usually has a PDF that you can show to a person. See [Receive documents](/guides/receiving-documents).

Use the list and download calls above to get these files.

## Test with a sandbox company

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

Create a document with `attachments[]` or `construct_pdf=true` in a sandbox company, send it, and open the XML from the email. Each file is in an `AdditionalDocumentReference` element as base64 content.

## Next Steps

<CardGroup cols={2}>
  <Card title="Create e-invoices" icon="file-invoice" href="/guides/creating-invoices">
    Build the invoice body to which you add attachments.
  </Card>

  <Card title="Create documents from PDF" icon="file-pdf" href="/guides/pdf-documents">
    Use a PDF as the input for a new document.
  </Card>

  <Card title="Receive documents" icon="inbox" href="/guides/receiving-documents">
    Read received invoices and their attachments.
  </Card>

  <Card title="Validation during development" icon="circle-check" href="/guides/validation">
    Examine the generated UBL before you create a document.
  </Card>
</CardGroup>
