Agent skill
Bindbee Webhooks Skill
Receive and verify Bindbee webhooks (unified HRIS, ATS and LMS API, version 2). Use when setting up a Bindbee webhook handler, debugging webhook-id / webhook-timestamp / webhook-signature verification, or handling events such as connector.linked, connector.sync.completed, connector.relink_needed and hris.employee.created.
Install this skill
npx skills add hookdeck/webhook-skills --skill bindbee-webhooks
Bindbee (bindbee.dev) is a unified API for HRIS, ATS and LMS integrations. It pushes connector and data changes to your endpoint as signed POST requests with a JSON body. Webhooks are created in the Bindbee dashboard, pinned to a version, and subscribed to events by name. This skill covers version 2 (2026.10.06, current). Version 1 (2025.01.01) is deprecated on December 31, 2026.
Version 2 only. Hookdeck's configuration alert for this source reads exactly: "Paste the security signature secret as-is. Only version 2 webhooks are supported; version 1 webhooks lack the webhook-* headers and fail verification."
When to Use This Skill
- How do I receive Bindbee webhooks?
- How do I verify Bindbee webhook signatures?
- Why does
standardwebhooks/svixverification fail on my Bindbee webhook? - How do I handle
connector.relink_neededorconnector.sync.completed? - How do I handle
hris.employee.createdand other data events, which carry record IDs? - Why do Bindbee version 1 webhooks fail verification?
Verification (core)
Bindbee signs every version 2 delivery per Standard Webhooks, with three headers: webhook-id, webhook-timestamp (Unix seconds) and webhook-signature (space-separated v1,<base64> entries). The signature is the base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{raw body}. The secret is used as raw UTF-8 bytes, not base64-decoded, so libraries that expect a whsec_ secret compute the wrong key. Verify by hand:
const key = secret.startsWith('whsec_') // paste the secret as shown, no whsec_ prefix
? Buffer.from(secret.slice(6), 'base64') // (a whsec_ value is base64, as Hookdeck treats it)
: Buffer.from(secret, 'utf8');
if (!/^[1-9][0-9]*$/.test(timestamp)) throw new Error('Invalid timestamp');
const expected = crypto.createHmac('sha256', key)
.update(`${id}.${timestamp}.`).update(rawBody) // rawBody: the exact bytes (Buffer), NOT parsed JSON
.digest();
const ok = signatureHeader.split(' ').some((entry) => { // any matching v1 entry; ignore other versions
const [version, sig] = entry.split(',');
const given = Buffer.from(sig ?? '', 'base64');
return version === 'v1' && given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
Rules the examples follow, matching Hookdeck's Bindbee source:
- Require all three headers; read
svix-id/svix-timestamp/svix-signatureinstead whenwebhook-idis absent. - A secret starting with
whsec_is base64-decoded after the prefix; any other secret is used as UTF-8 bytes. An empty key fails closed, as does an unsetBINDBEE_WEBHOOK_SECRET. - No timestamp window by default: Hookdeck retries and replays resend the original timestamp. Set
BINDBEE_to opt in.WEBHOOK_ TOLERANCE_ SECONDS - Invalid signature:
401. Bindbee treats other4xxas final and won't retry, so never return one for a temporary problem.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Common Event Types
The event name is the top-level event field. Subscribe to events by name; there are no wildcards. 92 events are documented.
| Event | Fires when |
|---|---|
connector.linked | A new connector finishes linking and its credentials validate |
connector.sync.started | A sync job begins |
connector.sync.completed | A sync job finishes, after every data event from that sync |
connector.sync.failed | A sync job fails (carries error) |
connector.relink_needed | Credentials stop working; your customer must reconnect (carries error) |
connector.relinked | A connector in RELINK_NEEDED is reconnected |
hris.employee.created / hris.employee.updated | A sync created or updated employee records (data is an array of record IDs) |
<category>.<model>.created / .updated | One pair per model, in hris, ats and lms (for example ats.application.updated, lms.enrollment.created) |
Data events carry record IDs, not records: read them with the ids filter on the model's endpoint. Use id (equal to webhook-id) as the dedupe key and sequence to order events per connector. See references/overview.md for every model.
Environment Variables
BINDBEE_WEBHOOK_SECRET=your_bindbee_security_signature_secret # as shown on the dashboard, no whsec_ prefix
BINDBEE_WEBHOOK_TOLERANCE_SECONDS=0 # optional replay window; 0 = off
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 bindbee --path /webhooks/bindbee
Use the tunnel URL as the webhook URL in the Bindbee dashboard (Webhooks > Add webhook). Bindbee rejects request-inspection hosts such as webhook.site.
Reference Materials
- references/overview.md - Events, payload structure, delivery and retries
- references/setup.md - Create a webhook and find the signing secret in the dashboard
- references/verification.md - Signature details, the library pitfall and debugging