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

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