Directory Sync
Webhooks
Every directory can have a webhook. OrbitID sends an HTTP POST to it whenever a user or group changes in your customer's directory.
With your coding agent
Claude Code, Cursor or Codex makes the change in your repo and tells you what it touched.
Add a verified OrbitID webhook endpoint to this app. Follow https://orbitid.com/docs/webhooks.md 1. Add POST /webhooks/orbitid and read the raw body before any JSON parsing. 2. Verify OrbitID-Signature (t=…,s=…): HMAC-SHA256 of "<t>.<body>" with ORBITID_WEBHOOK_SECRET, compared in constant time. Reject timestamps older than 5 minutes. 3. Accept one event or an array, queue each one, and return 200 straight away. 4. Add a test with a signed sample payload and one with a bad signature. 5. Run the tests, then list the files you changed.
Configure
Set the Webhook URL and Webhook secret when you create the directory, or later from the directory's settings. Use a different secret per directory if you want to tell them apart; the secret is never sent, only used to sign requests.
Turn on Log webhook events to keep a record of every delivery, with the payload and your endpoint's response, in the dashboard and at GET /api/v1/dsync/events.
Events
| Event | data |
|---|---|
user.created, user.updated, user.deleted | The user |
group.created, group.updated, group.deleted | The group |
group.user_added, group.user_removed | The user, with the group under data.group |
Payload
{"event": "user.created","tenant": "acme","product": "<product>","directory_id": "58b5cd9dfaa39d47eb8f5f88631f9a629a232016","data": {"id": "00u1abc…","first_name": "Jane","last_name": "Doe","active": true,"raw": { "…": "the SCIM resource as the directory sent it" }}}
Group events carry { "id", "name", "raw" } in data; membership events carry the user plus a group object with the same shape. When several changes arrive at once, the body may be a JSON array of events instead of a single object.
Verify the signature
Each request has an OrbitID-Signature header of the form t=<timestamp>,s=<signature>. The signature is an HMAC-SHA256 of <timestamp>.<raw request body> using the webhook secret, hex encoded. Verify it before you trust the payload:
import crypto from 'node:crypto'export const verifyWebhook = (header: string,rawBody: string,secret: string) => {const parts = Object.fromEntries(header.split(',').map((part) => part.split('=')))const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex')return (expected.length === parts.s.length &&crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.s)))}
Use the raw request body, not a re-serialised version of it. Reject requests whose timestamp is older than a few minutes to protect against replay.
Respond
Return a 2xx status quickly and process the event asynchronously. Events are delivered in the order they happen; treat them as idempotent, since the same change can be sent again if your endpoint fails.

