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

# Receive documents

> Find the documents that your suppliers send to you through Peppol, read their data, and download the original UBL, the PDF and the attachments.

## Overview

When a supplier sends an invoice or a [credit note](/guides/credit-notes) to your Peppol ID, e-invoice.be receives the document, stores it, and puts it in the inbox of your company. This guide shows how to find new documents, how to read their data, and how to download the original UBL file, the PDF and the other attachments.

A received document has `direction: "INBOUND"` and `state: "RECEIVED"`.

## Prerequisites

* An API key for the company that receives the documents. See [Authentication](/authentication).
* A production company must be registered as a receiver on the Peppol network. Without this registration, senders cannot find your Peppol ID and no document arrives.
* A sandbox company does not receive documents from the Peppol network. Use **Simulate inbound** to put documents in its inbox. See [Test with a sandbox company](#test-with-a-sandbox-company).

<Note>
  Resellers: a send-only tenant has no Peppol registration, thus its inbox stays empty. See [Send-only tenants](/admin-api#send-only-tenants) in the Admin API guide.
</Note>

## How a document arrives

<Steps>
  <Step title="e-invoice.be receives the document">
    The access point receives the Peppol message and finds the company from the receiver Peppol ID.
  </Step>

  <Step title="The UBL and its attachments are stored">
    The original UBL XML is stored. Each file that the sender embedded in the UBL is extracted and stored as an attachment of the document.
  </Step>

  <Step title="A PDF is made if the sender did not supply one">
    If the UBL contains no PDF attachment, e-invoice.be generates a PDF from the UBL and adds it as an attachment with the file name `{document_id}.pdf`.
  </Step>

  <Step title="The document goes to the RECEIVED state">
    The document data (parties, totals, line items) is available as JSON, and the document is in the inbox.
  </Step>

  <Step title="The webhook is sent">
    Each webhook of the company that subscribes to `document.received` gets an event with the `document_id`.
  </Step>
</Steps>

## Find new documents

There are two methods to find new documents: webhooks and polling. Webhooks are the recommended method. Polling is a good safety net in addition to webhooks.

<Warning>
  The API has no "mark as processed" endpoint and no "unread" filter. A document stays in the inbox after you read it. Your system must record which documents it has processed. See [Prevent double processing](#prevent-double-processing).
</Warning>

### Webhook (recommended)

Create a webhook that subscribes to `document.received`. e-invoice.be calls your URL when a document is in the `RECEIVED` state. [Webhooks](/essentials/webhooks) gives the setup procedure and the signature verification.

```json theme={null}
{
  "id": "evt-e7wyc7gtpqx4z73x2wqmwhebhbb3r8n3ovfhcsdbulxr3s2awf49de76yglrnri3",
  "tenant_id": "ten-5x7y9z1a3b5c7d9e",
  "created_at": 1790846142,
  "type": "document.received",
  "data": {
    "document_id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"
  }
}
```

The event contains only the `document_id`. Use this ID to [get the document](#get-the-document).

<Note>
  The event `document.received.failed` tells you that e-invoice.be could not process an incoming document. The `data` object has the same shape. Such a document is not in the `RECEIVED` state, thus `GET /api/inbox/` does not list it.
</Note>

### Polling

Call `GET /api/inbox/` at a fixed interval. The default sort is `created_at` in descending order, thus the newest documents are on the first page.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://api.e-invoice.be/api/inbox/?sort_by=created_at&sort_order=desc&page=1&page_size=50" \
       -H "Authorization: Bearer $E_INVOICE_API_KEY"
  ```

  ```javascript Node.js theme={null}
  const params = new URLSearchParams({
    sort_by: "created_at",
    sort_order: "desc",
    page: "1",
    page_size: "50",
  });

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

  if (!response.ok) {
    throw new Error(`Inbox request failed: ${response.status}`);
  }

  const inbox = await response.json();
  console.log(inbox.total, inbox.items.map((doc) => doc.id));
  ```

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

  import requests

  response = requests.get(
      "https://api.e-invoice.be/api/inbox/",
      headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"},
      params={
          "sort_by": "created_at",
          "sort_order": "desc",
          "page": 1,
          "page_size": 50,
      },
      timeout=30,
  )
  response.raise_for_status()

  inbox = response.json()
  print(inbox["total"], [doc["id"] for doc in inbox["items"]])
  ```

  ```php PHP theme={null}
  <?php
  $query = http_build_query([
      'sort_by' => 'created_at',
      'sort_order' => 'desc',
      'page' => 1,
      'page_size' => 50,
  ]);

  $ch = curl_init("https://api.e-invoice.be/api/inbox/?{$query}");
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('E_INVOICE_API_KEY')],
  ]);

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

  if ($status !== 200) {
      throw new RuntimeException("Inbox request failed: {$status}");
  }

  $inbox = json_decode($body, true);
  echo $inbox['total'] . PHP_EOL;
  ```

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

  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.GetAsync(
      "/api/inbox/?sort_by=created_at&sort_order=desc&page=1&page_size=50");
  response.EnsureSuccessStatusCode();

  using var inbox = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
  Console.WriteLine(inbox.RootElement.GetProperty("total").GetInt32());
  ```
</CodeGroup>

The response is one page of documents. Each item has the same shape as the response of `GET /api/documents/{document_id}`.

```json theme={null}
{
  "items": [
    {
      "id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
      "created_at": "2026-10-01T09:15:42.118Z",
      "document_type": "INVOICE",
      "state": "RECEIVED",
      "direction": "INBOUND",
      "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",
      "customer_name": "OpenPeppol VZW",
      "customer_tax_id": "BE0848934496",
      "subtotal": "1000.00",
      "total_tax": "210.00",
      "invoice_total": "1210.00",
      "amount_due": "1210.00"
    }
  ],
  "total": 1,
  "page": 1,
  "page_size": 50,
  "pages": 1,
  "has_next_page": false
}
```

Read the subsequent page while `has_next_page` is `true`.

### Prevent double processing

Because the API does not record what you have processed, keep this record in your own system. Use one of these patterns, or the two together.

<AccordionGroup>
  <Accordion title="Set of processed document IDs">
    Store the `id` of each document after you process it. Before you process a document, look for its `id` in your store and skip the document if the `id` is there.

    This pattern is correct for webhooks and for polling. Use it in each webhook handler, so that a repeated event does not process the same document twice.
  </Accordion>

  <Accordion title="Cursor on created_at">
    Store the `created_at` value and the `id` of the newest document that you processed. At each poll, read the inbox with `sort_by=created_at` and `sort_order=desc`, page by page, and stop when you find a document whose `created_at` is not later than the stored value. Process the documents that you collected from the oldest to the newest, then store the new cursor.

    `date_from` and `date_to` filter on the issue date of the invoice (`invoice_date`), not on the time of receipt. Do not use them as a cursor: a supplier can send an invoice with an issue date in the past.
  </Accordion>
</AccordionGroup>

<Accordion title="Complete polling example (Node.js and Python)">
  These programs read the inbox from the newest document to the stored cursor, download the UBL of each new document, and then move the cursor. The cursor is in a local JSON file; use your database in a production system.

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

    const BASE_URL = "https://api.e-invoice.be";
    const HEADERS = { Authorization: `Bearer ${process.env.E_INVOICE_API_KEY}` };
    const CURSOR_FILE = "inbox-cursor.json";

    async function api(path) {
      const response = await fetch(`${BASE_URL}${path}`, { headers: HEADERS });
      if (!response.ok) {
        throw new Error(`GET ${path} failed: ${response.status}`);
      }
      return response.json();
    }

    async function loadCursor() {
      try {
        return JSON.parse(await readFile(CURSOR_FILE, "utf8"));
      } catch {
        return { createdAt: null, processedIds: [] };
      }
    }

    async function collectNewDocuments(cursor) {
      const documents = [];
      let page = 1;

      while (true) {
        const inbox = await api(
          `/api/inbox/?sort_by=created_at&sort_order=desc&page=${page}&page_size=100`
        );

        for (const doc of inbox.items) {
          if (cursor.createdAt && new Date(doc.created_at) < new Date(cursor.createdAt)) {
            return documents;
          }
          if (!cursor.processedIds.includes(doc.id)) {
            documents.push(doc);
          }
        }

        if (!inbox.has_next_page) {
          return documents;
        }
        page += 1;
      }
    }

    async function processDocument(doc) {
      const ubl = await api(`/api/documents/${doc.id}/ubl`);
      const download = await fetch(ubl.signed_url);
      if (!download.ok) {
        throw new Error(`UBL download failed: ${download.status}`);
      }
      await writeFile(ubl.file_name, Buffer.from(await download.arrayBuffer()));
      console.log(`Processed ${doc.id} (${doc.document_type} ${doc.invoice_id})`);
    }

    const cursor = await loadCursor();
    const documents = await collectNewDocuments(cursor);

    // Oldest first, so that the cursor only moves forward.
    for (const doc of documents.reverse()) {
      await processDocument(doc);
      const sameInstant = cursor.createdAt === doc.created_at;
      cursor.processedIds = sameInstant ? [...cursor.processedIds, doc.id] : [doc.id];
      cursor.createdAt = doc.created_at;
      await writeFile(CURSOR_FILE, JSON.stringify(cursor));
    }
    ```

    ```python Python theme={null}
    import json
    import os
    from datetime import datetime
    from pathlib import Path

    import requests

    BASE_URL = "https://api.e-invoice.be"
    HEADERS = {"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"}
    CURSOR_FILE = Path("inbox-cursor.json")


    def api(path, **params):
        response = requests.get(f"{BASE_URL}{path}", headers=HEADERS, params=params, timeout=30)
        response.raise_for_status()
        return response.json()


    def parse(timestamp):
        return datetime.fromisoformat(timestamp.replace("Z", "+00:00"))


    def load_cursor():
        if CURSOR_FILE.exists():
            return json.loads(CURSOR_FILE.read_text())
        return {"created_at": None, "processed_ids": []}


    def collect_new_documents(cursor):
        documents = []
        page = 1
        while True:
            inbox = api(
                "/api/inbox/",
                sort_by="created_at",
                sort_order="desc",
                page=page,
                page_size=100,
            )
            for doc in inbox["items"]:
                if cursor["created_at"] and parse(doc["created_at"]) < parse(cursor["created_at"]):
                    return documents
                if doc["id"] not in cursor["processed_ids"]:
                    documents.append(doc)
            if not inbox["has_next_page"]:
                return documents
            page += 1


    def process_document(doc):
        ubl = api(f"/api/documents/{doc['id']}/ubl")
        download = requests.get(ubl["signed_url"], timeout=60)
        download.raise_for_status()
        Path(ubl["file_name"]).write_bytes(download.content)
        print(f"Processed {doc['id']} ({doc['document_type']} {doc.get('invoice_id')})")


    cursor = load_cursor()

    # Oldest first, so that the cursor only moves forward.
    for doc in reversed(collect_new_documents(cursor)):
        process_document(doc)
        if cursor["created_at"] == doc["created_at"]:
            cursor["processed_ids"].append(doc["id"])
        else:
            cursor["processed_ids"] = [doc["id"]]
        cursor["created_at"] = doc["created_at"]
        CURSOR_FILE.write_text(json.dumps(cursor))
    ```
  </CodeGroup>
</Accordion>

## Get the document

`GET /api/documents/{document_id}` returns the data of the document as JSON. This is the same endpoint and the same schema as for the documents that you send. See the [Document schema](/api-reference/schemas/document).

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

  ```javascript Node.js theme={null}
  const documentId = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d";

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

  if (!response.ok) {
    throw new Error(`Get document failed: ${response.status}`);
  }

  const doc = await response.json();
  console.log(doc.document_type, doc.vendor_name, doc.invoice_total);
  ```

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

  import requests

  document_id = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"

  response = requests.get(
      f"https://api.e-invoice.be/api/documents/{document_id}",
      headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"},
      timeout=30,
  )
  response.raise_for_status()

  doc = response.json()
  print(doc["document_type"], doc.get("vendor_name"), doc.get("invoice_total"))
  ```

  ```php PHP theme={null}
  <?php
  $documentId = 'doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d';

  $ch = curl_init("https://api.e-invoice.be/api/documents/{$documentId}");
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('E_INVOICE_API_KEY')],
  ]);

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

  if ($status !== 200) {
      throw new RuntimeException("Get document failed: {$status}");
  }

  $doc = json_decode($body, true);
  echo $doc['document_type'] . ' ' . $doc['vendor_name'] . PHP_EOL;
  ```

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

  var documentId = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d";

  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.GetAsync($"/api/documents/{documentId}");
  response.EnsureSuccessStatusCode();

  using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
  Console.WriteLine(doc.RootElement.GetProperty("document_type").GetString());
  ```
</CodeGroup>

```json theme={null}
{
  "id": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
  "created_at": "2026-10-01T09:15:42.118Z",
  "document_type": "INVOICE",
  "state": "RECEIVED",
  "direction": "INBOUND",
  "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",
  "subtotal": "1000.00",
  "total_tax": "210.00",
  "invoice_total": "1210.00",
  "amount_due": "1210.00",
  "items": [
    {
      "description": "Professional services",
      "quantity": "10",
      "unit": "C62",
      "unit_price": "100.00",
      "amount": "1000.00",
      "tax_rate": "21.00",
      "tax": "210.00"
    }
  ],
  "tax_details": [
    {
      "rate": "21.00",
      "amount": "210.00"
    }
  ],
  "payment_details": [
    {
      "iban": "BE68539007547034",
      "payment_reference": "INV-2026-001"
    }
  ],
  "attachments": [
    {
      "id": "doc-att-7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d",
      "file_name": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d.pdf",
      "file_type": "application/pdf",
      "file_size": 48213
    }
  ]
}
```

The response does not contain fields that have no value. Read optional fields defensively.

These fields are the most important for a received document:

| Field | Value for a received document |
| - | - |
| `direction` | `INBOUND` |
| `state` | `RECEIVED` |
| `document_type` | `INVOICE`, `CREDIT_NOTE`, `SELFBILLING_INVOICE` or `SELFBILLING_CREDIT_NOTE` |
| `vendor_name`, `vendor_tax_id`, `vendor_company_id`, `vendor_email`, `vendor_address` | The supplier that issued the document |
| `customer_name`, `customer_tax_id`, `customer_company_id` | The buyer on the document |
| `invoice_id`, `invoice_date`, `due_date` | The number and the dates that the issuer gave to the document |
| `subtotal`, `total_tax`, `invoice_total`, `amount_due` | The totals of the document, as strings |
| `payment_details` | The bank account and the payment reference for the payment |
| `attachments` | The ID, name, type and size of each attachment. This list has no download URL; see [Download the attachments and the PDF](#download-the-attachments-and-the-pdf) |
| `created_at` | The time at which e-invoice.be created the document record. Use this field for your cursor |

<Note>
  A received self-billing document has the type `SELFBILLING_INVOICE` or `SELFBILLING_CREDIT_NOTE`. In a self-billing document, the buyer issues the invoice in the name of the supplier, thus your company is the vendor on the document and not the customer. See [Self-billing and debit notes](/guides/self-billing).
</Note>

## Download the original UBL

The JSON data is a conversion of the UBL. Keep the original UBL XML for your archive: it is the legal document that the sender transmitted. `GET /api/documents/{document_id}/ubl` returns the metadata of the UBL file and a signed URL for the download.

<CodeGroup>
  ```bash cURL theme={null}
  # 1. Get the signed URL
  curl -X GET "https://api.e-invoice.be/api/documents/doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d/ubl" \
       -H "Authorization: Bearer $E_INVOICE_API_KEY"

  # 2. Download the file from the signed_url of the response
  curl -o invoice.xml "SIGNED_URL"
  ```

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

  const documentId = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d";

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

  if (!response.ok) {
    throw new Error(`Get UBL failed: ${response.status}`);
  }

  const ubl = await response.json();

  const download = await fetch(ubl.signed_url);
  if (!download.ok) {
    throw new Error(`UBL download failed: ${download.status}`);
  }

  await writeFile(ubl.file_name, Buffer.from(await download.arrayBuffer()));
  ```

  ```python Python theme={null}
  import os
  from pathlib import Path

  import requests

  document_id = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"

  response = requests.get(
      f"https://api.e-invoice.be/api/documents/{document_id}/ubl",
      headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"},
      timeout=30,
  )
  response.raise_for_status()
  ubl = response.json()

  download = requests.get(ubl["signed_url"], timeout=60)
  download.raise_for_status()
  Path(ubl["file_name"]).write_bytes(download.content)
  ```

  ```php PHP theme={null}
  <?php
  $documentId = 'doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d';

  $ch = curl_init("https://api.e-invoice.be/api/documents/{$documentId}/ubl");
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('E_INVOICE_API_KEY')],
  ]);

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

  if ($status !== 200) {
      throw new RuntimeException("Get UBL failed: {$status}");
  }

  $ubl = json_decode($body, true);

  $download = curl_init($ubl['signed_url']);
  curl_setopt_array($download, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_FOLLOWLOCATION => true,
  ]);
  $xml = curl_exec($download);
  curl_close($download);

  file_put_contents($ubl['file_name'], $xml);
  ```

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

  var documentId = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d";

  using var client = new HttpClient();
  using var request = new HttpRequestMessage(
      HttpMethod.Get, $"https://api.e-invoice.be/api/documents/{documentId}/ubl");
  request.Headers.Authorization = new AuthenticationHeaderValue(
      "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY"));

  var response = await client.SendAsync(request);
  response.EnsureSuccessStatusCode();

  using var ubl = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
  var signedUrl = ubl.RootElement.GetProperty("signed_url").GetString();
  var fileName = ubl.RootElement.GetProperty("file_name").GetString();

  // The signed URL needs no Authorization header.
  var xml = await client.GetByteArrayAsync(signedUrl);
  await File.WriteAllBytesAsync(fileName!, xml);
  ```
