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

Make a personal access token · 0:07
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/me

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

ScopeAllows
files:readList files, read a file's document, nodes, thumbnail and media
files:writeCreate files, replace a file's document and run editing commands on it
comments:readRead comment threads
comments:writePost, edit, resolve, react to and delete comments
versions:readList versions and read their snapshots
marketplace:publishPublish plugins, theme packs, brush and shader packs and templates (for CI)
sites:publishPublish a file as a website, roll it back and take it offline
integrations:readSee 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" } }
StatusCodeMeaning
401unauthenticated, invalid_tokenNo token, or a revoked, expired or malformed one
403insufficient_scope, forbiddenThe token lacks the scope, or you lack access
404not_foundDoes not exist, or you cannot see it (the two are deliberately the same)
409conflictThe file changed since the If-Match revision; error.details.revision is the current one
429rate_limitedToo 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 .document

The 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}/media uploads a picture, video or font the file uses; PUT /files/{key} and POST /files take a gzipped body; font.add and font.list run through /commands with a media:// src.
  • 1.2.0 — Writes: POST /files, PUT /files/{key} and POST /files/{key}/commands, with the files:write scope. File responses carry revision and live, and GET /files/{key} returns live edits.
  • 1.1.0 — Documented all /files filters 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.