Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Implement Figma Webhooks V2 for real-time file, comment, and library events. Use when setting up webhook endpoints, handling FILE_UPDATE events, or building event-driven Figma automation. Trigger with phrases like "figma webhook", "figma events", "figma FILE_UPDATE", "figma notifications", "figma real-time".
.claude/skills/jeremylongshore-figma-webhooks-events/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 24% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 110% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 51% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 138% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 15% | 0% |
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.
FIGMA_PAT with webhooks:write scopefigma.com/files/team/<TEAM_ID>/...)bash# 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 |
typescriptimport 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; } }
typescriptconst 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(); }
typescript// 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; }
| 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 |
bash# 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" }'
For performance optimization, see figma-performance-tuning.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 16,521 | 17,772 | +8% | 1 | 1 | 0% | 3,325 | 5,020 | +51% | 0 | 0 | — |
case-02 | fail→pass | 22,307 | 18,817 | -16% | 1 | 1 | 0% | 4,813 | 5,990 | +24% | 0 | 0 | — |
case-03 | fail→pass | 7,902 | 5,616 | -29% | 1 | 1 | 0% | 1,513 | 3,170 | +110% | 0 | 0 | — |
case-04 | pass→pass | 4,787 | 4,009 | -16% | 1 | 1 | 0% | 719 | 2,279 | +217% | 0 | 0 | — |
case-05 | pass→pass | 14,815 | 10,871 | -27% | 1 | 1 | 0% | 2,352 | 3,780 | +61% | 0 | 0 | — |
case-06 | fail→fail | 7,802 | 5,259 | -33% | 1 | 1 | 0% | 1,505 | 2,901 | +93% | 0 | 0 | — |
case-07 | pass→pass | 12,696 | 10,254 | -19% | 1 | 1 | 0% | 2,394 | 3,561 | +49% | 0 | 0 | — |
case-08 | pass→pass | 16,153 | 2,217 | -86% | 1 | 1 | 0% | 2,411 | 2,446 | +1% | 0 | 0 | — |
case-09 | fail→fail | 15,208 | 12,581 | -17% | 1 | 1 | 0% | 2,714 | 4,393 | +62% | 0 | 0 | — |
case-10 | pass→pass | 11,685 | 6,395 | -45% | 1 | 1 | 0% | 1,798 | 3,330 | +85% | 0 | 0 | — |
case-11 | pass→pass | 10,026 | 5,662 | -44% | 1 | 1 | 0% | 1,671 | 2,993 | +79% | 0 | 0 | — |
case-12 | fail→fail | 10,204 | 10,713 | +5% | 1 | 1 | 0% | 1,859 | 3,889 | +109% | 0 | 0 | — |
case-13 | fail→pass | 12,238 | 5,187 | -58% | 1 | 1 | 0% | 1,999 | 3,020 | +51% | 0 | 0 | — |
case-14 | pass→pass | 10,374 | 5,757 | -45% | 1 | 1 | 0% | 1,661 | 3,083 | +86% | 0 | 0 | — |
case-15 | fail→pass | 5,810 | 2,270 | -61% | 1 | 1 | 0% | 1,028 | 2,446 | +138% | 0 | 0 | — |
case-16 | pass→pass | 3,872 | 2,440 | -37% | 1 | 1 | 0% | 652 | 2,414 | +270% | 0 | 0 | — |
case-17 | fail→pass | 16,650 | 6,751 | -59% | 1 | 1 | 0% | 2,633 | 3,033 | +15% | 0 | 0 | — |
case-18 | pass→pass | 4,369 | 3,218 | -26% | 1 | 1 | 0% | 794 | 2,575 | +224% | 0 | 0 | — |
case-19 | pass→pass | 9,785 | 6,244 | -36% | 1 | 1 | 0% | 1,791 | 3,182 | +78% | 0 | 0 | — |
case-20 | pass→pass | 5,701 | 3,329 | -42% | 1 | 1 | 0% | 1,020 | 2,665 | +161% | 0 | 0 | — |
case-21 | fail→pass | 42,285 | 2,468 | -94% | 1 | 1 | 0% | 8,071 | 2,476 | -69% | 0 | 0 | — |
case-22 | pass→pass | 7,194 | 5,430 | -25% | 1 | 1 | 0% | 1,409 | 3,138 | +123% | 0 | 0 | — |
case-23 | pass→pass | 8,723 | 11,567 | +33% | 1 | 1 | 0% | 1,679 | 4,534 | +170% | 0 | 0 | — |
case-24 | pass→pass | 14,413 | 11,272 | -22% | 1 | 1 | 0% | 2,897 | 4,404 | +52% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 24 cases were attempted. The headline lift of +25 percentage points is the difference between those two pass rates over the 24 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.