Overview
Webhooks allow you to receive real-time notifications when events occur in Frontier. When a user performs an action (like creating an organization or updating a resource), Frontier can automatically send a POST request to your specified endpoint.
Webhooks are perfect for integrating Frontier with external systems like:
Analytics platforms
Customer relationship management (CRM) systems
Notification services
Audit and compliance tools
Custom automation workflows
How Webhooks Work
Webhook Events
Frontier sends webhooks for these event types:
User Events
app.user.created
app.user.updated
app.user.deleted
app.user.listed
app.serviceuser.created
app.serviceuser.deleted
Organization Events
app.organization.created
app.organization.updated
app.organization.deleted
app.organization.member.created
app.organization.member.deleted
Project Events
app.project.created
app.project.updated
app.project.deleted
Group Events
app.group.created
app.group.updated
app.group.deleted
Permission & Policy Events
app.permission.created
app.permission.updated
app.permission.deleted
app.permission.checked
app.policy.created
app.policy.deleted
Resource Events
app.resource.created
app.resource.updated
app.resource.deleted
Role Events
app.role.created
app.role.updated
app.role.deleted
Billing Events
app.billing.entitlement.checked
Creating a Webhook Endpoint
Register webhook via API
Send a POST request to create a webhook: curl -X POST http://localhost:8000/v1beta1/admin/webhooks \
-H "Content-Type: application/json" \
-H "X-Frontier-Email: [email protected] " \
-d '{
"url": "https://your-domain.com/webhooks/frontier",
"description": "Production webhook for user events",
"headers": {
"Authorization": "Bearer your-secret-token",
"Content-Type": "application/json"
}
}'
Leave subscribed_events empty to receive all events, or specify an array of event types to receive only those.
Save the signing secret
The response contains a secret key: {
"webhook" : {
"id" : "webhook_123abc" ,
"url" : "https://your-domain.com/webhooks/frontier" ,
"secrets" : [
{
"id" : "1" ,
"value" : "whsec_a1b2c3d4e5f6g7h8i9j0..."
}
]
}
}
Save this secret immediately! It’s only shown once and you’ll need it to verify webhook signatures.
Subscribing to Specific Events
To receive only certain event types:
curl -X POST http://localhost:8000/v1beta1/admin/webhooks \
-H "Content-Type: application/json" \
-H "X-Frontier-Email: [email protected] " \
-d '{
"url": "https://your-domain.com/webhooks/users",
"description": "User-related events only",
"subscribed_events": [
"app.user.created",
"app.user.updated",
"app.user.deleted"
],
"headers": {
"Authorization": "Bearer secret-token"
}
}'
Best Practice : Create separate webhook endpoints for different event categories (users, organizations, billing) to keep your handlers focused and maintainable.
Webhook Payload Structure
Every webhook POST request contains:
{
"id" : "evt_1234567890" ,
"created_at" : "2024-03-03T10:30:00Z" ,
"action" : "app.user.created" ,
"data" : {
"actor" : {
"id" : "user_abc123" ,
"type" : "user" ,
"name" : "[email protected] "
},
"target" : {
"id" : "user_xyz789" ,
"type" : "user" ,
"name" : "Jane Doe"
},
"org_id" : "org_123abc" ,
"source" : "frontier" ,
"metadata" : {
"email" : "[email protected] " ,
"department" : "Engineering"
}
}
}
Payload Fields
Field Type Description idstring Unique identifier for this webhook event created_attimestamp When the event occurred (ISO 8601) actionstring Event type (e.g., app.user.created) data.actorobject Who performed the action data.targetobject What was affected by the action data.org_idstring Organization context (if applicable) data.sourcestring Always “frontier” data.metadataobject Additional event-specific data
Implementing a Webhook Receiver
Node.js / Express
Go
Python / Flask
const express = require ( 'express' );
const crypto = require ( 'crypto' );
const app = express ();
app . use ( express . json ());
const WEBHOOK_SECRET = 'whsec_a1b2c3d4e5f6g7h8i9j0...' ;
app . post ( '/webhooks/frontier' , ( req , res ) => {
// 1. Verify the signature
const signature = req . headers [ 'x-signature' ];
if ( ! verifySignature ( req . body , signature , WEBHOOK_SECRET )) {
console . log ( 'Invalid signature' );
return res . status ( 401 ). send ( 'Invalid signature' );
}
// 2. Verify timestamp to prevent replay attacks
const event = req . body ;
const eventTime = new Date ( event . created_at );
const now = new Date ();
const fiveMinutes = 5 * 60 * 1000 ;
if ( now - eventTime > fiveMinutes ) {
console . log ( 'Event too old' );
return res . status ( 400 ). send ( 'Event too old' );
}
// 3. Process the event
console . log ( 'Received event:' , event . action );
switch ( event . action ) {
case 'app.user.created' :
handleUserCreated ( event . data );
break ;
case 'app.organization.created' :
handleOrgCreated ( event . data );
break ;
// ... handle other events
}
// 4. Respond quickly
res . status ( 200 ). json ({ received: true });
});
function verifySignature ( payload , signatureHeader , secret ) {
// Extract secret ID and signature
const [ secretId , signature ] = signatureHeader . split ( '=' );
// Compute HMAC
const payloadString = JSON . stringify ( payload );
const expectedSignature = crypto
. createHmac ( 'sha256' , Buffer . from ( secret , 'hex' ))
. update ( payloadString )
. digest ( 'hex' );
return crypto . timingSafeEqual (
Buffer . from ( signature ),
Buffer . from ( expectedSignature )
);
}
function handleUserCreated ( data ) {
console . log ( 'New user created:' , data . target . name );
// Send welcome email, create CRM record, etc.
}
function handleOrgCreated ( data ) {
console . log ( 'New organization:' , data . target . name );
// Initialize org resources, send notifications, etc.
}
app . listen ( 3000 );
Security Best Practices
1. Always Verify Signatures
The X-Signature header contains an HMAC-SHA256 signature:
X-Signature: 1=abc123def456...
Format: {secret_id}={signature}
Extract signature from header
const signatureHeader = req . headers [ 'x-signature' ];
const [ secretId , signature ] = signatureHeader . split ( '=' );
Compute expected signature
const crypto = require ( 'crypto' );
const payload = JSON . stringify ( req . body );
const expectedSignature = crypto
. createHmac ( 'sha256' , Buffer . from ( webhookSecret , 'hex' ))
. update ( payload )
. digest ( 'hex' );
Compare using timing-safe function
const isValid = crypto . timingSafeEqual (
Buffer . from ( signature ),
Buffer . from ( expectedSignature )
);
Never use === for signature comparison! Use crypto.timingSafeEqual() or equivalent to prevent timing attacks.
2. Verify Timestamp
Reject events older than 5 minutes:
const eventTime = new Date ( event . created_at );
const now = new Date ();
const fiveMinutes = 5 * 60 * 1000 ;
if ( now - eventTime > fiveMinutes ) {
return res . status ( 400 ). send ( 'Event too old' );
}
This prevents replay attacks.
3. Use HTTPS
Production webhooks must use HTTPS . Frontier will not send webhooks to HTTP endpoints in production.
4. Authenticate Requests (Optional)
Add a custom header for additional security:
curl -X POST http://localhost:8000/v1beta1/admin/webhooks \
-d '{
"url": "https://your-domain.com/webhooks/frontier",
"headers": {
"Authorization": "Bearer your-static-token",
"X-Webhook-Secret": "additional-secret"
}
}'
Verify this header in your webhook handler:
if ( req . headers [ 'x-webhook-secret' ] !== process . env . WEBHOOK_SECRET ) {
return res . status ( 401 ). send ( 'Unauthorized' );
}
Retry Policy
Frontier automatically retries failed webhook deliveries:
Retry count : Up to 3 attempts
Retry timing : Exponential backoff (3s, 6s, 12s)
Success criteria : HTTP 2xx response
Timeout : 3 seconds per attempt
If all retries fail, the webhook delivery is marked as failed. You can view failed deliveries in audit logs.
Managing Webhooks
List All Webhooks
curl http://localhost:8000/v1beta1/admin/webhooks \
-H "X-Frontier-Email: [email protected] "
Response:
{
"webhooks" : [
{
"id" : "webhook_123abc" ,
"url" : "https://your-domain.com/webhooks/frontier" ,
"description" : "Production webhook" ,
"state" : "enabled" ,
"subscribed_events" : [],
"created_at" : "2024-03-01T10:00:00Z"
}
]
}
Update a Webhook
curl -X PUT http://localhost:8000/v1beta1/admin/webhooks/{webhook_id} \
-H "Content-Type: application/json" \
-H "X-Frontier-Email: [email protected] " \
-d '{
"url": "https://new-domain.com/webhooks/frontier",
"subscribed_events": ["app.user.created", "app.user.updated"]
}'
Disable a Webhook
curl -X PUT http://localhost:8000/v1beta1/admin/webhooks/{webhook_id} \
-H "Content-Type: application/json" \
-H "X-Frontier-Email: [email protected] " \
-d '{
"state": "disabled"
}'
Delete a Webhook
curl -X DELETE http://localhost:8000/v1beta1/admin/webhooks/{webhook_id} \
-H "X-Frontier-Email: [email protected] "
Testing Webhooks
Local Development with ngrok
Start your webhook receiver locally
node webhook-server.js
# Listening on http://localhost:3000
Expose local server with ngrok
You’ll get a public URL like:
Register webhook with ngrok URL
curl -X POST http://localhost:8000/v1beta1/admin/webhooks \
-d '{
"url": "https://abc123.ngrok.io/webhooks/frontier"
}'
Trigger events and watch logs
Create a user or organization and watch your webhook receiver logs.
Testing with webhook.site
For quick testing without code:
Go to webhook.site
Copy your unique URL
Register it as a webhook in Frontier
Trigger events and watch them appear on webhook.site
Common Use Cases
Send Slack notifications on new users
async function handleUserCreated ( data ) {
await fetch ( process . env . SLACK_WEBHOOK_URL , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
text: `New user registered: ${ data . target . name } ` ,
blocks: [
{
type: 'section' ,
text: {
type: 'mrkdwn' ,
text: `*New User* \n Email: ${ data . metadata . email } `
}
}
]
})
});
}
Sync users to external CRM
async function handleUserCreated ( data ) {
// Create contact in your CRM
await crmClient . contacts . create ({
email: data . metadata . email ,
name: data . target . name ,
source: 'frontier' ,
frontier_user_id: data . target . id
});
}
async function handleUserUpdated ( data ) {
// Update CRM contact
await crmClient . contacts . update (
data . target . id ,
data . metadata
);
}
async function handleEvent ( event ) {
// Send to analytics platform
await analytics . track ({
userId: event . data . actor . id ,
event: event . action ,
properties: {
targetId: event . data . target . id ,
targetType: event . data . target . type ,
orgId: event . data . org_id ,
... event . data . metadata
},
timestamp: event . created_at
});
}
Trigger automation workflows
async function handleOrgCreated ( data ) {
// Create default resources
await createDefaultProjects ( data . target . id );
// Set up billing account
await billingService . createAccount ( data . target . id );
// Send welcome email to org owner
await sendWelcomeEmail ( data . actor . id );
}
Troubleshooting
Webhooks not being received
Checklist :
Verify webhook is enabled: Check state in webhook list
Check webhook URL is accessible: Test with curl
Ensure firewall allows incoming requests
Verify your endpoint returns 2xx status
Check Frontier logs for delivery errors
Signature verification fails
Common issues :
Using wrong secret (copy the hex value exactly)
Modifying request body before verification
Not decoding secret from hex to bytes
Using wrong hash algorithm (must be SHA-256)
String encoding issues (use UTF-8)
Receiving duplicate events
Causes :
Not responding with 2xx status (triggers retries)
Slow processing causing timeouts
Multiple webhooks registered with same URL
Solution : Use event id to deduplicate:const processedEvents = new Set ();
if ( processedEvents . has ( event . id )) {
return res . status ( 200 ). send ( 'Already processed' );
}
processedEvents . add ( event . id );
Webhook endpoint timing out
Solution : Process events asynchronouslyapp . post ( '/webhooks/frontier' , async ( req , res ) => {
// Verify signature
if ( ! verifySignature ( req . body , req . headers [ 'x-signature' ])) {
return res . status ( 401 ). send ( 'Invalid signature' );
}
// Respond immediately
res . status ( 200 ). json ({ received: true });
// Process asynchronously
processEventAsync ( req . body ). catch ( console . error );
});
async function processEventAsync ( event ) {
// Long-running processing here
await heavyOperation ( event );
}
Webhook Configuration
Configure webhook encryption in Frontier:
app :
webhook :
# Encryption key for secrets stored in database
# Must be 32 characters
encryption_key : "encryption-key-should-be-32-chars--"
Important : This encrypts webhook secrets in the database. Change this key requires re-registering all webhooks.
Next Steps
Audit Logs Learn about audit logging and compliance
Admin Portal Manage webhooks through the UI
API Reference Explore the full webhook API
Authorization Learn about permission checks