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

# Webhooks

> How to set up webhooks

Webhooks allow you to receive real-time notifications about events in your e-invoice.be account. This guide explains how to set up and use webhooks effectively.

## Overview

Webhooks are HTTP callbacks that notify your application when specific events occur, such as:

* Document received
* Document sent
* Document send failed
* Document receive failed

## Setting Up Webhooks

### 1. Create a Webhook

```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/webhooks" \
     -H "Authorization: Bearer YOUR_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "url": "https://your-domain.com/webhook",
       "events": ["document.received", "document.sent", "document.sent.failed"],
       "enabled": true
     }'
```

### 2. Configure Your Endpoint

Your webhook endpoint should:

* Accept POST requests
* Use HTTPS with a valid SSL/TLS certificate (recommended; self-signed certificates are not supported)
* Support HTTP Basic Authentication via URL format: `https://username:password@host.com/endpoint` (optional)
* Return a 200 OK response quickly
* Handle retries gracefully
* Verify webhook signatures

**Endpoint URL Requirements:**

* **Protocol**: HTTP and HTTPS are both supported (HTTPS strongly recommended for production)
* **Certificate**: If using HTTPS, SSL/TLS certificate must be valid and issued by a trusted Certificate Authority
* **Self-signed certificates**: Not supported - webhook delivery will fail with HTTPS
* **Redirects**: Not followed. The configured URL must be the final endpoint - any 3xx redirect response (301, 302, 307, 308, etc.) is treated as a delivery failure and will be retried. This is a deliberate security measure to prevent SSRF and redirect-based attacks.
* **Authentication**: You can include HTTP Basic Authentication credentials directly in the URL:
  ```
  https://username:password@your-domain.com/webhook
  http://username:password@your-domain.com/webhook
  ```
* **URL format**: Standard URL format with optional path, query parameters, and authentication

<Warning>
  Configure your webhook URL to point directly at the final endpoint. If your URL responds with a redirect (e.g., `http://` → `https://`, or apex → `www`), delivery will fail. Update the registered webhook URL to match the destination your server actually serves the handler from.
</Warning>

### 3. Delivery and Retries

Each event is delivered as a single POST request and must receive a `2xx` response within **10 seconds**. If your endpoint returns a non-`2xx` status, times out, or is unreachable, delivery is retried up to **3 attempts** in total, with exponential backoff between attempts (roughly 1 second, then 2 seconds). After the final attempt fails, the event is not delivered again.

Because a delivery may be retried, design your handler to be **idempotent** (use the event `id` to de-duplicate) and to return quickly — do any heavy processing asynchronously after acknowledging the request.

## Webhook Security

### Signature Verification

Webhook requests are signed using HMAC-SHA256 to ensure authenticity and integrity. The signature is included in the `X-Signature` header of each request.

#### How the Signature is Computed

The signature is computed over the **entire webhook event payload** (not just the `data` field). This ensures the integrity of all webhook data including the event ID, timestamp, type, and content.

1. The complete webhook event is serialized to JSON with sorted keys
2. The JSON string is encoded to UTF-8 bytes
3. An HMAC is created using your webhook secret and the SHA-256 algorithm
4. The resulting signature is formatted as `sha256={hexadecimal_signature}`

#### Example

Given this complete webhook event payload:

```json theme={null}
{
  "id": "evt-e7wyc7gtpqx4z73x2wqmwhebhbb3r8n3ovfhcsdbulxr3s2awf49de76yglrnri3",
  "tenant_id": "ten-abc123",
  "created_at": 1762780249,
  "type": "document.sent",
  "data": {
    "document_id": "doc-1"
  },
  "text": "⚡️ New webhook event: document.sent\n\n{\n  \"document_id\": \"doc-1\"\n}"
}
```

When serialized to JSON with sorted keys, this becomes the following string (before UTF-8 encoding):

```json theme={null}
{"created_at": 1762780249, "data": {"document_id": "doc-1"}, "id": "evt-e7wyc7gtpqx4z73x2wqmwhebhbb3r8n3ovfhcsdbulxr3s2awf49de76yglrnri3", "tenant_id": "ten-abc123", "text": "\u26a1\ufe0f New webhook event: document.sent\n\n{\n  \"document_id\": \"doc-1\"\n}", "type": "document.sent"}
```

**Important**: Notice how special characters are properly escaped in the JSON serialization:

* Newlines are represented as `\n` (literal backslash-n, not actual line breaks)
* Unicode characters like emojis are escaped as `\u26a1\ufe0f` (for ⚡️)
* Quotes inside the `text` field are escaped as `\"`

