Skip to main content

Securing your endpoint

Endpoints must be reachable over HTTPS. You can additionally authenticate deliveries with custom headers, request signatures, or both.

Custom headers

Set additionalHeaders on the webhook to send one or more fixed headers, such as a shared API token, and validate them on receipt. Headers are sent verbatim on every delivery.

"additionalHeaders": [{ "key": "X-Partner-Token", "value": "mytoken" }]

Custom headers prove the caller knows a shared secret. They do not prove the body is unmodified — use signatures for that.

Request signatures

Set signatureSecret on the webhook and every delivery includes an X-XY-Signature header:

X-XY-Signature: t=1780373861,s=76807D53FD29B7D00510CE1CCF10D963BB46D560E184AA0A2093192A8E0E92B1

The scheme, precisely:

  • t is the send time as Unix epoch seconds. It changes on every retry attempt.
  • The signed content is the UTF-8 bytes of {t}. followed by the raw, decompressed request body.
  • s is the HMAC-SHA-256 of that content, keyed with the UTF-8 bytes of your signature secret, encoded as uppercase hex.
  • The secret must be 64–256 characters and cannot be retrieved after it is set, so store it when you create the webhook.
Sign the bytes you received

Sign the raw body exactly as it arrived, not a re-serialized copy of the parsed JSON. Round-tripping through a JSON parser changes whitespace, key order, and number formatting, and the signature will not match. If you enabled compression, decompress first and sign the decompressed bytes.

Validating a delivery

  1. Extract t and s from X-XY-Signature (format t=<epoch seconds>,s=<hex>).
  2. Compute HMAC_SHA256(secret, "<t>." + rawBody) and hex-encode it.
  3. Compare against s using a constant-time comparison, case-insensitively.
  4. Reject stale timestamps. Five minutes is a reasonable tolerance for replay protection.

Worked example

import hashlib, hmac

secret = b"my-signature-secret-of-at-least-64-characters-abcdefghijklmnopqrs"

# the raw bytes as received, not a re-serialized dict
raw_body = (
b'{"Id":"3f1b8f0e-2c44-4a1d-9f52-6b3f0a2d1c77","Type":"Occupancy",'
b'"CustomerId":"2b7f9e10-4c3a-4b8e-9a11-7d2e5f6c8b90",'
b'"Created":"2026-06-02T04:17:40Z","Sent":"2026-06-02T04:17:41Z",'
b'"Data":[{"FloorSpaceId":"ac52e43d-7c21-4464-ab21-d0ace07a94b1",'
b'"OccupancyStatus":"CurrentlyOccupied","Headcount":2}]}'
)
t = "1780373861"

expected = hmac.new(secret, t.encode() + b"." + raw_body, hashlib.sha256).hexdigest().upper()
# 76807D53FD29B7D00510CE1CCF10D963BB46D560E184AA0A2093192A8E0E92B1

assert hmac.compare_digest(expected, received_signature.upper())

Example request

POST https://<your endpoint>
Content-Type: application/json; charset=utf-8
X-Partner-Token: mytoken
X-XY-Signature: t=1780373861,s=76807D53FD29B7D00510CE1CCF10D963BB46D560E184AA0A2093192A8E0E92B1

{"Id":"3f1b8f0e-...","Type":"Occupancy","Data":[ ... ]}

Rotating a secret

Call updateWebhook with a new signatureSecret, or clearSignatureSecret: true to stop signing altogether. There is no overlap window: the next delivery is signed with the new secret, so accept either the old or the new value on your receiver during the changeover.