Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Figma Plugin API and REST API as evidence for a programmable, testable editor surface

Document status: reviewed. Canonical source.

Summary

Figma exposes two programmatic surfaces. The Plugin API runs inside an open editor session: a sandboxed JavaScript main thread manipulates the document through a global figma object (node creation, selection, properties, events, export, undo grouping, per-node plugin data), while an optional iframe hosts UI and browser APIs. The REST API runs outside the editor without a user present; it is “largely read-only” for design content, returning the node tree as JSON, rendering nodes to images, and, on Enterprise plans, reading and writing variables. Figma’s documentation states that plugins cannot run in the background, that a user must initiate them, and that only one plugin runs at a time; no headless plugin execution is offered. A newer MCP server (remote or desktop) allows agents to read design context and create native content, but only through catalogued clients.

NUIF interpretation: Figma proves that a design editor’s entire semantic surface can be exercised without pointer input, which is the premise of apps/editor/QA.md. Its execution model also shows the gap NUIF must close: the programmable surface is bound to a running GUI session, so headless conformance testing is impossible against Figma itself.

Evidence

Retrieval date for all locators: 2026-08-30.

  • figma.createFrame(): FrameNode — “similar to using the F shortcut followed by a click”; the frame defaults to 100×100 with a white background and is parented to figma.currentPage. https://developers.figma.com/docs/plugins/api/properties/figma-createframe/.
  • The global object exposes currentPage: PageNode (settable), root: DocumentNode, editorType (‘figma’ | ‘figjam’ | ‘dev’ | ‘slides’ | ‘buzz’), mode (‘default’ | ‘textreview’ | ‘inspect’ | ‘codegen’ | ‘linkpreview’ | ‘auth’), create* constructors (Frame, Rectangle, Ellipse, Polygon, Star, Text, Component, Page, Section), getNodeByIdAsync, loadAllPagesAsync, on/off/once, commitUndo, triggerUndo, notify, closePlugin, viewport, ui, clientStorage, variables, teamLibrary, skipInvisibleInstanceChildren. https://developers.figma.com/docs/plugins/api/figma/.
  • PageNode.selection: ReadonlyArray<SceneNode>; “Each page stores its own selection separately”; order unspecified; selectedTextRange; loadAsync() required under dynamic page loading. https://developers.figma.com/docs/plugins/api/PageNode/. Whether assignment to selection is permitted was not confirmed in the retrieved text (unverified; the older URL /properties/figma-currentpage/ returns 404).
  • setPluginData(key: string, value: string): void — entry (pluginId, key, value) limited to 100 kB; private to the plugin ID; privacy is “for stability, not security”; empty string deletes the key. https://developers.figma.com/docs/plugins/api/properties/nodes-setplugindata/.
  • Private plug-in data becomes inaccessible if the plug-in ID changes. Shared plug-in data is namespaced, readable by every plug-in and also limited to 100 kB per entry. Locators: https://developers.figma.com/docs/plugins/api/properties/nodes-setplugindata/ and https://developers.figma.com/docs/plugins/api/properties/nodes-setsharedplugindata/.
  • New plug-ins must declare documentAccess: "dynamic-page". The manifest can constrain network requests with networkAccess.allowedDomains; ["none"] declares no network. Locator: Plugin Manifest, retrieved 2026-08-30: https://developers.figma.com/docs/plugins/manifest/.
  • The manifest id is assigned by Figma, normally through Create new Plugin; Figma can also allocate one during publication. A repository can therefore compile a credential-free manifest template, but an importable development manifest requires the reviewer’s assigned ID. The field is specified only as a string, and Figma’s official sample repository includes descriptive sample IDs, so tooling must not impose a numeric-only grammar. Locators, retrieved 2026-08-31: same manifest page and https://github.com/figma/plugin-samples/blob/main/post-message/manifest.json.
  • Figma recommends loading pages only as needed. Document-wide traversal under dynamic loading requires explicit page loads, and several DocumentNode searches require loadAllPagesAsync(). Locators: Accessing the Document and Migrating Plugins to Dynamically Load Pages, retrieved 2026-08-30: https://developers.figma.com/docs/plugins/accessing-document/ and https://developers.figma.com/docs/plugins/migrating-to-dynamic-loading/.
  • setSharedPluginData(namespace: string, key: string, value: string): void — readable by all plugins; namespace at least 3 alphanumeric characters; 100 kB limit; getSharedPluginDataKeys enumerates a namespace. https://developers.figma.com/docs/plugins/api/properties/nodes-setsharedplugindata/.
  • REST GET /v1/files/:key query plugin_data accepts “Comma separated list of plugin IDs and/or the string shared” and adds pluginData and sharedPluginData to nodes in the response; other parameters version, ids, depth, geometry=paths, branch_data; response includes document, components, componentSets, styles, schemaVersion, version. Tier 1, scope file_content:read. https://developers.figma.com/docs/rest-api/file-endpoints/.
  • REST GET /v1/files/:key/nodes (ids, version, depth, geometry, plugin_data); GET /v1/images/:key renders nodes with scale 0.01–4, format jpg/png/svg/pdf, svg_outline_text, svg_include_id, svg_include_node_id, svg_simplify_stroke, contents_only, use_absolute_bounds, version; GET /v1/files/:key/images returns image-fill URLs expiring within 14 days. Same page.
  • The REST API is “Largely read-only” except comments, comment reactions, variables and dev resources; it operates where “a user does not need to be present”; the Plugin API requires that “A user has a particular Figma design or FigJam file open” and can “only read and edit the current file that a user has open”. https://developers.figma.com/compare-apis/.
  • Plugin execution: the main thread runs in an ES2020+ sandbox without the DOM or the full browser API; UI runs in an iframe created by figma.showUI() and communicates with the main thread by messages. A plug-in must close when its work ends. Locator: How Plugins Run, retrieved 2026-08-30: https://developers.figma.com/docs/plugins/how-plugins-run/.
  • Current documentation now exposes a Figma Fetch API in the sandbox, governed by the manifest domain allow-list; UI iframes continue to provide browser APIs. A credential-free NUIF bridge does not require network access. Locator: Making Network Requests, retrieved 2026-08-30: https://developers.figma.com/docs/plugins/making-network-requests/.
  • By default, one undo reverses all actions performed by a plug-in run. figma.commitUndo() partitions later actions into another undo segment. Locator: commitUndo, retrieved 2026-08-30: https://developers.figma.com/docs/plugins/api/properties/figma-commitundo/.
  • “It’s not possible to build plugins that run in the background”; users run one plugin at a time; actions are “initiated by the user”. https://developers.figma.com/docs/plugins/ — introduction.
  • Dev Mode plugins (editorType: ["dev"], capabilities inspect / codegen) are read-only; “setter methods in the Plugin API do not work in Dev Mode” except metadata such as pluginData and relaunchData; pages are always dynamically loaded. https://developers.figma.com/docs/plugins/working-in-dev-mode/.
  • figma.on events: selectionchange, currentpagechange, documentchange, close, run, drop, timer events, stylechange, textreview. documentchange requires "documentAccess": "dynamic-page" in the manifest and a prior figma.loadAllPagesAsync(); Figma “will not call the ‘documentchange’ callback synchronously and will instead batch the updates”. https://developers.figma.com/docs/plugins/api/properties/figma-on/.
  • exportAsync overloads: (settings?: ExportSettings): Promise<Uint8Array> for PNG/JPG/PDF/SVG bytes; (ExportSettingsSVGString): Promise<string>; (ExportSettingsREST): Promise<Object> returning JSON_REST_V1, the REST-compatible node JSON; MP4/GIF/WEBM for animated top-level frames; default PNG at 1x. https://developers.figma.com/docs/plugins/api/properties/nodes-exportasync/.
  • figma.skipInvisibleInstanceChildren: boolean — default true in Dev Mode, false in Figma and FigJam; when enabled, children, findAll, findOne, findAllWithCriteria skip invisible instance descendants and getNodeByIdAsync returns null for them; findAll/findOne become “up to several times faster” and findAllWithCriteria “up to hundreds of times faster in large documents”. https://developers.figma.com/docs/plugins/api/properties/figma-skipinvisibleinstancechildren/.
  • resize(width, height) requires both dimensions to be at least 0.01 except that a line has height zero. The bounded frame/shape/text profile does not include lines, so 0.01 is the uniform import/export minimum. Locator: ResizeMixin, retrieved 2026-08-31: https://developers.figma.com/docs/plugins/api/ResizeMixin/.
  • layoutMode currently admits NONE, HORIZONTAL, VERTICAL and GRID. Changing it can move children and resize the frame; the documented padding, spacing and axis-alignment properties apply to HORIZONTAL or VERTICAL modes. A bounded mapper therefore cannot toggle layout merely to inspect it, and GRID needs a separate profile. Locator: layoutMode, retrieved 2026-08-31: https://developers.figma.com/docs/plugins/api/properties/nodes-layoutmode/.
  • TextNode exposes characters, fontName, fontSize and lineHeight, each of the style properties may be figma.mixed, and writes that affect rendered text require the font to be loaded. The API exposes a family/style identity, not the immutable font-byte SHA-256 required by NUIF. Exact text export must therefore carry previously verified NUIF font metadata or report the font as unsupported. Locators: TextNode and Working with Text, retrieved 2026-08-31: https://developers.figma.com/docs/plugins/api/TextNode/ and https://developers.figma.com/docs/plugins/working-with-text/.
  • Variables REST: GET .../variables/local and GET .../variables/published (scope file_variables:read), POST .../variables (scope file_variables:write, edit permission, 4 MB body, up to 5,000 variables per collection, 40 modes per collection); all require an Enterprise organisation. https://developers.figma.com/docs/rest-api/variables-endpoints/.
  • REST authentication is by personal access token or OAuth2; base URL https://api.figma.com. https://developers.figma.com/docs/rest-api/.
  • MCP server: remote (Figma-hosted) or desktop-app server; agents can read variables, components, layout and design context, generate code, and “create and modify native Figma content directly”; only clients in the Figma MCP Catalog can connect. https://developers.figma.com/docs/figma-mcp-server/.
  • After initial approval, plug-in updates publish immediately to every user and users cannot select an older version. Rollback requires republishing earlier code as a new update. Locator: plug-in introduction, Versioning, retrieved 2026-08-30: https://developers.figma.com/docs/plugins/.
  • Headless execution: no Figma developer page retrieved offers headless or CLI plugin execution. A community feature request confirms plugins cannot auto-run headlessly (secondary source). https://forum.figma.com/suggest-a-feature-11/are-headless-auto-start-figma-plugins-possible-39156.
  • Unverified: whether pluginData survives copy/paste and duplication of nodes; the setPluginData page does not state it.
  • Unverified: whether figma.currentPage.selection is assignable (commonly used in plugin code, but not confirmed in the retrieved text).
  • Unverified: MCP server plan tiers and write-capability details beyond the quoted summary.

