Payload and delivery

What a delivery looks like, which headers it carries, and how Complyance retries when your endpoint does not answer. Read this before you write the receiver.

Each delivery is a POST with a JSON body and a small set of headers.

Headers

HeaderValue
Content-Typeapplication/json
X-Webhook-EventThe event type, for example purchase.invoice.stored
X-Webhook-IDThe event’s ID. The same on every retry of the same event.
X-Webhook-TimestampWhen this delivery attempt was sent
X-Webhook-SignatureHMAC of the body, when signing is on. See Verify the signature.

Body

A purchase.invoice.stored deliveryjson
{
  "eventId": "purchase:01JABCDEFGHIJKLMNOPQRSTUV",
  "data": {
    "id": "purchase:01JABCDEFGHIJKLMNOPQRSTUV",
    "type": "purchase.invoice.stored",
    "timestamp": "2026-09-18T07:04:01.550Z",
    "data": {
      "documentId": "01JABCDEFGHIJKLMNOPQRSTUV",
      "documentNumber": "INV-2026-0142",
      "documentType": "tax_invoice",
      "country": "AE",
      "totalAmount": 12600.0,
      "currency": "AED",
      "sellerName": "Acme Trading LLC",
      "issueDate": "2026-09-17",
      "submittedAt": "2026-09-18T07:04:01.550Z"
    },
    "metadata": {
      "requestId": "01JABCDEFGHIJKLMNOPQRSTUV"
    }
  }
}
FieldMeaning
eventIdThe event’s ID. Also in data.id and the X-Webhook-ID header. Use it to spot duplicates.
data.typeThe event type
data.timestampWhen the event happened. Unlike X-Webhook-Timestamp, it does not change on retries.
data.dataThe event’s own fields. Each event type documents its own; see Purchase invoice events.
data.metadata.requestIdAn ID you can quote to support

Treat eventId as an opaque string. Do not read meaning into its format.

Verify the signature

When signing is on, every delivery carries an X-Webhook-Signature header: the HMAC of the request body, using your secret and the algorithm you chose, written as lowercase hex. Check it before you do anything with the payload.

Compute the HMAC over the raw request body exactly as received. Parsing and serialising the JSON again can change the bytes and make the check fail.

Verify a deliveryjavascript
import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody, secret, signature = '') {
  if (!/^[0-9a-f]{64}$/.test(signature)) return false;
  const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
  return timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'));
}

These examples use SHA-256. For SHA-512, change the algorithm and, in the Node.js example, the expected signature length from 64 to 128 hex characters. Pass an empty string when the signature header is missing. Reject the request with a 401 when the check fails, and watch for those rejections: a run of them usually means a misconfigured secret or unwanted traffic.

Retries

Your endpoint has to answer with a 2xx status. Anything else, or no answer in time, counts as a failure:

  • A 4xx response is treated as a permanent problem with the request, so it is not retried. Fix the endpoint before sending more events. Use the Purchases API to retrieve purchase invoices you missed.
  • Any other failure is retried: after 1 minute, then 5 minutes, then 15 minutes, then 15 minutes. That is up to five attempts, with 36 minutes of retry delays in total. Request time and delivery delays can make the elapsed time longer. If all attempts fail, delivery stops.

Every retry carries the same eventId. Use it to make your processing idempotent: receiving the same event again must not create another invoice or repeat an action.

Write the receiver

  1. Read the raw body.
  2. Verify the signature.
  3. Save the event durably with a unique constraint on eventId. If it is already saved, respond 200 and stop.
  4. Arrange for your worker to process saved events. Make its invoice updates safe to repeat too.
  5. Respond 200.

Do the slow work after you have responded, not before. A receiver that takes too long looks like a failure and gets retried.

Next: Purchase invoice events.

Last updated