Agent skill
Scalekit Webhooks Skill
Receive and verify Scalekit webhooks. Use when setting up a Scalekit webhook endpoint, getting the whsec_ signing secret, debugging webhook-id / webhook-timestamp / webhook-signature (Standard Webhooks) verification, or handling events like organization.directory.user_created, organization.created, organization.sso_enabled, user.login and role.updated.
Install this skill
npx skills add hookdeck/webhook-skills --skill scalekit-webhooks
Scalekit provides authentication for B2B SaaS (organizations, SSO, SCIM directory sync, RBAC, sessions) and AI agents (connected accounts, tool calling). Its webhooks report organization, user, directory, SSO, role/permission and connected-account events. Every delivery is signed with Standard Webhooks (webhook-id, webhook-timestamp, webhook-signature).
When to Use This Skill
- How do I receive Scalekit webhooks?
- How do I verify a Scalekit webhook signature?
- Where do I find my Scalekit webhook signing secret (
whsec_...)? - How do I handle
organization.directory.user_orcreated organization.sso_enabledevents? - Why does Scalekit webhook verification fail?
- Why do Hookdeck retries of a Scalekit webhook fail verification after a few minutes?
Verification (core)
| Header | Value |
|---|---|
webhook-id | Delivery ID, the same on every retry |
webhook-timestamp | Unix seconds of this attempt |
webhook-signature | One or more space-separated v1,<base64 HMAC-SHA256> entries |
Signed content is {webhook-id}.{webhook-timestamp}.{raw body}. The key is the base64-decoded part of the secret after whsec_. Verify with the standardwebhooks library (npm, PyPI), which compares every v1 entry in constant time:
const { Webhook } = require('standardwebhooks');
// rawBody: exact request body string, NOT re-serialised JSON
const prefix = req.get('webhook-id') ? 'webhook' : 'svix'; // fall back to svix-* like Hookdeck
const event = new Webhook(process.env.SCALEKIT_WEBHOOK_SECRET).verify(rawBody, {
'webhook-id': req.get(`${prefix}-id`),
'webhook-timestamp': req.get(`${prefix}-timestamp`),
'webhook-signature': req.get(`${prefix}-signature`),
}); // returns the parsed event; throws → respond 400
Python: from standardwebhooks.webhooks import Webhook, then Webhook(secret).verify(await request.body(), {...same three keys...}). Scalekit's own SDKs offer verifyWebhookPayload(secret, headers, payload); the library above uses the same algorithm.
Timestamp window. The library always enforces a fixed 5-minute window. Hookdeck retries and replays resend the original timestamp, so the library rejects late retries after 5 minutes. A manual verifier (HMAC-SHA256 over {id}.{timestamp}.{raw body}, every v1, entry compared in constant time) where the window can be dropped or widened is in references/verification.md.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Envelope
{ "spec_version": "1", "id": "evt_100000000000000001", "object": "DirectoryUser",
"environment_id": "env_100000000000000001", "occurred_at": "2026-10-01T09:30:00.000Z",
"organization_id": "org_100000000000000001", "type": "organization.directory.user_created",
"data": { "id": "diruser_100000000000000001", "email": "kai@example.com", "active": true } }
The event type is the top-level type. id (evt_…) is unique per event and differs from webhook-id; object names the data's type and is omitted on domain events; organization_id is not sent on user.* events. Dedupe on webhook-id or id, not data.id.
Common Event Types
| Event | Fires when |
|---|---|
organization.created / organization.updated / organization.deleted | An organization changes |
organization.directory.user_ / _updated / _deleted | A SCIM directory user is provisioned, changed or removed |
organization.directory.group_ / _updated / _deleted | A directory group changes |
organization.directory_ / organization.directory_ | A directory is switched on or off |
organization.sso_enabled / organization.sso_ | An SSO connection is switched on or off |
user.signup / user.login / user.logout | A user signs up, logs in or out |
user.organization_ / _updated / _deleted | A membership changes |
role.created / role.updated | A role changes |
All documented types (43; the list may not be exhaustive): references/overview.md and Scalekit's webhook guide.
Delivery Semantics
- Return a
2xxwithin 30 seconds (201recommended); enqueue and process asynchronously, but verify before queueing. Missing headers or a bad signature: return400(or401). - Any other status, including
4xx, is retried: 8 attempts (immediately, 5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 10 h), about 27 h 35 min. An endpoint failing every delivery for 5 days is disabled. - At least once and possibly out of order: dedupe, and compare
occurred_atbefore overwriting newer data. - No challenge or validation request is documented.
Environment Variables
SCALEKIT_WEBHOOK_SECRET=whsec_xxxxx # full whsec_ value; handlers fail closed (500) without it
One signing secret per endpoint, from Dashboard > Webhooks > any endpoint.
Local Development
# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 scalekit --path /webhooks/scalekit
Reference Materials
- references/overview.md - Envelope, event types, delivery and ordering
- references/setup.md - Dashboard setup, signing secret, retries, testing
- references/verification.md - Library and manual verification, timestamp window, debugging