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 / svix verification fail on my Bindbee webhook?
  • How do I handle connector.relink_needed or connector.sync.completed?
  • How do I handle hris.employee.created and 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-signature instead when webhook-id is 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 unset BINDBEE_WEBHOOK_SECRET.
  • No timestamp window by default: Hookdeck retries and replays resend the original timestamp. Set BINDBEE_WEBHOOK_TOLERANCE_SECONDS to opt in.
  • Invalid signature: 401. Bindbee treats other 4xx as 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.

EventFires when
connector.linkedA new connector finishes linking and its credentials validate
connector.sync.startedA sync job begins
connector.sync.completedA sync job finishes, after every data event from that sync
connector.sync.failedA sync job fails (carries error)
connector.relink_neededCredentials stop working; your customer must reconnect (carries error)
connector.relinkedA connector in RELINK_NEEDED is reconnected
hris.employee.created / hris.employee.updatedA sync created or updated employee records (data is an array of record IDs)
<category>.<model>.created / .updatedOne 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 11, 2026

View on GitHub →