agentleFS
ShanksgShanksg/clawskills/skills/figma/skill.md

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

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.
  • hoodiniSkill

    analytics-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

  • hoodiniSkill

    aws-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

  • LangChainSkill

    slideshow

    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-centrSkill

    talking-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

  • LangChainSkill

    faceless-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

  • ElyzeSolutionsSkill

    captions-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

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
  1. Distinctive:

    Read the full JSON structure of any Figma file (nodes, frames, components, styles, variables)

    line 25
  2. Distinctive:

    Create, read, and reply to comments on design files

    line 27
  3. Very specific:

    Read file structure: file_content:read

    line 197
  4. Very specific:

    Use ?depth=N on /v1/files/:key to limit traversal depth and reduce response size:

    line 743
  5. Distinctive:

    Comments: Not idempotent by design — deduplicate by checking existing comments before posting.

    line 815
  6. Very specific:

    Dev resources: Check existing dev resources by GET /v1/files/:key/dev_resources?node_ids=NODE_ID before creating.

    line 816
  7. Very specific:

    Webhooks: Check existing webhooks before registering to avoid duplicates: GET /v2/webhooks.

    line 817
  8. Very specific:

    Check data["err"] and data["images"][node_id] is None explicitly.

    line 839
  9. Very specific:

    Use ?depth=1 or ?depth=2 for shallow fetches

    line 844
  10. Very specific:

    Use /v1/files/:key/nodes?ids=NODE_ID to fetch only needed subtrees

    line 845
  11. Very specific:

    Use granular scopes — do not grant file_variables:write or org:activity_log_read unless explicitly needed.

    line 857
  12. Very specific:

    Use a strong random passcode (32+ bytes, hex or base64 encoded).

    line 861
  13. Very specific:

    Always validate the passcode field from the webhook body using hmac.compare_digest (constant-time).

    line 862
  14. Distinctive:

    Only accept requests over HTTPS.

    line 863
  15. Distinctive:

    If storing exported file content or images, ensure it is covered by your data retention policy.

    line 868
  16. Distinctive:

    All OAuth apps must complete Figma's publishing review flow.

    line 873

“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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
skill.md

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 /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 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 the file_metadata:read scope.
  • AI Usage API — June 12, 2026. Enterprise-only endpoint returning per-user, per-day AI credit usage. Requires the org:ai_metering_usage_read scope and a plan access token.

Docs moved: the developer docs are now at developers.figma.com/docs/rest-api/; the old figma.com/developers/api URL 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/webhooks deprecated → use GET /v2/webhooks. DEV_MODE_STATUS_UPDATE event added.
  • Nov 17, 2025 — OAuth apps must complete the new publishing flow (mandatory as of this date).
  • files:read scope 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 CaseTriggerSuccess Criteria
Design-to-ticket syncNew Figma comment mentioning @devJira/GitHub issue created with screenshot + frame link
Component catalog exportScheduled dailyAll published components exported to JSON/PNG with metadata
Design token extractionFile version publishedVariables/styles exported as JSON design tokens (Style Dictionary format)
Asset pipelineFrame or component exportedPNG/SVG assets committed to repo or uploaded to CDN
Design review notificationFILE_VERSION_UPDATE webhookSlack/email sent to reviewers with changelog summary
Spec link on PRPull request openedFigma frame URL attached to PR description automatically
Accessibility audit triggerFILE_UPDATE webhookAutomated accessibility checker run on changed frames
Dev Mode status trackingDEV_MODE_STATUS_UPDATE webhookEngineering ticket updated when frame marked "Ready for Dev"
Stale component detectionWeekly cronReport of components not updated in 90+ days
Design system analyticsMonthlyUsage 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 as 1%3A23
  • component_key — globally unique key for a published component (different from node_id)
  • style_key — globally unique key for a published style

Node types (common)

TypeDescription
DOCUMENTRoot node of a file
CANVASA page in the file
FRAMEArtboard / container
GROUPLogical group
COMPONENTReusable component definition
COMPONENT_SETContainer for component variants
INSTANCEInstance of a COMPONENT
TEXTText layer
VECTORVector shape
RECTANGLE, ELLIPSE, LINE, POLYGON, STARPrimitive shapes
BOOLEAN_OPERATIONUnion/subtract/intersect/exclude of shapes
SECTIONSection container (org/enterprise)

Components vs Instances

  • COMPONENT — the source definition; has a key used for cross-file references
  • INSTANCE — a linked copy; has componentId pointing back to the COMPONENT node, and mainComponent when 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=code

Exchange 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/token also accepts refresh (migrated May 2025); legacy /v1/oauth/refresh still supported.

Use OAuth token:

curl https://api.figma.com/v1/me \
  -H "Authorization: Bearer OAUTH_ACCESS_TOKEN"

Scopes reference (least-privilege)

