DocsBack to homepage

Start Here

  • Getting Started
  • Key Concepts

Design Tokens

  • Token Types
  • Token Modes
  • Token Enforcement
  • Deprecated Tokens
  • Quality and Accessibility

Components

  • Component Builder
  • Composition Rules

Publishing

  • Publishing
  • Docs Mode
  • Changelog Notifications
  • Notifications and Alerts

Integrations

CLI & Data

  • CLI Reference
  • CLI Configuration
  • Import Formats
  • Importing Tokens
  • Export Formats

Tooling

  • Studio AI Assistant
  • Figma Plugin
  • API Reference
  • Webhooks

Account & Billing

  • Audit Log
  • Security and Access
  • Account Security
  • Pricing and Payments

Documentation

Webhooks

ReframeUI sends an HTTP POST request to a URL you configure whenever a key event occurs in your workspace. Each request is signed so your server can confirm it came from ReframeUI.

Overview

Webhooks let your infrastructure react to changes in your design system without polling. When an event fires, ReframeUI sends a JSON payload to your configured URL and includes a signature you can verify before processing the request.

Event types

EventWhen it fires
token.publishedA token release is published from a workspace.
version.taggedA version is tagged in a workspace.
branch.mergedA branch is merged into the main line.
member.invitedA new member is invited to the workspace.

Payload shape

Every payload includes event, projectId, and timestamp (ISO 8601, UTC). Event-specific fields appear at the top level of the object.

token.published

{
  "event": "token.published",
  "projectId": "proj_abc123",
  "timestamp": "2024-06-01T12:00:00.000Z",
  "version": "1.4.0",
  "bumpType": "minor",
  "publishedBy": "user_xyz"
}

version.tagged

{
  "event": "version.tagged",
  "projectId": "proj_abc123",
  "timestamp": "2024-06-01T12:05:00.000Z",
  "version": "1.4.0",
  "bumpType": "minor",
  "publishedBy": "user_xyz"
}

branch.merged

{
  "event": "branch.merged",
  "projectId": "proj_abc123",
  "timestamp": "2024-06-01T12:10:00.000Z",
  "branchId": "branch_def456",
  "branchName": "feature/dark-mode",
  "mergedBy": "user_xyz"
}

member.invited

{
  "event": "member.invited",
  "projectId": "proj_abc123",
  "timestamp": "2024-06-01T12:15:00.000Z",
  "inviteeEmail": "designer@example.com",
  "role": "editor",
  "invitedBy": "user_xyz",
  "orgId": "org_ghi789"
}

Request headers

ReframeUI sends four headers with every webhook request.

Content-Type: application/json
X-ReframeUI-Event: token.published
X-Delivery-Id: 018f1a2b-3c4d-7e5f-a6b7-c8d9e0f1a2b3
X-ReframeUI-Signature: sha256=<64-char hex>
HeaderDescription
Content-TypeAlways application/json.
X-ReframeUI-EventThe event type string, e.g. token.published.
X-Delivery-IdA unique UUID per delivery attempt. Use this for deduplication.
X-ReframeUI-SignatureHMAC-SHA256 of the raw request body, formatted as sha256=<hex>.

Verifying signatures

Every request includes an X-ReframeUI-Signature header formatted as:

sha256=<64-character lowercase hex>

To verify the request, compute HMAC-SHA256 over the raw request body using your signing secret, then compare the result to the header value. Always use a constant-time comparison to prevent timing attacks.

Node.js example

import { createHmac, timingSafeEqual } from 'crypto';

function verifyWebhookSignature(rawBody, signingSecret, signatureHeader) {
  const expected = createHmac('sha256', signingSecret)
    .update(rawBody)
    .digest('hex');

  const received = signatureHeader.replace(/^sha256=/, '');

  return timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(received, 'hex'),
  );
}

Read the raw body before any JSON parsing. Parsing and re-serializing the payload can change whitespace and break the signature check.

Reject any request where the signature does not match.

Your signing secret

Your signing secret is shown once when you create a webhook in workspace settings. Copy it immediately — it cannot be retrieved again. If you lose it, delete the webhook and create a new one to get a fresh secret.

Handler examples

