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, scheduledStart and scheduledEnd operations?
  • 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_IO_WEBHOOK_SECRET is the exact, whole header value, including any Bearer prefix you typed. Compare it in full; don't strip or parse a prefix.
  • Look the header up case-insensitively. Return 401 when 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.

operationSent When
publishContent is published (previousValue is null on a first publish)
unpublishContent is unpublished
archiveContent is archived
deleteContent is deleted (newValue is null)
scheduledStartA scheduled publish window starts
scheduledEndA 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 4, 2026

View on GitHub →