ScopeAccess Granted
current_user:readUser profile (name, email, avatar)
file_content:readNode contents, editor type
file_metadata:readFile metadata (name, thumbnail, last modified)
file_versions:readVersion history
file_comments:readRead comments
file_comments:writePost/delete comments and reactions
file_variables:readVariables + collections (Enterprise only)
file_variables:writeCreate/update variables (Enterprise only)
file_dev_resources:readDev resources (links, specs)
file_dev_resources:writeCreate/update dev resources
library_assets:readPublished components and styles
library_analytics:readDesign system analytics (Enterprise only)
projects:readList projects and files in a team (v1 project endpoints — deprecated)
folders:readList folders, subfolders, and folder files (/v2/folders) — not usable by public OAuth apps
folder_metadata:readFolder metadata (GET /v2/folders/:folder_id/meta)
webhooks:readView webhook metadata
webhooks:writeCreate/update/delete webhooks
org:activity_log_readActivity logs (Enterprise admin only)

files:read is 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=true to embed node IDs as id attributes 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 components

Recipe 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 tokens

Recipe 6 — List projects and files in a team

⚠️ Changed 2026-08-10: GET /v1/teams/:team_id/projects, GET /v1/projects/:project_id/files, and GET /v1/projects/:project_id/meta are deprecated. Replacements: GET /v2/teams/:team_id/folders, GET /v2/folders/:folder_id/folders (subfolders), GET /v2/folders/:folder_id/files, and GET /v2/folders/:folder_id/meta. These need the folders:read scope (folder_metadata:read for /meta), and folders:read cannot 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}, 200

Figma webhook security note: Figma passes the passcode inside the JSON body (not an HTTP header). Compare it with hmac.compare_digest to prevent timing attacks. All webhook delivery uses HTTPS.

Webhook event types:

EventTrigger
PINGSent on webhook creation to verify endpoint
FILE_UPDATEFile content was updated (autosave)
FILE_VERSION_UPDATENamed version saved
FILE_DELETEFile deleted
LIBRARY_PUBLISHComponent/style library published
FILE_COMMENTComment posted or resolved on a file
DEV_MODE_STATUS_UPDATELayer 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}, 200

Query 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 results

Depth 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:

PlanTier 1 (file reads / images)Tier 2 (components / styles)Tier 3 (analytics)
Starter10 req/min25 req/min50 req/min
Professional15 req/min50 req/min100 req/min
Organization20 req/min100 req/min150 req/min
EnterpriseHigherHigherHigher

View/Collab seat types get lower limits than Dev/Full seats. See X-Figma-Rate-Limit-Type header.

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

HeaderMeaning
Retry-AfterSeconds to wait before retrying
X-Figma-Rate-Limit-Type"low" (View/Collab seat) or "high" (Dev/Full seat)
X-Figma-Plan-TierPlan of the resource owner: starter, pro, org, enterprise
X-Figma-Upgrade-LinkURL 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_ID before 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

CodeWhenAction
400 Bad RequestMalformed request (bad node_id, invalid params)Check node_id format (page:node, e.g. 1:23); validate required fields
401 UnauthorizedMissing or invalid tokenCheck X-Figma-Token header is present; verify token not expired; ensure token has required scope
403 ForbiddenValid token but no accessCheck token scopes (use granular scopes, not deprecated files:read); user may not have access to the file/team
404 Not FoundFile or node doesn't existVerify file_key from URL; verify node_id exists in the file; file may be deleted
429 Too Many RequestsRate limit hitRespect Retry-After header; consider upgrading plan/seat type; batch requests
500 / 503Figma server errorRetry 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=1 or ?depth=2 for shallow fetches
  • Use /v1/files/:key/nodes?ids=NODE_ID to 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:write or org:activity_log_read unless 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 passcode field from the webhook body using hmac.compare_digest (constant-time).
  • Only accept requests over HTTPS. Reject non-HTTPS webhook endpoints at registration time.
  • Log and alert on unexpected event_type values.

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:read accordingly.
  • For Enterprise: use org:activity_log_read for 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/me with your token; confirm 200 and your user info returned
  • Auth — scopes: Attempt an endpoint outside your granted scopes; confirm 403
  • File read: GET /v1/files/FILE_KEY returns 200 with document tree and styles/components maps
  • Nodes endpoint: GET /v1/files/FILE_KEY/nodes?ids=NODE_ID returns only the requested node
  • Images export: GET /v1/images/FILE_KEY?ids=NODE_ID&format=png returns a presigned URL; download the URL returns a valid PNG
  • Comments — read: GET /v1/files/FILE_KEY/comments returns comment list
  • Comments — write: POST /v1/files/FILE_KEY/comments with {"message": "test"} creates a comment; visible in Figma UI
  • Webhook — register: POST /v2/webhooks with event_type: PING creates 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 429 with Retry-After header; 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 404 handled gracefully
  • Images — null check: Request export of an invalid/empty node; confirm err field and null URL 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/versions returns version list with labels

Sources

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.