figma-code-agent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +133 -0
  2. package/bin/install.js +328 -0
  3. package/knowledge/README.md +62 -0
  4. package/knowledge/css-strategy.md +973 -0
  5. package/knowledge/design-to-code-assets.md +855 -0
  6. package/knowledge/design-to-code-layout.md +929 -0
  7. package/knowledge/design-to-code-semantic.md +1085 -0
  8. package/knowledge/design-to-code-typography.md +1003 -0
  9. package/knowledge/design-to-code-visual.md +1145 -0
  10. package/knowledge/design-tokens-variables.md +1261 -0
  11. package/knowledge/design-tokens.md +960 -0
  12. package/knowledge/figma-api-devmode.md +894 -0
  13. package/knowledge/figma-api-plugin.md +920 -0
  14. package/knowledge/figma-api-rest.md +742 -0
  15. package/knowledge/figma-api-variables.md +848 -0
  16. package/knowledge/figma-api-webhooks.md +876 -0
  17. package/knowledge/payload-blocks.md +1184 -0
  18. package/knowledge/payload-figma-mapping.md +1210 -0
  19. package/knowledge/payload-visual-builder.md +1004 -0
  20. package/knowledge/plugin-architecture.md +1176 -0
  21. package/knowledge/plugin-best-practices.md +1206 -0
  22. package/knowledge/plugin-codegen.md +1313 -0
  23. package/package.json +31 -0
  24. package/skills/README.md +103 -0
  25. package/skills/audit-plugin/SKILL.md +244 -0
  26. package/skills/build-codegen-plugin/SKILL.md +279 -0
  27. package/skills/build-importer/SKILL.md +320 -0
  28. package/skills/build-plugin/SKILL.md +199 -0
  29. package/skills/build-token-pipeline/SKILL.md +363 -0
  30. package/skills/ref-html/SKILL.md +290 -0
  31. package/skills/ref-layout/SKILL.md +150 -0
  32. package/skills/ref-payload-block/SKILL.md +415 -0
  33. package/skills/ref-react/SKILL.md +222 -0
  34. package/skills/ref-tokens/SKILL.md +347 -0
