# 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:

```javascript
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/bindbee-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/bindbee-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/bindbee-webhooks/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](https://github.com/hookdeck/webhook-skills/blob/main/skills/bindbee-webhooks/references/overview.md) for every model.

## Environment Variables

```bash
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

```bash
# 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](https://github.com/hookdeck/webhook-skills/blob/main/skills/bindbee-webhooks/references/overview.md) - Events, payload structure, delivery and retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/bindbee-webhooks/references/setup.md) - Create a webhook and find the signing secret in the dashboard
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/bindbee-webhooks/references/verification.md) - Signature details, the library pitfall and debugging