- A public HTTPS endpoint your code controls (the receiver)
- Permissions to create webhook subscriptions in the tenant
- A secret manager to store the signing secret
1. Build the receiver
Section titled “1. Build the receiver”The simplest viable receiver returns 2xx as soon as it's received the event and processes asynchronously. This pattern is cheap and dependable.
// Node + Expressimport express from 'express'import { createHmac, timingSafeEqual } from 'node:crypto'
const app = express()
app.post( '/webhooks/intelliauth', express.raw({ type: 'application/json' }), async (req, res) => { const signature = req.header('X-IntelliAuth-Signature') ?? '' const body = req.body as Buffer
const expected = createHmac('sha256', process.env.INTELLIAUTH_WEBHOOK_SECRET!) .update(body) .digest('hex')
if (!timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) { return res.status(401).json({ error: 'invalid_signature' }) }
// 2xx fast, do the work async. res.status(202).end()
const event = JSON.parse(body.toString('utf8')) queue.enqueue(event) // your background worker handles it },)
app.listen(3000)A few non-negotiables:
- Receive the body as raw bytes — JSON parsing must happen AFTER signature verification, because parsing canonicalises whitespace and the HMAC won't match.
- Use
timingSafeEqual— comparing strings character by character leaks timing information. - Return 2xx fast — the platform retries on >10 second responses.
See the signature verification topic for languages other than Node.
2. Deploy the receiver
Section titled “2. Deploy the receiver”The endpoint must be reachable over HTTPS from the public internet. The platform refuses to deliver to:
- HTTP (non-TLS) URLs.
- RFC 1918 / link-local addresses.
- localhost / 127.0.0.1.
- Self-signed certificates (real CA certificates only — Let's Encrypt is free if you don't have one).
For local development, use a tunnel: ngrok, Cloudflare Tunnel, or a similar service that exposes your local server over HTTPS.
3. Create the subscription
Section titled “3. Create the subscription”In the tenant admin console: Authentication → Webhooks → New subscription.
Fill in:
- Name — for your own bookkeeping ("Production CRM sync", "Dev tunnel").
- URL — your receiver, e.g.
https://api.cymmetri.com/webhooks/intelliauth. - Events — pick the events you care about. You can subscribe to all events (
*), specific event types (user.signed_up), or whole categories (user.*). - Description — optional, but useful when six teammates create six subscriptions and someone has to clean up.
Save. The console shows the signing secret — copy it into your secret manager IMMEDIATELY. The platform shows it once.
If you prefer to script this, use the webhooks API:
POST /api/v1/webhooks/subscriptionsContent-Type: application/jsonAuthorization: Bearer <token>
{ "name": "Production CRM sync", "url": "https://api.cymmetri.com/webhooks/intelliauth", "events": ["user.signed_up", "user.deleted"], "description": "Push new users to Salesforce."}The response carries signing_secret exactly once.
4. Set the secret in your receiver
Section titled “4. Set the secret in your receiver”Inject the signing secret from your secret manager as an environment variable. NEVER:
- Commit it to source control.
- Log it.
- Display it in any UI accessible to less-trusted operators.
Rotating the secret is a config change on the subscription (the next topic covers rotation).
5. Test it
Section titled “5. Test it”The console's "Test event" button sends a synthetic delivery to your receiver — useful for verifying connectivity and signature validation before real traffic.
The test event looks like a normal event but has data.test: true and event_type: "test.ping". Your receiver should accept and respond 2xx; if it fails, the console shows the response body so you can debug.
For a real-traffic smoke test, sign up a test user in the tenant. You should see the user.signed_up event arrive within a few seconds.
6. Pick what to filter at the receiver
Section titled “6. Pick what to filter at the receiver”The platform sends every event you subscribed to. Your receiver should still validate:
- That the
tenant.idmatches the one you expect (defends against cross-tenant misconfig). - That the
event_typeis one you actually handle (defends against future event types arriving and breaking your downstream). - That the
event_idhasn't been processed (idempotency for retries).
These three checks are short, cheap, and catch a lot of real incidents.
7. Monitor
Section titled “7. Monitor”The console exposes a per-subscription delivery feed: every recent attempt, success or failure, with the HTTP status the receiver returned. If you see a spike in non-2xx, your receiver is unhealthy — investigate before the events expire from the queue.
For long-running production use, also tail this feed from your monitoring system (alerts on >X% failures in 5m). The dashboard is a starting point, not a substitute for alerting.
Common setup mistakes
Section titled “Common setup mistakes”- Subscribing to
*"to be safe". The platform will deliver every event, including the ones you don't care about, which is bandwidth + risk for no gain. Subscribe to what you'll actually act on. - Pointing a dev subscription at a production receiver. Dev signups arriving at production CRM is messy. Use distinct receivers per environment.
- Forgetting to set the secret. The receiver accepts the first event, signature check fails, the event goes to retry, and you don't notice until the dead-letter queue alarms hours later.
- Returning 5xx for application-level rejection. If your receiver decides "I don't want this event", return 2xx and drop it — 5xx triggers retries, which you don't want for a permanent rejection.