Mechanism

Call surface relevant to a programmable editor, grouped by the QA capabilities in apps/editor/QA.md:

  1. Create/open: figma.root, figma.currentPage, figma.createFrame() and siblings, figma.createPage(), loadAllPagesAsync(); REST GET /v1/files/:key for out-of-editor read access.
  2. Query: node.findAll, findOne, findAllWithCriteria, getNodeByIdAsync, skipInvisibleInstanceChildren as a traversal filter; REST ids, depth, geometry=paths.
  3. Transact: property setters on nodes, commitUndo() to group actions into an undo step, triggerUndo(); figma.on('documentchange') as a batched change feed.
  4. Selection as state: PageNode.selection, selectionchange event.
  5. Opaque data: setPluginData (private, keyed by plugin ID) and setSharedPluginData (namespaced, public), each ≤100 kB per entry, surfaced by REST via plugin_data=<id>|shared.
  6. Render: exportAsync (PNG, JPG, SVG, PDF, SVG string, JSON_REST_V1, video); REST GET /v1/images/:key.
  7. Tokens: figma.variables in-editor; REST variables endpoints (Enterprise).
  8. Execution boundary: plugin main thread inside an open file, initiated by a user; REST outside the editor, read-mostly; MCP server as an agent bridge with catalogued clients.
  9. Delivery boundary: manifest API version and plug-in identifier are host contracts. Updates are global, so a NUIF bridge needs independent semantic versioning, fixture gates and an explicit rollback release.
  10. Pure mapping boundary: nuif-figma-plugin-snapshot-0 now maps a normalized one-frame subset in both directions and runs through the CLI. It requires visible, fully opaque, fixed-size nodes, packed row/column auto layout and exact pinned-font metadata. This is executable mapping evidence, not proof of page loading, object creation, undo, messaging or persistence in Figma.
  11. Static shell boundary: the pinned official typings, strict local sources, inline iframe UI and allowedDomains: ["none"] manifest template compile deterministically. A mock public-API object crosses the TypeScript normalizer and Rust importer in cargo xtask gate-figma. The manifest ID remains reviewer-assigned and all runtime host claims remain not_run.

