Webhook notifications
An organization owner or admin can have change receipts pushed to an HTTPS receiver. Webhooks are set up in the Gateway console on the Webhooks page, for one website and one installation credential.
Requirements
Section titled “Requirements”- The credential has
website_readandevent_readand no restrictions. - INSIDE has allowed the receiver’s origin.
- You’ve stored the signing secret, which is shown once when the webhook is created.
Payload
Section titled “Payload”{ "version": 1, "delivery_id": "whd_...", "webhook_id": "whk_...", "website_id": "web_4927_123", "event": { "cursor": 1, "type": "conversation.changed", "occurred_at": 1760313600000 }}Verify every delivery
Section titled “Verify every delivery”-
Compute HMAC-SHA256 over the exact bytes of
X-Gateway-Timestamp + "." + raw_body, using the base64-decoded signing secret. -
Compare it in constant time with the hex value in
X-Gateway-Signature: v1=<hex>. Verify before parsing the body. -
Reject stale timestamps.
X-Gateway-Timestampis in Unix seconds;occurred_atis in milliseconds. -
Check that
X-Gateway-Delivery-IDandX-Gateway-Webhook-IDmatch the body.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(headers, rawBody, base64Secret, maxAgeSeconds = 300) { const timestamp = headers['x-gateway-timestamp']; const signature = (headers['x-gateway-signature'] ?? '').replace(/^v1=/, ''); if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > maxAgeSeconds) return false; const expected = createHmac('sha256', Buffer.from(base64Secret, 'base64')) .update(Buffer.concat([Buffer.from(`${timestamp}.`), rawBody])) .digest(); const received = Buffer.from(signature, 'hex'); return received.length === expected.length && timingSafeEqual(received, expected);}import base64, hashlib, hmac, time
def verify(headers, raw_body: bytes, base64_secret: str, max_age_seconds=300) -> bool: timestamp = headers.get("X-Gateway-Timestamp", "") signature = headers.get("X-Gateway-Signature", "").removeprefix("v1=") if not timestamp or abs(time.time() - int(timestamp)) > max_age_seconds: return False expected = hmac.new(base64.b64decode(base64_secret), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature)Delivery
Section titled “Delivery”- Delivery is at least once. Return
2xxonly after you’ve stored the notification, and deduplicate ondelivery_id. - Each delivery gets up to five attempts, with a 10-second timeout and exponential backoff.
- Notifications are kept for seven days and can be replayed from the console.
After you receive a notification, read the changed resources through the REST API with your installation credential.