Webhooks
Get notified in near real-time about new messages and status changes, instead of polling MAPI.
With webhooks, Unimaze sends an HTTP POST request to an endpoint you host when something happens to one of your messages, for example:
- a new inbound invoice is ready to be fetched,
- an outbound invoice has been delivered, has failed, or has been reported to the tax authority.
A webhook is a notification. It describes the message but does not contain the business documents.
Your system fetches the documents through MAPI and then acknowledges them with a MAPI call.
sequenceDiagram
participant U as Unimaze
participant W as Your webhook endpoint
participant E as Your back-end (ERP)
U->>W: POST deliveryNotified (signed)
W-->>U: 204 No Content (within 5 seconds)
W->>E: Queue for processing
E->>U: Fetch the document (XML or JSON)
U-->>E: Document
E->>E: Persist the document
E->>U: POST /messages/:mid/mark-as-delivered
U->>W: POST messageStatusChanged (pendingdelivery → delivered)
Fetch the core business document in its own format, such as UBL XML, with GET /messages/:mid/documents/core, or its data as JSON with the GET /transactions/... call for its business transaction, for example GET /transactions/submit-invoice?messageId=:mid. See Receiving inbound messages.
Before you start. Your endpoint is added in the webhook setup, which you may not have access to yourself. Have your HTTPS endpoint URL ready, and agree how you will receive the signing secret. See Setting up webhooks.
Events
Every webhook has a transfer (inbound or outbound) and an action that tells you what happened.
A message your participant sends to itself is both outgoing and incoming. Each of its events is sent twice, once as outbound and once as inbound, and each is routed by the rules of its own category.
There are two kinds of events: message events describe the message, and status events describe a change of one of its statuses.
Ignore any action you don't recognize, and respond 2xx to it as usual. New actions may be added, and some participants have optional setups that send more events.
Message events
Sent when a message is created or needs your attention. The data object contains the full message descriptor (the same as GET /messages/:mid).
action | Inbound | Outbound | Sent when | What you must do |
|---|---|---|---|---|
created | ✔ | ✔ | The message was received by Unimaze. The core business document is always included. | Nothing required. Use it if your ERP needs to know about the message early: note the messageId, but rely on the documents only from deliveryNotified. |
deliveryNotified | ✔ | ✔ | Inbound: the message is validated and ready to be fetched (status pendingdelivery). Outbound: your message is ready to be sent: it has been validated and is in the queue for sending to the receiver (status pendingdelivery). | Inbound: fetch the document and call mark-as-delivered (or mark-as-failed). Outbound: nothing. The message passed validation and is pending delivery. Delivery is confirmed later by messageStatusChanged to delivered. |
For inbound messages, deliveryNotified is only sent when all documents are stored, and always lists all of them, including documents added along the way, such as a transformed document. Fetch and import on deliveryNotified, not on created. As in GET /messages/:mid, attachments embedded in the core document are not listed separately.
Outbound: ready to be sent
For an outbound message, deliveryNotified means that the message has been validated and is in the queue for sending. This is also the moment of the Acknowledgement (AB) message level response, if your participant is set up for acknowledgements (see Working with MLRs, eMLRs and MLAs).
Treat this event, not the response to your submit call, as the sign that the message passed validation. If validation found warnings, the messageStatusChanged event for pendingdelivery lists them in its validationResults property.
If validation fails after your submit call has returned, whether you sent XML or JSON, the message becomes failed straight away, without going through pendingfailure, and you get a messageStatusChanged event instead. Its properties carry the first error in errorCode and errorMessage, and all errors and warnings in validationResults (see Validation results). Fix the document and send it as a new message. If your participant is configured for pending review, the message waits in that status to be reviewed instead.
Status events
Sent when a status of the message changes. The data object contains the former and new value, and properties with details of the change when there are any, such as the tax authority's error code or the reason for a rejection.
action | Sent when | Example values |
|---|---|---|
messageStatusChanged | The delivery status changed to pendingdelivery, retrying, delivered, pendingfailure, failed or parked, or, for inbound messages with a processing step, to one of its statuses. For outbound messages this replaces polling GET /messages/:mid/status. See Delivery guarantees for final states. | pendingdelivery → delivered, pendingfailure → failed |
taxReportingStatusChanged | The tax reporting status changed (for example in Croatia or Slovakia). | none → electronic, none → non_electronic, none → failed |
lifecycleStatusChanged | The lifecycle status (lifecycleStatus) of the invoice changed, for example because a payment or a rejection was registered. | none → rejected, none → paid |
Status events are also sent for changes you make, so all your systems see the same state. For example, mark-as-delivered and mark-as-failed count as your own changes.
You can switch this off per routing rule, see Routing rules.
Acknowledging messages
Responding 2xx to a webhook only confirms that you received the notification. It never changes the message.
You must acknowledge each message explicitly with a MAPI call after you have safely stored it:
| Notification | Acknowledge with | On failure |
|---|---|---|
deliveryNotified (inbound) | POST /messages/:mid/mark-as-delivered | POST /messages/:mid/mark-as-failed |
This separation means you never have to hold the webhook connection open while you persist the document, and a message is never acknowledged before your system actually has it.
Unacknowledged messages
Each notification is sent once (with retries if your endpoint doesn't respond). If your endpoint accepted the webhook but your system never acknowledged the message, for example because it crashed after responding, the message stays pending.
We recommend a daily safety-net check, so nothing is left behind: GET /messages?status=pendingdelivery&transfer=inbound returns inbound messages that were never marked as delivered.
Endpoints and routing
Webhooks are set up in two parts:
- Routing decides which endpoint receives each event, using one or more rules per event category.
- Endpoints are the systems that receive webhooks. Each endpoint has a name, an HTTPS URL, its own signing secret and an optional credential. You can set up to 25 endpoints, and not all of them need to be used in routing.
The event categories are:
| Event category | Includes |
|---|---|
| Inbound messages | All inbound events |
| Outbound messages | All outbound events |
Optional setups add their own categories.
Routing rules
Each category has one or more rules. A rule sends the matching events to an endpoint, or marks them as not sent, and says whether you receive status events for changes you made yourself (see Status events).
- One rule: all events in the category go to its endpoint. No filter is needed.
- More than one rule: each rule needs a filter: a comma-separated list of
<document type>/<format>pairs, for exampleInvoice/XML,CreditNote/XML. The document type is the name of the message's core document (Invoice,CreditNote,Order, …) and the format is its syntax:XML,JSON,PDForEDI(EDIFACT, X12 and other plain-text documents). Use*for one side to match any value, for exampleOrder/*. A document type and format can only be covered by one rule. The filter field in the webhook setup suggests the document types and formats your participant has sent or received recently.
There is no fallback rule. When an event matches none of the rules in its category, or its category has no rules, it is not sent: it shows as exhausted in the delivery log straight away, with the reason, and Unimaze support can see it in the participant's log. Add or change a rule, then redeliver the event.
Status events follow the same rule as the message they belong to.
The first endpoint you create receives all categories. For example, a billing system creates the outbound invoices and follows their delivery, the ERP receives inbound invoices and credit notes, and a purchasing system receives inbound orders:
| Event category | Filter | Endpoint |
|---|---|---|
| Inbound messages | Invoice/XML,CreditNote/XML | ERP |
| Inbound messages | Order/* | Purchasing system |
| Outbound messages | Billing system |
Setting up webhooks
Webhooks are set up on the Webhooks tab of your participant in MUI, by your managing partner or Unimaze help desk.
Setting up an endpoint takes these steps:
- Endpoint: a name, for example
ERP, and the HTTPS URL of your endpoint. - Signing secret: created with the endpoint and shown only once, to the person who creates it. They pass it to your developers through a secure channel. Store it securely in your system and use it to verify every request (see Authentication).
- Credential (optional): if your gateway checks a header, provide the token or header value (see Credential).
- Routing: check that each event category goes to the right endpoint, and add rules with filters if different documents go to different systems (see Endpoints and routing).
- Test event: send a test event and check that your endpoint returns
2xx(see Testing).
When the signing secret is rotated, the old secret stays valid for 24 hours, so you can switch without missing events. During that time requests carry two signatures, see Verifying webhook signatures.
Authentication
Signature (always sent)
Every webhook is signed with the endpoint's signing secret, so you can check that it came from Unimaze and was not changed. This example is the signature of the test vector:
UMZ-Webhook-Signature: t=1790762400,v0=1b69720d9b66a6118096ed70b96311e9e2829c1608ae52dfd633b5b5b68864ff
Verify the signature on every request. See Verifying webhook signatures for code in C#, Java, Python and TypeScript, and a test vector.
Credential (optional)
If your API gateway or framework can only check a header, you can add a credential that Unimaze sends with every request, in addition to the signature:
| Type | Header sent |
|---|---|
| Bearer token | Authorization: Bearer <token> |
| Basic authentication | Authorization: Basic <base64(username:password)> |
| Custom header | <Header-Name>: <value>, for example X-Api-Key: <value> |
A credential alone does not protect against modified or replayed requests. Use it together with the signature where possible.
Production webhook URLs must use HTTPS. Plain HTTP is only accepted in the sandbox.
The request
Unimaze sends POST with Content-Type: application/json; charset=utf-8 and these headers:
| Header | Example | Description |
|---|---|---|
UMZ-Webhook-Signature | t=1790762400,v0=1b69… | Signature, see above |
UMZ-Webhook-Event-Id | 3f2b6c1e-8d4a-4e9b-9a51-0c7d2e6f4a10 | Same as eventId in the body |
UMZ-Webhook-Action | deliveryNotified | Same as action in the body |
UMZ-Webhook-Transfer | inbound | Same as transfer in the body |
User-Agent | Unimaze-Webhooks/2 |
The UMZ-Webhook-Event-Id, UMZ-Webhook-Action and UMZ-Webhook-Transfer headers let a gateway route or filter requests without parsing the body. They are not covered by the signature, so act on the values in the verified body.
Common fields
| Field | Description |
|---|---|
eventId | Unique identifier of the event. The same value is used when an event is retried or redelivered. Use it to detect duplicates. |
action | What happened, see Events. |
transfer | inbound or outbound. A message your participant sends to itself is notified in both directions, see Events. |
occurredAt | When the event happened (UTC, with milliseconds), not when it was sent. It stays the same when an event is retried or redelivered. Use it to order events. |
test | true for test events, see Testing. |
party | The identifier of the participant the webhook belongs to, for example 0196:6511230889. |
messageId | The message identifier (:mid) to use in MAPI calls. For status events, it is the business message whose status changed. |
data | The message descriptor (message events) or the status change (status events). |
Example: message event
{
"eventId": "a41c09d2-5b7e-4f3a-8c16-2e9d7b0f5c33",
"action": "deliveryNotified",
"transfer": "inbound",
"occurredAt": "2026-09-30T08:12:43.020Z",
"test": false,
"party": "0196:6511230889",
"messageId": "d27318f1-a43c-47b0-a604-3cccb27c4bae",
"data": {
"uniqueId": "d27318f1-a43c-47b0-a604-3cccb27c4bae",
"timeReceived": "2026-09-30T08:12:41Z",
"documentNo": "INV-10023",
"status": "pendingdelivery",
"transfer": "inbound",
"validationStatus": "approved",
"taxReportingStatus": "none",
"lifecycleStatus": "none",
"importStatus": "NotSet",
"action": "SubmitInvoice",
"originParty": { "identifier": "0196:5504030000", "name": "Supplier hf.", "countryCode": "IS" },
"destinationParty": { "identifier": "0196:6511230889", "name": "Customer ehf.", "countryCode": "IS" },
"documents": [
{
"index": 0, "name": "Invoice", "contentType": "application/xml", "attachmentType": "Core", "isTransformed": false,
"transaction": {
"transactionKeyName": "BISENUBL-3.0 SubmitInvoice",
"profileIdentifier": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0",
"customizationIdentifier": "urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0",
"documentIdentifier": "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"
}
}
]
}
}The example's data is shortened. It has the same fields and format as GET /messages/:mid, including its dates, which are in whole seconds.
If you import the JSON from the GET /transactions/... calls, data.action is all you need. If you parse a document in its own format, such as the UBL XML, use the document's transaction: each Core and Original document has one, with the profile, customization and document type identifiers of the standard it follows.
The data.action field is the business transaction (for example SubmitInvoice or CorrectWithCredit). Use it to choose the right GET /transactions/... call.
Example: status event
{
"eventId": "3f2b6c1e-8d4a-4e9b-9a51-0c7d2e6f4a10",
"action": "messageStatusChanged",
"transfer": "inbound",
"occurredAt": "2026-09-30T10:00:00.000Z",
"test": false,
"party": "0196:6511230889",
"messageId": "6cfa5c24-25c5-4eb4-bed5-3ed6cc6c25c5",
"data": {
"formerValue": "pendingdelivery",
"newValue": "delivered"
}
}| Field | Description |
|---|---|
formerValue | The status before the change. |
newValue | The status after the change. |
properties | Details of the change, as name/value pairs (all values are strings). Only present when the change carries any details, see Status event properties. |
messageId is always the business message whose status changed, such as the invoice or credit note, also when the change was caused by an action or a response message.
Example: status event with properties
A tax report of an outbound invoice was rejected by the Croatian Tax Administration:
{
"eventId": "c9e3a7d1-6f02-4b8e-9d45-1a7c3e5f8b20",
"action": "taxReportingStatusChanged",
"transfer": "outbound",
"occurredAt": "2026-09-30T08:13:05.410Z",
"test": false,
"party": "9934:11223344556",
"messageId": "d27318f1-a43c-47b0-a604-3cccb27c4bae",
"data": {
"formerValue": "none",
"newValue": "failed",
"properties": {
"actionMessageId": "f4e4d960-7711-4abd-b8b7-8509f4b640dc",
"actionName": "RegisterFiscalEvidence",
"jurisdiction": "Croatia",
"role": "Supplier",
"errorCode": "S0008",
"errorMessage": "Već postoji zabilježen eRačun s istim identifikatorom."
}
}
}Status event properties
properties carries what is known about the change. Which properties are present depends on the context, so don't assume any property is present. A property that doesn't apply is left out, and properties itself is left out when there is nothing to carry. New names may be added: ignore names you don't recognize.
Start with errorCode and errorMessage: when something failed, they say what went wrong, whatever the cause. The other properties belong to a specific context:
| Context | Event | Properties |
|---|---|---|
| Something failed: validation, delivery or a tax report | messageStatusChanged, taxReportingStatusChanged | errorCode and errorMessage |
Delivery failed and will be tried again: the message is retrying | messageStatusChanged | errorMessage with the first failure, for example that no endpoint was found for the receiver |
| Validation found errors or warnings in the message's document | messageStatusChanged to failed or pendingdelivery | Also validationResults, left out when there are none. An outbound document with errors normally ends as failed, with validationStatus rejected. A message that reaches pendingdelivery carries its warnings, or its errors if the network lets documents with errors through, inbound and outbound alike. |
| Tax reporting | taxReportingStatusChanged | actionMessageId, actionName, jurisdiction and role |
| Lifecycle change, for example a payment or a rejection was registered | lifecycleStatusChanged | For the participant that reported the change: actionMessageId, actionName, jurisdiction and role. Also rejectionType and reason for a rejection, and paidAmount and paymentDate for a payment. |
| The message was parked | messageStatusChanged | reason |
When the change was caused by an action, such as reporting to a tax authority, actionMessageId and actionName are listed first. actionMessageId is the message identifier of the action message (MLA), which you can fetch with MAPI like any other message. It is only present for the participant that took the action. Whether an action is needed at all differs by country. For example, in Croatia the supplier reports a payment and the customer reports a rejection, so the other party's lifecycleStatusChanged event has no actionMessageId. See Working with MLRs, eMLRs and MLAs.
| Name | Description | Example |
|---|---|---|
errorCode | Error code from the tax authority or the network, or the rule identifier of the first validation error. | S0008, BR-CL-11 |
errorMessage | The error text that goes with errorCode, or the reason for a failure without a code. | |
validationResults | The validation errors and warnings of the message's document, as a JSON array in a string. See Validation results. | |
actionMessageId | Message identifier (:mid) of the action message (MLA) that caused the change. | f4e4d960-7711-4abd-b8b7-8509f4b640dc |
actionName | The action. | RegisterFiscalEvidence, RegisterTaxableDelivery, RegisterPayment, RegisterRejection |
jurisdiction | The country whose rules apply. | Croatia, Slovakia |
role | The side that took the action. | Supplier, Customer |
reason | A reason given by a person, such as the reason for a rejection, or a note when a message is parked. | Did not receive goods |
rejectionType | The type of rejection. | MismatchNotAffectingTaxes, MismatchAffectingTaxes, Other |
paidAmount | The registered payment amount. | 200.23 |
paymentDate | The registered payment date. | 2026-09-30 |
The response messages (MLRs and eMLRs) are not referenced from status events. Each one is an inbound message of its own, announced with message events.
Validation results
validationResults lists the findings of the validation of the message's document, errors first. It is only present when there are findings, and only in webhook events: GET /messages/:mid doesn't return it. Like every property it is a string: parse it as a JSON array. Each entry has these fields:
| Field | Description | Example |
|---|---|---|
type | error or warning. | error |
code | The identifier of the rule, or schema for a schema error. | BR-CL-11 |
message | What is wrong. | Any registration identifier identification scheme identifier MUST be coded using one of the ISO 6523 ICD list. |
location | Where in the document: an XPath, or the line and position for a schema error. | /Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc:CompanyID |
An outbound invoice failed validation after the submit call:
{
"eventId": "5d0b8e2a-41c7-4f9e-b3a6-7e2c9f1d4a58",
"action": "messageStatusChanged",
"transfer": "outbound",
"occurredAt": "2026-10-12T09:21:14.250Z",
"test": false,
"party": "0196:6511230889",
"messageId": "8b1f6c3e-2d4a-4f7b-9e05-6a3c1d8f2b94",
"data": {
"formerValue": "received",
"newValue": "failed",
"properties": {
"errorCode": "BR-CL-11",
"errorMessage": "Any registration identifier identification scheme identifier MUST be coded using one of the ISO 6523 ICD list.",
"validationResults": "[{\"type\":\"error\",\"code\":\"BR-CL-11\",\"message\":\"Any registration identifier identification scheme identifier MUST be coded using one of the ISO 6523 ICD list.\",\"location\":\"/Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc:CompanyID\"},{\"type\":\"warning\",\"code\":\"PEPPOL-EN16931-R005\",\"message\":\"VAT accounting currency code should be different from invoice currency code when provided.\",\"location\":\"/Invoice/cbc:TaxCurrencyCode\"}]"
}
}
}Responding
- Respond with
200,201,202or204within 5 seconds. The response body is ignored. - Acknowledge first, process later. Verify the signature, store or queue the event, return
204 No Content, and then fetch and import in the background. - Any other status code, a timeout or a connection error counts as a failed attempt and is retried.
Retries
Failed attempts are retried up to 9 times over about two hours:
| Retry | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|
| Delay | 1 min | 1 min | 1 min | 1 min | 1 min | 10 min | 10 min | 30 min | 60 min |
After the last retry the event is marked exhausted and shown in the delivery log of the webhook setup, where you can redeliver it (with the same eventId).
An exhausted event is also recorded in the participant's log, so Unimaze support can see that your endpoint has a problem.
Exhausted events do not affect the message: it stays pending until you acknowledge it (see Unacknowledged messages).
The delivery log keeps events for 6 months.
Delivery guarantees
- At-least-once. The same event can arrive more than once. Use
eventIdto ignore duplicates, and make imports idempotent onmessageId. - Retrying. When a delivery attempt fails, the message becomes
retryingand is tried again a few times over a short period. You get onemessageStatusChangedevent when it starts retrying, with the first failure inerrorMessage; later attempts send no event. It ends asdelivered, or asfailedwith the final error. - Final states.
deliveredandfailedare final: no further delivery status changes follow on their own.parkedis also final, but only set manually in MUI. In rare cases a failed delivery needs manual handling, which delays the final state: the message ispendingfailureuntil it becomesfailed, ordeliveredif a retry succeeds. Treatpendingfailureas not final. A message in a final state changes again only when someone retries it manually, which starts a new delivery and sends new status events. - No ordering guarantee. Events for the same message can arrive out of order. Status events are self-contained, so you can apply
newValuedirectly without calling MAPI, as long as you don't overwrite a newer state: store theoccurredAtof the last change you applied per message and status (message, tax reporting, lifecycle), and ignore events that are older. - When in doubt, ask MAPI. If your stored value doesn't match
formerValue, or you may have missed events (for example after an outage longer than the retry period), fetch the current state withGET /messages/:mid.
Testing
Send test event
In the webhook setup, choose Send test event on an endpoint:
- Choose an event category and an action. The category defaults to the first category routed to this endpoint, and the actions are the ones that belong to that category.
- Check the payload preview. It is the exact request body that will be sent, and it updates when you change the category or action.
- Click Send. The result shows your endpoint's status code, response time and the start of its response body.
The test event is sent straight to the endpoint you chose: routing rules are not applied. It is signed with that endpoint's signing secret and carries its credential, just like a real webhook.
If the test fails, the result says what to check:
| Result | What to check |
|---|---|
401 or 403 | Your endpoint rejected the request. Check that it verifies UMZ-Webhook-Signature with this endpoint's signing secret, and that it accepts the credential you set. |
| Timeout | Respond within 5 seconds and process the event in the background (see Responding). |
404 | Check the URL. |
| TLS error | Check your endpoint's certificate. |
Test data
- Test events contain generated data (random ids, a sample
Invoice/XMLdocument and your participant's own identifier), never real messages. Ignore events with"test": truein your import logic. - Use the test vector in Verifying webhook signatures to unit-test your signature check.
- Real events, including exhausted ones, are listed in the delivery log, where you can see the request, the response and the last error for each attempt.
Checklist
-
HTTPS endpoint reachable from the internet
-
Signature verified on every request, with a timestamp check
-
Response within 5 seconds, processing done asynchronously
-
Duplicates ignored using
eventId -
Every inbound
deliveryNotifiedacknowledged withmark-as-deliveredafter the document is stored -
Status events handled, or status events for your own changes switched off per routing rule
-
Daily safety-net check for pending messages
-
Delivery log checked during go-live
-
Unknown
actionvalues ignored
Optional setups
Most integrations only need this page. Some participants have an optional setup that adds events, event categories and acknowledgement calls:
- a processing step, where a system such as an approval system works on inbound messages before the ERP gets them,
- an import queue, where messages created by one of your systems are imported by another,
- a system that adds documents later, such as booking data from an approval system.
If your participant has one of these, see Processing step and import queue.
Updated 8 minutes ago
