copyto.design/Docs/API reference
API · v1

HTML to Figma API

Two REST endpoints over the same engine: send raw HTML (or a URL) and either get back a clipboard payload to ⌘V into Figma, or stream a downloadable .fig file.

Base URLhttps://api.copyto.designAuthBearer sk_••••Requestapplication/jsonResponsetext/html · application/octet-stream

Overview

The copyto.design API exposes the same conversion engine that powers our web app and Figma paste flow. Pick one of two endpoints depending on whether you want to paste into Figma or open a file in Figma Desktop.

Endpoints
POST /v1/open/html_to_figma
POST /v1/open/html_to_fig_file
Inputs
Inline HTML · public URL
Outputs
Clipboard payload · .fig file
Mode
Synchronous
Using an agent?

If you're wiring this into Claude, Claude Code, Codex, or Cursor, start with the Agents guide — it has copy-paste skill files, MCP configs, and tool schemas.

Authentication

Both endpoints are authenticated with an Open API key sent in the Authorization header as a bearer token. Issue and rotate keys from Settings → API keys. Keys are scoped to a single workspace and grant full conversion access on its quota — treat them like passwords.

HTTP
Authorization: Bearer sk_8f3c9a2e4b1d5e6f7a8b9c0d
Content-Type: application/json

Key prefixes