@@ -0,0 +1,876 @@
1
+ # Figma Webhooks v2 Reference
2
+
3
+ ## Purpose
4
+
5
+ Authoritative reference for Figma Webhooks v2 covering webhook scoping via `context` + `context_id`, all CRUD endpoints, event types with payload structures, operational considerations (passcode verification, retries, idempotency), and practical webhook listener patterns. This module documents the server-side integration for reacting to Figma file changes in real time.
6
+
7
+ ## When to Use
8
+
9
+ Reference this module when you need to:
10
+
11
+ - Set up webhook listeners for Figma file change notifications
12
+ - Create, update, or delete webhooks via the REST API
13
+ - Handle webhook event payloads (FILE_UPDATE, LIBRARY_PUBLISH, etc.)
14
+ - Verify webhook authenticity using passcode comparison
15
+ - Understand retry behavior and operational requirements
16
+ - Build automated pipelines triggered by Figma design changes
17
+
18
+ ---
19
+
20
+ ## Content
21
+
22
+ ### Webhooks v2 Overview
23
+
24
+ Webhooks enable your server to receive HTTP POST notifications when specific events occur in Figma files, projects, or teams. Webhooks v2 uses a `context` + `context_id` scoping model that supports team-level, project-level, and file-level granularity.
25
+
26
+ > **Critical:** Webhook creation uses `context` + `context_id` fields, NOT the deprecated `team_id` field. The `team_id` field in the WebhookV2 response object is a legacy read-only field (empty string for project/file-scoped webhooks).
27
+
28
+ #### Scoping Model
29
+
30
+ | Context | `context` value | `context_id` value | Who can create | Receives events for |
31
+ |---------|----------------|-------------------|----------------|-------------------|
32
+ | Team | `"team"` | Team ID | Team admins | All files available to team members (excludes invite-only project files) |
33
+ | Project | `"project"` | Project ID | Users with edit access to project | All files in the project |
34
+ | File | `"file"` | File key | Users with edit access to file | The specific file only |
35
+
36
+ #### Webhook Limits
37
+
38
+ | Context | Max webhooks per context |
39
+ |---------|:------------------------:|
40
+ | Team | 20 |
41
+ | Project | 5 |
42
+ | File | 3 |
43
+
44
+ **File webhook totals by plan:**
45
+
46
+ | Plan | Max file webhooks total |
47
+ |------|:-----------------------:|
48
+ | Professional | 150 |
49
+ | Organization | 300 |
50
+ | Enterprise | 600 |
51
+
52
+ #### Webhook Statuses
53
+
54
+ | Status | Description |
55
+ |--------|-------------|
56
+ | `ACTIVE` | The webhook is healthy and receives all events |
57
+ | `PAUSED` | The webhook is paused and will not receive any events |
58
+
59
+ > **Note:** You cannot programmatically set a webhook to an error state. Figma manages error states internally based on delivery failures.
60
+
61
+ #### WebhookV2 Object
62
+
63
+ ```ts
64
+ interface WebhookV2 {
65
+ /** Unique webhook identifier */
66
+ id: number;
67
+
68
+ /** The event type this webhook listens for */
69
+ event_type: WebhookV2Event;
70
+
71
+ /** Scoping context: "team", "project", or "file" */
72
+ context: 'team' | 'project' | 'file';
73
+
74
+ /** ID of the context (team ID, project ID, or file key) */
75
+ context_id: string;
76
+
77
+ /** Team subscription ID (empty string for project/file webhooks) */
78
+ team_id: string;
79
+
80
+ /** Team/organization plan API ID */
81
+ plan_api_id: string;
82
+
83
+ /** Current webhook status */
84
+ status: 'ACTIVE' | 'PAUSED';
85
+
86
+ /** OAuth application identifier (if created via OAuth) */
87
+ client_id: string;
88
+
89
+ /** Security passcode (empty in GET responses for security) */
90
+ passcode: string;
91
+
92
+ /** Target URL for webhook event delivery */
93
+ endpoint: string;
94
+
95
+ /** Optional user-provided description (max 140 characters) */
96
+ description: string;
97
+ }
98
+ ```
99
+
100
+ ---
101
+
102
+ ### Webhook Endpoints
103
+
104
+ All webhook endpoints use the `/v2/` prefix (unlike most Figma API endpoints which use `/v1/`).
105
+
106
+ **Authentication:** All endpoints require a Figma API token (PAT or OAuth) with the appropriate scope.
107
+
108
+ | Operation | Scope Required |
109
+ |-----------|---------------|
110
+ | Read webhooks | `webhooks:read` |
111
+ | Create / Update / Delete | `webhooks:write` |
112
+
113
+ **Rate limit tier:** All webhook endpoints are Tier 2.
114
+
115
+ #### POST /v2/webhooks — Create Webhook
116
+
117
+ Creates a new webhook subscription. On success, Figma immediately sends a `PING` event to the endpoint (unless `status` is set to `PAUSED`).
118
+
119
+ ```bash
120
+ curl -X POST \
121
+ -H "X-Figma-Token: YOUR_FIGMA_TOKEN" \
122
+ -H "Content-Type: application/json" \
123
+ -d '{
124
+ "event_type": "FILE_UPDATE",
125
+ "context": "team",
126
+ "context_id": "123456",
127
+ "endpoint": "https://your-server.com/figma-webhook",
128
+ "passcode": "YOUR_WEBHOOK_PASSCODE"
129
+ }' \
130
+ "https://api.figma.com/v2/webhooks"
131
+ ```
132
+
133
+ **Request body fields:**
134
+
135
+ | Field | Type | Required | Description |
136
+ |-------|------|:--------:|-------------|
137
+ | `event_type` | string | Yes | Event type to subscribe to (see Event Types section) |
138
+ | `context` | string | Yes | `"team"`, `"project"`, or `"file"` |
139
+ | `context_id` | string | Yes | The ID of the team, project, or file |
140
+ | `endpoint` | string | Yes | URL to receive webhook payloads (max 2048 characters) |
141
+ | `passcode` | string | Yes | Secret passcode echoed back in payloads for verification (max 100 characters) |
142
+ | `status` | string | No | Initial status: `"ACTIVE"` (default) or `"PAUSED"` |
143
+ | `description` | string | No | Human-readable description (max 150 characters) |
144
+
145
+ **Response:** Returns the created `WebhookV2` object.
146
+
147
+ > **Important:** The endpoint must be ready to receive and acknowledge the `PING` event before or immediately after creation. If the PING fails, the webhook is still created but may be in a degraded state.
148
+
149
+ #### GET /v2/webhooks/:webhook_id — Get Webhook
150
+
151
+ Retrieves a single webhook by ID.
152
+
153
+ ```bash
154
+ curl -H "X-Figma-Token: YOUR_FIGMA_TOKEN" \
155
+ "https://api.figma.com/v2/webhooks/WEBHOOK_ID"
156
+ ```
157
+
158
+ **Response:** Returns the `WebhookV2` object (with `passcode` as empty string for security).
159
+
160
+ #### GET /v2/webhooks — List Webhooks
161
+
162
+ Lists webhooks with optional filtering by context.
163
+
164
+ ```bash
165
+ # List all webhooks for a team
166
+ curl -H "X-Figma-Token: YOUR_FIGMA_TOKEN" \
167
+ "https://api.figma.com/v2/webhooks?context=team&context_id=123456"
168
+
169
+ # List all webhooks for a file
170
+ curl -H "X-Figma-Token: YOUR_FIGMA_TOKEN" \
171
+ "https://api.figma.com/v2/webhooks?context=file&context_id=FILE_KEY"
172
+ ```
173
+
174
+ **Query parameters:**
175
+
176
+ | Parameter | Type | Description |
177
+ |-----------|------|-------------|
178
+ | `context` | string | Filter by context type (`"team"`, `"project"`, `"file"`) |
179
+ | `context_id` | string | Filter by context ID (used with `context`) |
180
+ | `plan_api_id` | string | Filter by plan API ID (cannot combine with `context`/`context_id`) |
181
+ | `cursor` | string | Pagination cursor from a previous response |
182
+
183
+ **Response:**
184
+
185
+ ```ts
186
+ interface ListWebhooksResponse {
187
+ webhooks: WebhookV2[];
188
+ pagination: {
189
+ next_page?: string;
190
+ prev_page?: string;
191
+ };
192
+ }
193
+ ```
194
+
195
+ > **Note:** The deprecated `GET /v2/teams/:team_id/webhooks` endpoint is replaced by `GET /v2/webhooks?context=team&context_id=TEAM_ID`.
196
+
197
+ #### PUT /v2/webhooks/:webhook_id — Update Webhook
198
+
199
+ Updates an existing webhook. Only include the fields you want to change.
200
+
201
+ ```bash
202
+ curl -X PUT \
203
+ -H "X-Figma-Token: YOUR_FIGMA_TOKEN" \
204
+ -H "Content-Type: application/json" \
205
+ -d '{
206
+ "event_type": "FILE_VERSION_UPDATE",
207
+ "status": "PAUSED"
208
+ }' \
209
+ "https://api.figma.com/v2/webhooks/WEBHOOK_ID"
210
+ ```
211
+
212
+ **Updatable fields:**
213
+
214
+ | Field | Type | Description |
215
+ |-------|------|-------------|
216
+ | `event_type` | string | Change the event type |
217
+ | `endpoint` | string | Change the delivery URL |
218
+ | `passcode` | string | Change the verification passcode |
219
+ | `status` | string | `"ACTIVE"` or `"PAUSED"` (cannot set to error state) |
220
+ | `description` | string | Change the description |
221
+
222
+ **Response:** Returns the updated `WebhookV2` object.
223
+
224
+ #### DELETE /v2/webhooks/:webhook_id — Delete Webhook
225
+
226
+ Permanently deletes a webhook. This operation is irreversible.
227
+
228
+ ```bash
229
+ curl -X DELETE \
230
+ -H "X-Figma-Token: YOUR_FIGMA_TOKEN" \
231
+ "https://api.figma.com/v2/webhooks/WEBHOOK_ID"
232
+ ```
233
+
234
+ **Response:** Returns the deleted `WebhookV2` object.
235
+
236
+ #### GET /v2/webhooks/:webhook_id/requests — Debug Webhook Activity
237
+
238
+ Retrieves the delivery history for a webhook from the last 7 days. Useful for debugging delivery issues.
239
+
240
+ ```bash
241
+ curl -H "X-Figma-Token: YOUR_FIGMA_TOKEN" \
242
+ "https://api.figma.com/v2/webhooks/WEBHOOK_ID/requests"
243
+ ```
244
+
245
+ ---
246
+
247
+ ### Event Types
248
+
249
+ Each webhook subscribes to exactly one event type. To listen for multiple event types, create multiple webhooks.
250
+
251
+ #### PING
252
+
253
+ Sent automatically when a webhook is created (unless created with `status: "PAUSED"`). Used to verify that your endpoint is configured correctly.
254
+
255
+ **Payload:**
256
+
257
+ ```json
258
+ {
259
+ "event_type": "PING",
260
+ "passcode": "YOUR_WEBHOOK_PASSCODE",
261
+ "timestamp": "2026-01-15T10:30:00Z",
262
+ "webhook_id": 12345
263
+ }
264
+ ```
265
+
266
+ > **Important:** The PING event does NOT include `file_key` or `file_name`. It is the only event type with this minimal payload structure.
267
+
268
+ #### FILE_UPDATE
269
+
270
+ Triggers within **30 minutes of editing inactivity** in a file. This is a debounced event — it does not fire on every keystroke or edit, but after the file has been idle for a period.
271
+
272
+ **Payload:**
273
+
274
+ ```json
275
+ {
276
+ "event_type": "FILE_UPDATE",
277
+ "file_key": "abc123DEF456",
278
+ "file_name": "Design System v2",
279
+ "passcode": "YOUR_WEBHOOK_PASSCODE",
280
+ "timestamp": "2026-01-15T10:30:00Z",
281
+ "webhook_id": 12345
282
+ }
283
+ ```
284
+
285
+ #### FILE_DELETE
286
+
287
+ Triggers when a file is deleted. Does NOT trigger for files within deleted folders — only for directly deleted files.
288
+
289
+ **Payload:**
290
+
291
+ ```json
292
+ {
293
+ "event_type": "FILE_DELETE",
294
+ "file_key": "abc123DEF456",
295
+ "file_name": "Deleted Design",
296
+ "passcode": "YOUR_WEBHOOK_PASSCODE",
297
+ "timestamp": "2026-01-15T10:30:00Z",
298
+ "triggered_by": {
299
+ "id": "12345",
300
+ "handle": "designer@example.com"
301
+ },
302
+ "webhook_id": 12345
303
+ }
304
+ ```
305
+
306
+ #### FILE_VERSION_UPDATE
307
+
308
+ Triggers when a user creates a named version in the file's version history.
309
+
310
+ **Payload:**
311
+
312
+ ```json
313
+ {
314
+ "event_type": "FILE_VERSION_UPDATE",
315
+ "file_key": "abc123DEF456",
316
+ "file_name": "Design System v2",
317
+ "created_at": "2026-01-15T10:30:00Z",
318
+ "version_id": "987654321",
319
+ "label": "v2.1 Release",
320
+ "description": "Updated button variants and color tokens",
321
+ "passcode": "YOUR_WEBHOOK_PASSCODE",
322
+ "timestamp": "2026-01-15T10:30:00Z",
323
+ "triggered_by": {
324
+ "id": "12345",
325
+ "handle": "designer@example.com"
326
+ },
327
+ "webhook_id": 12345
328
+ }
329
+ ```
330
+
331
+ #### FILE_COMMENT
332
+
333
+ Triggers when a user posts a comment on a file.
334
+
335
+ **Payload:**
336
+
337
+ ```json
338
+ {
339
+ "event_type": "FILE_COMMENT",
340
+ "file_key": "abc123DEF456",
341
+ "file_name": "Design System v2",
342
+ "comment": [
343
+ { "text": "Can we update the " },
344
+ { "mention": "67890" },
345
+ { "text": " color here?" }
346
+ ],
347
+ "comment_id": 1001,
348
+ "mentions": [
349
+ { "id": "67890", "handle": "dev@example.com" }
350
+ ],
351
+ "order_id": 5,
352
+ "parent_id": null,
353
+ "created_at": "2026-01-15T10:30:00Z",
354
+ "resolved_at": null,
355
+ "passcode": "YOUR_WEBHOOK_PASSCODE",
356
+ "timestamp": "2026-01-15T10:30:00Z",
357
+ "triggered_by": {
358
+ "id": "12345",
359
+ "handle": "designer@example.com"
360
+ },
361
+ "webhook_id": 12345
362
+ }
363
+ ```
364
+
365
+ **Comment-specific fields:**
366
+
367
+ | Field | Type | Description |
368
+ |-------|------|-------------|
369
+ | `comment` | CommentFragment[] | Array of text and mention fragments |
370
+ | `comment_id` | number | Unique comment identifier |
371
+ | `mentions` | User[] | Array of mentioned users |
372
+ | `order_id` | number | Top-level comment display number |
373
+ | `parent_id` | number or null | Parent comment ID if this is a reply |
374
+ | `created_at` | string | Comment creation timestamp |
375
+ | `resolved_at` | string or null | When the comment was resolved (null if unresolved) |
376
+
377
+ #### LIBRARY_PUBLISH
378
+
379
+ Triggers when a library file is published. Large publications may generate multiple events split by asset type.
380
+
381
+ **Payload:**
382
+
383
+ ```json
384
+ {
385
+ "event_type": "LIBRARY_PUBLISH",
386
+ "file_key": "abc123DEF456",
387
+ "file_name": "Design System v2",
388
+ "description": "Added new icon set and updated color tokens",
389
+ "created_components": [
390
+ { "key": "comp:abc", "name": "Icon/Arrow" }
391
+ ],
392
+ "created_styles": [],
393
+ "created_variables": [
394
+ { "key": "var:def", "name": "color/accent" }
395
+ ],
396
+ "modified_components": [
397
+ { "key": "comp:xyz", "name": "Button/Primary" }
398
+ ],
399
+ "modified_styles": [
400
+ { "key": "style:123", "name": "Heading/H1" }
401
+ ],
402
+ "modified_variables": [],
403
+ "deleted_components": [],
404
+ "deleted_styles": [],
405
+ "deleted_variables": [],
406
+ "passcode": "YOUR_WEBHOOK_PASSCODE",
407
+ "timestamp": "2026-01-15T10:30:00Z",
408
+ "triggered_by": {
409
+ "id": "12345",
410
+ "handle": "designer@example.com"
411
+ },
412
+ "webhook_id": 12345
413
+ }
414
+ ```
415
+
416
+ **Library item data:**
417
+
418
+ ```ts
419
+ interface LibraryItemData {
420
+ key: string; // Unique key for the library item
421
+ name: string; // Display name
422
+ }
423
+ ```
424
+
425
+ The `created_*`, `modified_*`, and `deleted_*` arrays provide granular detail about what changed in the library publish, including components, styles, and variables.
426
+
427
+ #### DEV_MODE_STATUS_UPDATE
428
+
429
+ Triggers when the Dev Mode status changes for a layer in a file (e.g., marked as "Ready for Dev" or "Completed").
430
+
431
+ **Payload:**
432
+
433
+ ```json
434
+ {
435
+ "event_type": "DEV_MODE_STATUS_UPDATE",
436
+ "file_key": "abc123DEF456",
437
+ "node_id": "1:234",
438
+ "status": "READY_FOR_DEV",
439
+ "change_message": "Header component ready for implementation",
440
+ "related_links": [
441
+ {
442
+ "id": "dev-resource-id",
443
+ "name": "React Component",
444
+ "url": "https://github.com/org/repo/blob/main/src/Header.tsx",
445
+ "file_key": "abc123DEF456",
446
+ "node_id": "1:234"
447
+ }
448
+ ],
449
+ "passcode": "YOUR_WEBHOOK_PASSCODE",
450
+ "timestamp": "2026-01-15T10:30:00Z",
451
+ "triggered_by": {
452
+ "id": "12345",
453
+ "handle": "designer@example.com"
454
+ },
455
+ "webhook_id": 12345
456
+ }
457
+ ```
458
+
459
+ **Status values:** `NONE`, `READY_FOR_DEV`, `COMPLETED`
460
+
461
+ #### Common Payload Fields
462
+
463
+ Every webhook payload includes these fields:
464
+
465
+ | Field | Type | Present in | Description |
466
+ |-------|------|-----------|-------------|
467
+ | `event_type` | string | All events | The event type identifier |
468
+ | `passcode` | string | All events | The passcode you provided at webhook creation (for verification) |
469
+ | `timestamp` | string | All events | UTC ISO 8601 timestamp of the event |
470
+ | `webhook_id` | number | All events | The webhook ID that generated this event |
471
+ | `file_key` | string | All except PING | The file key that triggered the event |
472
+ | `file_name` | string | All except PING and DEV_MODE_STATUS_UPDATE | The file name |
473
+ | `triggered_by` | User | All except PING and FILE_UPDATE | The user who triggered the event |
474
+
475
+ ---
476
+
477
+ ### Operational Considerations
478
+
479
+ #### Passcode Verification
480
+
481
+ Every webhook payload includes the `passcode` field, which echoes back the secret you provided when creating the webhook. **Always verify this passcode** to confirm the request originated from Figma.
482
+
483
+ ```ts
484
+ function verifyWebhookPasscode(
485
+ payload: Record<string, any>,
486
+ expectedPasscode: string
487
+ ): boolean {
488
+ return payload.passcode === expectedPasscode;
489
+ }
490
+ ```
491
+
492
+ > **Warning:** The passcode is NOT a cryptographic signature. It is a shared secret sent in the request body. Always use HTTPS for your webhook endpoint to protect the passcode in transit.
493
+
494
+ #### Response Requirements
495
+
496
+ Your webhook endpoint must respond with a **200 OK** status code **quickly** (ideally within a few seconds). Figma interprets slow responses or non-200 status codes as failures and will initiate retry logic.
497
+
498
+ **Best practice:** Accept the webhook payload, return 200 immediately, then process the event asynchronously:
499
+
500
+ ```ts
501
+ // Good: Respond immediately, process later
502
+ app.post('/figma-webhook', (req, res) => {
503
+ res.status(200).send('OK');
504
+ processEventAsync(req.body); // Fire-and-forget
505
+ });
506
+
507
+ // Bad: Process synchronously before responding
508
+ app.post('/figma-webhook', async (req, res) => {
509
+ await heavyProcessing(req.body); // May timeout
510
+ res.status(200).send('OK');
511
+ });
512
+ ```
513
+
514
+ #### Retry Behavior
515
+
516
+ When Figma fails to deliver a webhook event (non-200 response or timeout), it retries with **exponential backoff**:
517
+
518
+ | Retry | Delay after failure |
519
+ |:-----:|:-------------------:|
520
+ | 1st | 5 minutes |
521
+ | 2nd | 30 minutes |
522
+ | 3rd | 3 hours |
523
+
524
+ After **3 failed retries**, the delivery is abandoned for that event. Figma does not retry further for that specific event.
525
+
526
+ #### Idempotency
527
+
528
+ The same event may be delivered more than once (e.g., if your server responded slowly and Figma retried). **Always handle webhook events idempotently:**
529
+
530
+ - Use `webhook_id` + `timestamp` + `event_type` as a deduplication key
531
+ - Track processed event identifiers in a store (database, Redis, etc.)
532
+ - Ensure that processing the same event twice produces the same result
533
+
534
+ ```ts
535
+ const processedEvents = new Set<string>();
536
+
537
+ function handleWebhookEvent(payload: Record<string, any>): boolean {
538
+ const eventKey = `${payload.webhook_id}:${payload.event_type}:${payload.timestamp}`;
539
+
540
+ if (processedEvents.has(eventKey)) {
541
+ return false; // Already processed — skip
542
+ }
543
+
544
+ processedEvents.add(eventKey);
545
+ return true; // Process this event
546
+ }
547
+ ```
548
+
549
+ > **Note:** In production, use a persistent store (database or cache with TTL) instead of an in-memory Set to survive server restarts.
550
+
551
+ #### Webhook Health and Disabling
552
+
553
+ Figma monitors webhook delivery success. After repeated delivery failures, Figma may internally flag the webhook. Use the requests endpoint to debug:
554
+
555
+ ```bash
556
+ # Check delivery history for the last 7 days
557
+ curl -H "X-Figma-Token: YOUR_FIGMA_TOKEN" \
558
+ "https://api.figma.com/v2/webhooks/WEBHOOK_ID/requests"
559
+ ```
560
+
561
+ If a webhook becomes unreliable:
562
+ 1. Check the requests endpoint for error details
563
+ 2. Fix the endpoint issue
564
+ 3. If paused, update the webhook status back to `ACTIVE`
565
+
566
+ #### Team Context Notification Scope
567
+
568
+ Team-scoped webhooks receive events for:
569
+ - Files available to all team members
570
+ - Files in view-only projects
571
+
572
+ Team-scoped webhooks do **NOT** receive events for:
573
+ - Files in invite-only projects (use project-scoped or file-scoped webhooks for these)
574
+
575
+ ---
576
+
577
+ ### Practical Patterns
578
+
579
+ #### Setting Up a Webhook Listener (Express)
580
+
581
+ ```ts
582
+ import express from 'express';
583
+
584
+ const app = express();
585
+ app.use(express.json());
586
+
587
+ const WEBHOOK_PASSCODE = process.env.FIGMA_WEBHOOK_PASSCODE!;
588
+
589
+ app.post('/figma-webhook', (req, res) => {
590
+ const payload = req.body;
591
+
592
+ // 1. Verify passcode
593
+ if (payload.passcode !== WEBHOOK_PASSCODE) {
594
+ console.warn('Invalid webhook passcode — rejecting request');
595
+ res.status(401).send('Unauthorized');
596
+ return;
597
+ }
598
+
599
+ // 2. Respond immediately
600
+ res.status(200).send('OK');
601
+
602
+ // 3. Handle event asynchronously
603
+ handleFigmaEvent(payload).catch(err => {
604
+ console.error('Webhook processing error:', err);
605
+ });
606
+ });
607
+
608
+ async function handleFigmaEvent(payload: Record<string, any>): Promise<void> {
609
+ switch (payload.event_type) {
610
+ case 'PING':
611
+ console.log('Webhook verified:', payload.webhook_id);
612
+ break;
613
+
614
+ case 'FILE_UPDATE':
615
+ console.log(`File updated: ${payload.file_name} (${payload.file_key})`);
616
+ await syncFileChanges(payload.file_key);
617
+ break;
618
+
619
+ case 'FILE_VERSION_UPDATE':
620
+ console.log(`New version: ${payload.label} for ${payload.file_name}`);
621
+ await processNewVersion(payload.file_key, payload.version_id);
622
+ break;
623
+
624
+ case 'LIBRARY_PUBLISH':
625
+ console.log(`Library published: ${payload.file_name}`);
626
+ await syncLibraryChanges(payload);
627
+ break;
628
+
629
+ case 'FILE_COMMENT':
630
+ console.log(`Comment on ${payload.file_name} by ${payload.triggered_by.handle}`);
631
+ await notifyTeam(payload);
632
+ break;
633
+
634
+ case 'FILE_DELETE':
635
+ console.log(`File deleted: ${payload.file_name}`);
636
+ await handleFileDeletion(payload.file_key);
637
+ break;
638
+
639
+ case 'DEV_MODE_STATUS_UPDATE':
640
+ console.log(`Dev status changed: ${payload.status} for node ${payload.node_id}`);
641
+ await handleDevStatusChange(payload);
642
+ break;
643
+
644
+ default:
645
+ console.warn('Unknown event type:', payload.event_type);
646
+ }
647
+ }
648
+
649
+ app.listen(3000, () => console.log('Webhook listener running on port 3000'));
650
+ ```
651
+
652
+ #### Setting Up a Webhook Listener (Next.js API Route)
653
+
654
+ ```ts
655
+ // app/api/figma-webhook/route.ts
656
+ import { NextRequest, NextResponse } from 'next/server';
657
+
658
+ const WEBHOOK_PASSCODE = process.env.FIGMA_WEBHOOK_PASSCODE!;
659
+
660
+ export async function POST(request: NextRequest) {
661
+ const payload = await request.json();
662
+
663
+ // Verify passcode
664
+ if (payload.passcode !== WEBHOOK_PASSCODE) {
665
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
666
+ }
667
+
668
+ // Respond immediately — Next.js edge/serverless functions have timeouts
669
+ // Queue async processing to a background job system if heavy processing needed
670
+
671
+ switch (payload.event_type) {
672
+ case 'PING':
673
+ console.log('Webhook verified:', payload.webhook_id);
674
+ break;
675
+
676
+ case 'FILE_UPDATE':
677
+ // Queue for background processing
678
+ await queueJob('figma-file-sync', { fileKey: payload.file_key });
679
+ break;
680
+
681
+ case 'LIBRARY_PUBLISH':
682
+ await queueJob('figma-token-sync', {
683
+ fileKey: payload.file_key,
684
+ createdVariables: payload.created_variables,
685
+ modifiedVariables: payload.modified_variables,
686
+ deletedVariables: payload.deleted_variables,
687
+ });
688
+ break;
689
+ }
690
+
691
+ return NextResponse.json({ received: true }, { status: 200 });
692
+ }
693
+ ```
694
+
695
+ #### Creating a Webhook Programmatically
696
+
697
+ ```ts
698
+ async function createFigmaWebhook(options: {
699
+ eventType: string;
700
+ context: 'team' | 'project' | 'file';
701
+ contextId: string;
702
+ endpoint: string;
703
+ passcode: string;
704
+ description?: string;
705
+ }): Promise<WebhookV2> {
706
+ const response = await fetch('https://api.figma.com/v2/webhooks', {
707
+ method: 'POST',
708
+ headers: {
709
+ 'X-Figma-Token': process.env.FIGMA_TOKEN!,
710
+ 'Content-Type': 'application/json',
711
+ },
712
+ body: JSON.stringify({
713
+ event_type: options.eventType,
714
+ context: options.context,
715
+ context_id: options.contextId,
716
+ endpoint: options.endpoint,
717
+ passcode: options.passcode,
718
+ description: options.description,
719
+ }),
720
+ });
721
+
722
+ if (!response.ok) {
723
+ const error = await response.json();
724
+ throw new Error(`Failed to create webhook: ${response.status} ${JSON.stringify(error)}`);
725
+ }
726
+
727
+ return response.json();
728
+ }
729
+
730
+ // Example: Listen for file updates on a team
731
+ const webhook = await createFigmaWebhook({
732
+ eventType: 'FILE_UPDATE',
733
+ context: 'team',
734
+ contextId: '123456',
735
+ endpoint: 'https://your-server.com/figma-webhook',
736
+ passcode: process.env.FIGMA_WEBHOOK_PASSCODE!,
737
+ description: 'Design-to-code sync trigger',
738
+ });
739
+
740
+ console.log('Webhook created:', webhook.id);
741
+ ```
742
+
743
+ #### Processing FILE_UPDATE for Design-to-Code Sync
744
+
745
+ ```ts
746
+ async function syncFileChanges(fileKey: string): Promise<void> {
747
+ // 1. Fetch the latest file version
748
+ const fileRes = await fetch(
749
+ `https://api.figma.com/v1/files/${fileKey}?depth=2`,
750
+ { headers: { 'X-Figma-Token': process.env.FIGMA_TOKEN! } }
751
+ );
752
+ const file = await fileRes.json();
753
+
754
+ // 2. Compare with cached version
755
+ const cachedVersion = await getStoredVersion(fileKey);
756
+ if (file.version === cachedVersion) {
757
+ console.log('No changes detected (same version)');
758
+ return;
759
+ }
760
+
761
+ // 3. Extract updated components and tokens
762
+ const components = findComponents(file.document);
763
+ const tokens = await extractTokens(fileKey);
764
+
765
+ // 4. Regenerate code for changed components
766
+ for (const component of components) {
767
+ await generateCode(component, tokens);
768
+ }
769
+
770
+ // 5. Update stored version
771
+ await storeVersion(fileKey, file.version);
772
+ console.log(`Synced ${components.length} components from ${file.name}`);
773
+ }
774
+ ```
775
+
776
+ #### Processing LIBRARY_PUBLISH for Token Sync
777
+
778
+ ```ts
779
+ async function syncLibraryChanges(payload: Record<string, any>): Promise<void> {
780
+ const { file_key, created_variables, modified_variables, deleted_variables } = payload;
781
+
782
+ const hasVariableChanges =
783
+ created_variables?.length > 0 ||
784
+ modified_variables?.length > 0 ||
785
+ deleted_variables?.length > 0;
786
+
787
+ if (!hasVariableChanges) {
788
+ console.log('No variable changes in this publish — skipping token sync');
789
+ return;
790
+ }
791
+
792
+ // Fetch the latest variables from the file
793
+ const varsRes = await fetch(
794
+ `https://api.figma.com/v1/files/${file_key}/variables/local`,
795
+ { headers: { 'X-Figma-Token': process.env.FIGMA_TOKEN! } }
796
+ );
797
+ const { meta } = await varsRes.json();
798
+
799
+ // Regenerate CSS custom properties
800
+ const cssOutput = generateCSSVariables(meta.variables, meta.variableCollections);
801
+
802
+ // Write to tokens file
803
+ await writeTokensFile(cssOutput);
804
+
805
+ console.log(`Token sync complete: ${created_variables?.length || 0} created, ` +
806
+ `${modified_variables?.length || 0} modified, ${deleted_variables?.length || 0} deleted`);
807
+ }
808
+ ```
809
+
810
+ #### Managing Webhooks Lifecycle
811
+
812
+ ```ts
813
+ // List all webhooks for a team
814
+ async function listTeamWebhooks(teamId: string): Promise<WebhookV2[]> {
815
+ const res = await fetch(
816
+ `https://api.figma.com/v2/webhooks?context=team&context_id=${teamId}`,
817
+ { headers: { 'X-Figma-Token': process.env.FIGMA_TOKEN! } }
818
+ );
819
+ const data = await res.json();
820
+ return data.webhooks;
821
+ }
822
+
823
+ // Pause a webhook
824
+ async function pauseWebhook(webhookId: number): Promise<void> {
825
+ await fetch(`https://api.figma.com/v2/webhooks/${webhookId}`, {
826
+ method: 'PUT',
827
+ headers: {
828
+ 'X-Figma-Token': process.env.FIGMA_TOKEN!,
829
+ 'Content-Type': 'application/json',
830
+ },
831
+ body: JSON.stringify({ status: 'PAUSED' }),
832
+ });
833
+ }
834
+
835
+ // Resume a webhook
836
+ async function resumeWebhook(webhookId: number): Promise<void> {
837
+ await fetch(`https://api.figma.com/v2/webhooks/${webhookId}`, {
838
+ method: 'PUT',
839
+ headers: {
840
+ 'X-Figma-Token': process.env.FIGMA_TOKEN!,
841
+ 'Content-Type': 'application/json',
842
+ },
843
+ body: JSON.stringify({ status: 'ACTIVE' }),
844
+ });
845
+ }
846
+
847
+ // Delete a webhook
848
+ async function deleteWebhook(webhookId: number): Promise<void> {
849
+ await fetch(`https://api.figma.com/v2/webhooks/${webhookId}`, {
850
+ method: 'DELETE',
851
+ headers: { 'X-Figma-Token': process.env.FIGMA_TOKEN! },
852
+ });
853
+ }
854
+ ```
855
+
856
+ ---
857
+
858
+ ### Error Handling
859
+
860
+ | Status | Meaning | Action |
861
+ |--------|---------|--------|
862
+ | `400` | Invalid parameters (bad context, invalid event_type, endpoint too long) | Check request body against field constraints |
863
+ | `401` | Authentication failed | Verify token and `webhooks:read` or `webhooks:write` scope |
864
+ | `403` | Insufficient permissions (not a team admin for team context, no edit access for project/file context) | Confirm appropriate permissions for the webhook context |
865
+ | `404` | Webhook not found | Verify the webhook ID exists |
866
+ | `429` | Rate limited | Use `Retry-After` header. See `figma-api-rest.md` for backoff strategy |
867
+
868
+ ---
869
+
870
+ ## Cross-References
871
+
872
+ - **`figma-api-rest.md`** — Core REST API reference (authentication, rate limits, file endpoints, node structure)
873
+ - **`figma-api-variables.md`** — Variables API for design tokens (consumed by LIBRARY_PUBLISH sync patterns)
874
+ - **`figma-api-plugin.md`** — Plugin API for in-editor access (alternative to webhook-triggered server-side processing)
875
+ - **`figma-api-devmode.md`** — Dev Mode and Dev Resources (DEV_MODE_STATUS_UPDATE event, Dev Resources linking)
876
+ - **`design-tokens.md`** — Token extraction strategies (used in LIBRARY_PUBLISH token sync pipelines)