Webhooks
Real-time notifications when your content changes. Configure webhook endpoints to receive events for content updates and user interactions.
Webhooks allow you to receive real-time HTTP notifications when events occur in Ask0, such as new conversations, feedback received, or source ingestion completed.
See the API Reference for additional REST endpoints and authentication details.
Configuration
Configure webhooks in your Scalar configuration file:
export default defineConfig({
webhooks: {
enabled: true,
endpoints: [
{
url: 'https://your-app.com/api/webhook',
events: ['create', 'update', 'delete'],
models: ['blogPost', 'page'],
secret: process.env.WEBHOOK_SECRET,
headers: {
'X-Custom-Header': 'value',
},
timeout: 5000, // 5 seconds
retries: 3,
},
{
url: 'https://api.vercel.com/v1/integrations/deploy/prj_xxx/yyy',
events: ['update', 'delete'],
models: ['blogPost'],
method: 'POST',
},
],
},
});Configuration Options
| Option | Type | Description |
|---|---|---|
url | string | The endpoint URL to send webhooks to |
events | string[] | Events to trigger webhooks: create, update, delete |
models | string[] | Content models to watch for changes |
secret | string | Secret for verifying webhook authenticity |
headers | object | Custom headers to include in requests |
timeout | number | Request timeout in milliseconds (default: 5000) |
retries | number | Number of retry attempts on failure (default: 3) |
method | string | HTTP method (default: 'POST') |
Webhook Events
Scalar sends webhooks for the following events:
Content Events
create- When new content is createdupdate- When existing content is modifieddelete- When content is deletedpublish- When content status changes to publishedunpublish- When content status changes from published
System Events
user.create- When a new user is createduser.login- When a user logs inbackup.complete- When a backup is completed
Payload Structure
Webhooks send a JSON payload with the following structure:
{
"id": "webhook_event_123",
"event": "create",
"model": "blogPost",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"id": "post_456",
"title": "My New Blog Post",
"slug": "my-new-blog-post",
"status": "published",
"author": {
"id": "user_789",
"name": "John Doe"
},
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
},
"previous": null,
"user": {
"id": "user_789",
"name": "John Doe",
"email": "john@example.com"
}
}Payload Fields
| Field | Type | Description |
|---|---|---|
id | string | Unique webhook event ID |
event | string | The event type that triggered the webhook |
model | string | The content model that was changed |
timestamp | string | ISO 8601 timestamp of when the event occurred |
data | object | The current state of the content |
previous | object | Previous state (only for update events) |
user | object | User who triggered the change |
Implementing Webhook Handlers
Next.js API Route
import { NextApiRequest, NextApiResponse } from 'next';
import crypto from 'crypto';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
// Verify webhook signature
const signature = req.headers['x-scalar-signature'] as string;
const isValid = verifyWebhookSignature(
JSON.stringify(req.body),
signature,
process.env.WEBHOOK_SECRET!,
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { event, model, data } = req.body;
try {
switch (event) {
case 'create':
await handleCreate(model, data);
break;
case 'update':
await handleUpdate(model, data);
break;
case 'delete':
await handleDelete(model, data);
break;
default:
console.log(`Unhandled event: ${event}`);
}
res.status(200).json({ success: true });
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).json({ error: 'Processing failed' });
}
}
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string,
): boolean {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature.replace('sha256=', '')),
Buffer.from(expectedSignature),
);
}
async function handleCreate(model: string, data: any) {
if (model === 'blogPost' && data.status === 'published') {
// Trigger static site rebuild
await fetch('https://api.vercel.com/v1/integrations/deploy/xxx', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
});
// Update search index
await updateSearchIndex(data);
// Send notification
await sendNotification(`New blog post published: ${data.title}`);
}
}
async function handleUpdate(model: string, data: any) {
if (model === 'blogPost') {
// Update search index with new content
await updateSearchIndex(data);
// Clear CDN cache for this post
await clearCache(`/blog/${data.slug}`);
}
}
async function handleDelete(model: string, data: any) {
if (model === 'blogPost') {
// Remove from search index
await removeFromSearchIndex(data.id);
// Clear CDN cache
await clearCache(`/blog/${data.slug}`);
}
}Express.js Handler
const express = require('express');
const crypto = require('crypto');
const router = express.Router();
router.post(
'/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-scalar-signature'];
const payload = req.body;
// Verify signature
const expectedSignature = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(payload)
.digest('hex');
if (`sha256=${expectedSignature}` !== signature) {
return res.status(401).send('Invalid signature');
}
const webhookData = JSON.parse(payload);
// Process webhook
processWebhook(webhookData)
.then(() => res.status(200).send('OK'))
.catch((error) => {
console.error('Webhook error:', error);
res.status(500).send('Error processing webhook');
});
},
);
async function processWebhook({ event, model, data }) {
switch (event) {
case 'create':
if (model === 'blogPost') {
await notifySlack(`New blog post: ${data.title}`);
}
break;
case 'update':
await invalidateCache(model, data.id);
break;
case 'delete':
await removeFromIndex(model, data.id);
break;
}
}
module.exports = router;Common Use Cases
Static Site Regeneration
Trigger builds when content changes:
// Trigger Vercel deployment
async function triggerVercelBuild() {
const response = await fetch(
`https://api.vercel.com/v1/integrations/deploy/${process.env.VERCEL_HOOK_ID}`,
{ method: 'POST' }
);
if (!response.ok) {
throw new Error('Failed to trigger build');
}
return response.json();
}// Trigger Netlify build
async function triggerNetlifyBuild() {
const response = await fetch(
`https://api.netlify.com/build_hooks/${process.env.NETLIFY_HOOK_ID}`,
{ method: 'POST' }
);
return response.json();
}// Trigger GitHub Actions workflow
async function triggerGitHubAction() {
const response = await fetch(
`https://api.github.com/repos/${process.env.GITHUB_REPO}/dispatches`,
{
method: 'POST',
headers: {
'Authorization': `token ${process.env.GITHUB_TOKEN}`,
'Accept': 'application/vnd.github.v3+json',
},
body: JSON.stringify({
event_type: 'content-update',
}),
}
);
return response.json();
}Search Index Updates
Keep search indexes synchronized:
import { Client } from '@elastic/elasticsearch';
const client = new Client({
node: process.env.ELASTICSEARCH_URL,
});
export async function updateSearchIndex(data: any) {
await client.index({
index: 'content',
id: data.id,
body: {
title: data.title,
content: data.content,
slug: data.slug,
publishedAt: data.publishedAt,
model: data.model,
},
});
}
export async function removeFromSearchIndex(id: string) {
await client.delete({
index: 'content',
id: id,
});
}Cache Invalidation
Clear CDN and application caches:
import { CloudFront } from 'aws-sdk';
const cloudfront = new CloudFront();
export async function clearCDNCache(paths: string[]) {
const params = {
DistributionId: process.env.CLOUDFRONT_DISTRIBUTION_ID!,
InvalidationBatch: {
CallerReference: Date.now().toString(),
Paths: {
Quantity: paths.length,
Items: paths,
},
},
};
await cloudfront.createInvalidation(params).promise();
}
export async function clearApplicationCache(key: string) {
// Clear Redis cache
await redis.del(key);
// Clear in-memory cache
cache.delete(key);
}Notifications
Send notifications to team members:
// Slack notification
export async function notifySlack(message: string) {
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: message }),
});
}
// Discord notification
export async function notifyDiscord(message: string) {
await fetch(process.env.DISCORD_WEBHOOK_URL!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: message }),
});
}
// Email notification
export async function sendEmail(subject: string, body: string) {
// Using your preferred email service
await emailService.send({
to: process.env.NOTIFICATION_EMAIL,
subject,
html: body,
});
}Testing Webhooks
Local Development
Use tools like ngrok to test webhooks locally:
npm install -g ngrok
npm run dev
ngrok http 3000Webhook Testing Tool
Create a simple webhook testing endpoint:
import { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
console.log('Webhook received:');
console.log('Headers:', req.headers);
console.log('Body:', req.body);
// Log to file for debugging
const fs = require('fs');
const logEntry = {
timestamp: new Date().toISOString(),
headers: req.headers,
body: req.body,
};
fs.appendFileSync(
'webhook-logs.json',
JSON.stringify(logEntry, null, 2) + '\n',
);
res.status(200).json({ success: true });
}Unit Tests
import { createMocks } from 'node-mocks-http';
import handler from '@/pages/api/webhook';
import crypto from 'crypto';
describe('/api/webhook', () => {
it('should process valid webhook', async () => {
const payload = {
event: 'create',
model: 'blogPost',
data: { id: '1', title: 'Test Post' },
};
const signature = crypto
.createHmac('sha256', 'test-secret')
.update(JSON.stringify(payload))
.digest('hex');
const { req, res } = createMocks({
method: 'POST',
headers: {
'x-scalar-signature': `sha256=${signature}`,
},
body: payload,
});
await handler(req, res);
expect(res._getStatusCode()).toBe(200);
});
it('should reject invalid signature', async () => {
const { req, res } = createMocks({
method: 'POST',
headers: {
'x-scalar-signature': 'sha256=invalid',
},
body: { event: 'create' },
});
await handler(req, res);
expect(res._getStatusCode()).toBe(401);
});
});Troubleshooting
Common Issues
-
Webhook not firing
- Check webhook configuration
- Verify model names match exactly
- Ensure events are configured correctly
-
Invalid signature errors
- Verify webhook secret matches configuration
- Check signature generation algorithm
- Ensure raw body is used for signature verification
-
Timeout errors
- Increase timeout value in configuration
- Optimize webhook handler performance
- Consider async processing for heavy operations
Debugging
Enable webhook logging in development:
export default defineConfig({
webhooks: {
enabled: true,
debug: process.env.NODE_ENV === 'development',
logging: {
level: 'debug',
file: './webhook-debug.log',
},
},
});Security Note: Always verify webhook signatures in production to ensure requests are from your Scalar instance.
Reliability: Scalar automatically retries failed webhooks with exponential backoff. Failed webhooks are logged and can be manually retried from the admin panel.
Next Steps
- API Reference - Explore REST API endpoints
- API Authentication - Secure your API requests
- Conversations Management - View and analyze user interactions
- Projects Management - Configure your Ask0 projects