Guide to WorkOS Webhooks: Features and Best Practices
WorkOS webhooks notify your application about enterprise identity activity: a directory user is created or updated, an SSO connection is activated, a user signs in. If you're building on WorkOS for SSO, SCIM directory sync, or auth, webhooks are how you react to these events without polling.
This guide covers how WorkOS webhooks work, the events you'll handle, how to verify the WorkOS-Signature, and the best practices for production.
What are WorkOS webhooks?
WorkOS webhooks are JSON POSTs delivered to a URL you configure. Each is signed with a WorkOS-Signature header (no X- prefix) carrying a Stripe-style composite value: t=<timestamp>,v1=<signature>, comma-delimited, so you split it before comparing. The signature is an HMAC-SHA256 (hex) over <issued_timestamp> + "." + raw_body, with the timestamp taken from inside the header value. The official Node SDK verifies all of this.
WorkOS webhook features
| Feature | Details |
|---|---|
| Configuration | WorkOS dashboard > Webhooks |
| Signature header | WorkOS-Signature (no X-), composite t=<ts>,v1=<sig> |
| Signature scheme | HMAC-SHA256 (hex) over <timestamp>.<raw_body> |
| Tolerance | An SDK parameter (default ~3-5 minutes), not a server rule |
| Event field | Top-level event (not type), dotted |
| Retries | Production: 6 retries, exponential backoff over 3 days; staging: only "several minutes" |
| SDK | npm @workos-inc/node webhooks.constructEvent |
Common events
WorkOS reports the event type in a top-level field named event (not type), dotted:
| Event | Fires when |
|---|---|
dsync.user.created / dsync.user.updated | A directory-sync user changes |
dsync.group.user_added | A user is added to a group |
connection.activated / connection.deactivated | An SSO connection changes |
user.created | A user is created |
authentication.sso_succeeded | An SSO authentication succeeds |
session.revoked | A session is revoked |
organization.created / organization_membership.created | An organization or membership changes |
The envelope carries event, id, data, created_at, and context. Note a documented absence: Audit Logs have no webhook event type, you can't receive audit-log entries by webhook.
Setting up WorkOS webhooks
In the WorkOS dashboard, go to Webhooks, add your endpoint, and copy the signing secret into WORKOS_WEBHOOK_SECRET. Note that staging retries only over "several minutes" while production retries over 3 days, so staging isn't a faithful rehearsal of production failure behavior.
Securing WorkOS webhooks
In Node, use the official SDK's webhooks.constructEvent, which parses the composite header, reconstructs the signed string, and verifies (throwing on a bad signature or stale timestamp). In Python (the SDK's own examples verify manually), split the header, rebuild <timestamp> + "." + raw_body from the timestamp string as delivered, and compare a hex HMAC-SHA256 in constant time. Look the header up case-insensitively, some proxies lowercase it.
const { WorkOS } = require("@workos-inc/node");
const workos = new WorkOS(process.env.WORKOS_API_KEY);
const SECRET = process.env.WORKOS_WEBHOOK_SECRET;
app.post("/webhooks/workos", express.raw({ type: "application/json" }), async (req, res) => {
try {
const event = await workos.webhooks.constructEvent({
payload: req.body.toString("utf8"), // raw body
sigHeader: req.headers["workos-signature"],
secret: SECRET,
});
res.sendStatus(200); // acknowledge fast
processQueue.add(event); // branch on event.event, async
} catch {
res.sendStatus(401); // invalid signature or stale timestamp
}
});
The same verification in Python (manual, matching the composite scheme):
import hmac
import hashlib
import os
SECRET = os.environ["WORKOS_WEBHOOK_SECRET"].encode()
def verify(raw_body: bytes, signature_header: str) -> bool:
params = dict(p.split("=", 1) for p in (signature_header or "").split(",") if "=" in p)
timestamp, provided = params.get("t", ""), params.get("v1", "")
signed = timestamp.encode() + b"." + raw_body # timestamp used as delivered
expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, provided)
WorkOS webhook limitations and pain points
The header is a composite you must split
The Problem: WorkOS-Signature is t=<timestamp>,v1=<signature>. Comparing the whole header, or forgetting the timestamp goes into the signed string, never matches.
Why It Happens: WorkOS uses a Stripe-style composite with the timestamp inside the header.
Workarounds:
- Split on the comma, rebuild
<timestamp>.<raw_body>, and compare thev1value; or use the SDK.
How Hookdeck Can Help: Hookdeck parses and verifies the composite header at the edge, so your app receives pre-verified events.
The event field is event, not type
The Problem: Many providers put the event name in type. WorkOS uses event. A handler switching on type finds nothing.
Why It Happens: WorkOS names the field event.
Workarounds:
- Branch on the top-level
eventfield.
How Hookdeck Can Help: Hookdeck's filters route on the event field you actually receive.
Audit Logs aren't available by webhook
The Problem: There's no audit-log webhook event type, so you can't stream audit-log entries via webhooks.
Why It Happens: WorkOS doesn't emit audit logs as webhook events.
Workarounds:
- Use the Audit Logs API or export mechanisms for that data, not webhooks.
How Hookdeck Can Help: Hookdeck gives you reliable delivery and observability for the events that do exist; audit-log retrieval stays on WorkOS's API.
Staging doesn't mirror production retries
The Problem: Production retries 6 times over 3 days; staging retries only over several minutes. Testing failure handling in staging gives a false sense of resilience.
Why It Happens: WorkOS uses a shorter retry policy in staging.
Workarounds:
- Don't treat staging retry behavior as representative; design for the production policy.
How Hookdeck Can Help: Hookdeck applies a consistent retry policy you control across environments, so behavior doesn't change between staging and production.
Best practices
Verify with the SDK (or the composite scheme manually)
Use webhooks.constructEvent in Node; in Python split the header, rebuild <timestamp>.<raw_body>, and compare a hex HMAC-SHA256 in constant time.
Read the header case-insensitively
Proxies may lowercase WorkOS-Signature, so look it up case-insensitively.
Branch on the event field and dedupe on id
Route on event, and make handlers idempotent on the event id. See our guide to webhook idempotency.
Acknowledge fast, process asynchronously
Return 200 quickly and defer work to a queue. See why to process webhooks asynchronously.
Make WorkOS webhooks production-ready
Hookdeck verifies WorkOS-Signature, deduplicates, and durably queues every event
Conclusion
WorkOS webhooks are verified with a WorkOS-Signature composite header (t=<ts>,v1=<sig>), an HMAC-SHA256 over <timestamp>.<raw_body>. Split the header, verify with the official Node SDK (or the scheme manually in Python), branch on the top-level event field, remember that Audit Logs aren't available by webhook, and design for the production retry policy rather than staging's.
Hookdeck verifies the signature, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique events.
Get started with Hookdeck for free and handle WorkOS webhooks reliably in minutes.