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.
- package/README.md +133 -0
- package/bin/install.js +328 -0
- package/knowledge/README.md +62 -0
- package/knowledge/css-strategy.md +973 -0
- package/knowledge/design-to-code-assets.md +855 -0
- package/knowledge/design-to-code-layout.md +929 -0
- package/knowledge/design-to-code-semantic.md +1085 -0
- package/knowledge/design-to-code-typography.md +1003 -0
- package/knowledge/design-to-code-visual.md +1145 -0
- package/knowledge/design-tokens-variables.md +1261 -0
- package/knowledge/design-tokens.md +960 -0
- package/knowledge/figma-api-devmode.md +894 -0
- package/knowledge/figma-api-plugin.md +920 -0
- package/knowledge/figma-api-rest.md +742 -0
- package/knowledge/figma-api-variables.md +848 -0
- package/knowledge/figma-api-webhooks.md +876 -0
- package/knowledge/payload-blocks.md +1184 -0
- package/knowledge/payload-figma-mapping.md +1210 -0
- package/knowledge/payload-visual-builder.md +1004 -0
- package/knowledge/plugin-architecture.md +1176 -0
- package/knowledge/plugin-best-practices.md +1206 -0
- package/knowledge/plugin-codegen.md +1313 -0
- package/package.json +31 -0
- package/skills/README.md +103 -0
- package/skills/audit-plugin/SKILL.md +244 -0
- package/skills/build-codegen-plugin/SKILL.md +279 -0
- package/skills/build-importer/SKILL.md +320 -0
- package/skills/build-plugin/SKILL.md +199 -0
- package/skills/build-token-pipeline/SKILL.md +363 -0
- package/skills/ref-html/SKILL.md +290 -0
- package/skills/ref-layout/SKILL.md +150 -0
- package/skills/ref-payload-block/SKILL.md +415 -0
- package/skills/ref-react/SKILL.md +222 -0
- 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)
|