> For the complete documentation index, see [llms.txt](https://api.qvalia.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://api.qvalia.io/api-documentation/apis/partner-api/webhooks.md).

# Webhooks

Qvalia delivers platform events to your server through HTTPS POST requests.

Qvalia can POST an event to your endpoint when a document arrives, changes delivery status, or fails. This page covers subscribing, what arrives, and what to expect of delivery. Signing is covered separately under Verifying a delivery.

> **NB! All subscribed endpoints must use `https`!**

### Subscribing

An account, or a partner, has a single webhook subscription:

| Endpoint                                                               | Scope                                 |
| ---------------------------------------------------------------------- | ------------------------------------- |
| `PUT /account/{accountRegNo}/webhook/configure`                        | your own account                      |
| `PUT /partner/{partnerRegNo}/webhook/configure`                        | every account the partner may receive |
| `PUT /partner/{partnerRegNo}/account/{accountRegNo}/webhook/configure` | one child account                     |

The first `PUT` creates the subscription and returns a generated `id` (UUID v4). Subsequent `PUT` requests update the same subscription's `url` and `types`. The `url` must be `https`.

Use the returned `id` with `.../webhook/{webhookId}/auth` to attach outbound authentication, and with `.../webhook/{webhookId}/secret` to manage the signing secret.

A `PUT` that creates a subscription also returns a `signingSecret` in clear text, in that response only.

### Event types

Choose which to receive in `types`:

| Type                | Sent when                                        |
| ------------------- | ------------------------------------------------ |
| `new_document`      | a document is received or created for an account |
| `document_delivery` | a document's delivery status changes             |
| `document_delayed`  | a delivery is being retried and has not finished |
| `document_error`    | a document's delivery fails                      |

`document_delayed` arrives once per retry cycle while a Peppol send works through its retry window, which can run up to 24 hours. A terminal `document_delivery` or `document_error` follows. Leave the type out of `types` if you do not want that traffic; the terminal events arrive either way.

To distinguish a retry from a real failure, branch on the event type rather than on the text of `error`.

### Delivery

Events are delivered as an HTTP `POST` with a JSON body. Respond with `2xx`; the request times out after 10 seconds.

Every delivery carries:

```
X-Qvalia-Event-Id: 4d1c9f84-3a2b-5c6d-9e0f-1a2b3c4d5e6f
X-Qvalia-Signature: t=1757846400,v1=5257a869e7ecebeda32affa62cdca3fa...
```

`X-Qvalia-Signature` is present once the subscription has a signing secret. Any outbound authentication you configured is applied as request headers on the same request.

**Delivery is at-least-once.** Make your endpoint idempotent and deduplicate on `eventId`, which identifies the event rather than the delivery attempt and is identical across redeliveries. Do not deduplicate on the whole body: redeliveries differ in `status.updatedAt`.

**A failed delivery is not retried.** A non-2xx response, a timeout or an unreachable endpoint is recorded against the document and not attempted again.

**A missed event is permanently lost.** Delivery is at-least-once for what we do send, but a delivery we fail to make is never retried — so webhooks cannot be your only source of truth. Keep polling `GET /transaction/…` as a safety net, on whatever interval your reconciliation needs. Signing and deduplication make a delivery you *receive* trustworthy; neither makes delivery guaranteed.

Ordering is not guaranteed. A `document_delivery` can arrive before the `new_document` for the same document. Order by `occurredAt`, not by arrival time.

### Payload

One flat JSON object: the event fields, plus the document detail on the same level.

| Field                 | Notes                                                                                                                                                    |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `eventId`             | Identifies the event, not the attempt. Deduplicate on this.                                                                                              |
| `eventType`           | One of the four types above.                                                                                                                             |
| `accountRegNo`        | The account the document belongs to.                                                                                                                     |
| `documentType`        | `Invoice`, `CreditNote`, `Order`.                                                                                                                        |
| `direction`           | `incoming` or `outgoing`.                                                                                                                                |
| `integrationId`       | Qvalia's document id. Use it to `GET` the document.                                                                                                      |
| `occurredAt`          | When the event happened, ISO-8601.                                                                                                                       |
| `documentId`          | Document number. Usually on `new_document`, often absent later.                                                                                          |
| `globalTransactionId` | Platform-wide id for the document exchange.                                                                                                              |
| `status`              | Delivery-lifecycle detail; see below.                                                                                                                    |
| `error`               | Failure reason. Present on `document_error`.                                                                                                             |
| `peppol_metadata`     | Peppol exchange detail. `null` for non-Peppol deliveries, and `{}` on a Peppol event that carries no exchange detail of its own, such as a retry report. |

Fields are omitted when they have no value, so treat a missing key and an empty value the same. `peppol_metadata` is the exception: it is explicitly `null`.

#### The `status` object

| Field            | Notes                                                           |
| ---------------- | --------------------------------------------------------------- |
| `status`         | Delivery status from the upstream channel. Open set; see below. |
| `event`          | Internal lifecycle event. Maps 1:1 onto `eventType`.            |
| `deliveryMethod` | `peppol`, `print`, `email`, `api`, `kivra`, and others.         |
| `updatedAt`      | When we recorded this status. Differs between redeliveries.     |

`status.status` is **not an enum**. It is free text reported by the delivery channel, and a new channel can introduce a value not listed here. The values emitted today are:

| Value                    | Final? | Meaning                                   |
| ------------------------ | ------ | ----------------------------------------- |
| `pending`                | no     | Logged; nothing attempted yet.            |
| `delayed`                | no     | Being retried.                            |
| `warning`                | no     | Reopened by a rules-processor reset.      |
| `processed`              | yes    | Delivered.                                |
| `processed_with_warning` | yes    | Delivered, with something worth flagging. |
| `error`                  | yes    | Delivery failed.                          |

It may also be absent. Branch on the values you care about and treat anything unrecognised, or a missing `status`, as "no change".

#### Examples

`new_document`:

```json
{
  "eventId": "4d1c9f84-3a2b-5c6d-9e0f-1a2b3c4d5e6f",
  "eventType": "new_document",
  "accountRegNo": "SE5560004755",
  "documentType": "Invoice",
  "direction": "outgoing",
  "integrationId": "6b928ef1-fb0d-4b9a-a56f-b5dbca7a0fd4",
  "occurredAt": "2026-08-19T09:25:36.512Z",
  "documentId": "123456-INV",
  "globalTransactionId": "6b928ef1-fb0d-4b9a-a56f-b5dbca7a0fd4",
  "status": {
    "event": "message-log/create",
    "deliveryMethod": "peppol",
    "updatedAt": "2026-08-19T09:25:37.228Z"
  },
  "peppol_metadata": {
    "messageId": "9cab8ba5-d2a4-45d0-842d-8ede91dcac9f@QVALIA-PSE000094",
    "accessPoint": "PSE000094",
    "docTypeId": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1",
    "processId": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0",
    "exchangeDateTime": "2026-08-19T09:25:34.226Z"
  }
}
```

`document_delayed`, one per retry cycle:

```json
{
  "eventId": "2a9d5c31-7b8e-5f4a-9c1d-3e5f7a9b1c3d",
  "eventType": "document_delayed",
  "accountRegNo": "SE5560004755",
  "documentType": "Invoice",
  "direction": "outgoing",
  "integrationId": "6b928ef1-fb0d-4b9a-a56f-b5dbca7a0fd4",
  "occurredAt": "2026-08-19T11:26:10.104Z",
  "globalTransactionId": "6b928ef1-fb0d-4b9a-a56f-b5dbca7a0fd4",
  "documentId": "123456-INV",
  "status": {
    "status": "delayed",
    "event": "message-log/delayed",
    "deliveryMethod": "peppol",
    "updatedAt": "2026-08-19T11:26:09.881Z"
  },
  "peppol_metadata": {}
}
```

`document_delivery`, a status transition on a document already announced by `new_document`. `documentId` is often absent at this stage:

```json
{
  "eventId": "8b3e1a07-6c5d-5e4f-9a2b-7c8d9e0f1a2b",
  "eventType": "document_delivery",
  "accountRegNo": "SE5560004755",
  "documentType": "Invoice",
  "direction": "outgoing",
  "integrationId": "6b928ef1-fb0d-4b9a-a56f-b5dbca7a0fd4",
  "occurredAt": "2026-08-19T09:26:10.104Z",
  "globalTransactionId": "6b928ef1-fb0d-4b9a-a56f-b5dbca7a0fd4",
  "status": {
    "status": "processed",
    "event": "message-log/update",
    "deliveryMethod": "peppol",
    "updatedAt": "2026-08-19T09:26:09.881Z"
  },
  "peppol_metadata": {
    "messageId": "9cab8ba5-d2a4-45d0-842d-8ede91dcac9f@QVALIA-PSE000094",
    "accessPoint": "PSE000094",
    "docTypeId": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1",
    "processId": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0",
    "exchangeDateTime": "2026-08-19T09:25:34.226Z"
  }
}
```

`document_error`, with `error` carrying the reason:

```json
{
  "eventId": "c71f2d93-4e8a-5b6c-8d7e-2f3a4b5c6d7e",
  "eventType": "document_error",
  "accountRegNo": "SE5560004755",
  "documentType": "Invoice",
  "direction": "outgoing",
  "integrationId": "6b928ef1-fb0d-4b9a-a56f-b5dbca7a0fd4",
  "occurredAt": "2026-08-19T09:26:10.104Z",
  "globalTransactionId": "6b928ef1-fb0d-4b9a-a56f-b5dbca7a0fd4",
  "status": {
    "status": "error",
    "event": "message-log/error",
    "deliveryMethod": "peppol",
    "updatedAt": "2026-08-19T09:26:09.881Z"
  },
  "error": "Peppol validation failed: invoice does not conform to UBL 2.1",
  "peppol_metadata": null
}
```

### Verifying a delivery

Deliveries are signed with HMAC-SHA256 once the subscription has a signing secret, so your endpoint can confirm the body came from Qvalia unaltered. This is independent of the outbound credential under `.../auth`, which authenticates Qvalia to your endpoint. Use both.

The secret is symmetric, so treat it like a password. There is no public key to fetch.

To verify:

1. Parse `t` and every `v1=` value from `X-Qvalia-Signature`, ignoring prefixes you do not recognise.
2. Reject the delivery if `|now - t|` exceeds your tolerance. 5 minutes is recommended.
3. Compute `HMAC-SHA256(secret, "<t>.<raw body>")` and hex-encode it.
4. Compare against each `v1=` value in constant time. One match is enough.
5. Reject the delivery if you have already processed its `X-Qvalia-Event-Id`.

Sign the exact bytes you received, not a re-serialised JSON object. Key order and whitespace change the hash, and this is the most common cause of a failing first implementation.

Steps 2 and 5 are both needed. The timestamp caps how long a captured delivery stays usable; the event-id check is what rejects a replay inside that window. The signature itself proves only origin and integrity.

#### Rotation

`POST .../webhook/{webhookId}/secret` issues a new secret and expires the previous ones `overlapHours` (default 24) from now. While the window is open each delivery carries a `v1=` part per active secret, so you can deploy the new one at your own pace.

Use `overlapHours: 0` if a secret has leaked. A subscription holds at most 5 secrets.

Subscriptions created before signing existed carry no secret and stay unsigned until you call that endpoint.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://api.qvalia.io/api-documentation/apis/partner-api/webhooks.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
