Webhooks

Receive real-time notifications for authentication events.

Overview

Webhooks notify your application when authentication events occur. Use webhooks to sync user data, trigger workflows, and audit authentication activity.

Event Types

User Events

EventDescription
user.createdNew user account created
user.updatedUser profile or settings changed
user.deletedUser account deleted
user.blockedUser account blocked
user.unblockedUser account unblocked

Authentication Events

EventDescription
login.successSuccessful login
login.failedFailed login attempt
logoutUser logged out
session.createdNew session started
session.revokedSession ended

Password Events

EventDescription
password.resetPassword was reset
password.changedUser changed password
password.reset_requestedPassword reset email sent

MFA Events

EventDescription
mfa.enrolledMFA factor enrolled
mfa.removedMFA factor removed
mfa.challenge.successMFA challenge passed
mfa.challenge.failedMFA challenge failed

Organization Events

EventDescription
organization.createdOrganization created
organization.updatedOrganization updated
organization.deletedOrganization deleted
organization.member.addedMember added
organization.member.removedMember removed
organization.invitation.sentInvitation sent
organization.invitation.acceptedInvitation accepted

Token Events

EventDescription
token.createdNew token issued
token.revokedToken revoked
token.refreshedToken refreshed

Creating Webhooks

Via Dashboard

  1. Go to Auth > Webhooks
  2. Click Create Webhook
  3. Enter the webhook URL
  4. Select events to subscribe to
  5. Copy the signing secret

Via API

TypeScript:

const webhook = await client.auth.webhooks.create({
  url: 'https://yourapp.com/webhooks/auth',
  events: [
    'user.created',
    'login.success',
    'login.failed',
    'mfa.enrolled',
  ],
  headers: {
    'X-Custom-Header': 'custom-value',
  },
});
 
console.log('Webhook ID:', webhook.id);
console.log('Secret:', webhook.secret);

Python:

webhook = client.auth.webhooks.create(
    url="https://yourapp.com/webhooks/auth",
    events=[
        "user.created",
        "login.success",
        "login.failed",
    ],
)
 
print(f"Webhook ID: {webhook.id}")
print(f"Secret: {webhook.secret}")

Webhook Payload

Format

{
  "id": "evt_xxx",
  "type": "user.created",
  "timestamp": "2024-01-01T12:00:00.000Z",
  "data": {
    "user": {
      "id": "user_xxx",
      "email": "user@example.com",
      "createdAt": "2024-01-01T12:00:00.000Z"
    }
  },
  "metadata": {
    "ip": "203.0.113.1",
    "userAgent": "Mozilla/5.0..."
  }
}

Event-Specific Data

user.created:

{
  "type": "user.created",
  "data": {
    "user": {
      "id": "user_xxx",
      "email": "user@example.com",
      "emailVerified": false,
      "profile": {
        "firstName": "John",
        "lastName": "Doe"
      }
    }
  }
}

login.success:

{
  "type": "login.success",
  "data": {
    "user": {
      "id": "user_xxx",
      "email": "user@example.com"
    },
    "session": {
      "id": "session_xxx"
    },
    "connection": "email", // or "google", "saml", etc.
  },
  "metadata": {
    "ip": "203.0.113.1",
    "userAgent": "Mozilla/5.0...",
    "location": {
      "country": "US",
      "city": "San Francisco"
    }
  }
}

login.failed:

{
  "type": "login.failed",
  "data": {
    "email": "user@example.com",
    "reason": "INVALID_PASSWORD",
    "attemptCount": 3
  },
  "metadata": {
    "ip": "203.0.113.1"
  }
}

mfa.enrolled:

{
  "type": "mfa.enrolled",
  "data": {
    "user": {
      "id": "user_xxx",
      "email": "user@example.com"
    },
    "factor": {
      "id": "factor_xxx",
      "type": "TOTP",
      "name": "Authenticator App"
    }
  }
}

Verifying Webhooks

All webhooks are signed with HMAC-SHA256. Verify signatures to ensure authenticity.

Signature Header

