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

actionInboundOutboundSent whenWhat 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.

actionSent whenExample values
messageStatusChangedThe 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
taxReportingStatusChangedThe tax reporting status changed (for example in Croatia or Slovakia).none → electronic, none → non_electronic, none → failed
lifecycleStatusChangedThe 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:

NotificationAcknowledge withOn failure
deliveryNotified (inbound)POST /messages/:mid/mark-as-deliveredPOST /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 categoryIncludes
Inbound messagesAll inbound events
Outbound messagesAll 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 example Invoice/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, PDF or EDI (EDIFACT, X12 and other plain-text documents). Use * for one side to match any value, for example Order/*. 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 categoryFilterEndpoint
Inbound messagesInvoice/XML,CreditNote/XMLERP
Inbound messagesOrder/*Purchasing system
Outbound messagesBilling 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:

  1. Endpoint: a name, for example ERP, and the HTTPS URL of your endpoint.
  2. 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).
  3. Credential (optional): if your gateway checks a header, provide the token or header value (see Credential).
  4. 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).
  5. 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:

TypeHeader sent
Bearer tokenAuthorization: Bearer <token>
Basic authenticationAuthorization: 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:

HeaderExampleDescription
UMZ-Webhook-Signaturet=1790762400,v0=1b69…Signature, see above
UMZ-Webhook-Event-Id3f2b6c1e-8d4a-4e9b-9a51-0c7d2e6f4a10Same as eventId in the body
UMZ-Webhook-ActiondeliveryNotifiedSame as action in the body
UMZ-Webhook-TransferinboundSame as transfer in the body
User-AgentUnimaze-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

FieldDescription
eventIdUnique identifier of the event. The same value is used when an event is retried or redelivered. Use it to detect duplicates.
actionWhat happened, see Events.
transferinbound or outbound. A message your participant sends to itself is notified in both directions, see Events.
occurredAtWhen 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.
testtrue for test events, see Testing.
partyThe identifier of the participant the webhook belongs to, for example 0196:6511230889.
messageIdThe message identifier (:mid) to use in MAPI calls. For status events, it is the business message whose status changed.
dataThe 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"
  }
}
FieldDescription
formerValueThe status before the change.
newValueThe status after the change.
propertiesDetails 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:

ContextEventProperties
Something failed: validation, delivery or a tax reportmessageStatusChanged, taxReportingStatusChangederrorCode and errorMessage
Delivery failed and will be tried again: the message is retryingmessageStatusChangederrorMessage with the first failure, for example that no endpoint was found for the receiver
Validation found errors or warnings in the message's documentmessageStatusChanged to failed or pendingdeliveryAlso 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 reportingtaxReportingStatusChangedactionMessageId, actionName, jurisdiction and role
Lifecycle change, for example a payment or a rejection was registeredlifecycleStatusChangedFor 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 parkedmessageStatusChangedreason

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.

NameDescriptionExample
errorCodeError code from the tax authority or the network, or the rule identifier of the first validation error.S0008, BR-CL-11
errorMessageThe error text that goes with errorCode, or the reason for a failure without a code.
validationResultsThe validation errors and warnings of the message's document, as a JSON array in a string. See Validation results.
actionMessageIdMessage identifier (:mid) of the action message (MLA) that caused the change.f4e4d960-7711-4abd-b8b7-8509f4b640dc
actionNameThe action.RegisterFiscalEvidence, RegisterTaxableDelivery, RegisterPayment, RegisterRejection
jurisdictionThe country whose rules apply.Croatia, Slovakia
roleThe side that took the action.Supplier, Customer
reasonA reason given by a person, such as the reason for a rejection, or a note when a message is parked.Did not receive goods
rejectionTypeThe type of rejection.MismatchNotAffectingTaxes, MismatchAffectingTaxes, Other
paidAmountThe registered payment amount.200.23
paymentDateThe 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:

FieldDescriptionExample
typeerror or warning.error
codeThe identifier of the rule, or schema for a schema error.BR-CL-11
messageWhat is wrong.Any registration identifier identification scheme identifier MUST be coded using one of the ISO 6523 ICD list.
locationWhere 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, 202 or 204 within 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:

Retry123456789
Delay1 min1 min1 min1 min1 min10 min10 min30 min60 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 eventId to ignore duplicates, and make imports idempotent on messageId.
  • Retrying. When a delivery attempt fails, the message becomes retrying and is tried again a few times over a short period. You get one messageStatusChanged event when it starts retrying, with the first failure in errorMessage; later attempts send no event. It ends as delivered, or as failed with the final error.
  • Final states. delivered and failed are final: no further delivery status changes follow on their own. parked is also final, but only set manually in MUI. In rare cases a failed delivery needs manual handling, which delays the final state: the message is pendingfailure until it becomes failed, or delivered if a retry succeeds. Treat pendingfailure as 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 newValue directly without calling MAPI, as long as you don't overwrite a newer state: store the occurredAt of 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 with GET /messages/:mid.

Testing

Send test event

In the webhook setup, choose Send test event on an endpoint:

  1. 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.
  2. Check the payload preview. It is the exact request body that will be sent, and it updates when you change the category or action.
  3. 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:

ResultWhat to check
401 or 403Your 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.
TimeoutRespond within 5 seconds and process the event in the background (see Responding).
404Check the URL.
TLS errorCheck your endpoint's certificate.

Test data

  • Test events contain generated data (random ids, a sample Invoice/XML document and your participant's own identifier), never real messages. Ignore events with "test": true in 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 deliveryNotified acknowledged with mark-as-delivered after 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 action values 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.


Did this page help you?