</CodeGroup>

```json theme={null}
{
  "id": "ubl-4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a",
  "file_name": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d.xml",
  "file_size": 12840,
  "signed_url": "https://storage.example.com/ubl/doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d.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:42.118Z"
}
```

`validated_at` is present when the UBL was validated. The `signed_url` is valid for 1 hour. For the description of each field, see [Download the UBL](/guides/attachments#download-the-ubl).

<Warning>
  Do not store `signed_url`. It stops working after 1 hour. Store the `document_id`, and request a new URL each time that you need the file. The URL contains its own signature, thus do not add your API key to the download request.
</Warning>

## Download the attachments and the PDF

A received document can have attachments: the PDF of the invoice and the other files that the sender embedded in the UBL (for example a timesheet or a delivery note). If the sender embedded no PDF, the list contains the PDF that e-invoice.be generated, with the file name `{document_id}.pdf`.

### List the attachments

`GET /api/documents/{document_id}/attachments` returns all attachments of the document. Each item has a `file_url` that you can download immediately.

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

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

  const documentId = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d";

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

  // 404 means that the document has no attachments.
  const attachments = response.status === 404 ? [] : await response.json();

  for (const attachment of attachments) {
    const download = await fetch(attachment.file_url);
    if (!download.ok) {
      throw new Error(`Attachment download failed: ${download.status}`);
    }
    await writeFile(attachment.file_name, Buffer.from(await download.arrayBuffer()));
  }
  ```

  ```python Python theme={null}
  import os
  from pathlib import Path

  import requests

  document_id = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"

  response = requests.get(
      f"https://api.e-invoice.be/api/documents/{document_id}/attachments",
      headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"},
      timeout=30,
  )

  # 404 means that the document has no attachments.
  if response.status_code == 404:
      attachments = []
  else:
      response.raise_for_status()
      attachments = response.json()

  for attachment in attachments:
      download = requests.get(attachment["file_url"], timeout=60)
      download.raise_for_status()
      Path(attachment["file_name"]).write_bytes(download.content)
  ```

  ```php PHP theme={null}
  <?php
  $documentId = 'doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d';

  $ch = curl_init("https://api.e-invoice.be/api/documents/{$documentId}/attachments");
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('E_INVOICE_API_KEY')],
  ]);

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

  // 404 means that the document has no attachments.
  $attachments = $status === 404 ? [] : json_decode($body, true);

  foreach ($attachments as $attachment) {
      $download = curl_init($attachment['file_url']);
      curl_setopt_array($download, [
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_FOLLOWLOCATION => true,
      ]);
      file_put_contents($attachment['file_name'], curl_exec($download));
      curl_close($download);
  }
  ```

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

  var documentId = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d";

  using var client = new HttpClient();
  using var request = new HttpRequestMessage(
      HttpMethod.Get, $"https://api.e-invoice.be/api/documents/{documentId}/attachments");
  request.Headers.Authorization = new AuthenticationHeaderValue(
      "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY"));

  var response = await client.SendAsync(request);

  // 404 means that the document has no attachments.
  if (response.StatusCode != HttpStatusCode.NotFound)
  {
      response.EnsureSuccessStatusCode();
      using var attachments = JsonDocument.Parse(await response.Content.ReadAsStringAsync());

      foreach (var attachment in attachments.RootElement.EnumerateArray())
      {
          var fileUrl = attachment.GetProperty("file_url").GetString();
          var fileName = attachment.GetProperty("file_name").GetString();
          await File.WriteAllBytesAsync(fileName!, await client.GetByteArrayAsync(fileUrl));
      }
  }
  ```
