Level 1 · Connected
Webhooks
What arrives, how to check its signature and the retries.
We let you know what happens right away, with a POST to the URL you set in your area (https://), in JSON.
{
"id": "evt_01jb7q8m2z9wk4c6n3d5h7f2qa",
"type": "facility.reviewed",
"createdAt": "2026-10-26T08:20:00Z",
"connectionId": "6f1c…",
"data": {
"facilityId": "madrid-01",
"decision": "returned",
"reasons": [{ "code": "photos", "message": "Photos" }],
"message": "Please upload photos of the inside."
}
}What arrives
| Webhook | data | When |
|---|---|---|
connection.created | {connectionId, operator, scopes} | An operator gives you access |
connection.revoked | {connectionId} | They withdraw it |
facility.linked | {facilityId, status} | The operator says whether a centre is one they already had or a new one |
facility.reviewed | {facilityId, decision, reasons, message} | The team publishes it, returns it with its reasons or withdraws it |
facility.issues | {facilityId, issues} | What the listing is missing |
lead.created | The request | A new request, if the operator wants them in your software |
test.ping | {} | From your area or with POST /webhooks/test |
Its signature
Every webhook carries the Qbox-Signature: t=<Unix seconds>,v1=<signature> header. The signature is the HMAC-SHA256, in hex, of <t>.<raw body>, with your area's webhook secret (whsec_…). Check the signature before using the webhook, with the untouched body, and reject those older than 5 minutes. While you rotate the secret, two v1= arrive, one per secret: one match is enough.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function isValid(rawBody, header, secret) {
const parts = header.split(',').map((part) => part.trim().split('='));
const t = Number(parts.find(([key]) => key === 't')?.[1]);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
return parts.some(([key, value]) => {
if (key !== 'v1') return false;
const given = Buffer.from(value, 'hex');
return given.length === expected.length && timingSafeEqual(given, expected);
});
}Retries
- Reply with a 2xx within 10 seconds. Otherwise we retry after 1, 5, 15 and 60 minutes, then every hour for a day. You can retry one by hand from your area.
- Discard duplicates by their
id. Order isn't guaranteed: look atcreatedAt. - Only to public
https://URLs, without redirects.
The signature test
In your area, “Test the signature” sends you two webhooks: one correctly signed, which you must accept (2xx), and one with a forged signature, which you must reject (4xx). It's one of the certification tests.