Webhooks
A webhook is a message we send to an address you give us when something changes, so you don't have to keep asking. We send them for order status changes, shipment updates and new invoices.
https:// address on your side and the events you want. We turn it on, send you a signing secret, and can send a test message to check it works. Polling keeps working either way (see Staying in Sync).Events
| Event | Sent when |
|---|---|
order.status_changed | One of your orders moves to a new status (see Order status values). |
shipment.updated | A courier or tracking number is assigned to one of your orders, or its shipment status changes (BOOKED, IN_TRANSIT, OUT_FOR_DELIVERY, DELIVERED, FAILED, RETURNED). |
invoice.created | A GST invoice is generated for one of your orders. Fetch it from Invoices. |
ping | A test message sent by us when you ask for one. |
Only your own orders ever produce events for you. Events carry identifiers and the new state, not full records: fetch the details through the API when you need them.
The request we send
A POST with a JSON body, to your address:
{
"id": "cmu7x2k9q0004b2l8f1d3e5ab",
"type": "order.status_changed",
"apiVersion": "1",
"createdAt": "2026-10-13T09:30:12.000Z",
"data": {
"orderId": "cmu5brc6n0002bcskplvjkh5n",
"orderNumber": "GB-3801",
"partnerReference": "order-48213",
"previousStatus": "ACCEPTED",
"status": "PAID",
"occurredAt": "2026-10-13T09:30:11.000Z"
}
}{
"shipmentId": "cmu7y0n3a0007b2l8h2c4d6ef",
"orderId": "cmu5brc6n0002bcskplvjkh5n",
"orderNumber": "GB-3801",
"partnerReference": "order-48213",
"courier": "DTDC",
"trackingNumber": "D1234567890",
"status": "IN_TRANSIT"
}{
"invoiceId": "cmu7z4p8c0009b2l8j3e5f7gh",
"invoiceNumber": "INV3460",
"invoiceDate": "2026-10-13T09:31:00.000Z",
"totalAmount": 2816,
"orderId": "cmu5brc6n0002bcskplvjkh5n",
"orderNumber": "GB-3801",
"partnerReference": "order-48213"
}Headers
| Header | Meaning |
|---|---|
X-GlamBug-Event | The event type, for example order.status_changed. |
X-GlamBug-Delivery | The event id (the same as id in the body). Use it to ignore repeats. |
X-GlamBug-Timestamp | When we sent it, in Unix seconds. |
X-GlamBug-Signature | sha256= followed by the signature, described next. |
X-GlamBug-Attempt | 1 for the first try, higher for retries. |
Every header is also sent under its original name, X-Ornato-Event, X-Ornato-Delivery,X-Ornato-Timestamp, X-Ornato-Signature and X-Ornato-Attempt, with exactly the same values. If your integration already reads the X-Ornato-* headers it keeps working. Use theX-GlamBug-* names in new integrations.
Check the signature
Anyone could post to your address, so confirm every message came from us. The signature is an HMAC-SHA256 of the text timestamp.body (the timestamp header, a dot, then the exact raw body), using the signing secret we gave you, written in lower-case hex.
- Read the raw request body, before any JSON parsing.
- Reject the message if the timestamp is more than 5 minutes old or in the future. That stops someone replaying an old message.
- Compute the signature and compare it with the header using a constant-time comparison.
HTTP header names are not case-sensitive. Node and many frameworks give you them in lower case, which is what the Node example reads, while the Python and PHP examples use the capitalisation we send. If your framework hands you headers as a plain dictionary, look them up the way it stores them (or convert the names to one case first), otherwise the check can fail even though the message is genuine.
import crypto from 'node:crypto';
export function verify(rawBody, headers, secret) {
const ts = Number(headers['x-glambug-timestamp']);
if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false;
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(ts + '.' + rawBody).digest('hex');
const given = String(headers['x-glambug-signature'] || '');
return given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}import hmac, hashlib, time
def verify(raw_body: bytes, headers: dict, secret: str) -> bool:
ts = int(headers.get("X-GlamBug-Timestamp", "0"))
if abs(time.time() - ts) > 300:
return False
signed = f"{ts}.".encode() + raw_body
expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(headers.get("X-GlamBug-Signature", ""), expected)function verify(string $rawBody, array $headers, string $secret): bool {
$ts = (int)($headers['X-GlamBug-Timestamp'] ?? 0);
if (abs(time() - $ts) > 300) return false;
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $rawBody, $secret);
return hash_equals($expected, $headers['X-GlamBug-Signature'] ?? '');
}Reply, retries and repeats
- Answer with any
2xxstatus within 5 seconds. Do the real work afterwards, in the background. - Anything else, or no answer in time, counts as a failure and we retry. After the first try we retry after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours: seven attempts in all.
- An event we couldn't deliver is not lost: your GlamBug contact can send it again.
- If an address keeps failing for more than a week, we switch webhooks off for it (it shows on our side). Fix the address and ask your contact to turn it back on.
- Delivery is at least once. The same event can arrive twice, so keep the ids you have handled and ignore repeats.
- Events can arrive out of order, especially after a retry. Use
previousStatusandoccurredAt, or re-read the order, rather than trusting arrival order.
Requirements for your address
- It must start with
https://and be reachable from the public internet. Addresses that resolve to private or internal networks are refused. - We don't follow redirects: give us the final address.
- No username or password in the address, and port 443 or 1024 and above.
- Keep the signing secret private, and tell us if it may have leaked so we can replace it.
Versioning
Every message carries apiVersion, currently "1". We may add new fields and new event types without changing it, so ignore fields and events you don't recognise. A change that could break your code would use a new version, announced in the Changelog.