Appearance
Webhooks
A webhook makes Site Connect call your server whenever a chosen event happens in your workspace. Each call is an HTTP POST with a JSON body and a signature (use an https:// address), so your server can check it really came from Site Connect.
In Site Connect, the event you can subscribe to is tenant.status_changed — your workspace's account status changed, for example from active to past_due or suspended. Use it to alert your office or pause an integration when the account needs attention.
Where to find it
Menu: System › Webhooks · Address: hub.siteconnect.ai/webhooksWho can use it: Admins by default. Other roles need the Webhooks permission: view, create, edit (pause, rotate, replay) or delete — see Roles and permissions. Like API keys, webhooks only work once API access is turned on for your company — if the page stays empty or adding a webhook shows an error, contact support.
Some labels aren't translated yet
A few buttons on this page aren't in English yet, so this guide describes them by where they are.
Add a webhook
- Go to System › Webhooks (the page is titled Webhooks outbound) and click the blue button with the + at the top right. A dialog opens.
- In the URL webhook field, enter your server's address, for example
https://example.com/site-connect-webhook. It must be reachable from the internet; private network addresses are refused. - Under Events, click
message.createdto untick it (it's selected by default), then clicktenant.status_changed. Selected events turn blue. The other events in the list belong to features that aren't part of Site Connect — leave them unticked. - Click Subscribe.
- A yellow box at the top of the page shows the signing secret (it starts with
whsec_). Click Copy and store it on your server.
You only see the secret once
If you lose it, use Rotate (below) to get a new one.
What your server receives
Each call carries these headers:
| Header | What it is |
|---|---|
X-Webhook-Signature | The signature — see the next section. |
X-Webhook-Event-Type | The event, for example tenant.status_changed. |
X-Webhook-Id | The event's ID. It stays the same on retries and replays — use it to ignore duplicates. |
X-Webhook-Delivery-Id | The ID of this particular attempt. |
The JSON body looks like this:
json
{
"id": "…",
"type": "tenant.status_changed",
"schema_version": 1,
"tenant_id": "…",
"aggregate_type": "tenant",
"aggregate_id": "…",
"payload": {
"tenant_id": "…",
"from": "active",
"to": "past_due",
"reason": "…",
"actor_type": "system",
"actor_id": null
},
"created_at": "2026-10-01T14:05:00.000Z",
"delivery_id": "…",
"attempt": 1
}tenant_id is your workspace's ID. from and to are one of provisioning, active, past_due, suspended, grace_period or terminated. reason may be empty.
Check the signature
The X-Webhook-Signature header looks like t=1759327500,v1=5f8a…:
tis the time the call was signed, in Unix seconds.v1is an HMAC-SHA256 of the text<t>.<raw body>, using your signing secret as the key, written in hex.- Right after you rotate the secret, a third part
v1d=…is added, signed with your old secret.
To verify a call:
- Read the raw request body exactly as received — before any JSON parsing.
- Reject the call if
tis more than 5 minutes away from your server's clock. - Compute the HMAC-SHA256 of
t, a dot, and the raw body, with your secret. - Accept the call if it matches
v1(orv1dwhen you check with the old secret). Use a constant-time comparison.
js
const crypto = require('crypto');
// secrets: your current signing secret, plus the old one during a rotation
function verifySiteConnectWebhook(rawBody, signatureHeader, secrets) {
const parts = Object.fromEntries(
signatureHeader.split(',').map((p) => p.trim().split('=')),
);
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
const received = [parts.v1, parts.v1d].filter(Boolean);
return secrets.some((secret) => {
const expected = crypto.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
return received.some((sig) => sig.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex')));
});
}Reply quickly — and what happens if you don't
- Reply with any
2xxstatus within 10 seconds. Do slow work after you've replied. - Site Connect doesn't follow redirects. Give the final address.
- These are tried again: timeouts, network errors, and the statuses
408,425,429and5xx. Each event gets up to 5 attempts, about 1 minute, 5 minutes, 30 minutes and 2 hours apart. If you reply429with aRetry-Afterheader, Site Connect waits that long (up to an hour). - Other
4xxreplies, such as400,401,403or404, are not tried again. - Replying
410 Goneswitches the webhook off. - Each failed attempt adds one to the webhook's Failures count, and a successful delivery resets it to zero. A webhook with 1,000 failures and no success for 3 days is switched off automatically.
Check deliveries and resend
The webhook list shows URL, Events, Status, Failures and Last success for each webhook. Dates on this page are shown day first (day/month).
- Click Deliveries on a webhook's row. The Delivery log opens with the last 100 attempts. It refreshes every few seconds; click Refresh to update it now.
- Each row shows Time, Event, Att. (attempt number), Status, HTTP (your server's reply), Duration and Error. Status is one of
succeeded,pending,in_flight,failed,dead_letter(out of attempts) orskipped. - To send a failed attempt again, click Replay on its row. Replay only works while the webhook is Active.
Rotate the signing secret
- Click Rotate on the webhook's row, then confirm with Rotate.
- The new secret appears in the yellow box. Click Copy.
- Add the new secret to your server. For the next 7 days each call is signed with both secrets (
v1new,v1dold), so nothing breaks while you switch. After that, only the new secret works.
Pause or delete a webhook
- Pause: click the Active badge on the row. It changes to Disabled and nothing is sent. Click it again to switch the webhook back on.
- Delete: click the bin icon on the row and confirm. This can't be undone.
