figma
Skill2 starsChanged 3 months agoMITView on GitHub
Last validated: 2026-10-14 API version: REST API v1 (stable) | Webhooks V2 (stable) | Pin X-Figma-Api-Version not required — versioning is path-based (/v1/, /v2/) Base URL: https://api.figma.com (Government: https://api.figma-gov.com) New since 2026-02: - Folders API — August 10, 2026. New /v2 folder endpoints (GET /v2/teams/:team_id/folders, /v2/folders/:folder_id/folders, /files, /meta) for plans with nested folders; the v1 project endpoints are deprecated in favour of them. "Projects" are now called "folders" in the docs. See Recipe 6. - Plan access tokens — GA…
- Reads credentials
- Sends data out
52 other projects use a version of this.
Adopted by
Most recently taken up by
- JustinWangJP/cc-agent-teams-action-monitortheir own version, first seen Nov 8, 2026
- skyfox675/agents-skillstheir own version, first seen Nov 6, 2026
- edenfunf/reelmimictheir own version, first seen Nov 4, 2026
- SFKislev/Fluetheir own version, first seen Nov 1, 2026
- getsentry/juniortheir own version, first seen Nov 1, 2026
- aipphilie/claude-skillstheir own version, first seen Nov 1, 2026
Used with
Projects with it also use analytics-metrics, aws-account-management and 10 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
hoodiniSkillanalytics-metrics
Build data visualization and analytics dashboards. Use when creating charts, KPI displays, metrics dashboards, or data visualization components. Triggers on analytics, dashboard, charts, metrics, KPI, data visualization, Recharts.
Also in 3 projects that use this
hoodiniSkillaws-account-management
Manage AWS accounts, organizations, IAM, and billing. Use when setting up AWS Organizations, managing IAM policies, controlling costs, or implementing multi-account strategies. Triggers on AWS Organizations, AWS IAM, AWS billing, Cost Explorer, SCPs, multi-account, AWS SSO, Identity Center.
Also in 3 projects that use this
LangChainSkillslideshow
Author a HyperFrames slideshow — a presentation, pitch deck, or interactive deck with discrete slides, fragment reveals, branching, hotspot navigation, and built-in presenter mode with speaker notes; also converts an existing page into a deck. Output is a navigable deck, not a rendered MP4. If the user didn't explicitly ask for a slideshow, confirm before authoring. Unclear → /hyperframes.
Also in 8 projects that use this
jaron-goh-centrSkilltalking-head-recut
Package an existing talking-head / interview / podcast video with timed, designed GRAPHIC OVERLAY cards — kinetic titles, lower-thirds, data callouts, quotes, side panels, picture-in-picture — synced to the transcript, on a 16:9 / 9:16 / 4:5 canvas of your choice; the clip plays untouched underneath. Trigger on "graphic overlays", "on-screen graphics", "package / dress up my video". Not plain subtitles (/embedded-captions). Unclear → /hyperframes.
Also in 6 projects that use this
LangChainSkillfaceless-explainer
Turn arbitrary text — an article, notes, a topic, a brief — into a faceless explainer video: there is no site or footage to capture, so the visuals are invented per scene (typography, abstract graphics, diagrams, data-viz). Use for topic explainers, concept breakdowns, how-tos, listicles. Not a video built from a website (/product-launch-video — promo or tour). Unclear → /hyperframes.
Also in 6 projects that use this
ElyzeSolutionsSkillcaptions-overlay
Overlay doctrine for the embedded-captions workflow — the caption MODEL (drop / rail / embed) and the rule that captions are an OVERLAY composited on top of the film, never a reserved bottom band you shift content up to avoid. Load when adding captions/subtitles to a talking-head or launch video, when deciding whether a phrase should be dropped, ride the verbatim rail, or be promoted to a scarce embedded climax, when laying out a composition that will carry captions (do NOT reserve a keep-out band), or when centering a composition on the true frame center under captions. Quotes the rail+embed model from embedded-captions and constraint #13 (captions overlay, keep-out band retired) from the product-launch-video scene agent. Applies ON TOP of embedded-captions.
Also in 3 projects that use this
- +4 more from hoodini
Other versions52 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 52
- heygen-com/hyperframes★ 81ksame name
- openai/skills★ 27ksame name
- edenfunf/reelmimic★ 1.8ksame name
- epam/ai-dial-chat★ 501same name
- getsentry/junior★ 409same name
- JetBrains/skills★ 386same name
- hoodini/ai-agents-skills★ 295same name
- SFKislev/Flue★ 102same name
- dylantarre/design-system-skills★ 50same name
- spmtitsolution/OpenBA-Kit★ 38same name
42 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository13 other files it gives its agents.
- asanaSkill
- dynamics365Skill
- githubSkill
- hubspotSkill
- jiraSkill
- linearSkill
- mondaySkill
- notionSkill
- salesforceSkill
- servicenowSkill
- slackSkill
- stripeSkill
- zendeskSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
16 rules. 0 are common, 6 are distinctive, and 10 are very specific to this project.
0
Common
Most of the 44 edited versions we read keep these.
6
Distinctive
This author's own choices. Worth a look.
10
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 25
Read the full JSON structure of any Figma file (nodes, frames, components, styles, variables)
- Distinctive: line 27
Create, read, and reply to comments on design files
- Very specific: line 197
Read file structure: file_content:read
- Very specific: line 743
Use ?depth=N on /v1/files/:key to limit traversal depth and reduce response size:
- Distinctive: line 815
Comments: Not idempotent by design — deduplicate by checking existing comments before posting.
- Very specific: line 816
Dev resources: Check existing dev resources by GET /v1/files/:key/dev_resources?node_ids=NODE_ID before creating.
- Very specific: line 817
Webhooks: Check existing webhooks before registering to avoid duplicates: GET /v2/webhooks.
- Very specific: line 839
Check data["err"] and data["images"][node_id] is None explicitly.
- Very specific: line 844
Use ?depth=1 or ?depth=2 for shallow fetches
- Very specific: line 845
Use /v1/files/:key/nodes?ids=NODE_ID to fetch only needed subtrees
- Very specific: line 857
Use granular scopes — do not grant file_variables:write or org:activity_log_read unless explicitly needed.
- Very specific: line 861
Use a strong random passcode (32+ bytes, hex or base64 encoded).
- Very specific: line 862
Always validate the passcode field from the webhook body using hmac.compare_digest (constant-time).
- Distinctive: line 863
Only accept requests over HTTPS.
- Distinctive: line 868
If storing exported file content or images, ensure it is covered by your data retention policy.
- Distinctive: line 873
All OAuth apps must complete Figma's publishing review flow.
“Common” is measured: kept by at least 6 in 10 of the 44 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Figma Skill Reference
Last validated: 2026-10-14
API version: REST API v1 (stable) | Webhooks V2 (stable) | Pin X-Figma-Api-Version not required — versioning is path-based (/v1/, /v2/)
Base URL: https://api.figma.com (Government: https://api.figma-gov.com)
New since 2026-02:
- Folders API — August 10, 2026. New
/v2folder endpoints (GET /v2/teams/:team_id/folders,/v2/folders/:folder_id/folders,/files,/meta) for plans with nested folders; the v1 project endpoints are deprecated in favour of them. "Projects" are now called "folders" in the docs. See Recipe 6.- Plan access tokens — GA July 23, 2026. Organization- and Enterprise-plan tokens that are not tied to an individual user account, with expiry up to one year and optional allowlists, managed by plan admins. This is now the recommended credential for CI/CD and org automation — it survives the token owner leaving the company, which personal access tokens do not. See Authentication below.
- oEmbed API — March 25, 2026.
oEmbed 1.0-spec endpoint returning metadata for Figma files and published Makes. Requires thefile_metadata:readscope.- AI Usage API — June 12, 2026. Enterprise-only endpoint returning per-user, per-day AI credit usage. Requires the
org:ai_metering_usage_readscope and a plan access token.Docs moved: the developer docs are now at
developers.figma.com/docs/rest-api/; the oldfigma.com/developers/apiURL redirects there.
Breaking changes to be aware of:
- Nov 17, 2025 — New tiered rate limits in effect; rate limits now vary by plan (Starter/Professional/Organization/Enterprise) and seat type (View/Collab vs Dev/Full).
- May 28, 2025 — Webhooks V2 major update: webhooks now attach to teams, files, or projects.
GET /v2/teams/:team_id/webhooksdeprecated → useGET /v2/webhooks.DEV_MODE_STATUS_UPDATEevent added.- Nov 17, 2025 — OAuth apps must complete the new publishing flow (mandatory as of this date).
files:readscope is deprecated — use granular scopes (file_content:read,file_metadata:read, etc.).
What this skill enables
- Read the full JSON structure of any Figma file (nodes, frames, components, styles, variables)
- Export/render nodes as PNG, SVG, PDF, or JPEG via the Images API
- Create, read, and reply to comments on design files
- Inspect published components, styles, and variables for design-system automation
- List team projects and files programmatically
- Register webhooks that fire on file changes, comments, version publishes, and Dev Mode updates
- Automate design-to-dev handoff workflows: extract dev resources, inspect variable values
Best-fit use cases
| Use Case | Trigger | Success Criteria |
| Design-to-ticket sync | New Figma comment mentioning @dev | Jira/GitHub issue created with screenshot + frame link |
| Component catalog export | Scheduled daily | All published components exported to JSON/PNG with metadata |
| Design token extraction | File version published | Variables/styles exported as JSON design tokens (Style Dictionary format) |
| Asset pipeline | Frame or component exported | PNG/SVG assets committed to repo or uploaded to CDN |
| Design review notification | FILE_VERSION_UPDATE webhook | Slack/email sent to reviewers with changelog summary |
| Spec link on PR | Pull request opened | Figma frame URL attached to PR description automatically |
| Accessibility audit trigger | FILE_UPDATE webhook | Automated accessibility checker run on changed frames |
| Dev Mode status tracking | DEV_MODE_STATUS_UPDATE webhook | Engineering ticket updated when frame marked "Ready for Dev" |
| Stale component detection | Weekly cron | Report of components not updated in 90+ days |
| Design system analytics | Monthly | Usage report of which components appear in which files |
Key concepts & data model
Document hierarchy
File (file_key)
└── Document (node type: DOCUMENT)
└── Canvas / Page (node type: CANVAS) — one per page tab
└── Frame (node type: FRAME) — top-level frames (artboards)
└── Group / Component / Instance / Text / Vector / etc.Key IDs:
file_key— alphanumeric string in the Figma file URL:figma.com/file/<file_key>/node_id— identifies any node within a file; format<page>:<node>e.g.1:23. URL-encoded as1%3A23component_key— globally unique key for a published component (different from node_id)style_key— globally unique key for a published style
Node types (common)
| Type | Description |
DOCUMENT | Root node of a file |
CANVAS | A page in the file |
FRAME | Artboard / container |
GROUP | Logical group |
COMPONENT | Reusable component definition |
COMPONENT_SET | Container for component variants |
INSTANCE | Instance of a COMPONENT |
TEXT | Text layer |
VECTOR | Vector shape |
RECTANGLE, ELLIPSE, LINE, POLYGON, STAR | Primitive shapes |
BOOLEAN_OPERATION | Union/subtract/intersect/exclude of shapes |
SECTION | Section container (org/enterprise) |
Components vs Instances
- COMPONENT — the source definition; has a
keyused for cross-file references - INSTANCE — a linked copy; has
componentIdpointing back to the COMPONENT node, andmainComponentwhen resolved - COMPONENT_SET — groups related variants; child components are the individual variants
Styles
Published styles (colors, text, effects, grids) have a key, name, description, style_type (FILL, TEXT, EFFECT, GRID), and remote flag.
Variables (Enterprise only)
Variables are scoped to a collection. Types: BOOLEAN, COLOR, FLOAT, STRING. Modes define different values per variable (e.g., Light/Dark). Accessed via /v1/files/:key/variables/local and /v1/files/:key/variables/published.
Authentication & permissions
Option 1 — Plan access token [recommended for org automation and CI/CD, GA 2026-07-23]
Organization- and Enterprise-plan only. Created and managed by plan admins, not tied to a user account, so the token keeps working when the person who created it leaves. Supports expiry up to one year and optional allowlists restricting where it can be used.
curl https://api.figma.com/v1/me \
-H "X-Figma-Token: YOUR_PLAN_ACCESS_TOKEN"Same X-Figma-Token header as a PAT — only the provenance and lifecycle differ. Prefer this over a PAT for anything running unattended: a PAT dies with its owner's account, which is the most common cause of silently broken Figma automations.
Option 2 — Personal Access Token (PAT) [scripts and local tooling]
Generate at Figma → Account Settings → Personal access tokens. Set an expiry (required since 2024) and select scopes at creation time.
curl https://api.figma.com/v1/me \
-H "X-Figma-Token: YOUR_PAT"On Organization/Enterprise plans, use a plan access token for shared automation and keep PATs for individual development work.
Option 3 — OAuth 2.0 [recommended for user-facing apps]
Authorization URL:
GET https://www.figma.com/oauth
?client_id=CLIENT_ID
&redirect_uri=REDIRECT_URI
&scope=file_content:read,file_comments:write
&state=RANDOM_STATE
&response_type=codeExchange code for token:
curl -X POST https://api.figma.com/v1/oauth/token \
-d "client_id=CLIENT_ID" \
-d "client_secret=CLIENT_SECRET" \
-d "redirect_uri=REDIRECT_URI" \
-d "code=AUTH_CODE" \
-d "grant_type=authorization_code"Response:
{
"access_token": "...",
"refresh_token": "...",
"expires_in": 7776000,
"token_type": "bearer"
}Refresh token (access tokens expire after 90 days):
curl -X POST https://api.figma.com/v1/oauth/refresh \
-d "client_id=CLIENT_ID" \
-d "client_secret=CLIENT_SECRET" \
-d "refresh_token=REFRESH_TOKEN"Note:
POST /v1/oauth/tokenalso accepts refresh (migrated May 2025); legacy/v1/oauth/refreshstill supported.
Use OAuth token:
curl https://api.figma.com/v1/me \
-H "Authorization: Bearer OAUTH_ACCESS_TOKEN"Scopes reference (least-privilege)
| Scope | Access Granted |
current_user:read | User profile (name, email, avatar) |
file_content:read | Node contents, editor type |
file_metadata:read | File metadata (name, thumbnail, last modified) |
file_versions:read | Version history |
file_comments:read | Read comments |
file_comments:write | Post/delete comments and reactions |
file_variables:read | Variables + collections (Enterprise only) |
file_variables:write | Create/update variables (Enterprise only) |
file_dev_resources:read | Dev resources (links, specs) |
file_dev_resources:write | Create/update dev resources |
library_assets:read | Published components and styles |
library_analytics:read | Design system analytics (Enterprise only) |
projects:read | List projects and files in a team (v1 project endpoints — deprecated) |
folders:read | List folders, subfolders, and folder files (/v2/folders) — not usable by public OAuth apps |
folder_metadata:read | Folder metadata (GET /v2/folders/:folder_id/meta) |
webhooks:read | View webhook metadata |
webhooks:write | Create/update/delete webhooks |
org:activity_log_read | Activity logs (Enterprise admin only) |
files:readis deprecated — use granular scopes above.
Minimum scopes for common tasks:
- Read file structure:
file_content:read - Post a comment:
file_comments:write(implies read) - Register webhooks:
webhooks:write - Export components:
file_content:read,library_assets:read
Common workflows (recipes)
Recipe 1 — Read a file's full node tree
curl "https://api.figma.com/v1/files/FILE_KEY" \
-H "X-Figma-Token: YOUR_PAT"Response (abbreviated):
{
"name": "My Design File",
"lastModified": "2026-01-15T10:30:00Z",
"thumbnailUrl": "https://...",
"version": "1234567890",
"document": {
"id": "0:0",
"name": "Document",
"type": "DOCUMENT",
"children": [
{
"id": "0:1",
"name": "Page 1",
"type": "CANVAS",
"children": [ ... ]
}
]
},
"components": { "component_key": { "name": "...", "description": "..." } },
"styles": { "style_key": { "name": "...", "style_type": "FILL" } }
}Fetch specific nodes only (avoid downloading the full file for large docs):
curl "https://api.figma.com/v1/files/FILE_KEY/nodes?ids=1:23,1:45" \
-H "X-Figma-Token: YOUR_PAT"import requests
def get_figma_nodes(file_key: str, node_ids: list[str], token: str) -> dict:
ids = ",".join(node_ids)
r = requests.get(
f"https://api.figma.com/v1/files/{file_key}/nodes",
params={"ids": ids},
headers={"X-Figma-Token": token},
)
r.raise_for_status()
return r.json()["nodes"]Recipe 2 — Export nodes as images (PNG/SVG/PDF)
Renders specified node IDs at a given scale and format. Returns pre-signed S3 URLs (valid ~30 minutes).
curl "https://api.figma.com/v1/images/FILE_KEY?ids=1:23&scale=2&format=png" \
-H "X-Figma-Token: YOUR_PAT"Response:
{
"err": null,
"images": {
"1:23": "https://figma-alpha-api.s3.us-west-2.amazonaws.com/..."
}
}import requests
def export_nodes(
file_key: str,
node_ids: list[str],
token: str,
format: str = "png", # png | svg | pdf | jpg
scale: float = 2.0,
) -> dict[str, str]:
"""Returns {node_id: presigned_url}."""
r = requests.get(
f"https://api.figma.com/v1/images/{file_key}",
params={"ids": ",".join(node_ids), "scale": scale, "format": format},
headers={"X-Figma-Token": token},
)
r.raise_for_status()
data = r.json()
if data.get("err"):
raise RuntimeError(f"Figma image error: {data['err']}")
return data["images"]
def download_image(url: str, dest_path: str) -> None:
r = requests.get(url)
r.raise_for_status()
with open(dest_path, "wb") as f:
f.write(r.content)SVG export tip: Use
svg_include_id=trueto embed node IDs asidattributes for post-processing.
Recipe 3 — List and read comments
# Get all comments on a file
curl "https://api.figma.com/v1/files/FILE_KEY/comments" \
-H "X-Figma-Token: YOUR_PAT"Response:
{
"comments": [
{
"id": "123",
"uuid": "abc-def",
"file_key": "FILE_KEY",
"parent_id": "",
"user": { "id": "...", "handle": "Alice", "img_url": "..." },
"created_at": "2026-01-10T09:00:00Z",
"resolved_at": null,
"message": "This button padding looks off @Bob",
"client_meta": { "node_id": "1:23", "node_offset": { "x": 0, "y": 0 } }
}
]
}Post a comment:
import requests
def post_comment(
file_key: str,
message: str,
token: str,
node_id: str | None = None, # anchor comment to a specific node
) -> dict:
payload = {"message": message}
if node_id:
payload["client_meta"] = {"node_id": node_id}
r = requests.post(
f"https://api.figma.com/v1/files/{file_key}/comments",
json=payload,
headers={"X-Figma-Token": token, "Content-Type": "application/json"},
)
r.raise_for_status()
return r.json()Recipe 4 — List published components in a team
curl "https://api.figma.com/v1/teams/TEAM_ID/components" \
-H "X-Figma-Token: YOUR_PAT"Response:
{
"status": 200,
"error": false,
"meta": {
"components": [
{
"key": "abc123",
"file_key": "FILE_KEY",
"node_id": "1:45",
"thumbnail_url": "https://...",
"name": "Button/Primary",
"description": "Primary CTA button",
"created_at": "2025-06-01T00:00:00Z",
"updated_at": "2026-01-10T00:00:00Z",
"containing_frame": { "name": "Buttons", "node_id": "1:10" }
}
],
"cursor": "eyJsaW1pdCI6MzAsIm9mZnNldCI6MzB9"
}
}Paginate using the cursor value:
import requests
def get_all_components(team_id: str, token: str) -> list[dict]:
components = []
params = {"page_size": 30}
while True:
r = requests.get(
f"https://api.figma.com/v1/teams/{team_id}/components",
params=params,
headers={"X-Figma-Token": token},
)
r.raise_for_status()
data = r.json()["meta"]
components.extend(data["components"])
cursor = data.get("cursor")
if not cursor:
break
params["cursor"] = cursor
return componentsRecipe 5 — Extract design tokens (styles + variables)
Published styles:
curl "https://api.figma.com/v1/teams/TEAM_ID/styles" \
-H "X-Figma-Token: YOUR_PAT"Local variables (Enterprise):
curl "https://api.figma.com/v1/files/FILE_KEY/variables/local" \
-H "X-Figma-Token: YOUR_PAT"import requests, json
def extract_tokens(file_key: str, token: str) -> dict:
"""
Extract color and text styles as a Style Dictionary-compatible token map.
Requires file_content:read and library_assets:read scopes.
"""
r = requests.get(
f"https://api.figma.com/v1/files/{file_key}",
params={"depth": 1}, # depth=1 → only top-level nodes + styles map
headers={"X-Figma-Token": token},
)
r.raise_for_status()
file_data = r.json()
tokens = {}
for style_key, style in file_data.get("styles", {}).items():
if style["style_type"] == "FILL":
tokens[style["name"]] = {"$type": "color", "$value": style_key}
elif style["style_type"] == "TEXT":
tokens[style["name"]] = {"$type": "typography", "$value": style_key}
return tokensRecipe 6 — List projects and files in a team
⚠️ Changed 2026-08-10:
GET /v1/teams/:team_id/projects,GET /v1/projects/:project_id/files, andGET /v1/projects/:project_id/metaare deprecated. Replacements:GET /v2/teams/:team_id/folders,GET /v2/folders/:folder_id/folders(subfolders),GET /v2/folders/:folder_id/files, andGET /v2/folders/:folder_id/meta. These need thefolders:readscope (folder_metadata:readfor/meta), andfolders:readcannot be used by public OAuth apps. On nested-folder plans the v1 team-projects endpoint returns only top-level folders. The v1 calls below still work. Sources: changelog, Folders endpoints.
# Team projects
curl "https://api.figma.com/v1/teams/TEAM_ID/projects" \
-H "X-Figma-Token: YOUR_PAT"
# Files in a project
curl "https://api.figma.com/v1/projects/PROJECT_ID/files" \
-H "X-Figma-Token: YOUR_PAT"Projects response:
{
"name": "Engineering",
"projects": [
{ "id": "12345", "name": "Design System" },
{ "id": "67890", "name": "Product App" }
]
}Files response:
{
"files": [
{
"key": "FILE_KEY",
"name": "Component Library",
"thumbnail_url": "https://...",
"last_modified": "2026-01-20T12:00:00Z"
}
]
}Recipe 7 — Register a webhook
As of May 2025, webhooks attach to a team, file, or project (not just team).
import requests
def register_webhook(
token: str,
event_type: str, # e.g. "FILE_VERSION_UPDATE"
endpoint: str, # Your HTTPS receiver URL
passcode: str, # Secret for HMAC verification
context: str = "FILE", # "TEAM" | "FILE" | "PROJECT"
context_id: str = "", # team_id, file_key, or project_id
) -> dict:
payload = {
"event_type": event_type,
"endpoint": endpoint,
"passcode": passcode,
"status": "ACTIVE",
"description": f"Auto-registered {event_type}",
}
if context == "TEAM":
payload["team_id"] = context_id
elif context == "FILE":
payload["file_key"] = context_id
elif context == "PROJECT":
payload["project_id"] = context_id
r = requests.post(
"https://api.figma.com/v2/webhooks",
json=payload,
headers={"X-Figma-Token": token, "Content-Type": "application/json"},
)
r.raise_for_status()
return r.json()Validate incoming webhook signature:
import hashlib, hmac
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_PASSCODE = "your-passcode-here"
@app.route("/figma-webhook", methods=["POST"])
def figma_webhook():
# Figma sends the passcode in the JSON body, not a header
payload = request.get_json(force=True)
received_passcode = payload.get("passcode", "")
if not hmac.compare_digest(received_passcode, WEBHOOK_PASSCODE):
abort(403, "Invalid passcode")
event_type = payload.get("event_type")
if event_type == "FILE_VERSION_UPDATE":
file_key = payload["file_key"]
label = payload.get("label", "")
# trigger downstream pipeline...
elif event_type == "FILE_COMMENT":
comment = payload.get("comment", [])
# notify team...
return {"ok": True}, 200Figma webhook security note: Figma passes the
passcodeinside the JSON body (not an HTTP header). Compare it withhmac.compare_digestto prevent timing attacks. All webhook delivery uses HTTPS.
Webhook event types:
| Event | Trigger |
PING | Sent on webhook creation to verify endpoint |
FILE_UPDATE | File content was updated (autosave) |
FILE_VERSION_UPDATE | Named version saved |
FILE_DELETE | File deleted |
LIBRARY_PUBLISH | Component/style library published |
FILE_COMMENT | Comment posted or resolved on a file |
DEV_MODE_STATUS_UPDATE | Layer Dev Mode status changed (e.g., "Ready for Dev") |
Retry schedule: Failed deliveries retried 3 times — 5 min, 30 min, 3 hours after each failure.
Webhook limits:
- Per team: 20 webhooks
- Per project: 5 webhooks
- Per file: 3 (Professional), up to 150/300/600 (Pro/Org/Enterprise)
Recipe 8 — Get file version history
curl "https://api.figma.com/v1/files/FILE_KEY/versions" \
-H "X-Figma-Token: YOUR_PAT"{
"versions": [
{
"id": "1234567890",
"created_at": "2026-01-15T10:30:00Z",
"label": "v2.1 — Button updates",
"description": "Updated padding and color tokens",
"user": { "id": "...", "handle": "Alice" }
}
],
"pagination": { "cursor": "eyJsaW1pdCI6MzB9" }
}To fetch the file at a specific version:
curl "https://api.figma.com/v1/files/FILE_KEY?version=VERSION_ID" \
-H "X-Figma-Token: YOUR_PAT"Recipe 9 — Create dev resources (design-to-dev links)
Dev resources attach external links (e.g., Storybook, GitHub) to nodes for handoff.
import requests
def create_dev_resource(
file_key: str,
node_id: str,
name: str,
url: str,
token: str,
) -> dict:
r = requests.post(
f"https://api.figma.com/v1/files/{file_key}/dev_resources",
json={
"dev_resources": [
{"node_id": node_id, "name": name, "url": url}
]
},
headers={"X-Figma-Token": token, "Content-Type": "application/json"},
)
r.raise_for_status()
return r.json()
# Example: link a Storybook story to a Button component node
create_dev_resource(
file_key="AbCdEfGhIj",
node_id="1:23",
name="Storybook: Button",
url="https://storybook.example.com/?path=/docs/button--default",
token=PAT,
)Recipe 10 — Full asset pipeline (webhook → export → upload)
import requests, boto3, hmac
from flask import Flask, request, abort
app = Flask(__name__)
PASSCODE = "your-passcode"
FIGMA_TOKEN = "your-figma-pat"
S3_BUCKET = "my-design-assets"
@app.route("/webhook", methods=["POST"])
def handle_webhook():
payload = request.get_json(force=True)
if not hmac.compare_digest(payload.get("passcode", ""), PASSCODE):
abort(403)
if payload.get("event_type") != "FILE_VERSION_UPDATE":
return {"ok": True}, 200
file_key = payload["file_key"]
version_id = payload.get("version_id")
# 1. Fetch top-level frames from page 1
nodes_r = requests.get(
f"https://api.figma.com/v1/files/{file_key}",
params={"depth": 2},
headers={"X-Figma-Token": FIGMA_TOKEN},
)
pages = nodes_r.json()["document"]["children"]
page1_frames = [n for n in pages[0]["children"] if n["type"] == "FRAME"]
node_ids = [f["id"] for f in page1_frames]
# 2. Get image export URLs
img_r = requests.get(
f"https://api.figma.com/v1/images/{file_key}",
params={"ids": ",".join(node_ids), "scale": 2, "format": "png"},
headers={"X-Figma-Token": FIGMA_TOKEN},
)
images = img_r.json()["images"]
# 3. Download and upload to S3
s3 = boto3.client("s3")
for node_id, url in images.items():
if not url:
continue
img_data = requests.get(url).content
key = f"figma/{file_key}/{version_id}/{node_id.replace(':', '_')}.png"
s3.put_object(Bucket=S3_BUCKET, Key=key, Body=img_data, ContentType="image/png")
return {"ok": True}, 200Query patterns & filtering
Pagination
Most list endpoints return a cursor in meta.cursor. Pass it as the cursor query parameter on subsequent requests. There is no page / offset parameter.
def paginate(url: str, token: str, page_size: int = 30) -> list[dict]:
results = []
params = {"page_size": page_size}
while True:
r = requests.get(url, params=params, headers={"X-Figma-Token": token})
r.raise_for_status()
meta = r.json().get("meta", {})
key = next((k for k in meta if isinstance(meta[k], list)), None)
if key:
results.extend(meta[key])
cursor = meta.get("cursor")
if not cursor:
break
params["cursor"] = cursor
return resultsDepth limiting (performance)
Use ?depth=N on /v1/files/:key to limit traversal depth and reduce response size:
depth=1— only the document root + page list (no children)depth=2— pages + top-level frames- Omit for full tree (can be very large for complex files)
Incremental sync
Files expose a lastModified timestamp and a version string. Cache these and compare on next poll:
cached_version = load_cache(file_key)
r = requests.get(f"https://api.figma.com/v1/files/{file_key}",
params={"depth": 1}, headers={"X-Figma-Token": token})
current_version = r.json()["version"]
if current_version != cached_version:
# file changed — fetch full data or react to webhook
save_cache(file_key, current_version)Reliability: rate limits, retries, idempotency
Tier definitions (as of Nov 17, 2025)
Figma uses a leaky bucket algorithm. Rate limits depend on plan and seat type:
| Plan | Tier 1 (file reads / images) | Tier 2 (components / styles) | Tier 3 (analytics) |
| Starter | 10 req/min | 25 req/min | 50 req/min |
| Professional | 15 req/min | 50 req/min | 100 req/min |
| Organization | 20 req/min | 100 req/min | 150 req/min |
| Enterprise | Higher | Higher | Higher |
View/Collab seat types get lower limits than Dev/Full seats. See
X-Figma-Rate-Limit-Typeheader.
Tier assignment by endpoint:
- Tier 1 —
GET /v1/files/:key,GET /v1/images/:key(content-heavy) - Tier 2 — components, styles, versions, comments, projects
- Tier 3 — library analytics, activity logs
Response headers on 429
| Header | Meaning |
Retry-After | Seconds to wait before retrying |
X-Figma-Rate-Limit-Type | "low" (View/Collab seat) or "high" (Dev/Full seat) |
X-Figma-Plan-Tier | Plan of the resource owner: starter, pro, org, enterprise |
X-Figma-Upgrade-Link | URL to upgrade page if limit is plan-based |
Retry with exponential backoff
import requests, time, random
def figma_request(url: str, token: str, params: dict = None, max_retries: int = 5) -> dict:
headers = {"X-Figma-Token": token}
for attempt in range(max_retries):
r = requests.get(url, params=params, headers=headers)
if r.status_code == 429:
retry_after = int(r.headers.get("Retry-After", 60))
jitter = random.uniform(0, retry_after * 0.1)
wait = retry_after + jitter
print(f"Rate limited. Waiting {wait:.1f}s (attempt {attempt + 1}/{max_retries})")
time.sleep(wait)
continue
r.raise_for_status()
return r.json()
raise RuntimeError(f"Exceeded {max_retries} retries for {url}")Idempotency
- Comments: Not idempotent by design — deduplicate by checking existing comments before posting.
- Dev resources: Check existing dev resources by
GET /v1/files/:key/dev_resources?node_ids=NODE_IDbefore creating. - Webhooks: Check existing webhooks before registering to avoid duplicates:
GET /v2/webhooks. - Image exports: Presigned URLs are short-lived (~30 min); do not cache URLs — cache the downloaded binary instead.
Error handling & troubleshooting
| Code | When | Action |
400 Bad Request | Malformed request (bad node_id, invalid params) | Check node_id format (page:node, e.g. 1:23); validate required fields |
401 Unauthorized | Missing or invalid token | Check X-Figma-Token header is present; verify token not expired; ensure token has required scope |
403 Forbidden | Valid token but no access | Check token scopes (use granular scopes, not deprecated files:read); user may not have access to the file/team |
404 Not Found | File or node doesn't exist | Verify file_key from URL; verify node_id exists in the file; file may be deleted |
429 Too Many Requests | Rate limit hit | Respect Retry-After header; consider upgrading plan/seat type; batch requests |
500 / 503 | Figma server error | Retry with exponential backoff; check status.figma.com |
Special error: Images API err field
The images endpoint returns HTTP 200 but includes "err" in the body if rendering failed:
{ "err": "Image error (render failed)", "images": { "1:23": null } }Check data["err"] and data["images"][node_id] is None explicitly.
Special error: File too large
Large files may time out on GET /v1/files/:key. Mitigate:
- Use
?depth=1or?depth=2for shallow fetches - Use
/v1/files/:key/nodes?ids=NODE_IDto fetch only needed subtrees
Common mistake: Wrong node_id format
Node IDs in API responses use : separator (e.g., 1:23). In URLs, they must be URL-encoded: 1%3A23. The Figma web app uses - instead of : in URLs — convert: 1-23 → 1:23.
Security & compliance
Token hygiene
- Set token expiry — never create non-expiring PATs; rotate on a schedule.
- Use granular scopes — do not grant
file_variables:writeororg:activity_log_readunless explicitly needed. - Store tokens in a secrets manager (AWS Secrets Manager, HashiCorp Vault); never hardcode or commit tokens.
Webhook security
- Use a strong random
passcode(32+ bytes, hex or base64 encoded). - Always validate the
passcodefield from the webhook body usinghmac.compare_digest(constant-time). - Only accept requests over HTTPS. Reject non-HTTPS webhook endpoints at registration time.
- Log and alert on unexpected
event_typevalues.
Data privacy
- Figma files may contain PII in text layers (user-submitted content, emails, names).
- If storing exported file content or images, ensure it is covered by your data retention policy.
- Variables and styles may contain sensitive design system data — restrict
file_variables:readaccordingly. - For Enterprise: use
org:activity_log_readfor audit trails; retain logs per your compliance requirements.
OAuth app publication (mandatory Nov 2025)
All OAuth apps must complete Figma's publishing review flow. Apps not published will lose OAuth functionality.
Testing checklist
- Auth — PAT: Send
GET /v1/mewith your token; confirm200and your user info returned - Auth — scopes: Attempt an endpoint outside your granted scopes; confirm
403 - File read:
GET /v1/files/FILE_KEYreturns200withdocumenttree andstyles/componentsmaps - Nodes endpoint:
GET /v1/files/FILE_KEY/nodes?ids=NODE_IDreturns only the requested node - Images export:
GET /v1/images/FILE_KEY?ids=NODE_ID&format=pngreturns a presigned URL; download the URL returns a valid PNG - Comments — read:
GET /v1/files/FILE_KEY/commentsreturns comment list - Comments — write:
POST /v1/files/FILE_KEY/commentswith{"message": "test"}creates a comment; visible in Figma UI - Webhook — register:
POST /v2/webhookswithevent_type: PINGcreates webhook and your endpoint receives a PING - Webhook — passcode: Send a fake request to your webhook endpoint with a wrong passcode; confirm
403 - Rate limit: Deliberately exceed the rate limit for your plan tier; confirm
429withRetry-Afterheader; confirm retry logic waits correctly - Pagination: On a team with 30+ components, confirm cursor-based pagination fetches all items
- Error — 404: Request a non-existent file key; confirm
404handled gracefully - Images — null check: Request export of an invalid/empty node; confirm
errfield andnullURL handled - Node ID format: Confirm node IDs from web URL (hyphen-separated) are converted to colon-separated before API calls
- Version history:
GET /v1/files/FILE_KEY/versionsreturns version list with labels
Sources
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

