> 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/webhook-payload.md).

# Webhook payload

An overview of the Qvalia Webhook format

#### Verify before you trust <a href="#verify-before-you-trust" id="verify-before-you-trust"></a>

Once the subscription has a signing secret, every delivery carries `X-Qvalia-Signature`.\
Verify it before acting on the payload — see\
[Webhooks](/api-documentation/apis/partner-api/webhooks.md#verifying-a-delivery) for the full procedure.

Sign the **exact bytes you received**, not a re-serialised JSON object. That is the most\
common cause of a failing first implementation.

{% hint style="info" %}
Webhook deliveries are made *by* Qvalia to your endpoint and do not count against your\
API rate limit.
{% endhint %}

The JSON body delivered to a subscriber's `url:`

```json
{
  "eventId": "4d1c9f84-3a2b-5c6d-9e0f-1a2b3c4d5e6f",
  "eventType": "document_delivery",
  "accountRegNo": "SE5560004755",
  "documentType": "Invoice",
  "direction": "outgoing",
  "integrationId": "6b928ef1-fb0d-4b9a-a56f-b5dbca7a0fd4",
  "occurredAt": "2026-08-19T09:26:10.104Z",
  "documentId": "123456-INV",
  "globalTransactionId": "6b928ef1-fb0d-4b9a-a56f-b5dbca7a0fd4",
  "status": {
    "status": "processed",
    "event": "message-log/update",
    "deliveryMethod": "peppol",
    "updatedAt": "2026-08-19T09:26:09.881Z"
  },
  "error": null,
  "peppol_metadata": { "…": "…" }
}
```

### Top level

| Field                 | Type           | Notes                                                                                                                                    |
| --------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `eventId`             | uuid           | Identifies the event, not the delivery attempt. Remains the same across redeliveries; dedupe on this. Also sent as `X-Qvalia-Event-Id`.  |
| `eventType`           | enum           | `new_document` \| `document_delivery` \| `document_delayed` \| `document_error`. Always present.                                         |
| `accountRegNo`        | string         | The account the document belongs to. Always present.                                                                                     |
| `documentType`        | string         | `Invoice`, `CreditNote`, `Order`, etc. Follows Transaction API.                                                                          |
| `direction`           | enum           | `incoming` \| `outgoing` from account point of view.                                                                                     |
| `integrationId`       | uuid           | Qvalia's document id — use it to `GET` the document over the API.                                                                        |
| `occurredAt`          | ISO-8601       | When the event happened upstream.                                                                                                        |
| `documentId`          | string         | Document number (e.g. invoice number). Usually on `new_document`; often absent on the later status events.                               |
| `globalTransactionId` | uuid           | Platform-wide id for the whole document exchange.                                                                                        |
| `status`              | object         | Delivery-lifecycle detail — see below.                                                                                                   |
| `error`               | string         | Human-readable failure reason. Present on `document_error`. Free text — see [The `error` text](#the-error-text).                         |
| `peppol_metadata`     | object \| null | Peppol exchange detail recorded for the document so far; `null` unless `status.deliveryMethod` is `peppol`, `{}` when none is known yet. |

### The `status` object

| Field            | Type     | Notes                                                                                                                                                                                                         |
| ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`         | string   | Delivery status from the upstream channel. Open set, see below. May be absent.                                                                                                                                |
| `event`          | enum     | `message-log/create`                                                                                                                                                                                          |
| `deliveryMethod` | string   | How the document was sent. Observed: `peppol`, `print`, `email`, `api`, plus channel-specific values such as `kivra`, `billo`, `minameddelanden`, `internetbank`, `postal`, `dynamic_routing`. May be absent. |
| `updatedAt`      | ISO-8601 | When we recorded this status. Differs between redeliveries of the same event.                                                                                                                                 |

#### `Statuses`

| Value                    | Terminal? | Meaning                                                                                                              |
| ------------------------ | --------- | -------------------------------------------------------------------------------------------------------------------- |
| `pending`                | no        | Logged; nothing attempted yet. Also the `message_log.status` column default.                                         |
| `delayed`                | no        | Being retried: a Peppol send still inside its retry window. A terminal status follows; do not treat it as a failure. |
| `warning`                | no        | Reopened by a rules-processor reset or Quarantined document.                                                         |
| `processed`              | yes       | Delivered successfully.                                                                                              |
| `processed_with_warning` | yes       | Delivered, but something about the exchange is worth flagging.                                                       |
| `error`                  | yes       | Delivery failed. The top-level `error` carries the reason.                                                           |
| *(absent)*               | —         | No status reported for this event. Documents entering through the Public API announce themselves without one.        |

Which statuses go with which event:

| `eventType`         | `status.event`        | Typical `status.status`                        |
| ------------------- | --------------------- | ---------------------------------------------- |
| `new_document`      | `message-log/create`  | `pending`, or absent                           |
| `document_delayed`  | `message-log/delayed` | `delayed`                                      |
| `document_delivery` | `message-log/update`  | `processed`, `processed_with_warning`, `error` |
| `document_error`    | `message-log/error`   | `error`                                        |

`error` also arrives on `document_delivery`: the event type and the reported status are set independently.

#### Telling a retry from a real failure

Branch on the event type, not the text of `error`:

* `document_delayed` — still inside the retry window. Another event is coming.
* `document_error` — stopped deliveries if retries are exhausted, may be retry-able from Qvalia supportive functions.

A struggling Peppol send produces one `document_delayed` per retry cycle before settling on `document_delivery` or `document_error`. Leave `document_delayed` out of your subscription's `types` if you do not want that traffic; the terminal events arrive either way. Each cycle has its own `eventId`, so they do not dedupe against each other.

### The `error` text <a href="#the-error-text" id="the-error-text"></a>

`error` is written by the part of the platform that hit the problem — validation, Peppol lookup, a delivery channel, rules processing — and reaches you unchanged. It is meant for your logs and for showing to your users. **It is not a fixed list and not a contract:** wording can change, and new messages appear as delivery channels are added. Branch on `eventType` and `status.status`, never on the text.

{% hint style="warning" %}
Treat `status.status: "error"` as a failure on **any** event type. Some failures arrive as `document_delivery` with `status.status` `error` and no `error` text at all. To find out why, contact Qvalia support with the document's `integrationId`.
{% endhint %}

The messages sent today, by cause. `${…}` marks a value filled in from the document.

#### Validation

| Text                                                          | When                                                                                               |
| ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `Peppol message is not valid according to Peppol BIS rules!`  | The document fails Peppol BIS validation.                                                          |
| `CII message is not valid according to its validation rules!` | A CII document fails its validation rules.                                                         |
| `CII message could not be converted to UBL!`                  | A CII document could not be converted to UBL.                                                      |
| `Invalid SBDH / StandardBusinessDocument envelope!`           | The SBDH envelope around the document could not be read.                                           |
| `Invalid message`                                             | An invalid Peppol document was quarantined, because the account does not accept invalid documents. |

#### The receiver cannot be reached

| Text                                                                                           | When                                                                                                                     |
| ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `Peppol recipient ${peppolId} was not found in the Peppol network!`                            | The receiver is not registered in Peppol for this document type.                                                         |
| `Peppol recipient EndpointID is missing a valid schemeID or identifier!`                       | The receiver's `cbc:EndpointID` cannot be read.                                                                          |
| `Peppol recipient ${peppolId} can not receive this XRechnung Extension credit note: ${reason}` | An XRechnung Extension credit note can only be sent as its CII original, and that is not possible. `${reason}` says why. |
| `recipient_not_reachable`                                                                      | Dynamic routing found no delivery channel for the receiver: no Peppol, e-invoice, e-mail or print option.                |
| `invalid_type`                                                                                 | Dynamic routing found no usable address, and the account has neither print nor e-mail.                                   |
| `Unknown recipient of invoice [${orgNo}]`                                                      | An incoming invoice is addressed to an organisation that is not the account or one of its consolidated accounts.         |
| `Error sending document via Peppol - see log for details`                                      | The Peppol transmission failed. The cause is in Qvalia's logs — contact support with the `integrationId`.                |

#### Delivery channels other than Peppol

| Text                                  | When                                                                                                       |
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `Failed Internetbank delivery`        | The internet bank delivery failed.                                                                         |
| `Delivery failed at partner`          | A digital mailbox delivery was refused by the partner handling it.                                         |
| `Unknown processing error at partner` | Delivery through a partner network failed.                                                                 |
| `Unknown error`                       | A print or e-mail delivery failed without a more specific reason.                                          |
| `Internal processing failed`          | The document could not be prepared for print or e-mail delivery. Contact support with the `integrationId`. |

A print or e-mail delivery can also report a technical message as it was raised, for example `Invalid time value` when a date on the document cannot be read.

#### Uploads (SFTP and file conversion)

Most use one template: `Message failed with ${type}! ${details} - SFTP file renamed to: ${file}`, where `${file}` is the name your file now has on the SFTP server. A message not uploaded over SFTP ends after `${details}`.

| `${type}` · `${details}`                                                                    | When                                                                                                          |
| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `invalid_path` · `Invalid message for ${documentType}`                                      | `dynamic_routing` was requested but is not enabled for the account, or a file was uploaded into `/processed`. |
| `invalid_type` · `Invalid message for ${documentType}`                                      | Dynamic routing on a document that is not Peppol BIS, or an unsupported delivery method.                      |
| `invalid_type` · `Invalid/unknown message for ${documentType}`                              | The uploaded file was not recognised as any supported format.                                                 |
| `invalid_type` · `CII (${customizationId}) is not supported for upload, use the Public API` | CII uploaded over SFTP.                                                                                       |
| `invalid_type` · `Invalid message type ${documentType}`                                     | Internet bank delivery is not enabled for the account.                                                        |
| `invalid_messagetype` · `Invalid message type ${documentType}`                              | The sender or receiver `cbc:EndpointID` cannot be read.                                                       |
| `validation` · `Invalid message for ${documentType}`                                        | A file uploaded to `/send/` is not Peppol XML, or the receiver's `cbc:EndpointID` is invalid.                 |
| `unknown_internal` · `Invalid message handling for ${documentType}`                         | The upload failed for a reason not listed here.                                                               |
| `unknown_internal` · `Unknown processing error at partner`                                  | Delivery through a partner network failed.                                                                    |

Two more upload messages do not follow the template:

* `Invalid Svefaktura XML uploaded. Renamed to ${file}` — a Svefaktura file fails Peppol validation after conversion.
* `Invalid custom transform format uploaded. Renamed to ${file}` — a custom-format file is not supported.

#### Rules and processing

| Text                           | When                                                   |
| ------------------------------ | ------------------------------------------------------ |
| `Rejected by rules processing` | A rule on the account rejected the document.           |
| `Failed rules processing`      | A rule on the account sent the document to quarantine. |
| `Failed to handle message`     | An incoming e-invoice could not be created.            |

### `peppol_metadata`

Present when `status.deliveryMethod` is `peppol`, `null` otherwise. Every event about a Peppol document carries the exchange detail recorded for it so far, not only the event that reported it — a later status report fills it in from what was stored when the document was sent or received. `{}` means nothing is known yet, e.g. a retry report before any exchange succeeded.

| Field              | Notes                                                 |
| ------------------ | ----------------------------------------------------- |
| `messageId`        | Peppol message id, e.g. `9cab8ba5-…@QVALIA-PSE000094` |
| `accessPoint`      | Sending Access Point id (C2), e.g. `PSE000094`        |
| `docTypeId`        | Peppol document type identifier                       |
| `processId`        | Peppol process identifier                             |
| `exchangeDateTime` | When the exchange happened                            |
| `senderPeppolId`   | Sender's Peppol participant id                        |
| `receiverPeppolId` | Receiver's Peppol participant id                      |

`transactionId` is omitted; it duplicates `globalTransactionId`.

#### How stable `eventId` is

A UUIDv5 over `eventType`, `accountRegNo`, `documentType`, `direction`, `integrationId`, `documentId`, `globalTransactionId`, `status.status` and `occurredAt`.

`status.updatedAt` is not part of it: Qvalia re-stamp it on each internal processing attempt, so including it would give a reprocessed event a new id.

`occurredAt` is, and separates repeats of the same transition — successive retry cycles report the same status and differ only in when they happened.

An event arriving with no `occurredAt` gets a random id instead, and is logged. Treat it as a distinct event. Everything from the message-log pipeline carries `occurredAt`.

### Delivery semantics

* `POST`, `Content-Type: application/json`. Respond `2xx`; time out after 10 seconds.
* **A failed delivery is not retried.** A non-2xx, timeout or unreachable endpoint is recorded against the document and not attempted again.
* **A missed event can be fetched again for 30 days**, by its `eventId`, with `GET …/webhook/{eventId}` — see [Webhooks](/api-documentation/apis/partner-api/webhooks.md). Webhooks still cannot be your only source of truth: keep polling `GET /transaction/…` as a safety net. Signing and `eventId` deduplication make a delivery you receive trustworthy; neither makes delivery guaranteed.
* **You may still receive the same event twice.** An internal retry upstream, or a document published by more than one producer, can re-deliver it. Dedupe on `eventId`: it is derived from the event's identity, so redeliveries share it. (Redeliveries also differ in `status.updatedAt`, so do not dedupe on the whole body.)
* Ordering is not guaranteed. A `document_delivery` can arrive before the `new_document` for the same document. Use `occurredAt` to order, not arrival time.
* A subscription covers exactly one account. A partner-level subscription (`/partner/{partnerRegNo}/webhook`) receives the partner's own events only — **not** its children's; a partner subscribes to each child individually with `/partner/{partnerRegNo}/account/{accountRegNo}/webhook`.
* An account and its partner can each subscribe to the same account. The two are separate subscriptions, and an event for the child is delivered to both, carrying the same `eventId`. A partner's subscription to a child only delivers while the partner may act for it (the child's `parentAccount` is the partner and the partner holds the `webhook` scope).


---

# 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 by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

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

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

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.
