Figma Webhooks & Events
Overview
Figma Webhooks V2 push real-time notifications when files change, comments are posted, or libraries are published. Webhooks can be scoped to teams, projects, or individual files. Authentication uses a passcode echoed back in each payload.
Prerequisites
- HTTPS endpoint accessible from the internet
FIGMA_PATwithwebhooks:writescope- Team ID (from Figma URL:
figma.com/files/team/<TEAM_ID>/...)
Instructions
Step 1: Create a Webhook
# POST /v2/webhooks -- requires webhooks:write scope
curl -X POST https://api.figma.com/v2/webhooks \
-H "X-Figma-Token: ${FIGMA_PAT}" \
-H "Content-Type: application/json" \
-d '{
"event_type": "FILE_UPDATE",
"team_id": "123456789",
"endpoint": "https://yourapp.com/webhooks/figma",
"passcode": "your-secret-passcode",
"description": "Sync design tokens on file update"
}'
# Response:
# { "id": "wh_abc123", "event_type": "FILE_UPDATE", "status": "ACTIVE", ... }
Available event types:
| Event Type | Trigger | Payload Contains |
|------------|---------|-----------------|
| FILE_UPDATE | File saved to version history | file_key, file_name, timestamp |
| FILE_DELETE | File deleted | file_key, file_name |
| FILE_VERSION_UPDATE | Named version created | file_key, version_id, label |
| FILE_COMMENT | Comment added | file_key, comment, comment_id |
| LIBRARY_PUBLISH | Library published | file_key, description, variables |
Step 2: Handle Webhook Events
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.json());
// Figma webhook payload types
interface FigmaWebhookBase {
event_type: string;
passcode: string;
timestamp: string;
webhook_id: string;
}
interface FileUpdateEvent extends FigmaWebhookBase {
event_type: 'FILE_UPDATE';
file_key: string;
file_name: string;
triggered_by: { id: string; handle: string };
}
interface FileCommentEvent extends FigmaWebhookBase {
event_type: 'FILE_COMMENT';
file_key: string;
file_name: string;
comment: Array<{ text: string }>;
comment_id: string;
triggered_by: { id: string; handle: string };
}
interface LibraryPublishEvent extends FigmaWebhookBase {
event_type: 'LIBRARY_PUBLISH';
file_key: string;
file_name: string;
description: string;
triggered_by: { id: string; handle: string };
}
type FigmaWebhookEvent = FileUpdateEvent | FileCommentEvent | LibraryPublishEvent;
app.post('/webhooks/figma', (req, res) => {
const event: FigmaWebhookEvent = req.body;
// 1. Verify passcode (timing-safe)
const expected = process.env.FIGMA_WEBHOOK_PASSCODE!;
if (event.passcode.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(event.passcode), Buffer.from(expected))) {
return res.status(401).json({ error: 'Invalid passcode' });
}
// 2. Respond quickly (Figma expects 200 within seconds)
res.status(200).json({ received: true });
// 3. Process async
processEvent(event).catch(err =>
console.error(`Failed to process ${event.event_type}:`, err)
);
});
async function processEvent(event: FigmaWebhookEvent) {
switch (event.event_type) {
case 'FILE_UPDATE':
console.log(`File updated: ${event.file_name} by ${event.triggered_by.handle}`);
// Re-extract design tokens, invalidate cache, notify Slack
await syncDesignTokens(event.file_key);
break;
case 'FILE_COMMENT':
console.log(`Comment on ${event.file_name}: ${event.comment[0]?.text}`);
// Forward to Slack, create Jira ticket, etc.
break;
case 'LIBRARY_PUBLISH':
console.log(`Library published: ${event.file_name}`);
// Trigger downstream rebuilds
await triggerTokenRebuild(event.file_key);
break;
}
}
Step 3: Manage Webhooks
const FIGMA_API = 'https://api.figma.com';
// List all webhooks for a team
async function listWebhooks(teamId: string) {
const res = await fetch(`${FIGMA_API}/v2/webhooks?team_id=${teamId}`, {
headers: { 'X-Figma-Token': process.env.FIGMA_PAT! },
});
return res.json(); // { webhooks: [...] }
}
// Delete a webhook
async function deleteWebhook(webhookId: string) {
await fetch(`${FIGMA_API}/v2/webhooks/${webhookId}`, {
method: 'DELETE',
headers: { 'X-Figma-Token': process.env.FIGMA_PAT! },
});
}
// Update a webhook (e.g., change endpoint)
async function updateWebhook(webhookId: string, updates: Record<string, any>) {
const res = await fetch(`${FIGMA_API}/v2/webhooks/${webhookId}`, {
method: 'PUT',
headers: {
'X-Figma-Token': process.env.FIGMA_PAT!,
'Content-Type': 'application/json',
},
body: JSON.stringify(updates),
});
return res.json();
}
Step 4: Idempotency for Duplicate Events
// Figma may deliver the same event multiple times
const processedEvents = new Set<string>();
function deduplicateEvent(event: FigmaWebhookEvent): boolean {
const key = `${event.webhook_id}:${event.timestamp}`;
if (processedEvents.has(key)) {
console.log(`Duplicate event skipped: ${key}`);
return false;
}
processedEvents.add(key);
// Clean up old entries (keep last 1000)
if (processedEvents.size > 1000) {
const oldest = Array.from(processedEvents).slice(0, 500);
oldest.forEach(k => processedEvents.delete(k));
}
return true;
}
Output
- Webhook created and receiving Figma events
- Passcode verification on every incoming request
- Event handlers for FILE_UPDATE, FILE_COMMENT, LIBRARY_PUBLISH
- Idempotency preventing duplicate processing
Error Handling
| Issue | Cause | Solution |
|-------|-------|----------|
| Webhook not firing | Endpoint not HTTPS | Figma requires TLS |
| Invalid passcode | Wrong secret configured | Verify passcode in webhook creation |
| Webhook status PAUSED | Too many delivery failures | Fix endpoint, then recreate webhook |
| Missing triggered_by | Older event format | Check webhook V2 vs V1 |
Examples
Test Webhook Locally
# Use ngrok to expose local server
ngrok http 3000
# Create webhook pointing to ngrok URL
curl -X POST https://api.figma.com/v2/webhooks \
-H "X-Figma-Token: ${FIGMA_PAT}" \
-H "Content-Type: application/json" \
-d '{
"event_type": "FILE_UPDATE",
"team_id": "YOUR_TEAM_ID",
"endpoint": "https://YOUR-NGROK.ngrok.io/webhooks/figma",
"passcode": "test-passcode"
}'
Resources
Next Steps
For performance optimization, see figma-performance-tuning.