Prefix
Where
Notes
sk_
Open API
Issued from the dashboard. Allowed only on /v1/open/* endpoints; counts against your workspace quota.

Quickstart

Convert a small snippet end-to-end in three lines. The response body is an HTML string you write to the system clipboard as text/html — when you switch to Figma and press ⌘V, native nodes appear.

Request
# html_to_figma → clipboard payload (HTML)
curl https://api.copyto.design/v1/open/html_to_figma \
  -H "Authorization: Bearer $CTD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<section><h1>Hello</h1></section>",
    "options": { "viewportWidth": 1440 }
  }'
const res = await fetch("https://api.copyto.design/v1/open/html_to_figma", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.CTD_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ html, options: { viewportWidth: 1440 } }),
});

const clipboard = await res.text();
await navigator.clipboard.write([
  new ClipboardItem({ "text/html": new Blob([clipboard], { type: "text/html" }) }),
]);
// → switch to Figma, press ⌘V
import os, requests

res = requests.post(
    "https://api.copyto.design/v1/open/html_to_figma",
    headers={"Authorization": f"Bearer {os.environ['CTD_API_KEY']}"},
    json={"html": html_string, "options": {"viewportWidth": 1440}},
)

clipboard_html = res.text
# Then write `clipboard_html` to the system clipboard as text/html

Want a downloadable .fig instead? Hit POST /v1/open/html_to_fig_file with the same body and pipe the binary response to a file.

POST/v1/open/html_to_figmaStable

HTML to Figma clipboard

Renders the supplied HTML (or URL), maps the layout into a Figma node tree, and returns an HTML string carrying the clipboard payload. Write the body verbatim to the system clipboard as text/html — Figma's native paste handler picks it up and creates real frames / Auto Layout / text styles / components.

Request body

One of html or url is required.

Field
Type
Required
Description
html
string
required
Single-file HTML to render. Required if url is not provided.
url
string
required
Public URL the engine will fetch and render server-side. Required if html is not provided.
baseUrl
string
optional
Base URL used to resolve relative resources. Only meaningful with html.
options
object
optional
Render-time tuning. See render options.
imageDataMap
object
optional
Pre-loaded images keyed by imageRef. Each value is base64 bytes; merged with images the engine collects during render.

Example

Request · cURL
curl https://api.copyto.design/v1/open/html_to_figma \
  -H "Authorization: Bearer $CTD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url":     "https://stripe.com/pricing",
    "options": {
      "viewportWidth":     1440,
      "deviceScaleFactor": 2,
      "waitUntil":         "networkidle",
      "timeoutMs":         20000
    }
  }'
Response · 200 OK · text/html200
<meta charset='utf-8'><span data-buffer="<!--(figma)eyJ2IjoxLCJub2RlcyI6Wy4uLl19...(/figma)-->"></span>

The response body is exactly the string shown above (one line). See Figma paste protocol for what each piece is and how Figma decodes it.

POST/v1/open/html_to_fig_fileStable

HTML to .fig file

Same render pipeline as html_to_figma, but the response is a binary .fig file — application/octet-stream with Content-Disposition: attachment; filename="output.fig". Drop the file into Figma Desktop with File → Open (or upload to the web app).

Request body

Identical to /v1/open/html_to_figma — see the table above.

Response headers

Header
Value
Content-Type
application/octet-stream
Content-Disposition
attachment; filename="output.fig"
Content-Length
Bytes in the response body — useful for client-side progress.

Example

cURL
curl https://api.copyto.design/v1/open/html_to_fig_file \
  -H "Authorization: Bearer $CTD_API_KEY" \
  -H "Content-Type: application/json" \
  --output output.fig \
  -d '{ "html": "<!doctype html><body><h1>hi</h1></body>",
       "options": { "viewportWidth": 1440 } }'
Browser

In a browser, fetch().blob() the response and trigger a download via URL.createObjectURL + an anchor with download="output.fig". The request itself is fine from any origin (CORS is enabled).

Render options

Both endpoints accept the same options object. Defaults are tuned for marketing pages and dashboards (1440-wide @ 1x). Reach for these knobs when the page needs JS hydration, a different breakpoint, or a longer timeout.

Field
Type
Required
Description
options.viewportWidth
number
optional
Viewport width in CSS pixels. Default 1440.
options.viewportHeight
number
optional
Viewport height. Auto-fits to actual content height when omitted.
options.deviceScaleFactor
number
optional
Device pixel ratio (DPR). Default 1.
options.waitUntil
enum
optional
When to consider the page ready before snapshotting. load | domcontentloaded | networkidle. Default networkidle.
options.waitForSelector
string
optional
Extra selector to await before snapshot — useful for hydrating SPAs.
options.timeoutMs
number
optional
Global render timeout in milliseconds. Default 15000.

imageDataMap

Optional dictionary of pre-fetched images: keys are the imageRef our engine emits for an <img> or CSS background; values are base64 bytes. Use it when the assets live behind auth, or when the source HTML references blob URLs the renderer can't reach.

json
{
  "imageDataMap": {
    "[email protected]": "iVBORw0KGgoAAAA...",
    "logo.svg": "PHN2ZyB4bWxucz0i..."
  }
}

Figma paste protocol

The string returned by /v1/open/html_to_figma is Figma's clipboard hand-off format — a single <span> with a data-buffer attribute that wraps a base64-encoded clipboard payload between <!--(figma) ... (/figma)--> markers:

Body shape
<meta charset='utf-8'><span data-buffer="<!--(figma)BASE64_PAYLOAD(/figma)-->"></span>

Write it to the system clipboard as text/html (not plain text!). Figma's web client and desktop app both accept it on paste.

Browser · clipboard write
async function copyToFigma(html) {
  const blob = new Blob([html], { type: "text/html" });
  await navigator.clipboard.write([
    new ClipboardItem({ "text/html": blob }),
  ]);
}
Browser caveat

navigator.clipboard.write requires a user gesture and a secure context. Trigger your copy from a click handler — Safari is strictest about this. For server-side flows, ship the user a .fig file via /v1/open/html_to_fig_file instead.

Errors

Errors come back as standard HTTP status codes with a JSON body. The code is stable and safe to switch on; the message is for humans.

Error envelope4xx · 5xx
{
  "code": 400,
  "message": "Either `html` or `url` is required"
}
Status
Code
Meaning
Retry?
400
invalid_request
Body validation failed (missing html/url, malformed JSON, bad option type).
No
401
auth_error
Missing / malformed Authorization header.
No
402
quota_exhausted
Workspace credits used up for the month.
No
403
endpoint_not_allowed
API key prefix is not allowed on this endpoint (e.g. non-sk_ key on /v1/open/*).
No
422
render_failed
Headless render error — bad URL, navigation timeout, JS exception.
Maybe
429
rate_limited
IP / workspace rate limit hit. Honor Retry-After.
Yes
500
server_error
Internal — file a bug, we log a request id server-side.
Yes

Rate limits

Limits are enforced per IP and per workspace. Hitting either yields a 429 with a Retry-After header — back off the indicated number of seconds before retrying.

Header
Example
Meaning
Retry-After
14
Sent on 429. Wait this many seconds.
Tip

Concurrent renders are scarce — each conversion holds a full browser session. Serialize requests within a job and prefer bursts of small payloads over single huge ones.