X-Transactional-Signature: sha256=xxxxxxxxxxxx
X-Transactional-Timestamp: 1704067200

Verification Code

TypeScript:

import crypto from 'crypto';
 
function verifyWebhook(
  payload: string,
  signature: string,
  timestamp: string,
  secret: string
): boolean {
  // Check timestamp (within 5 minutes)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    return false;
  }
 
  // Compute expected signature
  const signedPayload = `${timestamp}.${payload}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');
 
  // Compare signatures (timing-safe)
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(`sha256=${expected}`)
  );
}
 
// Express middleware
app.post('/webhooks/auth', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-transactional-signature'];
  const timestamp = req.headers['x-transactional-timestamp'];
 
  if (!verifyWebhook(req.body.toString(), signature, timestamp, WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
 
  const event = JSON.parse(req.body);
  handleWebhookEvent(event);
 
  res.status(200).send('OK');
});

Python:

import hmac
import hashlib
import time
 
def verify_webhook(payload: str, signature: str, timestamp: str, secret: str) -> bool:
    # Check timestamp
    now = int(time.time())
    if abs(now - int(timestamp)) > 300:
        return False
 
    # Compute expected signature
    signed_payload = f"{timestamp}.{payload}"
    expected = hmac.new(
        secret.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()
 
    # Compare signatures
    return hmac.compare_digest(signature, f"sha256={expected}")

Handling Webhooks

Best Practices

  1. Respond quickly - Return 200 within 30 seconds
  2. Process asynchronously - Queue events for later processing
  3. Handle duplicates - Events may be delivered multiple times
  4. Verify signatures - Always validate webhook authenticity

Example Handler

app.post('/webhooks/auth', async (req, res) => {
  // Verify signature first
  if (!verifySignature(req)) {
    return res.status(401).send('Invalid signature');
  }
 
  const event = req.body;
 
  // Acknowledge receipt immediately
  res.status(200).send('OK');
 
  // Process asynchronously
  await processEventAsync(event);
});
 
async function processEventAsync(event) {
  switch (event.type) {
    case 'user.created':
      await syncUserToDatabase(event.data.user);
      break;
 
    case 'login.success':
      await recordLoginActivity(event.data);
      break;
 
    case 'login.failed':
      await checkForBruteForce(event.data);
      break;
 
    case 'mfa.enrolled':
      await updateSecurityScore(event.data.user);
      break;
  }
}

Retry Logic

Failed webhook deliveries are retried automatically.

Retry Schedule

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours

After 5 failed attempts, the webhook delivery is marked as failed.

View Delivery Logs

const deliveries = await client.auth.webhooks.listDeliveries('webhook_xxx', {
  status: 'FAILED',
  count: 50,
});
 
for (const delivery of deliveries) {
  console.log(`Event: ${delivery.eventType}`);
  console.log(`Status: ${delivery.status}`);
  console.log(`Response: ${delivery.responseStatus}`);
  console.log(`Error: ${delivery.error}`);
}

Retry Manually

await client.auth.webhooks.retryDelivery('delivery_xxx');

Managing Webhooks

List Webhooks

const webhooks = await client.auth.webhooks.list();
 
for (const webhook of webhooks) {
  console.log(`${webhook.url}: ${webhook.isActive ? 'Active' : 'Inactive'}`);
}

Update Webhook

await client.auth.webhooks.update('webhook_xxx', {
  events: ['user.created', 'user.updated'],
  isActive: true,
});

Delete Webhook

await client.auth.webhooks.delete('webhook_xxx');

Rotate Secret

const { secret } = await client.auth.webhooks.rotateSecret('webhook_xxx');
console.log('New secret:', secret);

Testing Webhooks

Send Test Event

await client.auth.webhooks.test('webhook_xxx', {
  eventType: 'user.created',
});

Local Development

For local testing, use a tunnel service:

  1. Install ngrok: npm install -g ngrok
  2. Start tunnel: ngrok http 3000
  3. Use ngrok URL as webhook endpoint

Next Steps