Both examples follow the same pattern: capture the raw request body before parsing, verify the signature, acknowledge with a 200, then process the event asynchronously.

Node.js + Express

import express from 'express';
import { createHmac, timingSafeEqual } from 'crypto';

const app = express();

// Capture raw body before JSON parsing
app.post('/webhooks/reframeui', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-reframeui-signature'];

  if (!verifyWebhookSignature(req.body, process.env.REFRAMEUI_SIGNING_SECRET, signature)) {
    return res.status(401).send('Invalid signature');
  }

  // Acknowledge immediately
  res.status(200).send('OK');

  // Process asynchronously
  const event = JSON.parse(req.body.toString());
  processEvent(event).catch(console.error);
});

function verifyWebhookSignature(rawBody, secret, header) {
  const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
  const received = header.replace(/^sha256=/, '');
  return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(received, 'hex'));
}

async function processEvent(event) {
  // Handle event types
  if (event.event === 'token.published') {
    await triggerSync(event.projectId, event.version);
  }
}

Next.js App Router

import { createHmac, timingSafeEqual } from 'crypto';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(req: NextRequest) {
  // Capture raw body before any parsing
  const rawBody = await req.text();
  const signature = req.headers.get('x-reframeui-signature') ?? '';

  if (!verifyWebhookSignature(rawBody, process.env.REFRAMEUI_SIGNING_SECRET!, signature)) {
    return new NextResponse('Invalid signature', { status: 401 });
  }

  // Acknowledge immediately
  const event = JSON.parse(rawBody);

  // Process asynchronously — do not await long-running work here
  processEvent(event).catch(console.error);

  return NextResponse.json({ ok: true });
}

function verifyWebhookSignature(rawBody: string, secret: string, header: string): boolean {
  const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
  const received = header.replace(/^sha256=/, '');
  return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(received, 'hex'));
}

async function processEvent(event: Record<string, unknown>) {
  if (event.event === 'token.published') {
    await triggerSync(event.projectId as string, event.version as string);
  }
}

Retries

ReframeUI makes one initial attempt and up to 3 retries, for a total of 4 attempts. Each request times out after 10 seconds. If all 4 attempts fail, an in-app notification is sent to the project.

Respond with a 2xx status as quickly as possible and process the event asynchronously. Long-running work in the response handler will cause timeouts.

Managing webhooks

Use the REST API to register and manage webhooks programmatically. Only workspace owners can create, update, or delete webhooks.

List webhooks

List all webhooks for a project. The signing secret is never included in list responses.

GET /api/projects/:projectId/webhooks
curl https://reframeui.app/api/projects/proj_abc123/webhooks \
  -H "Authorization: Bearer <token>"

Create a webhook

Create a webhook. Requires a url (must be https) and an events array. The signing secret is returned once on creation and cannot be retrieved again. Store it securely.

POST /api/projects/:projectId/webhooks
curl -X POST https://reframeui.app/api/projects/proj_abc123/webhooks \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/reframeui",
    "events": ["token.published", "branch.merged"]
  }'

Response (201)

{
  "id": "wh_abc123",
  "url": "https://example.com/webhooks/reframeui",
  "events": ["token.published", "branch.merged"],
  "createdAt": "2024-06-01T12:00:00.000Z",
  "secret": "a1b2c3d4e5f6..."
}

Update a webhook

Update the URL or events for an existing webhook.

PATCH /api/projects/:projectId/webhooks/:webhookId
curl -X PATCH https://reframeui.app/api/projects/proj_abc123/webhooks/wh_abc123 \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "events": ["token.published", "branch.merged", "member.invited"] }'

Delete a webhook

Delete a webhook. Returns 204 with no body.

DELETE /api/projects/:projectId/webhooks/:webhookId
curl -X DELETE https://reframeui.app/api/projects/proj_abc123/webhooks/wh_abc123 \
  -H "Authorization: Bearer <token>"

List delivery attempts

List the last 50 delivery attempts for a webhook.

GET /api/projects/:projectId/webhooks/:webhookId/deliveries
curl https://reframeui.app/api/projects/proj_abc123/webhooks/wh_abc123/deliveries \
  -H "Authorization: Bearer <token>"

Next steps

Build on webhooks with the full REST API, including project, token, and member endpoints.

API Reference →