</CodeGroup>

```json theme={null}
[
  {
    "id": "doc-att-7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d",
    "file_name": "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d.pdf",
    "file_type": "application/pdf",
    "file_size": 48213,
    "file_url": "https://storage.example.com/attachments/doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d.pdf?X-Amz-Expires=3600&X-Amz-Signature=..."
  },
  {
    "id": "doc-att-0f1e2d3c4b5a69788796a5b4c3d2e1f0",
    "file_name": "timesheet-october.xlsx",
    "file_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    "file_size": 20417,
    "file_url": "https://storage.example.com/attachments/timesheet-october.xlsx?X-Amz-Expires=3600&X-Amz-Signature=..."
  }
]
```

<Note>
  This endpoint returns `404 Not Found` with `"detail": "Document attachments not found"` when the document has no attachments. Treat this response as an empty list.
</Note>

### Get one attachment

`GET /api/documents/{document_id}/attachments/{attachment_id}` returns one attachment with a new `file_url`. Use this call when you stored the attachment ID and you need the file again later.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://api.e-invoice.be/api/documents/doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d/attachments/doc-att-7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d" \
       -H "Authorization: Bearer $E_INVOICE_API_KEY"
  ```

  ```javascript Node.js theme={null}
  const documentId = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d";
  const attachmentId = "doc-att-7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d";

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

  if (!response.ok) {
    throw new Error(`Get attachment failed: ${response.status}`);
  }

  const attachment = await response.json();
  console.log(attachment.file_url);
  ```

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

  import requests

  document_id = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"
  attachment_id = "doc-att-7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d"

  response = requests.get(
      f"https://api.e-invoice.be/api/documents/{document_id}/attachments/{attachment_id}",
      headers={"Authorization": f"Bearer {os.environ['E_INVOICE_API_KEY']}"},
      timeout=30,
  )
  response.raise_for_status()
  attachment = response.json()
  print(attachment["file_url"])
  ```

  ```php PHP theme={null}
  <?php
  $documentId = 'doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d';
  $attachmentId = 'doc-att-7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d';

  $ch = curl_init("https://api.e-invoice.be/api/documents/{$documentId}/attachments/{$attachmentId}");
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('E_INVOICE_API_KEY')],
  ]);

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

  if ($status !== 200) {
      throw new RuntimeException("Get attachment failed: {$status}");
  }

  $attachment = json_decode($body, true);
  echo $attachment['file_url'], PHP_EOL;
  ```

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

  var documentId = "doc-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d";
  var attachmentId = "doc-att-7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d";

  using var client = new HttpClient();
  using var request = new HttpRequestMessage(
      HttpMethod.Get,
      $"https://api.e-invoice.be/api/documents/{documentId}/attachments/{attachmentId}");
  request.Headers.Authorization = new AuthenticationHeaderValue(
      "Bearer", Environment.GetEnvironmentVariable("E_INVOICE_API_KEY"));

  var response = await client.SendAsync(request);
  response.EnsureSuccessStatusCode();

  using var attachment = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
  Console.WriteLine(attachment.RootElement.GetProperty("file_url").GetString());
  ```
</CodeGroup>

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

If the attachment does not exist, the endpoint returns `404 Not Found` with `"detail": "Document attachment not found"`. See also [Download one attachment](/guides/attachments#download-one-attachment).

<Warning>
  `file_url` is valid for 1 hour. Do not store it and do not show it to your users as a permanent link. Store the `document_id` and the attachment `id`, and request a new URL when you need the file.
</Warning>

To find the PDF of the invoice, select the attachment with `file_type` equal to `application/pdf`. A sender can embed more than one PDF; in that case use `file_name` to identify the correct file.

See [Attachments and PDF](/guides/attachments) for the permitted file types and for the procedure to add attachments to the documents that you send.

## Filter the inbox

`GET /api/inbox/` returns the documents with `direction: "INBOUND"` and `state: "RECEIVED"`. It accepts the parameters in this table. All parameters are optional, and you can combine them.

| Parameter | Type | Default | Description |
| - | - | - | - |
| `page` | integer | `1` | The page number. The minimum is 1. |
| `page_size` | integer | `20` | The number of documents on each page. The minimum is 1 and the maximum is 100. |
| `type` | string | All types | One document type: `INVOICE`, `CREDIT_NOTE`, `DEBIT_NOTE`, `SELFBILLING_INVOICE` or `SELFBILLING_CREDIT_NOTE`. |
| `sender` | string | No filter | A text in `vendor_name`, `vendor_email`, `vendor_tax_id` or `vendor_company_id`. |
| `date_from` | date-time | No filter | The first issue date. |
| `date_to` | date-time | No filter | The last issue date. |
| `search` | string | No filter | A text in the invoice number, the vendor name, the customer name or the note. |
| `sort_by` | string | `created_at` | The sort field: `created_at`, `invoice_date`, `due_date`, `invoice_total`, `customer_name`, `vendor_name` or `invoice_id`. |
| `sort_order` | string | `desc` | The sort direction: `asc` or `desc`. |

See [List, filter and manage documents](/guides/managing-documents#filters) for the full description of each parameter.

These rules are important for received documents:

* `date_from` and `date_to` filter on the issue date of the invoice (`invoice_date`), not on the time of receipt.
* `sender` finds a text in `vendor_name`, `vendor_email`, `vendor_tax_id` or `vendor_company_id`.
* Received documents do not get the type `DEBIT_NOTE`, thus `type=DEBIT_NOTE` returns an empty list.

Example: all credit notes from E-INVOICE BV with an issue date in October 2026, the oldest first.

```bash cURL theme={null}
curl -G "https://api.e-invoice.be/api/inbox/" \
     -H "Authorization: Bearer $E_INVOICE_API_KEY" \
     --data-urlencode "type=CREDIT_NOTE" \
     --data-urlencode "sender=BE1018265814" \
     --data-urlencode "date_from=2026-10-01T00:00:00" \
     --data-urlencode "date_to=2026-10-31T00:00:00" \
     --data-urlencode "sort_by=invoice_date" \
     --data-urlencode "sort_order=asc"
