Webhooks
Overview
Keymaster sends outbound webhooks to notify your app of identity events in real time. Webhooks are HMAC-SHA256 signed, retried on failure, and logged for debugging.
Supported Events
| Event | Triggered when |
|---|---|
user.signup |
New user creates an account |
user.login |
User successfully authenticates |
user.logout |
User's SSO session is revoked |
user.role_changed |
User's roles are updated for this app |
user.suspended |
User's access is suspended |
user.deleted |
User is removed from the app |
ping |
Manual test from Console |
Setup
1. Register a Webhook Endpoint
Via Console: App Detail → Webhooks → Add Endpoint
Via API:
POST /admin/apps/{app_id}/webhooks
Authorization: Bearer {admin_jwt}
{
"url": "https://yourapp.com/hooks/keymaster",
"events": ["user.signup", "user.login", "user.role_changed"]
}
Response includes the HMAC secret (shown once):
{
"id": "webhook-uuid",
"secret": "a1b2c3d4...64hexchars",
"url": "https://yourapp.com/hooks/keymaster",
"events": ["user.signup", "user.login", "user.role_changed"]
}
2. Receive Webhook Deliveries
Keymaster POSTs to your URL with these headers:
POST https://yourapp.com/hooks/keymaster
Content-Type: application/json
X-Keymaster-Event: user.login
X-Keymaster-Delivery: delivery-uuid
X-Keymaster-Signature: sha256=a1b2c3...
X-Keymaster-Timestamp: 1710547200
3. Payload Format
{
"event": "user.login",
"timestamp": "2026-03-17T12:00:00Z",
"app_id": "a56e4998-...",
"data": {
"user_id": "550e8400-...",
"email": "user@example.com",
"name": "Jane Doe"
}
}
Signature Verification
Always verify the signature. This confirms the webhook came from Keymaster, not an attacker.
import hmac
import hashlib
def verify_keymaster_webhook(body: bytes, signature_header: str, secret: str) -> bool:
"""Verify HMAC-SHA256 signature from Keymaster webhook."""
expected = hmac.new(
secret.encode(),
body,
hashlib.sha256,
).hexdigest()
received = signature_header.replace("sha256=", "")
return hmac.compare_digest(expected, received)
# In your webhook handler:
@app.post("/hooks/keymaster")
async def handle_webhook(request):
body = await request.body()
signature = request.headers.get("X-Keymaster-Signature", "")
if not verify_keymaster_webhook(body, signature, WEBHOOK_SECRET):
return Response(status_code=401)
payload = json.loads(body)
event = payload["event"]
# Handle the event...
// Node.js
const crypto = require('crypto');
function verifyKeymasterWebhook(body, signatureHeader, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
const received = signatureHeader.replace('sha256=', '');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(received),
);
}
Retry Behavior
| Attempt | Delay | Behavior |
|---|---|---|
| 1st | Immediate | First delivery attempt |
| 2nd | 1 second | After first failure |
| 3rd | 2 seconds | After second failure |
| (fail) | — | Delivery marked as failed, logged in Console |
Keymaster considers 2xx responses as success. Any non-2xx or timeout (10s) triggers a retry.
Delivery Log
View delivery history in the Console: App Detail → Webhooks → Click endpoint → Deliveries.
Each delivery shows: - Event type - Payload - HTTP status code received - Response body (first 1KB) - Number of attempts - Delivered-at timestamp
Testing
Send a test ping event from the Console:
POST /admin/apps/{app_id}/webhooks/{webhook_id}/test
Authorization: Bearer {admin_jwt}
Or click Test next to the endpoint in the Console.
Security Notes
- SSRF protection: Webhook URLs are validated against private/reserved IP ranges. URLs resolving to
127.0.0.1,10.x,172.16-31.x,192.168.x,169.254.xare blocked. - No redirects: Keymaster does not follow HTTP redirects on webhook delivery. Your URL must return a direct response.
- Secret rotation: Delete the endpoint and recreate it to get a new secret.
- URL validation: Both on registration and at dispatch time (defense-in-depth).
Managing Webhooks via API
| Endpoint | Method | Description |
|---|---|---|
/admin/apps/{app_id}/webhooks |
POST | Register a new webhook |
/admin/apps/{app_id}/webhooks |
GET | List all webhooks for an app |
/admin/apps/{app_id}/webhooks/{id} |
DELETE | Remove a webhook |
/admin/apps/{app_id}/webhooks/{id}/test |
POST | Send a test ping |
/admin/apps/{app_id}/webhooks/{id}/deliveries |
GET | List delivery history |