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 tofigma.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 toselectionis 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 withnetworkAccess.allowedDomains;["none"]declares no network. Locator: Plugin Manifest, retrieved 2026-08-30: https://developers.figma.com/docs/plugins/manifest/. - The manifest
idis 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
DocumentNodesearches requireloadAllPagesAsync(). 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;getSharedPluginDataKeysenumerates a namespace. https://developers.figma.com/docs/plugins/api/properties/nodes-setsharedplugindata/.- REST
GET /v1/files/:keyqueryplugin_dataaccepts “Comma separated list of plugin IDs and/or the string shared” and addspluginDataandsharedPluginDatato nodes in the response; other parametersversion,ids,depth,geometry=paths,branch_data; response includesdocument,components,componentSets,styles,schemaVersion,version. Tier 1, scopefile_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/:keyrenders nodes withscale0.01–4,formatjpg/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/imagesreturns 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"], capabilitiesinspect/codegen) are read-only; “setter methods in the Plugin API do not work in Dev Mode” except metadata such aspluginDataandrelaunchData; pages are always dynamically loaded. https://developers.figma.com/docs/plugins/working-in-dev-mode/. figma.onevents: selectionchange, currentpagechange, documentchange, close, run, drop, timer events, stylechange, textreview.documentchangerequires"documentAccess": "dynamic-page"in the manifest and a priorfigma.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/.exportAsyncoverloads:(settings?: ExportSettings): Promise<Uint8Array>for PNG/JPG/PDF/SVG bytes;(ExportSettingsSVGString): Promise<string>;(ExportSettingsREST): Promise<Object>returningJSON_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— defaulttruein Dev Mode,falsein Figma and FigJam; when enabled,children,findAll,findOne,findAllWithCriteriaskip invisible instance descendants andgetNodeByIdAsyncreturns null for them;findAll/findOnebecome “up to several times faster” andfindAllWithCriteria“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 least0.01except that a line has height zero. The bounded frame/shape/text profile does not include lines, so0.01is the uniform import/export minimum. Locator:ResizeMixin, retrieved 2026-08-31: https://developers.figma.com/docs/plugins/api/ResizeMixin/.layoutModecurrently admitsNONE,HORIZONTAL,VERTICALandGRID. 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/.TextNodeexposescharacters,fontName,fontSizeandlineHeight, each of the style properties may befigma.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:TextNodeand 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/localandGET .../variables/published(scopefile_variables:read),POST .../variables(scopefile_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
pluginDatasurvives copy/paste and duplication of nodes; the setPluginData page does not state it. - Unverified: whether
figma.currentPage.selectionis 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:
- Create/open:
figma.root,figma.currentPage,figma.createFrame()and siblings,figma.createPage(),loadAllPagesAsync(); RESTGET /v1/files/:keyfor out-of-editor read access. - Query:
node.findAll,findOne,findAllWithCriteria,getNodeByIdAsync,skipInvisibleInstanceChildrenas a traversal filter; RESTids,depth,geometry=paths. - Transact: property setters on nodes,
commitUndo()to group actions into an undo step,triggerUndo();figma.on('documentchange')as a batched change feed. - Selection as state:
PageNode.selection,selectionchangeevent. - Opaque data:
setPluginData(private, keyed by plugin ID) andsetSharedPluginData(namespaced, public), each ≤100 kB per entry, surfaced by REST viaplugin_data=<id>|shared. - Render:
exportAsync(PNG, JPG, SVG, PDF, SVG string, JSON_REST_V1, video); RESTGET /v1/images/:key. - Tokens:
figma.variablesin-editor; REST variables endpoints (Enterprise). - 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.
- 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.
- Pure mapping boundary:
nuif-figma-plugin-snapshot-0now 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. - 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 incargo xtask gate-figma. The manifest ID remains reviewer-assigned and all runtime host claims remainnot_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 inrfcs/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
nuifnamespace, 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
exportAsyncoutput (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?