Webhooks API Reference
Overview
The Webhooks API enables real-time event notifications for organization activities. Webhooks deliver HTTP POST requests to configured URLs when specific events occur, allowing seamless integration with external systems for monitoring, analytics, alerting, and workflow automation.
All webhook endpoints require organization owner or admin permissions unless otherwise noted.
Key Features
- Real-time Event Delivery: Instant HTTP POST notifications when events occur
- Automatic Retry Logic: Exponential backoff with up to 5 retry attempts for failed deliveries
- Signature Verification: HMAC-SHA256 signatures for webhook authenticity verification
- Delivery Tracking: Complete audit trail of webhook deliveries with response details
- Event Filtering: Subscribe to specific event types or all events
- Active/Inactive Toggle: Enable or disable webhooks without deletion
Endpoints Summary
| Method | Path | Description |
|---|---|---|
POST |
/api/organizations/:org_slug/webhooks |
Create a new webhook |
GET |
/api/organizations/:org_slug/webhooks |
List all webhooks |
GET |
/api/organizations/:org_slug/webhooks/:webhook_id |
Get webhook details |
PATCH |
/api/organizations/:org_slug/webhooks/:webhook_id |
Update webhook configuration |
DELETE |
/api/organizations/:org_slug/webhooks/:webhook_id |
Delete a webhook |
GET |
/api/organizations/:org_slug/webhooks/:webhook_id/deliveries |
Get webhook delivery history |
GET |
/api/organizations/:org_slug/webhooks/event-types |
List available event types |
Webhook Management
POST /api/organizations/:org_slug/webhooks
Create a new webhook for an organization. The webhook secret is generated automatically and returned only once upon creation.
Authentication: Required (Organization Management JWT)
Permissions: Organization owner or admin
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
org_slug |
string | Organization slug |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Webhook name (must be unique within organization) |
url |
string | Yes | Target URL for webhook deliveries (must start with http:// or https://) |
events |
array of strings | Yes | List of event types to subscribe to (at least one required) |
Request Headers:
Authorization: Bearer {jwt_token}
Content-Type: application/json
Example Request:
curl -X POST https://sso.example.com/api/organizations/acme-corp/webhooks \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"name": "Production Webhook",
"url": "https://api.acme.com/webhooks/sso",
"events": [
"user.login.success",
"user.signup.success",
"user.mfa.enabled"
]
}'
Response (200 OK):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Production Webhook",
"url": "https://api.acme.com/webhooks/sso",
"events": [
"user.login.success",
"user.signup.success",
"user.mfa.enabled"
],
"is_active": true,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z"
}
Response Fields:
id(string): Webhook unique identifiername(string): Webhook nameurl(string): Target URLevents(array of strings): Subscribed event typesis_active(boolean): Whether webhook is activecreated_at(string): ISO 8601 timestampupdated_at(string): ISO 8601 timestamp
Error Responses:
400 Bad Request: Invalid input (empty name/url, invalid URL format, invalid event types, duplicate name){ "error": "A webhook with this name already exists" }{ "error": "Invalid event type: invalid.event. Valid types: user.login.success, ..." }401 Unauthorized: Missing or invalid JWT token403 Forbidden: User is not an admin or owner404 Not Found: Organization not found
Important Notes:
- The webhook secret is generated automatically and must be stored securely for signature verification
- Webhook names must be unique within the organization
- At least one event type must be specified
- Invalid event types will return an error with the list of valid event types
GET /api/organizations/:org_slug/webhooks
List all webhooks configured for an organization.
Authentication: Required (Organization Management JWT)
Permissions: Organization owner or admin
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
org_slug |
string | Organization slug |
Query Parameters: None
Request Headers:
Authorization: Bearer {jwt_token}
Example Request:
curl -X GET https://sso.example.com/api/organizations/acme-corp/webhooks \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
Response (200 OK):
{
"webhooks": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Production Webhook",
"url": "https://api.acme.com/webhooks/sso",
"events": ["user.login.success", "user.signup.success"],
"is_active": true,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z"
},
{
"id": "660e8400-e29b-41d4-a716-446655440111",
"name": "Staging Webhook",
"url": "https://staging-api.acme.com/webhooks/sso",
"events": ["user.mfa.enabled", "user.mfa.disabled"],
"is_active": false,
"created_at": "2025-01-14T08:15:00Z",
"updated_at": "2025-01-14T08:15:00Z"
}
],
"total": 2
}
Response Fields:
webhooks(array): List of webhook objectstotal(integer): Total number of webhooks
Error Responses:
401 Unauthorized: Missing or invalid JWT token403 Forbidden: User is not an admin or owner404 Not Found: Organization not found
GET /api/organizations/:org_slug/webhooks/:webhook_id
Get details for a specific webhook.
Authentication: Required (Organization Management JWT)
Permissions: Organization owner or admin
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
org_slug |
string | Organization slug |
webhook_id |
string | Webhook ID |
Query Parameters: None
Request Headers:
Authorization: Bearer {jwt_token}
Example Request:
curl -X GET https://sso.example.com/api/organizations/acme-corp/webhooks/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
Response (200 OK):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Production Webhook",
"url": "https://api.acme.com/webhooks/sso",
"events": ["user.login.success", "user.signup.success", "user.mfa.enabled"],
"is_active": true,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z"
}
Error Responses:
401 Unauthorized: Missing or invalid JWT token403 Forbidden: User is not an admin or owner404 Not Found: Organization or webhook not found
PATCH /api/organizations/:org_slug/webhooks/:webhook_id
Update webhook configuration. All fields are optional - only provided fields will be updated.
Authentication: Required (Organization Management JWT)
Permissions: Organization owner or admin
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
org_slug |
string | Organization slug |
webhook_id |
string | Webhook ID |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | No | New webhook name (must be unique) |
url |
string | No | New target URL |
events |
array of strings | No | New list of event types |
is_active |
boolean | No | Enable or disable webhook |
Request Headers:
Authorization: Bearer {jwt_token}
Content-Type: application/json
Example Request:
curl -X PATCH https://sso.example.com/api/organizations/acme-corp/webhooks/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"is_active": false,
"events": [
"user.login.success",
"user.signup.success",
"user.mfa.enabled",
"user.mfa.disabled"
]
}'
Response (200 OK):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Production Webhook",
"url": "https://api.acme.com/webhooks/sso",
"events": [
"user.login.success",
"user.signup.success",
"user.mfa.enabled",
"user.mfa.disabled"
],
"is_active": false,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T12:45:00Z"
}
Error Responses:
400 Bad Request: Invalid input (empty name/url, invalid URL format, invalid event types, duplicate name){ "error": "A webhook with this name already exists" }401 Unauthorized: Missing or invalid JWT token403 Forbidden: User is not an admin or owner404 Not Found: Organization or webhook not found
Notes:
- Only provided fields are updated
- Webhook name must be unique if changed
- At least one event must be specified if updating events
- Setting
is_active: falsedisables the webhook without deleting delivery history
DELETE /api/organizations/:org_slug/webhooks/:webhook_id
Delete a webhook. This action is permanent and will also delete all delivery history.
Authentication: Required (Organization Management JWT)
Permissions: Organization owner or admin
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
org_slug |
string | Organization slug |
webhook_id |
string | Webhook ID |
Query Parameters: None
Request Headers:
Authorization: Bearer {jwt_token}
Example Request:
curl -X DELETE https://sso.example.com/api/organizations/acme-corp/webhooks/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
Response: 200 OK with empty JSON object {}
Error Responses:
401 Unauthorized: Missing or invalid JWT token403 Forbidden: User is not an admin or owner404 Not Found: Organization or webhook not found
Warning: This action is irreversible. All delivery history associated with this webhook will be permanently deleted.
Webhook Delivery
GET /api/organizations/:org_slug/webhooks/:webhook_id/deliveries
Get paginated delivery history for a webhook, including request payload, response status, and retry information.
Authentication: Required (Organization Management JWT)
Permissions: Organization owner or admin
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
org_slug |
string | Organization slug |
webhook_id |
string | Webhook ID |
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
page |
integer | No | 1 | Page number (minimum: 1) |
limit |
integer | No | 50 | Results per page (maximum: 100) |
event_type |
string | No | - | Filter by event type |
delivered |
boolean | No | - | Filter by delivery status (true/false) |
Request Headers:
Authorization: Bearer {jwt_token}
Example Request:
curl -X GET "https://sso.example.com/api/organizations/acme-corp/webhooks/550e8400-e29b-41d4-a716-446655440000/deliveries?page=1&limit=20&event_type=user.login.success" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
Response (200 OK):
{
"deliveries": [
{
"id": "770e8400-e29b-41d4-a716-446655440222",
"webhook_id": "550e8400-e29b-41d4-a716-446655440000",
"webhook_name": "Production Webhook",
"event_type": "user.login.success",
"payload": {
"event": "user.login.success",
"timestamp": "2025-01-15T10:35:00Z",
"organization_id": "org-123",
"actor_user_id": "user-456",
"actor_email": "user@example.com",
"data": {
"provider": "github",
"service_id": "service-789"
}
},
"response_status_code": 200,
"response_body": "{\"status\":\"received\"}",
"attempt_count": 1,
"max_attempts": 5,
"next_retry_at": null,
"delivered": true,
"delivery_error": null,
"created_at": "2025-01-15T10:35:00Z",
"updated_at": "2025-01-15T10:35:01Z"
},
{
"id": "880e8400-e29b-41d4-a716-446655440333",
"webhook_id": "550e8400-e29b-41d4-a716-446655440000",
"webhook_name": "Production Webhook",
"event_type": "user.signup.success",
"payload": {
"event": "user.signup.success",
"timestamp": "2025-01-15T10:30:00Z",
"organization_id": "org-123",
"actor_user_id": "user-789",
"actor_email": "newuser@example.com",
"data": {
"provider": "google",
"service_id": "service-789"
}
},
"response_status_code": 500,
"response_body": "{\"error\":\"Internal Server Error\"}",
"attempt_count": 3,
"max_attempts": 5,
"next_retry_at": "2025-01-15T10:45:00Z",
"delivered": false,
"delivery_error": "HTTP 500: Internal Server Error",
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:40:00Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 156,
"total_pages": 8,
"has_next": true,
"has_prev": false
}
}
Response Fields:
Delivery Object:
id(string): Delivery unique identifierwebhook_id(string): Webhook IDwebhook_name(string): Webhook nameevent_type(string): Event type that triggered deliverypayload(object): Full webhook payload sentresponse_status_code(integer, nullable): HTTP status code from target endpointresponse_body(string, nullable): Response body from target endpointattempt_count(integer): Number of delivery attemptsmax_attempts(integer): Maximum retry attempts (5)next_retry_at(string, nullable): ISO 8601 timestamp of next retrydelivered(boolean): Whether delivery was successfuldelivery_error(string, nullable): Error message if delivery failedcreated_at(string): ISO 8601 timestamp of first delivery attemptupdated_at(string): ISO 8601 timestamp of last delivery attempt
Pagination Object:
page(integer): Current page numberlimit(integer): Results per pagetotal(integer): Total number of deliveriestotal_pages(integer): Total number of pageshas_next(boolean): Whether there is a next pagehas_prev(boolean): Whether there is a previous page
Error Responses:
401 Unauthorized: Missing or invalid JWT token403 Forbidden: User is not an admin or owner404 Not Found: Organization or webhook not found
Event Types
GET /api/organizations/:org_slug/webhooks/event-types
Get a list of all available webhook event types with descriptions and categories.
Authentication: Required (Organization Management JWT)
Permissions: Organization owner or admin
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
org_slug |
string | Organization slug |
Query Parameters: None
Request Headers:
Authorization: Bearer {jwt_token}
Example Request:
curl -X GET https://sso.example.com/api/organizations/acme-corp/webhooks/event-types \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
Response (200 OK):
[
{
"value": "user.signup.success",
"label": "USER SIGNUP SUCCESS",
"category": "User Management"
},
{
"value": "user.login.success",
"label": "USER LOGIN SUCCESS",
"category": "User Management"
},
{
"value": "user.login.failed",
"label": "USER LOGIN FAILED",
"category": "User Management"
},
{
"value": "user.logout",
"label": "USER LOGOUT",
"category": "User Management"
},
{
"value": "user.mfa.enabled",
"label": "USER MFA ENABLED",
"category": "User Management"
}
]
Response Fields:
value(string): Event type identifier (use this in webhook subscription)label(string): Human-readable event namecategory(string): Event category
Event Categories:
- User Management
- Service Management
- Organization Management
- Plan Management
- Subscription Management
- Invitation Management
- Security
- API Keys
- Custom Domains
- Branding
Error Responses:
401 Unauthorized: Missing or invalid JWT token403 Forbidden: User is not an admin or owner404 Not Found: Organization not found
Available Event Types
Webhooks can subscribe to the following event types:
User Management
Lifecycle Events:
user.signup.success- New user account createduser.login.success- Successful user authenticationuser.login.failed- Failed authentication attemptuser.logout- User logged out
MFA Events:
user.mfa.enabled- User enabled MFAuser.mfa.disabled- User disabled MFAuser.mfa.verify.success- Successful MFA verificationuser.mfa.verify.failed- Failed MFA verification
Administrative Actions:
user.invited- User invited to organizationuser.joined- User accepted invitationuser.removed- User removed from organizationuser.role_updated- User role changed
Service Management
service.created- New service createdservice.updated- Service details updatedservice.deleted- Service deletedservice.oauth_credentials.updated- OAuth credentials updated
Organization Management
organization.updated- Organization details updatedorganization.smtp.configured- SMTP settings configuredorganization.smtp.removed- SMTP settings removed
Plan Management
plan.created- New subscription plan createdplan.updated- Plan updatedplan.deleted- Plan deleted
Subscription Management
subscription.created- User subscribed to plansubscription.updated- Subscription modifiedsubscription.canceled- Subscription canceled
Invitation Management
invitation.accepted- Invitation acceptedinvitation.declined- Invitation declinedinvitation.expired- Invitation expiredinvitation.revoked- Invitation revoked
Security Events
security.mfa.enabled- Organization-level MFA enabledsecurity.mfa.disabled- Organization-level MFA disabledsecurity.password.changed- Password changed
API Key Management
api_key.created- API key createdapi_key.deleted- API key deleted
Custom Domains & Branding
domain.set- Custom domain setdomain.verified- Custom domain verifieddomain.deleted- Custom domain deletedbranding.updated- Branding settings updated
Webhook Payload Structure
All webhook deliveries follow a consistent payload structure:
{
"event": "user.login.success",
"timestamp": "2025-01-15T10:30:00Z",
"organization_id": "org-uuid",
"actor_user_id": "user-uuid",
"actor_email": "user@example.com",
"target_type": "user",
"target_id": "user-uuid",
"data": {
"service_id": "service-uuid",
"provider": "github",
"custom_field": "value"
}
}
Common Fields:
event(string): Event type that triggered the webhooktimestamp(string): ISO 8601 timestamp of event occurrenceorganization_id(string): Organization ID (if applicable)actor_user_id(string): User ID who triggered the event (if applicable)actor_email(string): Email of user who triggered the event (if applicable)target_type(string): Type of resource affected (e.g., “user”, “service”, “organization”)target_id(string): ID of the affected resourcedata(object): Event-specific additional data
Example Payloads
User Signup:
{
"event": "user.signup.success",
"timestamp": "2025-01-15T10:30:00Z",
"organization_id": "org-123",
"actor_user_id": "user-456",
"actor_email": "newuser@example.com",
"target_type": "user",
"target_id": "user-456",
"data": {
"provider": "google",
"service_id": "service-789"
}
}
User Login:
{
"event": "user.login.success",
"timestamp": "2025-01-15T10:30:00Z",
"organization_id": "org-123",
"actor_user_id": "user-456",
"actor_email": "user@example.com",
"target_type": "user",
"target_id": "user-456",
"data": {
"provider": "github",
"service_id": "service-789"
}
}
MFA Enabled:
{
"event": "user.mfa.enabled",
"timestamp": "2025-01-15T10:30:00Z",
"organization_id": "org-123",
"actor_user_id": "user-456",
"actor_email": "user@example.com",
"target_type": "user",
"target_id": "user-456",
"data": {
"method": "totp",
"backup_codes_count": 10
}
}
Service Created:
{
"event": "service.created",
"timestamp": "2025-01-15T10:30:00Z",
"organization_id": "org-123",
"actor_user_id": "admin-789",
"actor_email": "admin@acme.com",
"target_type": "service",
"target_id": "service-456",
"data": {
"service_name": "Mobile App",
"service_slug": "mobile-app",
"service_type": "mobile"
}
}
Security
Signature Verification
All webhook deliveries include security headers for authenticity verification:
Headers:
X-Webhook-Signature: HMAC-SHA256 signature of the payloadX-Webhook-Timestamp: Unix timestamp in seconds since epoch (UTC)
Signature Format:
sha256={hmac_hex_digest}
Verification Example (Node.js):
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
// Create HMAC using webhook secret
const hmac = crypto.createHmac('sha256', secret);
// Update with stringified payload
hmac.update(JSON.stringify(payload));
// Generate expected signature
const expectedSignature = `sha256=${hmac.digest('hex')}`;
// Use timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express.js middleware example
app.post('/webhooks/sso', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
const payload = req.body;
// Verify timestamp to prevent replay attacks
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - timestamp) > 300) {
return res.status(400).json({ error: 'Timestamp too old' });
}
// Verify signature
if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook payload
console.log('Received event:', payload.event);
res.status(200).json({ status: 'received' });
});
Verification Example (Python):
import hmac
import hashlib
import time
import json
def verify_webhook_signature(payload, signature, secret):
# Create HMAC using webhook secret
hmac_gen = hmac.new(
secret.encode('utf-8'),
json.dumps(payload).encode('utf-8'),
hashlib.sha256
)
# Generate expected signature
expected_signature = f"sha256={hmac_gen.hexdigest()}"
# Use constant-time comparison
return hmac.compare_digest(signature, expected_signature)
# Flask example
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks/sso', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Webhook-Signature')
timestamp = int(request.headers.get('X-Webhook-Timestamp'))
payload = request.json
# Verify timestamp
current_time = int(time.time())
if abs(current_time - timestamp) > 300:
return jsonify({'error': 'Timestamp too old'}), 400
# Verify signature
if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
return jsonify({'error': 'Invalid signature'}), 401
# Process webhook
print(f"Received event: {payload['event']}")
return jsonify({'status': 'received'}), 200
Security Best Practices:
- Always verify signatures before processing webhook payloads
- Validate timestamp to prevent replay attacks (within 5 minutes)
- Use constant-time comparison to prevent timing attacks
- Store webhook secrets securely (environment variables, secret managers)
- Use HTTPS endpoints only
- Implement rate limiting on your webhook endpoints
Retry Logic
Failed webhook deliveries are automatically retried with exponential backoff:
Retry Configuration:
- Max Retries: 5 attempts total
- Initial Delay: 5 seconds
- Max Delay: 30 minutes
- Backoff Formula:
delay = base_delay � 2^(attempt - 1) + jitter - Jitter: 0-9 seconds (prevents thundering herd)
Retry Schedule Example:
- Attempt 1: Immediate
- Attempt 2: 5 seconds + jitter
- Attempt 3: 10 seconds + jitter
- Attempt 4: 20 seconds + jitter
- Attempt 5: 40 seconds + jitter (capped at 30 minutes)
Success Criteria:
- HTTP status codes 2xx (200-299) are considered successful
- All other status codes (3xx, 4xx, 5xx) trigger retries
- Network timeouts and connection errors trigger retries
Retry Headers: Each retry includes headers indicating the attempt number:
X-Webhook-Delivery-Id: {delivery_id}
X-Webhook-Attempt: {attempt_number}
Best Practices for Webhook Endpoints:
- Return 2xx status codes immediately upon receipt
- Process webhooks asynchronously in background jobs
- Implement idempotency using
X-Webhook-Delivery-Id - Return non-2xx only for malformed payloads or authentication failures
- Avoid long-running operations in webhook handlers
Rate Limiting
Webhook endpoints should implement rate limiting to prevent abuse:
Recommended Limits:
- Per Webhook: 100 deliveries per minute
- Per Organization: 1000 deliveries per minute across all webhooks
Rate Limit Headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1642244400
Error Responses
Common Errors
400 Bad Request:
{
"error": "Webhook name cannot be empty",
"error_code": "BAD_REQUEST",
"timestamp": "2025-01-15T10:30:00Z"
}
401 Unauthorized:
{
"error": "Missing or invalid Authorization header",
"error_code": "UNAUTHORIZED",
"timestamp": "2025-01-15T10:30:00Z"
}
403 Forbidden:
{
"error": "Organization admin or owner role required",
"error_code": "FORBIDDEN",
"timestamp": "2025-01-15T10:30:00Z"
}
404 Not Found:
{
"error": "Webhook not found",
"error_code": "NOT_FOUND",
"timestamp": "2025-01-15T10:30:00Z"
}
Complete Workflow Example
1. Create Webhook
curl -X POST https://sso.example.com/api/organizations/acme-corp/webhooks \
-H "Authorization: Bearer {jwt}" \
-H "Content-Type: application/json" \
-d '{
"name": "Production Events",
"url": "https://api.acme.com/webhooks/sso",
"events": ["user.login.success", "user.signup.success"]
}'
2. Implement Webhook Endpoint
// Express.js webhook handler
app.post('/webhooks/sso', express.json(), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const payload = req.body;
// Verify signature
if (!verifySignature(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process event
switch (payload.event) {
case 'user.login.success':
console.log(`User ${payload.actor_email} logged in`);
break;
case 'user.signup.success':
console.log(`New user ${payload.actor_email} signed up`);
// Send welcome email, create account in CRM, etc.
break;
}
res.status(200).json({ status: 'received' });
});
3. Monitor Deliveries
curl -X GET "https://sso.example.com/api/organizations/acme-corp/webhooks/{webhook_id}/deliveries?page=1&limit=50" \
-H "Authorization: Bearer {jwt}"
4. Handle Failed Deliveries
# Check for failed deliveries
curl -X GET "https://sso.example.com/api/organizations/acme-corp/webhooks/{webhook_id}/deliveries?delivered=false" \
-H "Authorization: Bearer {jwt}"
# Review error messages and fix endpoint
# Optionally disable webhook while fixing
curl -X PATCH https://sso.example.com/api/organizations/acme-corp/webhooks/{webhook_id} \
-H "Authorization: Bearer {jwt}" \
-H "Content-Type: application/json" \
-d '{ "is_active": false }'
Best Practices
Webhook Design
- Subscribe only to events you need to reduce traffic
- Use descriptive webhook names for easy identification
- Implement proper error handling and logging
- Return 2xx status codes quickly (process asynchronously)
- Use signature verification for security
Endpoint Implementation
- Implement idempotency using delivery IDs
- Process webhooks in background jobs/queues
- Log all webhook deliveries for debugging
- Return non-2xx only for invalid requests
- Implement rate limiting and request validation
Monitoring & Maintenance
- Monitor delivery success rates
- Set up alerts for failed deliveries
- Review delivery history regularly
- Test webhook endpoints before enabling
- Document your webhook integrations
Security
- Always verify webhook signatures
- Validate timestamps to prevent replay attacks
- Use HTTPS endpoints only
- Store webhook secrets securely
- Implement IP allowlisting if possible
- Rate limit webhook endpoints