This is critical for signature verification. The JSON must be serialized with:

* **Sorted keys** (`sort_keys=True` in Python's `json.dumps()`)
* **No extra whitespace** (compact format, not pretty-printed)
* **Proper escaping** of special characters (standard JSON encoding)

Your JSON library should handle this automatically when using `json.dumps(payload, sort_keys=True)` in Python, `JSON.stringify()` in JavaScript, or equivalent functions in other languages.

With the webhook secret `"secret"`, this produces the signature:

```
sha256=2f8ec8fab5adedd8f82a2b4064f559c40b14aed0c6da27ed394e51176b10bd1b
```

#### Computing the Signature

All webhook requests from e-invoice.be include a cryptographic signature that allows you to verify the request's authenticity. This prevents unauthorized parties from sending fake webhook events to your endpoint.

Here's the exact code used by e-invoice.be to compute webhook signatures:

```python theme={null}
import hmac
import hashlib
import json

def compute_signature(webhook_event_json: str, secret: str) -> str:
    """
    Compute HMAC-SHA256 signature for the entire webhook event payload.

    Args:
        webhook_event_json: JSON string of the complete webhook event
                           (serialized with sort_keys=True, exclude_none=True)
        secret: Your webhook secret

    Returns:
        Signature in format "sha256={hex_digest}"
    """
    payload_bytes = webhook_event_json.encode("utf-8")
    signature = hmac.new(
        secret.encode("utf-8"), payload_bytes, hashlib.sha256
    ).hexdigest()
    return f"sha256={signature}"
```

**How it works:**

1. **Serialize the entire webhook event**: The complete webhook event (including `id`, `tenant_id`, `created_at`, `type`, `data`, and `text` fields) is serialized to JSON with `sort_keys=True` to ensure consistent key ordering. This is critical because `{"a": 1, "b": 2}` and `{"b": 2, "a": 1}` are semantically identical but would produce different signatures without key sorting.

2. **Convert to bytes**: The JSON string is encoded to UTF-8 bytes, which is required for the HMAC operation.

3. **Generate HMAC**: Using your webhook secret as the key, an HMAC (Hash-based Message Authentication Code) is computed using the SHA-256 hashing algorithm. This creates a cryptographic signature that only someone with your secret can reproduce.

4. **Format the result**: The hexadecimal digest is prefixed with `sha256=` to indicate the algorithm used, matching the format in the `X-Signature` header.

**Why HMAC?** HMAC is preferred over simple hashing because it requires a secret key. Even if an attacker knows the payload and hashing algorithm, they cannot generate a valid signature without your webhook secret.

**Important**: The signature covers the **entire webhook event payload** you receive in the POST request body, not just the `data` field. This ensures the integrity and authenticity of all event information including the event ID, timestamp, type, and all data.

#### Verifying the Signature

To verify the signature in your webhook handler:

1. Extract the signature from the `X-Signature` header (format: `sha256={hex_digest}`)
2. **Parse the request body as JSON**, then re-serialize it with **sorted keys** and no extra whitespace (e.g. `json.dumps(payload, sort_keys=True)` in Python, or the equivalent in your language)
3. Encode that canonical JSON string to UTF-8 bytes
4. Compute the HMAC-SHA256 over those bytes using your webhook secret
5. Compare the computed signature with the one in the header using a constant-time comparison

<Warning>
  The signature is computed over the JSON serialized with `sort_keys=True`, but the body delivered over the wire is **not guaranteed** to use that same key order. You **must** parse the JSON and re-serialize it with sorted keys before computing the HMAC — verifying directly against the raw request body will fail whenever the transport-level key order differs from the canonical (sorted) order.
</Warning>

**Important Notes:**

* Always verify the signature **before** processing the webhook
* Use constant-time comparison to prevent timing attacks
* The signature covers the **entire webhook event** (all fields: `id`, `tenant_id`, `created_at`, `type`, `data`, `text`) — not just the `data` field
* Re-serialize with `sort_keys=True` and no extra whitespace; do not pretty-print
* If the signatures match, the webhook request is authentic and hasn't been tampered with

## Available Events

The following webhook events are supported:

* **`document.received`**: A new document is successfully received via Peppol or other channels
* **`document.received.failed`**: A document failed to be received/processed
* **`document.sent`**: A document is successfully sent via Peppol or other channels
* **`document.sent.failed`**: A document failed to send (e.g., validation error, network error, Peppol transmission failure)

## Webhook Payload

When a webhook is triggered, it sends a POST request to your configured URL with the following structure:

```json theme={null}
{
  "id": "evt-3k8d9f2h4j6m8n0p",
  "tenant_id": "ten-5x7y9z1a3b5c7d9e",
  "created_at": 1729468923,
  "type": "document.sent",
  "data": {
    "document_id": "doc-2f4h6j8k0m2n4p6q"
  },
  "text": "⚡️ New webhook event: document.sent\n\n{\n  \"document_id\": \"doc-2f4h6j8k0m2n4p6q\"\n}"
}
```

**Note**: Currently, the `data` object only contains the `document_id`. You can use this ID to fetch additional document details via the API (`GET /api/documents/{document_id}`).

### Headers

Each webhook request includes the following headers:

* **`X-Signature`**: HMAC-SHA256 signature for verifying authenticity (format: `sha256=...`)
* **`X-Event-Type`**: The event type (e.g., `document.sent`)
* **`Content-Type`**: `application/json`
* **`User-Agent`**: `e-invoice-be-webhook-service`

### Event Data Structure

The `data` object contains event-specific information:

* For all document events (`document.received`, `document.sent`, `document.sent.failed`, `document.received.failed`):
  ```json theme={null}
  {
    "document_id": "doc-2f4h6j8k0m2n4p6q"
  }
  ```

The `document_id` can be used to retrieve full document details:

```bash theme={null}
curl -X GET "https://api.e-invoice.be/api/documents/{document_id}" \
     -H "Authorization: Bearer YOUR_API_KEY"
```

### Complete Example Requests

#### Success Event Example

Here's what a successful `document.sent` webhook HTTP request looks like:

```http theme={null}
POST /webhook HTTP/1.1
Host: your-domain.com
Content-Type: application/json
User-Agent: e-invoice-be-webhook-service
X-Signature: sha256=a3f8d9e2c1b5a7f9e3d2c1b5a7f9e3d2c1b5a7f9e3d2c1b5a7f9e3d2c1b5
X-Event-Type: document.sent

{
  "id": "evt-3k8d9f2h4j6m8n0p",
  "tenant_id": "ten-5x7y9z1a3b5c7d9e",
  "created_at": 1729468923,
  "type": "document.sent",
  "data": {
    "document_id": "doc-2f4h6j8k0m2n4p6q"
  },
  "text": "⚡️ New webhook event: document.sent\n\n{\n  \"document_id\": \"doc-2f4h6j8k0m2n4p6q\"\n}"
}
```

#### Failure Event Example

Here's what a failed `document.sent.failed` webhook HTTP request looks like:

```http theme={null}
POST /webhook HTTP/1.1
Host: your-domain.com
Content-Type: application/json
User-Agent: e-invoice-be-webhook-service
X-Signature: sha256=7e9c3a1d5f8b2e6a4c9d7f3b1e8a6c2d9f7e3a1c5b8d6f2a4e9c7b3d1f8a5c
X-Event-Type: document.sent.failed

{
  "id": "evt-9m2p4r6t8v0x2z4b",
  "tenant_id": "ten-5x7y9z1a3b5c7d9e",
  "created_at": 1729469845,
  "type": "document.sent.failed",
  "data": {
    "document_id": "doc-8h3j5k7m9n1p3q5r"
  },
  "text": "⚡️ New webhook event: document.sent.failed\n\n{\n  \"document_id\": \"doc-8h3j5k7m9n1p3q5r\"\n}"
}
```

**Note**: For failure events, you'll need to retrieve the document details via the API to understand what went wrong. The document state will typically be set to `FAILED` and may contain error information. You can retry sending a failed document by calling `POST /api/documents/{document_id}/send` again.

## Webhook Management

### List Webhooks

```bash theme={null}
curl -X GET "https://api.e-invoice.be/api/webhooks" \
     -H "Authorization: Bearer YOUR_API_KEY"
```

### Update Webhook

```bash theme={null}
curl -X PUT "https://api.e-invoice.be/api/webhooks/{webhook_id}" \
     -H "Authorization: Bearer YOUR_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "enabled": false
     }'
```

### Delete Webhook

```bash theme={null}
curl -X DELETE "https://api.e-invoice.be/api/webhooks/{webhook_id}" \
     -H "Authorization: Bearer YOUR_API_KEY"
```

### Test Webhook

Send a test event to verify your webhook is working correctly:

```bash theme={null}
curl -X POST "https://api.e-invoice.be/api/webhooks/{webhook_id}/test" \
     -H "Authorization: Bearer YOUR_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "event_type": "document.sent",
       "data": {
         "document_id": "test_doc_123"
       }
     }'
```
