Agent skill
Builder.io Webhooks Skill
Receive and authenticate Builder.io model webhooks. Use when setting up a Builder.io webhook endpoint, checking the custom secret header, debugging 401s, or handling content operations like publish, unpublish, archive, delete, scheduledStart and scheduledEnd.
Install this skill
npx skills add hookdeck/webhook-skills --skill builder-io-webhooks
Builder.io POSTs a JSON webhook to your URL when content in a model is published, unpublished, archived or deleted, or when a scheduled publish starts or ends. Webhooks are not signed. You authenticate them by adding a secret custom header to the webhook in Builder.io, then comparing that header on every request with the value you stored.
When to Use This Skill
- How do I receive Builder.io webhooks?
- How do I authenticate a Builder.io webhook if there is no signature?
- How do I add a custom header (secret) to a Builder.io model webhook?
- How do I handle
publish,unpublish,archive,delete,scheduledStartandscheduledEndoperations? - Why does my Builder.io webhook endpoint return 401?
Verification (core)
const crypto = require('crypto');
function safeEqual(a, b) {
const x = Buffer.from(String(a), 'utf8');
const y = Buffer.from(String(b), 'utf8');
return x.length === y.length && crypto.timingSafeEqual(x, y); // lengths first: it throws otherwise
}
function verifyBuilderRequest(headers, {
header = process.env.BUILDER_IO_WEBHOOK_HEADER || 'Authorization',
secret = process.env.BUILDER_IO_WEBHOOK_SECRET,
} = {}) {
if (!secret) return false; // never accept when unconfigured
const received = headers[header.toLowerCase()]; // Node lowercases header names
return typeof received === 'string' && safeEqual(received, secret); // WHOLE value
}
- The header name is whatever you typed under + Custom Header (default here:
Authorization).BUILDER_is the exact, whole header value, including anyIO_ WEBHOOK_ SECRET Bearerprefix you typed. Compare it in full; don't strip or parse a prefix. - Look the header up case-insensitively. Return
401when it is missing or doesn't match. - Read the body raw (
express.raw({ type: '*/*' })) and authenticate before parsing JSON. - A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Use HTTPS, and rotate the credential in Builder.io and your environment if it leaks.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Operations
The body is { newValue, previousValue, operation, modelName }. Dispatch on operation.
operation | Sent When |
|---|---|
publish | Content is published (previousValue is null on a first publish) |
unpublish | Content is unpublished |
archive | Content is archived |
delete | Content is deleted (newValue is null) |
scheduledStart | A scheduled publish window starts |
scheduledEnd | A scheduled publish window ends |
Environment Variables
BUILDER_IO_WEBHOOK_HEADER=Authorization # name of the custom header you added
BUILDER_IO_WEBHOOK_SECRET=your-header-value # exact whole value, e.g. "Bearer abc123" if you typed that
Local Development
Builder.io can't reach localhost through its proxy. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 builder-io --path /webhooks/builder-io
Use the printed URL as the webhook URL, and add the secret custom header in Builder.io.
Resources
- references/overview.md - What Builder.io webhooks are, operations and payload shape
- references/setup.md - Add a model webhook and a custom header
- references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests