Webhooks
BriefQR pushes signed events to endpoints you register. Subscribe, verify the signature, and return 2xx.
Subscribe
curl -X POST https://api.briefqr.com/v1/webhook-endpoints
-H "Authorization: Bearer bqr_test_..."
-H "Content-Type: application/json"
-d '{
"url": "https://example.com/hooks/briefqr",
"enabled_events": ["session.completed", "session.flagged"]
}' The response includes the signing secret (whsec_…) once. enabled_events accepts exact types, a resource.* wildcard (e.g. operator.*), or *. Send yourself a test delivery any time:
curl -X POST https://api.briefqr.com/v1/webhook-endpoints/{id}/send-test
-H "Authorization: Bearer bqr_test_..." The event payload
Each delivery is a POST with this body:
{
"id": "evt_9c1f…",
"type": "session.completed",
"api_version": "2026-06-23",
"livemode": false,
"created": "2026-06-23T14:02:11.482Z",
"data": { "object": { "object": "session", "id": "…", "total_hours": 7.5, "...": "..." } }
} And these headers:
| Header | Value |
|---|---|
X-BriefQR-Signature | t=<unix>,v1=<hex> (see below) |
X-BriefQR-Id | whd_… — the delivery id, stable across retries |
X-BriefQR-Event | the event type |
Dedupe on the envelope id (or X-BriefQR-Id) — a delivery may arrive more than once.
Verify the signature
The signature header is t=<timestamp>,v1=<hmac>. Compute HMAC-SHA256 over the string `${t}.${rawBody}` (the unix timestamp, a literal dot, then the exact raw request body), keyed by your endpoint secret, and compare hex digests in constant time. Reject if the timestamp is more than 5 minutes from now.
During a secret rotation the header carries two
v1=values (new and previous secret). Accept the delivery if either matches.
Node / Express
const crypto = require("crypto");
function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=").map((s) => s.trim()))
);
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
// header may hold several v1= values during rotation
const provided = header.split(",").filter((p) => p.startsWith("v1=")).map((p) => p.slice(3));
return provided.some((v) =>
v.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(v), Buffer.from(expected))
);
}
// Express — read the RAW body, not the parsed JSON
app.post("/hooks/briefqr", express.raw({ type: "*/*" }), (req, res) => {
const raw = req.body.toString("utf8");
if (!verify(raw, req.headers["x-briefqr-signature"], process.env.BRIEFQR_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}
const event = JSON.parse(raw);
if (event.type === "session.completed") { /* … */ }
res.sendStatus(200);
}); Python / Flask
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
provided = [p[3:] for p in header.split(",") if p.startswith("v1=")]
return any(hmac.compare_digest(v, expected) for v in provided)
@app.post("/hooks/briefqr")
def hook():
raw = request.get_data() # raw bytes, before JSON parsing
if not verify(raw, request.headers["X-BriefQR-Signature"], os.environ["BRIEFQR_WEBHOOK_SECRET"]):
return "", 400
event = request.get_json()
return "", 200 The official SDKs ship a constructEvent / construct_event helper that does exactly this.
Event types
session.completed, session.updated, session.deleted, session.flagged, operator.created, operator.updated, operator.deactivated, device.created, device.updated, device.deleted, location.created, location.updated, location.deleted, scan.checked_in, scan.checked_out, scan.flagged.
Retries
A delivery is retried with exponential backoff (up to 6 attempts, ~60s → 1h) until your endpoint returns 2xx. Return 410 Gone to stop retries permanently. After sustained failures an endpoint is auto-disabled — re-enable it with a PATCH. Inspect and replay deliveries in the delivery log or via POST /v1/webhook-deliveries/{id}/resend.
Rotating the secret
curl -X POST https://api.briefqr.com/v1/webhook-endpoints/{id}/rotate-secret
-H "Authorization: Bearer bqr_test_..."
-d '{ "grace_hours": 24 }' The new secret is returned once. For grace_hours the previous secret stays valid too (deliveries are signed with both), so you can roll the secret on your receiver without dropping events.