Qbox
Documentation

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.

A webhook
{
  "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

WebhookdataWhen
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.createdThe requestA 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 at createdAt.
  • 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.

Questions? Write to us at team@findqbox.com.