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

# Document lifecycle and delivery tracking

> Follow a document through its states, read its timeline, and interpret the Peppol responses that the receiver returns.

## Overview

Each document has a [state](/glossary#document-state) that shows where it is in its life. The timeline endpoint gives the events behind that state: creation, each transmission, and each response from the receiver. Use this page to find out why a document is `FAILED` or to see if the buyer accepted an invoice.

## Document states

A document has one of five states: `DRAFT`, `TRANSIT`, `SENT`, `FAILED` and `RECEIVED`. The `state` field of the document contains the value. An outbound document uses the first four states. An inbound document is `RECEIVED`, or `FAILED` when its processing failed.

### Outbound flow

```mermaid theme={null}
stateDiagram-v2
    direction LR
    [*] --> DRAFT: Create document
    DRAFT --> TRANSIT: Send
    TRANSIT --> SENT: Transmission successful
    TRANSIT --> FAILED: Transmission failed
    FAILED --> TRANSIT: Send again
    DRAFT --> [*]: Delete
    FAILED --> [*]: Delete
    SENT --> [*]
```

| State | Next states | Webhook event on entry | Production company | Sandbox company |
| - | - | - | - | - |
| `DRAFT` | `TRANSIT`, or deleted | None | `POST /api/documents/` creates the draft. | Same. |
| `TRANSIT` | `SENT`, `FAILED` | None | The send request puts the document in the queue for Peppol transmission. | The send request puts the document in the queue for email delivery. |
| `SENT` | None (final) | `document.sent` | The access point of the receiver accepted the document. | The UBL XML was sent by email to the contact email address of the company. No Peppol traffic occurs. |
| `FAILED` | `TRANSIT`, or deleted | `document.sent.failed` | The transmission failed. You can send the document again. | Same. |

### Inbound flow

```mermaid theme={null}
stateDiagram-v2
    direction LR
    [*] --> RECEIVED: Document received and processed
    [*] --> FAILED: Processing failed
    RECEIVED --> [*]
```

| State | Next states | Webhook event on entry | Production company | Sandbox company |
| - | - | - | - | - |
| `RECEIVED` | None (final) | `document.received` | The document arrived through Peppol and is in the inbox. | The document is seeded with Simulate inbound. A sandbox company receives no Peppol traffic. |
| `FAILED` | None | `document.received.failed` | The document arrived, but the processing failed. | Not applicable. |

See [Webhooks](/essentials/webhooks) for the payload of each event.

### Retry behaviour

For a production company, the API makes a maximum of 3 attempts to deliver the document to the access point of the receiver, with 5 seconds between the attempts. If the last attempt fails, the document goes to `FAILED`. To try again, send the document again: see [Read a failed transmission](#read-a-failed-transmission).

## State rules

* **Send.** `POST /api/documents/{document_id}/send` is permitted only for a document in the state `DRAFT` or `FAILED`. For other states the API returns `405 Method Not Allowed`.
* **Delete.** `DELETE /api/documents/{document_id}` is permitted only for a document in the state `DRAFT` or `FAILED`. For other states the API returns `400 Bad Request` with the detail `Document is not in draft state`.
* **Update.** There is no endpoint that changes the content of a draft. Delete the draft and create a new document.

See [List, filter and manage documents](/guides/managing-documents) for the related procedures.

## Get the timeline

The timeline endpoint returns all events of one document in chronological order.

<ParamField path="document_id" type="string" required>
  The ID of the document.
</ParamField>

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

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

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

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

  const timeline = await response.json();
  for (const event of timeline.events) {
    console.log(event.timestamp, event.event_type);
  }
  ```

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

  import requests

  document_id = "doc-4f8a1c2e9b7d4a6f8e3c5b1a2d9f7e60"

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

  for event in response.json()["events"]:
      print(event["timestamp"], event["event_type"])
  ```

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

  $ch = curl_init("https://api.e-invoice.be/api/documents/{$documentId}/timeline");
  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("Request failed: {$status}");
  }

  foreach (json_decode($body, true)['events'] as $event) {
      echo $event['timestamp'] . ' ' . $event['event_type'] . PHP_EOL;
  }
  ```

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

  var documentId = "doc-4f8a1c2e9b7d4a6f8e3c5b1a2d9f7e60";

  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}/timeline");
  response.EnsureSuccessStatusCode();

  using var timeline = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
  foreach (var timelineEvent in timeline.RootElement.GetProperty("events").EnumerateArray())
  {
      Console.WriteLine(
          $"{timelineEvent.GetProperty("timestamp")} {timelineEvent.GetProperty("event_type")}");
  }
  ```
</CodeGroup>

The response for an invoice that a production company sent, and that the buyer accepted:

```json theme={null}
{
  "document_id": "doc-4f8a1c2e9b7d4a6f8e3c5b1a2d9f7e60",
  "events": [
    {
      "event_type": "document_created",
      "timestamp": "2026-10-01T09:14:02.118000Z",
      "id": "doc-4f8a1c2e9b7d4a6f8e3c5b1a2d9f7e60",
      "details": {
        "invoice_id": "INV-2026-001",
        "vendor_tax_id": "BE1018265814",
        "customer_tax_id": "BE0848934496",
        "sender_peppol_id": "0208:1018265814",
        "receiver_peppol_id": "0208:0848934496"
      }
    },
    {
      "event_type": "send_success",
      "timestamp": "2026-10-01T09:14:05.402000Z",
      "id": "7c1d2a90-5b3e-4f6a-9d2c-8e1f0a4b6c3d",
      "details": {
        "sender_peppol_id": "0208:1018265814",
        "receiver_peppol_id": "0208:0848934496"
      }
    },
    {
      "event_type": "mlr_received",
      "timestamp": "2026-10-01T09:14:31.870000Z",
      "id": "ar-9a2b4c6d8e0f1a3b5c7d9e1f2a4b6c8d",
      "details": {
        "response_type": "mlr",
        "response_code": "AB",
        "sender_name": "OpenPeppol VZW"
      }
    },
    {
      "event_type": "imr_received",
      "timestamp": "2026-10-03T14:02:47.006000Z",
      "id": "ar-1f3e5d7c9b0a2c4e6f8a0b1c3d5e7f9a",
      "details": {
        "response_type": "invoice_response",
        "response_code": "AP",
        "status_reason_code": "NON",
        "sender_name": "OpenPeppol VZW"
      }
    }
  ]
}
```

<Note>
  For a [sandbox company](/environments), the timeline of an outbound document contains only the `document_created` event. The email delivery makes no transmission record, and Peppol responses are not delivered to sandbox companies. Read the `state` of the document to see the result of the send.
</Note>

### Response fields

<ResponseField name="document_id" type="string" required>
  The ID of the document.
</ResponseField>

<ResponseField name="events" type="array" required>
  The events, sorted by `timestamp` from oldest to newest.

  <Expandable title="Event fields">
    <ResponseField name="event_type" type="string" required>
      One of the nine event types in the table below.
    </ResponseField>

    <ResponseField name="timestamp" type="string" required>
      The date and time of the event (ISO 8601).
    </ResponseField>

    <ResponseField name="id" type="string">
      The ID of the record behind the event: the email, the document, the transmission or the response.
    </ResponseField>

    <ResponseField name="details" type="object">
      More data about the event. The keys are different for each event type. A key is absent when it has no value, and the object is absent when it has no keys.
    </ResponseField>
  </Expandable>
</ResponseField>

The API returns `404 Not Found` when the document does not exist in your company.

With the command-line tool, `peppol document timeline <document-id>` shows the same events. See [peppol CLI](/cli).

## Timeline events

| Event type | Meaning | Keys in `details` |
| - | - | - |
| `email_received` | An inbound email that is linked to the document was received. See [Send invoices by email (Mailbox)](/guides/mailbox). | `subject`, `attachments` |
| `email_processed` | The email was processed. | `subject`, `attachments` |
| `document_created` | The document record was created. | `invoice_id`, `vendor_tax_id`, `customer_tax_id`, `vendor_company_id`, `customer_company_id`, `sender_peppol_id`, `receiver_peppol_id` |
| `send_attempted` | The newest transmission is in progress (the document is `TRANSIT`). | `sender_peppol_id`, `receiver_peppol_id` |
| `send_failed` | A transmission failed. | `sender_peppol_id`, `receiver_peppol_id` |
| `send_success` | The transmission was successful (the document is `SENT`). | `sender_peppol_id`, `receiver_peppol_id` |
| `receive_success` | The document was received through Peppol. | `sender_peppol_id`, `receiver_peppol_id` |
| `mlr_received` | A Message Level Response arrived for the document. | `response_type`, `response_code`, `status_reason_code`, `status_reason`, `sender_name` |
| `imr_received` | An Invoice Response arrived for the document. | `response_type`, `response_code`, `status_reason_code`, `status_reason`, `sender_name` |

## Read a failed transmission

Each send request that reaches the transmission step makes one transmission record, and thus one event in the timeline. The API sets the event type of these records as follows:

* Each record before the newest one is `send_failed`.
* The newest record is `send_success` when the document is `SENT`, `send_attempted` when the document is `TRANSIT`, and `send_failed` in all other cases.

A document that failed two times and was then delivered shows two `send_failed` events and one `send_success` event.

<Steps>
  <Step title="Confirm the state">
    Call `GET /api/documents/{document_id}` and make sure that `state` is `FAILED`. You also get the `document.sent.failed` webhook event when the transmission fails.
  </Step>

  <Step title="Read the timeline">
    Find the `send_failed` events. Examine `details.receiver_peppol_id` to make sure that the document went to the correct participant.
  </Step>

  <Step title="Check the receiver">
    Use [Look up Peppol participants](/guides/lookup-participants) to make sure that the receiver is registered on Peppol and can receive the document type.
  </Step>

  <Step title="Send again">
    Call `POST /api/documents/{document_id}/send` again. Give the Peppol IDs as query parameters if the routing was incorrect. The document goes back to `TRANSIT` and the timeline gets a new event.
  </Step>
</Steps>

<Note>
  The timeline does not contain the cause of a failed transmission. If the cause is not clear after these steps, contact support and give the document ID.
</Note>

## Peppol responses

After a successful transmission, the receiver can return two types of response. The API attaches each response to the original document and shows it in the timeline.

| | Message Level Response (MLR) | Invoice Response |
| - | - | - |
| Timeline event | `mlr_received` | `imr_received` |
| `details.response_type` | `mlr` | `invoice_response` |
| Subject | The technical result: the receiving system accepted or rejected the message. | The business status: the buyer processes, accepts, rejects or pays the invoice. |

Companies that you register through e-invoice.be are registered on Peppol as receivers of the two response types. No configuration is necessary.

<Warning>
  Responses are optional in Peppol. Many receivers send no Message Level Response and no Invoice Response. A timeline without response events does not mean that the buyer rejected or ignored the invoice.
</Warning>

### Response codes

`details.response_code` contains one of these codes (UNCL4343 subset for Peppol BIS Invoice Response 3.0).

| Code | Meaning |
| - | - |
| `AB` | Message acknowledgement |
| `IP` | In process |
| `UQ` | Under query |
| `CA` | Conditionally accepted |
| `AP` | Accepted |
| `RE` | Rejected |
| `PD` | Paid |

### Status reason codes

`details.status_reason_code` gives the reason for the status (OpenPeppol `OPStatusReason` code list). `details.status_reason` contains the free text of the sender, if there is one.

| Code | Meaning |
| - | - |
| `NON` | No issue |
| `REF` | References incorrect |
| `LEG` | Legal information incorrect |
| `REC` | Receiver unknown |
| `QUA` | Item quality insufficient |
| `DEL` | Delivery issues |
| `PRI` | Prices incorrect |
| `QTY` | Quantity incorrect |
| `ITM` | Items incorrect |
| `PAY` | Payment terms incorrect |
| `UNR` | Not recognised |
| `FIN` | Finance incorrect |
| `PPD` | Partially paid |
| `OTH` | Other |

## Limits

* **No webhook event for responses.** There is no webhook event type for a Message Level Response or an Invoice Response. To follow the business status of an invoice, poll the timeline of the document.
* **No outbound Invoice Response.** The API has no endpoint to send an Invoice Response for a document that you received.
* **No responses for sandbox companies.** A sandbox company has no Peppol traffic, thus its documents get no `mlr_received` or `imr_received` events.
* **A response does not change the state.** The `state` of a document stays `SENT` when a response arrives, also when the response code is `RE`.

## Next Steps

<CardGroup cols={2}>
  <Card title="Webhooks" icon="webhook" href="/essentials/webhooks">
    Receive an event when a document is sent, received or failed.
  </Card>

  <Card title="Errors and troubleshooting" icon="triangle-exclamation" href="/guides/errors">
    Find the cause of an API error and correct it.
  </Card>

  <Card title="Receive documents" icon="inbox" href="/guides/receiving-documents">
    Read and process the documents in your inbox.
  </Card>

  <Card title="List, filter and manage documents" icon="list" href="/guides/managing-documents">
    Use the lists, the filters and the draft operations.
  </Card>
</CardGroup>