NUIF relevance

Borrow

  • The principle that every inspector control has an API-level property and event, so that tests drive the document through operations rather than pointer input (nuif:claim:semantic-automation).
  • Batched, asynchronous change notification (documentchange) as the model for the editor’s event log and replay capture in rfcs/0004-headless-qa-contract.md.
  • Undo grouping via an explicit commit (commitUndo) as the pattern for NUIF transactions with inverse logs.
  • A traversal flag that skips invisible instance descendants as a documented performance lever for query evaluation in large documents.
  • Opaque, size-bounded, namespaced per-node string stores that survive file save and external export, as precedent for extension preservation (rfcs/0002-extension-preservation.md).

Adapt

  • Replace the plugin-ID-keyed private store with NUIF’s dialect/extension namespaces so preserved data is portable rather than tied to a vendor plugin identity (spec/07-extensions-and-dialects.md).
  • Make the export-to-JSON path (JSON_REST_V1) the canonical serialisation rather than a secondary export, because NUIF’s canonical document is the neutral format itself.
  • Provide the same operation surface in-process and over a local endpoint so the editor is not required to be open (contrast with Figma’s editor-bound plugin runtime).
  • Use dynamic page loading, a no-network manifest by default and one undo group per confirmed import. Store portable identity in a shared nuif namespace, while treating host node IDs and duplicate detection as the authoritative correspondence evidence.
  • Normalize host objects into a bounded, serializable snapshot before invoking the core. This lets credential-free CI test mapping, identity repair and loss reports while leaving the thin host shell and live certification separate.

Reject

  • Editor-bound plugin execution with no headless mode; single-plugin-at-a-time and user-initiated constraints; Enterprise-gated variables API; MCP access restricted to a client catalogue; comments and dev-resources write endpoints; FigJam/Slides/Buzz editor types. Reason: the NUIF test editor must be scriptable headlessly and without plan or client gating.

Open questions

  • Does Figma document plugin-data persistence across copy, paste, duplicate and component instantiation? Needed to compare with NUIF’s preservation guarantees.
  • Is there an official statement on determinism of exportAsync output (identical bytes for identical documents)? Relevant to snapshot comparison.
  • Can the MCP server’s write path be characterised as a semantic operation API, and does it expose undo grouping or change events?