WorkOS Integration
Receive and route WorkOS webhooks for Directory Sync, SSO connections and authentication events.
Setup
1. Create a Source in Hookbase
curl -X POST https://api.hookbase.app/api/sources \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "WorkOS Production",
"slug": "workos",
"provider": "workos",
"signingSecret": "your-workos-webhook-secret"
}'Save your webhook URL:
https://api.hookbase.app/ingest/{orgSlug}/workos2. Configure the WorkOS Webhook
- Go to WorkOS Dashboard → Webhooks
- Click Create Endpoint and paste your Hookbase ingest URL
- Select the events you want to subscribe to
- Copy the endpoint's Webhook Secret — WorkOS generates it when you create the endpoint — and
paste it into your Hookbase source as
signingSecret
Each endpoint has its own webhook secret. See WorkOS's webhook documentation for the event catalog.
3. Create Destinations and Routes
# Create a destination
curl -X POST https://api.hookbase.app/api/destinations \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "Directory Sync Worker", "slug": "workos-dsync", "url": "https://api.myapp.com/webhooks/workos"}'
# Create a route
curl -X POST https://api.hookbase.app/api/routes \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "WorkOS to Directory Sync", "sourceId": "src_...", "destinationId": "dst_..."}'Signature Verification
WorkOS signs a timestamp and the body together, not the body alone. The WorkOS-Signature header
carries both parts as comma-separated key=value pairs:
WorkOS-Signature: t=1700000000000,v1=b7e1c9a3d5f70286c4a6e8d0f2b4a6c8e0d2f4a6b8c0d2e4f6a8b0c2d4e6f8a0t is the send time in milliseconds since the Unix epoch. v1 is the hex HMAC-SHA256 of the
string {t}.{body} — the timestamp, a literal dot, then the raw request body — keyed with the
endpoint's webhook secret.
Hookbase verifies this automatically once the source has both fields set:
{
"provider": "workos",
"signingSecret": "your-workos-webhook-secret"
}Info
The timestamp is checked, not just signed. Hookbase rejects a WorkOS signature whose t is more
than 300 seconds away from the receiving clock. A correctly signed request that arrives outside
that window, or is replayed later, does not verify — which is what a signed timestamp is for.
Warning
Known issue: the tolerance check currently reads t as seconds. WorkOS sends t in
milliseconds, so until this is fixed a genuine WorkOS delivery fails the timestamp check and is stored
with signature_valid: false even when the signature itself is correct. Keep
rejectInvalidSignatures off on WorkOS sources for now — otherwise every delivery is refused with
401.
Once events are arriving with signature_valid: true, set rejectInvalidSignatures: true on the
source to have unverified requests refused with 401 instead of stored.
Common Events
| Event | Description |
|---|---|
dsync.user.created | A directory user was created |
dsync.user.updated | A directory user was updated |
dsync.user.deleted | A directory user was deleted |
dsync.group.created | A directory group was created |
dsync.group.updated | A directory group was updated |
connection.activated | An SSO connection was activated |
connection.deactivated | An SSO connection was deactivated |
authentication.email_verification_succeeded | Email verification succeeded |
Directory User Created
{
"id": "event_01H8MWQR4XJ3FQKXRB3V2QY4N6",
"event": "dsync.user.created",
"created_at": "2024-01-15T10:30:00.000Z",
"data": {
"id": "directory_user_01H8MWQR4XJ3FQKXRB3V2QY4N6",
"directory_id": "directory_01H8MWQR4XJ3FQKXRB3V2QY4N6",
"first_name": "Jane",
"last_name": "Developer",
"emails": [
{
"type": "work",
"value": "jane@example.com",
"primary": true
}
],
"state": "active"
}
}Connection Activated
{
"id": "event_02H9NXRS5YK4GRLYSCAT3RZ5O7",
"event": "connection.activated",
"created_at": "2024-01-15T11:00:00.000Z",
"data": {
"id": "conn_01H8MWQR4XJ3FQKXRB3V2QY4N6",
"connection_type": "OktaSAML",
"name": "Acme Inc SSO",
"state": "active",
"organization_id": "org_01H8MWQR4XJ3FQKXRB3V2QY4N6"
}
}Transform Examples
A javascript transform receives the parsed payload and returns the body Hookbase delivers.
Provisioning Row
function transform(payload) {
const d = payload.data || {};
const primary = (d.emails || []).find(e => e.primary);
return {
event_id: payload.id,
event: payload.event,
directory_user_id: d.id || null,
directory_id: d.directory_id || null,
email: primary ? primary.value : null,
first_name: d.first_name || null,
last_name: d.last_name || null,
state: d.state || null,
occurred_at: payload.created_at
};
}Slack Alert on SSO Connection Changes
function transform(payload) {
const d = payload.data || {};
return {
text: `WorkOS connection ${d.state}: ${d.name}`,
blocks: [
{
type: "section",
fields: [
{ type: "mrkdwn", text: `*Connection:*\n${d.name || d.id}` },
{ type: "mrkdwn", text: `*Type:*\n${d.connection_type || "unknown"}` },
{ type: "mrkdwn", text: `*State:*\n${d.state || "unknown"}` },
{ type: "mrkdwn", text: `*Organization:*\n${d.organization_id || "unknown"}` }
]
}
]
};
}Filter Examples
Filter conditions read dotted paths out of the payload. logic must be AND or OR.
Directory Sync Events Only
{
"name": "Directory Sync",
"logic": "AND",
"conditions": [
{
"field": "event",
"operator": "starts_with",
"value": "dsync."
}
]
}Deprovisioning Only
{
"name": "Deprovisioning",
"logic": "OR",
"conditions": [
{
"field": "event",
"operator": "equals",
"value": "dsync.user.deleted"
},
{
"field": "event",
"operator": "equals",
"value": "connection.deactivated"
}
]
}One Directory
{
"name": "Acme Directory",
"logic": "AND",
"conditions": [
{
"field": "data.directory_id",
"operator": "equals",
"value": "directory_01H8MWQR4XJ3FQKXRB3V2QY4N6"
}
]
}Headers
| Header | Description |
|---|---|
WorkOS-Signature | t=<unix milliseconds>,v1=<hex HMAC-SHA256 of "{t}.{body}"> |
Hookbase does not forward these headers to your destination. A delivery is a fresh request:
Hookbase sets Content-Type, User-Agent: Hookbase/1.0, X-Delivery-ID and X-Event-ID, then
adds the headers and auth you configured on the destination. Nothing WorkOS sent reaches your
handler as a header — read what you need out of the body, or set it on the destination yourself.
Of the incoming headers, only content-type, user-agent, x-github-event, x-gitlab-event and
stripe-signature are stored on the event, so those are the only ones visible later in the
dashboard or the API.
Troubleshooting
Signature Verification Failed
- Confirm the source's
providerisworkos— a source left oncustomreadsX-Signature,X-Webhook-SignatureandX-Hub-Signature-256, and WorkOS sendsWorkOS-Signature - Confirm
signingSecretis that endpoint's webhook secret. An account with several endpoints has several secrets, and they are not interchangeable - Check the timestamp: the signed timestamp must be within 300 seconds. A delivery replayed from a
capture will not verify even though the signature is genuine. See the known issue above — a
timestamp failure on fresh deliveries is the millisecond
tbeing read as seconds, not a wrong secret
Missing Events
- Check the endpoint's event subscriptions in Dashboard → Webhooks
- Directory Sync events only fire for directories that are connected and syncing
- Check the endpoint is enabled and pointed at the right ingest URL
Duplicate Events
WorkOS puts a unique id (event_...) on every event. On a source with dedupEnabled: true, the
default auto strategy dedups WorkOS traffic on that payload id: two deliveries carrying the same
id inside the dedup window collapse into one. A payload without id falls back to hashing the body.
Use payload.id in your handler as the idempotency key as well.