The agent protocol
An AI agent can draw here. Not generate a picture of a drawing — draw: place a circle with a centre and a radius, group three shapes, align the group against a wordmark. What it produces is a real vector document, and every shape in it is one you can afterwards select, move, recolour and delete.
That works because there is only one set of verbs. When you press Ctrl+G in the
editor, the keyboard sends group.create. When you click the group button, the
toolbar sends group.create. When you run jd group create, the CLI sends
group.create. When a language model decides two shapes belong together, it
sends group.create. Four seats, one steering wheel — so an agent’s edit lands
in your undo history, appears on your screen as it happens, and autosaves like
any other edit.
Start here
Paste a project URL at an agent and it will work the rest out, because the URL teaches. Every project publishes a briefing generated from its own live contents:
GET https://app.justdraw.fyi/p/<uuid>.md
That briefing lists what is currently on the canvas with real ids, the whole
command surface, and curl examples with that project’s id already substituted
in — so they run as written. An agent that has never heard of justdraw.fyi can be
handed a link and get to work. This page is the same material, minus the project.
Three steps
1. Make a project. No account, no key.
curl -sX POST https://app.justdraw.fyi/api/v1/projects \
-H 'Origin: https://app.justdraw.fyi'
→ {"uuid":"0192a4e1-7c33-7f21-9b0e-2f1a6c9d8e40","url":"/p/0192a4e1-7c33-7f21-9b0e-2f1a6c9d8e40"}
2. Open it in a browser. https://app.justdraw.fyi/p/<uuid> — see the constraint below.
3. Send commands.
curl -sX POST https://app.justdraw.fyi/api/v1/agent \
-H 'Content-Type: application/json' \
-H 'Origin: https://app.justdraw.fyi' \
-d '{"cmd":"element.draw","project":"0192a4e1-7c33-7f21-9b0e-2f1a6c9d8e40",
"args":{"shape":"rect","x1":0,"y1":0,"x2":40,"y2":30,"fill":"#5b7cfa"}}'
→ {"ok":true,"result":{"id":"<new-uuid>","type":"rect","name":"Rectangle 1"}}
Every command that creates something returns its id. Ids are opaque: pass them straight back into the next command, and never parse them.
The two things that will cost you a retry
The Origin header is required. Leave it out and you get
403 Origin or Referer header required. It is a cross-site guard: a browser will
not attach that header when another website tries to POST here on a user’s behalf,
which is the attack it exists to stop. curl doesn’t send one unless you say so.
Set it to this server and forget about it.
A drawing command needs the project open in a browser. With no tab, you get
409. The editor in the browser is the engine — it owns the geometry, the
layers, the undo stack — and the server only relays commands down the WebSocket
that the open editor holds. That is a real constraint, and it is the reason there
is exactly one implementation of what every command means, shared by the toolbar,
the keyboard, the CLI and any agent. Nothing can drift.
What does work with no tab open, because it is answered from the database:
proj.create|destroy|list|show|rename, all of snap.*, and doc.export with
{"format":"yaml"}. Everything else — including element.list, doc.get, and
doc.export to SVG — is executed by the editor and returns 409 without one.
Commands
Each is the cmd and args of the POST body above, with
"project":"0192a4e1-7c33-7f21-9b0e-2f1a6c9d8e40" alongside them.
| cmd | args | what it does |
|---|---|---|
element.draw | {"shape":"rect","x1":0,"y1":0,"x2":40,"y2":30,"fill":"#e5614c"} | rect / square / ellipse: two opposite corners |
element.draw | {"shape":"circle","cx":80,"cy":15,"r":15} | circle: centre and radius |
element.draw | {"shape":"triangle","x1":0,"y1":0,"x2":40,"y2":30} | triangle: base flush with the bottom, apex centred |
element.draw | {"shape":"line","x1":0,"y1":0,"x2":50,"y2":50} | line: two endpoints |
element.draw | {"shape":"polygon","x1":0,"y1":0,"x2":40,"y2":40,"sides":6} | polygon: a REGULAR n-gon inscribed in the box (sides defaults to 5) |
element.draw | {"shape":"star","x1":0,"y1":0,"x2":40,"y2":40,"points":5} | star: points defaults to 5 |
element.draw | {"shape":"text","x":0,"y":0,"text":"hello","size":16} | text: size defaults to 16 |
element.style | {"ids":["<id>"],"fill":"#5b7cfa","stroke":"none"} | colour: fill, stroke, strokeWidth, opacity, rx |
element.style | {"ids":["<id>"],"fontSize":48,"weight":700,"fontFamily":"Georgia"} | type: fontSize, weight (100–900), fontFamily, align, text |
element.list | {} | every element: id, type, name, bounding box, fill, stroke |
element.move | {"ids":["<id>"],"dx":10,"dy":-5} | translate by a delta |
element.name | {"id":"<id>","name":"hero"} | rename an element, group, layer — or the project, with no id |
element.clone | {"ids":["<id>"]} | duplicate; the copy is named |
element.delete | {"ids":["<id>","<id2>"]} | remove elements |
group.create | {"ids":["<id>","<id2>"]} | group them; returns the group id |
group.ungroup | {"ids":["<id>"]} | dissolve the group that element is in |
arrange.align | {"mode":"left","ids":["<id>","<id2>"]} | up, down, left, right, hcenter, vcenter |
arrange.distribute | {"axis":"h","ids":["<a>","<b>","<c>"]} | even spacing: h or v |
arrange.order | {"mode":"front","ids":["<id>"]} | front, back, forward, backward |
doc.export | {"format":"svg"} | the whole drawing as SVG (or yaml) |
doc.get | {} | the raw document |
snap.create | {"name":"before my changes"} | freeze the document; returns the snapshot’s id |
snap.list | {} | the timeline: id, name, element count, when |
snap.show | {"id":"<snapshot>"} | that snapshot’s document, as YAML |
snap.restore | {"id":"<snapshot>"} | put it back (what it replaces is frozen first) |
snap.delete | {"id":"<snapshot>"} | forget a snapshot |
Notes that will save you a retry
- Coordinates are world pixels, y grows downwards, and the canvas is infinite — negative coordinates are fine. Shapes land where you put them, not on a page.
- Box shapes (
rect,square,ellipse,triangle,polygon,star) take two opposite corners in any order; they are normalised for you. - Everything you draw is the same grey until you colour it (
#202128). Passfilltoelement.draw, or set it later withelement.style. A drawing where every shape is the default grey is one nobody can read. - Use
trianglefor a triangle.polygonwithsides:3is a regular 3-gon inscribed in a circle, so its base sits above the bottom edge of its box — which is almost never what you want for a roof. idsaccepts an element id or a group id. Group what belongs together: when youalignordistribute, a group moves as one rigid piece, so a mark you have assembled keeps its composition while it is centred against a wordmark.- A
circleis an ellipse, and the reply says"type":"ellipse". That is the same shape you asked for, not a mistake. - Your edits go into the same undo history as the human’s, and are saved before the command answers. If you draw something wrong, delete it; don’t ask them to undo.
- Take a snapshot before you rework someone’s drawing —
snap.createfreezes the whole document under a name, andsnap.restoreputs it back. See below. alignneeds ≥2 elements,distributeneeds ≥3,group.createneeds ≥2.- Check your work:
doc.exportwith{"format":"svg"}returns the drawing.
The canvas is dark — #141518. You cannot see it, so take this on trust: a
near-black shape is invisible on it, and so is dark text. If you want something to
read as black (a wordmark, a door), it needs to sit on a light shape you drew, or
be light itself.
A palette that works on it: #5b7cfa blue, #e5614c red, #8bd450 green,
#e0b64a amber, #4ad0c0 teal, #c86bd6 violet, #ffffff white, #e8e8ec
off-white (the default for text). #111319 near-black is for use on top of a
light shape — not against the canvas.
Going back
A project holds one document, and every save replaces it — so without something else, “what did this look like before?” is a question only an open tab’s undo stack can answer, and only until it closes. Snapshots are that something else: a frozen copy of the whole document, taken at a moment worth coming back to.
curl -sX POST https://app.justdraw.fyi/api/v1/agent -H 'Origin: https://app.justdraw.fyi' \
-H 'Content-Type: application/json' \
-d '{"cmd":"snap.create","project":"0192a4e1-7c33-7f21-9b0e-2f1a6c9d8e40","args":{"name":"before the rewrite"}}'
snap.list prints the timeline, snap.restore puts one back, and a restore
freezes the document it is about to replace first — so restoring the wrong one
costs you nothing but another restore. The server also takes one on its own
every few minutes while a drawing is being changed, and the ones nobody named
are the only ones it ever evicts.
None of this needs a browser: a snapshot is a copy of the document as a blob,
and copying a blob does not require knowing what is drawn in it. So snap.*
works on a project nobody has open — unlike everything that touches geometry.
If a human is watching, a restore is handed to their editor as an ordinary
edit, so Ctrl+Z takes it back.
Every one of those notes is a scar
They are not style advice. Each was written after watching a real agent, handed nothing but a URL, get that exact thing wrong:
- it drew a monochrome picture, because nothing told it shapes start grey;
- it used
polygonwith 3 sides as a roof, and got a triangle floating above its box with daylight underneath; - it copied the
curlexample and got a 403, because the example had noOrigin; - it aligned a group and watched its composition atomise — so a group now aligns and distributes as one rigid unit, which is what every design tool does, and what the UI now does too;
- it drew a near-black wordmark on the near-black canvas.
The last two were bugs in the product, not in the model. The machine did the obvious thing and revealed that the obvious thing was broken.
MCP
If your agent speaks the Model Context Protocol — Claude, Cursor, Windsurf and
most of the rest do — jd-mcp gives it these commands as tools, so it never has
to write a curl at all. Download it from the
releases and point your
client at it:
{
"mcpServers": {
"justdraw": {
"command": "/path/to/jd-mcp",
"args": ["-server", "https://app.justdraw.fyi"]
}
}
}
Tools: create_project, list_projects, draw, list_elements, style,
move, delete, group, ungroup, align, distribute, order, export.
It is a thin adapter over the same HTTP calls above — it does not know what
“align” means, and it keeps no second copy of the document. Ask it to create a
project, open the URL it gives you, and tell it what to draw.
The command line
jd speaks this exact protocol from a shell. Every command prints the id of what
it made, so ids pipe from one command into the next:
jd proj create logo # prints the project id
jd snap take <project> "before I touch it"
jd draw <project> rect 0 0 120 80 # prints the new element's id
jd style <id> fill=#5b7cfa
jd group create <id> <id2>
jd align vcenter <group> <id3>
jd export <project> logo.svg
jd snap list <project> # …and jd snap restore <project> <snapshot>
An element id works anywhere a project id is expected — the server finds the
project it belongs to. Run jd help for the full surface.