REST API
Read, create and edit files, read nodes, images and versions, read and write comments, and browse the marketplace from scripts and integrations.
REST API
The PopCraft REST API lets scripts, CI jobs and other tools read and edit your files and work with comments. It lives at
https://<your popcraft host>/api/v1 and is described by an OpenAPI 3.1 document you can
feed to any client generator. The endpoint reference is generated from that document.
The popcraft command line has a subcommand for every endpoint; Packages lists
every npm package PopCraft publishes.
Authentication
Description
Home, signed in. The avatar opens the account menu and Personal access tokens lists the tokens there are. A new one is named “CI publish”, given a scope, and created; its secret is shown once, ready to copy into the service that calls the API.
Create a personal access token from the account menu (Account → Personal access tokens). The secret is shown
once and starts with pop_. Send it on every request:
curl -H "Authorization: Bearer pop_…" https://popcraft.app/api/v1/meA token acts as you: it can only reach files you can open, and it can never do more than its scopes allow. Revoke a token from the same dialog; revocation takes effect within a minute.
| Scope | Allows |
|---|---|
files:read | List files, read a file's document, nodes, thumbnail and media |
files:write | Create files, replace a file's document and run editing commands on it |
comments:read | Read comment threads |
comments:write | Post, edit, resolve, react to and delete comments |
versions:read | List versions and read their snapshots |
marketplace:publish | Publish plugins, theme packs, brush and shader packs and templates (for CI) |
sites:publish | Publish a file as a website, roll it back and take it offline |
integrations:read | See which accounts a workspace has connected (never the credentials) |
GET /me works with any token. Marketplace endpoints need no token at all.
Errors
Every error is JSON with a stable code:
{ "error": { "code": "insufficient_scope", "message": "This token needs the comments:write scope" } }| Status | Code | Meaning |
|---|---|---|
| 401 | unauthenticated, invalid_token | No token, or a revoked, expired or malformed one |
| 403 | insufficient_scope, forbidden | The token lacks the scope, or you lack access |
| 404 | not_found | Does not exist, or you cannot see it (the two are deliberately the same) |
| 409 | conflict | The file changed since the If-Match revision; error.details.revision is the current one |
| 429 | rate_limited | Too many requests; wait Retry-After seconds |
Rate limits
Each token may make 120 requests per minute. Every response carries X-RateLimit-Limit,
X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the window resets); a 429 adds Retry-After.
Paging
Lists (/files, /files/{key}/comments) return a page and a nextCursor. Pass it back as ?cursor= for the
next page; it is null on the last one. Cursors are opaque and tied to the sort order you asked for.
curl -H "Authorization: Bearer pop_…" "https://popcraft.app/api/v1/files?limit=50"
curl -H "Authorization: Bearer pop_…" "https://popcraft.app/api/v1/files?limit=50&cursor=2026-09-18T10:00:00.000Z|abc123"Reading a design
# Metadata only (fast), then the nodes you care about with their subtrees
curl -H "Authorization: Bearer pop_…" "https://popcraft.app/api/v1/files/$KEY?depth=metadata"
curl -H "Authorization: Bearer pop_…" "https://popcraft.app/api/v1/files/$KEY/nodes?ids=1:2,1:9"
# The whole document
curl -H "Authorization: Bearer pop_…" "https://popcraft.app/api/v1/files/$KEY" | jq .documentThe document format is described in File format. Images referenced by the document as
media://<hash> are listed by /files/{key}/images, which gives a URL for each one's bytes.
Editing a design
Every file response carries a revision (also its ETag) that moves with every edit, live ones included. Send it
back as If-Match and a write only lands if nobody changed the file in between; otherwise you get a 409 with the
current revision, and can read again and retry.
# A new file, with its document (the document is optional)
curl -X POST -H "Authorization: Bearer pop_…" -H "content-type: application/json" \
-d @<(jq '{name: "Poster", document: .document}' poster.popcraft) "https://popcraft.app/api/v1/files"
# Replace a file's document, only if it is still at the revision you read
curl -X PUT -H "Authorization: Bearer pop_…" -H "content-type: application/json" -H 'If-Match: "y1234"' \
--data-binary @poster.popcraft "https://popcraft.app/api/v1/files/$KEY"A request is capped at about 4.5 MB, so a document heavy with pictures or fonts goes up in pieces: upload each
one's bytes to /files/{key}/media with its hash, and send a document that names them as media://<hash>.
Gzip the document too (Content-Encoding: gzip). popcraft push does both for you.
# Upload a font, then carry it in the file
HASH=$(shasum -a 256 Brand.otf | cut -c1-32)
curl -X POST -H "Authorization: Bearer pop_…" -H "x-media-hash: $HASH" --data-binary @Brand.otf \
"https://popcraft.app/api/v1/files/$KEY/media"
curl -X POST -H "Authorization: Bearer pop_…" -H "content-type: application/json" "https://popcraft.app/api/v1/files/$KEY/commands" \
-d "{\"commands\": [{\"command\": \"font.add\", \"params\": {\"src\": \"media://$HASH\"}}]}"For targeted edits, run commands instead of sending a whole document. They are the
same commands the editor and the agent run; up to 100 run in order, and the result is saved as one edit. A step can
use what an earlier one returned: "$0.id" is the id step 0 created.
curl -X POST -H "Authorization: Bearer pop_…" -H "content-type: application/json" "https://popcraft.app/api/v1/files/$KEY/commands" -d '{
"commands": [
{ "command": "node.create", "params": { "type": "FRAME", "name": "Card", "width": 320, "height": 200 } },
{ "command": "node.create", "params": { "type": "TEXT", "text": "Hello", "parentId": "$0.id" } },
{ "command": "paint.setFills", "params": { "ids": ["$0.id"], "color": "#ff0055" } }
]
}'If a step fails, nothing is saved and the answer is a 422 listing what each step did; "keepGoing": true saves
what the other steps did, and "dryRun": true saves nothing either way. Commands that need the canvas (exports,
rendering), generate media or drive the editor's screen do not run here; the CLI renders exports
in a headless editor.
People editing the file at the time see the change merge in as if a collaborator made it. Private (PopCorn) layers are never part of what the API reads or writes. Before the first write in 30 minutes, the file's current state is kept as an automatic version.
Commenting
curl -X POST -H "Authorization: Bearer pop_…" -H "content-type: application/json" \
-d '{"body":"Looks good @[Sam](usr_123)","nodeId":"1:9","offsetX":12,"offsetY":8}' \
"https://popcraft.app/api/v1/files/$KEY/comments"
curl -X PATCH -H "Authorization: Bearer pop_…" -H "content-type: application/json" \
-d '{"resolved":true}' "https://popcraft.app/api/v1/files/$KEY/comments/$ID"Marketplace
The public marketplace is readable without a token, from any origin:
curl "https://popcraft.app/api/v1/marketplace?kind=theme"
curl "https://popcraft.app/api/v1/marketplace/plugin/popcraft.rename-layers"Versions
The API version is in the spec's info.version. Additive changes (new fields, endpoints, parameters) do not
change the path; a breaking change would be served under /api/v2. Changes:
- 1.4.0 —
POST /files/{key}/mediauploads a picture, video or font the file uses;PUT /files/{key}andPOST /filestake a gzipped body;font.addandfont.listrun through/commandswith amedia://src. - 1.2.0 — Writes:
POST /files,PUT /files/{key}andPOST /files/{key}/commands, with thefiles:writescope. File responses carryrevisionandlive, andGET /files/{key}returns live edits. - 1.1.0 — Documented all
/filesfilters and paging, the comment PATCH body, response schemas for every endpoint and the rate-limit headers; added the public marketplace endpoints. - 1.0.0 — Files, nodes, images, comments and versions.