```

The response has the same paginated shape as the response in [Polling](#polling).

### Shortcut endpoints

Two endpoints return one document type only. They accept `page`, `page_size`, `sort_by` and `sort_order`, and return the same paginated response. They do not accept `sender`, `date_from`, `date_to` or `search`; use `GET /api/inbox/` with the `type` parameter when you need these filters.

| Endpoint | Returns |
| - | - |
| `GET /api/inbox/invoices` | Received documents of the type `INVOICE` |
| `GET /api/inbox/credit-notes` | Received documents of the type `CREDIT_NOTE` |

## Test with a sandbox company

A sandbox company gets no documents from the Peppol network. Use **Simulate inbound** to put a document in its inbox:

<Steps>
  <Step title="Open the inbox of the sandbox company">
    In [app.e-invoice.be](https://app.e-invoice.be), select your sandbox company and open **Inbox**.
  </Step>

  <Step title="Simulate an inbound document">
    Click **Simulate inbound**. Select the built-in sample invoice, or upload your own UBL XML file.
  </Step>

  <Step title="Run your integration">
    The document is in the `RECEIVED` state, `GET /api/inbox/` lists it, and the `document.received` webhooks of the company are sent. All samples on this page operate on this document with the API key of the sandbox company.
  </Step>
</Steps>

<Warning>
  A document that you send to your own company does not come into the inbox. In a sandbox company, a send goes to the contact email address of the company and not to Peppol, thus the document gets the `SENT` state and stays in the outbox. `POST /api/documents/` ignores a `direction` value of `INBOUND` and always makes an outbound draft. Simulate inbound is the only method to fill the inbox of a sandbox company.
</Warning>

[Testing received documents](/environments#testing-received-documents) gives the full description. Resellers can do the same step through the Admin API: see [Simulate an inbound document](/admin-api#simulate-an-inbound-document).

## Frequently asked questions

<AccordionGroup>
  <Accordion title="Can I mark a document as read or processed?">
    No. The API has no such endpoint and no such field. Keep the processing status in your own system, with the document `id` as the key. See [Prevent double processing](#prevent-double-processing).
  </Accordion>

  <Accordion title="Must I poll if I use webhooks?">
    It is not mandatory, but it is good practice. A poll at a long interval (for example one time each hour) finds the documents for which your endpoint missed the webhook. Because you record the processed document IDs, the poll does not process a document twice.
  </Accordion>

  <Accordion title="How long are the download URLs valid?">
    `signed_url` (UBL) and `file_url` (attachments) are valid for 1 hour. Each call to the endpoint returns a new URL.
  </Accordion>

  <Accordion title="Why is my inbox empty?">
    For a production company: make sure that the company is registered as a receiver on Peppol with [Look up Peppol participants](/guides/lookup-participants). For a sandbox company: the inbox stays empty until you use Simulate inbound.
  </Accordion>

  <Accordion title="Which document types can I receive?">
    A received document gets one of four types: `INVOICE`, `CREDIT_NOTE`, `SELFBILLING_INVOICE` or `SELFBILLING_CREDIT_NOTE`. The type comes from the UBL document (Invoice or CreditNote) and its self-billing marker. Use the `type` parameter of `GET /api/inbox/` to get one type.
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Webhooks" icon="webhook" href="/essentials/webhooks">
    Get an event for each received document and verify its signature.
  </Card>

  <Card title="List, filter and manage documents" icon="list" href="/guides/managing-documents">
    Use the filters, the sort options and the pagination of the inbox.
  </Card>

  <Card title="Test mode and sandbox companies" icon="flask" href="/environments">
    Use a sandbox company and Simulate inbound to test your integration.
  </Card>

  <Card title="Document lifecycle and delivery tracking" icon="arrows-rotate" href="/guides/document-lifecycle">
    Read the timeline of a received document.
  </Card>
</CardGroup>
