# 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

| Header | Value |
| - | - |
| `Content-Type` | `application/json` |
| `X-Webhook-Event` | The event type, for example `purchase.invoice.stored` |
| `X-Webhook-ID` | The event’s ID. The same on every retry of the same event. |
| `X-Webhook-Timestamp` | When this delivery attempt was sent |
| `X-Webhook-Signature` | HMAC of the body, when signing is on. See [Verify the signature](https://docs.complyance.io/one-platform/developer-portal/webhooks/payload-and-delivery/#verify-the-signature). |

## Body

```json title="A purchase.invoice.stored delivery"
{
  "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"
    }
  }
}
```

| Field | Meaning |
| - | - |
| `eventId` | The event’s ID. Also in `data.id` and the `X-Webhook-ID` header. Use it to spot duplicates. |
| `data.type` | The event type |
| `data.timestamp` | When the event happened. Unlike `X-Webhook-Timestamp`, it does not change on retries. |
| `data.data` | The event’s own fields. Each event type documents its own; see [Purchase invoice events](https://docs.complyance.io/one-platform/developer-portal/webhooks/purchase-invoices/). |
| `data.metadata.requestId` | An 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.

**Node.js**

```javascript title="Verify a delivery"
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'));
}
```

**Python**

```python title="Verify a delivery"
import hmac, hashlib

def verify(raw_body: bytes, secret: str, signature: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected.encode('ascii'), signature.encode('utf-8'))
```

**Java**

```java title="Verify a delivery"
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.HexFormat;

static boolean verify(byte[] rawBody, String secret, String signature) throws Exception {
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"));
    String expected = HexFormat.of().formatHex(mac.doFinal(rawBody));
    return MessageDigest.isEqual(expected.getBytes(), signature.getBytes());
}
```

**Go**

```go title="Verify a delivery"
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
)

func verify(rawBody []byte, secret, signature string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(rawBody)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}
```

**PHP**

```php title="Verify a delivery"
function verify(string $rawBody, string $secret, string $signature): bool {
    $expected = hash_hmac('sha256', $rawBody, $secret);
    return hash_equals($expected, $signature);
}
```

**.NET**

```c# title="Verify a delivery"
using System.Security.Cryptography;
using System.Text;

static bool Verify(byte[] rawBody, string secret, string signature)
{
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var expected = Convert.ToHexString(hmac.ComputeHash(rawBody)).ToLowerInvariant();
    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(signature));
}
```

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 within 30 seconds. Anything else, or no answer in that 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](https://docs.complyance.io/api-reference/purchases/) 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 longer than 30 seconds to answer counts as a failure and gets retried.

Next: [Purchase invoice events](https://docs.complyance.io/one-platform/developer-portal/webhooks/purchase-invoices/).

Source: https://docs.complyance.io/one-platform/developer-portal/webhooks/payload-and-delivery/
