Skip to main content

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

1

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.
2

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

FieldTypeDescription
idstringUnique identifier for this webhook event
created_attimestampWhen the event occurred (ISO 8601)
actionstringEvent type (e.g., app.user.created)
data.actorobjectWho performed the action
data.targetobjectWhat was affected by the action
data.org_idstringOrganization context (if applicable)
data.sourcestringAlways “frontier”
data.metadataobjectAdditional event-specific data

Implementing a Webhook Receiver

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}
1

Extract signature from header

const signatureHeader = req.headers['x-signature'];
const [secretId, signature] = signatureHeader.split('=');
2

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');
3

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

1

Start your webhook receiver locally

node webhook-server.js
# Listening on http://localhost:3000
2

Expose local server with ngrok

ngrok http 3000
You’ll get a public URL like:
https://abc123.ngrok.io
3

Register webhook with ngrok URL

curl -X POST http://localhost:8000/v1beta1/admin/webhooks \
  -d '{
    "url": "https://abc123.ngrok.io/webhooks/frontier"
  }'
4

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:
  1. Go to webhook.site
  2. Copy your unique URL
  3. Register it as a webhook in Frontier
  4. Trigger events and watch them appear on webhook.site

Common Use Cases

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*\nEmail: ${data.metadata.email}`
          }
        }
      ]
    })
  });
}
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
  });
}
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

Checklist:
  1. Verify webhook is enabled: Check state in webhook list
  2. Check webhook URL is accessible: Test with curl
  3. Ensure firewall allows incoming requests
  4. Verify your endpoint returns 2xx status
  5. Check Frontier logs for delivery errors
Common issues:
  1. Using wrong secret (copy the hex value exactly)
  2. Modifying request body before verification
  3. Not decoding secret from hex to bytes
  4. Using wrong hash algorithm (must be SHA-256)
  5. String encoding issues (use UTF-8)
Causes:
  1. Not responding with 2xx status (triggers retries)
  2. Slow processing causing timeouts
  3. 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);
Solution: Process events asynchronously
app.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:
config.yaml
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

Build docs developers (and LLMs) love