Webhooks
Configuration
Section titled “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
Section titled “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
Section titled “Webhook Events”Scalar sends webhooks for the following events:
Content Events
Section titled “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
Section titled “System Events”user.create
- When a new user is createduser.login
- When a user logs inbackup.complete
- When a backup is completed
Payload Structure
Section titled “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
Section titled “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
Section titled “Implementing Webhook Handlers”Next.js API Route
Section titled “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
Section titled “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
Section titled “Common Use Cases”Static Site Regeneration
Section titled “Static Site Regeneration”Trigger builds when content changes:
// Trigger Vercel deploymentasync 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 buildasync 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 workflowasync 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
Section titled “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
Section titled “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
Section titled “Notifications”Send notifications to team members:
// Slack notificationexport 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 notificationexport 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 notificationexport 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
Section titled “Testing Webhooks”Local Development
Section titled “Local Development”Use tools like ngrok to test webhooks locally:
# Install ngroknpm install -g ngrok
# Start your local servernpm run dev
# In another terminal, expose local serverngrok http 3000
# Use the ngrok URL in your webhook configuration# https://abc123.ngrok.io/api/webhook
Webhook Testing Tool
Section titled “Webhook 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
Section titled “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
Section titled “Troubleshooting”Common Issues
Section titled “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
Section titled “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', }, },});