Webhooks
Captive POSTs each new guest signup to your endpoint as JSON, signed with a per-endpoint secret. Add endpoints under Dashboard → Webhooks (up to 5), where you can also send test events and watch recent deliveries.
The signup.created event
POST <your endpoint>
Content-Type: application/json
X-Captive-Event: signup.created
X-Captive-Delivery: 6b1f6a2e-...
X-Captive-Signature: t=1751830000,v1=5f3a...
{
"id": "6b1f6a2e-...",
"event": "signup.created",
"createdAt": "2026-07-06T18:00:00.000Z",
"data": {
"id": "…",
"email": "guest@example.com",
"clientMac": "aa:bb:cc:dd:ee:ff",
"apMac": "11:22:33:44:55:66",
"ssid": "Lake Cabin Guest",
"authorized": true,
"siteId": "…",
"siteSlug": "lake-cabin",
"siteName": "Lake Cabin",
"createdAt": "2026-07-06T18:00:00.000Z"
}
}
Signups are delivered whether or not device authorization succeeded — check
data.authorized if you only want guests who got online.
Verifying signatures
The X-Captive-Signature header is t=<unix seconds>,v1=<hex HMAC> where the
HMAC is SHA-256 over `${t}.${rawBody}` keyed with your endpoint's
secret. Verify with the raw request body, before any JSON parsing:
import { createHmac, timingSafeEqual } from "crypto";
function verify(secret, signatureHeader, rawBody) {
const match = /^t=(\d+),v1=([0-9a-f]+)$/.exec(signatureHeader);
if (!match) return false;
const [, t, given] = match;
// reject stale timestamps to prevent replay (5 minutes is plenty)
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(given));
}
Delivery semantics
- Respond 2xx quickly (within 10 seconds) — anything else counts as a failure.
- At-least-once: retries mean you may see the same delivery twice. Dedupe
on the
idfield (also inX-Captive-Delivery). - Retries: immediately, ~30s, ~2min, then on a backoff schedule (5m → 15m → 1h → 4h → 12h) up to 8 attempts total.
- Auto-disable: an endpoint whose last 20 deliveries all failed is disabled and you're emailed. Re-enable it from the dashboard once fixed.
- Ordering is not guaranteed.