Gareth Wilson Gareth Wilson

Guide to WorkOS Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationWorkOS dashboard > Webhooks
Signature headerWorkOS-Signature (no X-), composite t=<ts>,v1=<sig>
Signature schemeHMAC-SHA256 (hex) over <timestamp>.<raw_body>
ToleranceAn SDK parameter (default ~3-5 minutes), not a server rule
Event fieldTop-level event (not type), dotted
RetriesProduction: 6 retries, exponential backoff over 3 days; staging: only "several minutes"
SDKnpm @workos-inc/node webhooks.constructEvent

Common events

WorkOS reports the event type in a top-level field named event (not type), dotted:

EventFires when
dsync.user.created / dsync.user.updatedA directory-sync user changes
dsync.group.user_addedA user is added to a group
connection.activated / connection.deactivatedAn SSO connection changes
user.createdA user is created
authentication.sso_succeededAn SSO authentication succeeds
session.revokedA session is revoked
organization.created / organization_membership.createdAn 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 the v1 value; 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 event field.

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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.