cURL quickstart
Seven commands, from an unused token to a canvas you have verifiably changed. Nothing here spends provider credit.
You need curl and jq, an Agent Access token with canvas:read and
canvas:write, and about five minutes.
Put the token in the environment
read -rs 'GAVANA_AGENT_TOKEN?Paste Agent Access token: '; printf '\n'
export GAVANA_AGENT_TOKEN
export GAVANA_BASE_URL='https://app.gavana.ai/api/canvas-agent/v1'In bash use read -rsp 'Paste Agent Access token: ' GAVANA_AGENT_TOKEN. Either
way the token stays out of shell history and out of ps output.
Never put the token in the URL, in a --data field, or in a script you commit.
-H "Authorization: Bearer $GAVANA_AGENT_TOKEN" is the only correct placement.
Confirm the token works
curl -sS "$GAVANA_BASE_URL/auth/status" \
-H "Authorization: Bearer $GAVANA_AGENT_TOKEN" | jq{
"authenticated": true,
"authType": "agent",
"email": "you@example.com",
"scopes": ["canvas:read", "canvas:write"],
"agentTokenId": "2f1c9b40-7a3e-4c58-9d21-0b6e4f8a1c33",
"agentLabel": "Quickstart"
}This is the cheapest call in the API and the only one that needs no product
scope. If it fails, the problem is the credential itself — see
Authentication. If it succeeds and something
later fails with 403, the problem is scopes, and the message will name the
missing one.
Check scopes against what you expect before going further. A token that turns
out to hold image:generate when you only wanted to read is worth fixing now.
See the request id
Every response carries one. Ask cURL to show you the headers once so you know where to find it:
curl -sS -D - -o /dev/null "$GAVANA_BASE_URL/auth/status" \
-H "Authorization: Bearer $GAVANA_AGENT_TOKEN" | grep -i '^x-request-id'x-request-id: 8c1f2d34-5a6b-4c7d-8e9f-0a1b2c3d4e5fLog this on every call in real code. It is safe to share with support; the token never is.
List canvases
curl -sS --get "$GAVANA_BASE_URL/canvases" \
--data-urlencode 'limit=10' \
-H "Authorization: Bearer $GAVANA_AGENT_TOKEN" | jq '.canvases[] | {handle, title, revision, nodeCount}'{
"handle": "canvas:9Kd2Jv7QpR:product-launch",
"title": "Product launch",
"revision": "2026-08-04T09:12:44.318726Z",
"nodeCount": 14
}Canvas pages cap at 25 because each summary is derived from a complete graph
document. When page.hasMore is true, pass page.nextCursor back as cursor
with the same filters — see Pagination.
Pick one and keep its plain id — the part after the last colon:
export CANVAS_ID='product-launch'If the canvas is shared with you rather than owned by you, its handle is
canvas:<ownerUid>:<id> and you also need ownerUid as a query parameter on the
calls below.
Read the graph and capture the revision
CANVAS=$(curl -sS "$GAVANA_BASE_URL/canvases/$CANVAS_ID" \
-H "Authorization: Bearer $GAVANA_AGENT_TOKEN")
export BASE_REVISION=$(printf '%s' "$CANVAS" | jq -r '.canvas.revision')
printf '%s' "$CANVAS" | jq '{title: .canvas.title, revision: .canvas.revision, nodes: (.canvas.nodes | length)}'{
"title": "Product launch",
"revision": "2026-08-04T09:12:44.318726Z",
"nodes": 14
}revision is the whole point of this step. It is an opaque, server-owned string
that identifies the exact state you just read, and the next step proves to the
server that you read it. Do not parse it or construct one.
Have a look at what is actually there before changing anything:
printf '%s' "$CANVAS" | jq '.canvas.nodes[] | {handle, type, title}'Validate the change for free
validateOnly: true runs the real operation engine and graph validator, writes
nothing, consumes no idempotency key, and needs only canvas:read.
curl -sS -X POST "$GAVANA_BASE_URL/canvases/$CANVAS_ID/operations" \
-H "Authorization: Bearer $GAVANA_AGENT_TOKEN" \
-H 'Content-Type: application/json' \
-d "$(jq -n --arg rev "$BASE_REVISION" '{
validateOnly: true,
baseRevision: $rev,
operations: [
{
type: "node.create",
clientId: "client:quickstart-note",
node: {
type: "sticky",
title: "API quickstart",
position: { x: 0, y: 0 },
width: 240,
height: 160,
metadata: { content: "Written from the cURL quickstart." }
}
}
]
}')" | jq '{proposal: .proposal, destructive: .destructiveImpact, findings: (.validation.findings | length)}'{
"proposal": {
"clientReferences": ["client:quickstart-note"],
"changedNodeReferences": ["client:quickstart-note"],
"deletedNodeReferences": [],
"changedConnectionReferences": [],
"deletedConnectionReferences": []
},
"destructive": {
"deletedNodeHandles": [],
"deletedConnectionHandles": [],
"mediaNodeHandles": [],
"generatedNodeHandles": []
},
"findings": 0
}Read destructiveImpact every time. Empty arrays mean nothing is being
destroyed. If mediaNodeHandles or generatedNodeHandles is non-empty, you are
about to delete images or generated output — stop and confirm with a human first.
Apply it
Same operations, validateOnly dropped, plus an idempotencyKey.
export IDEMPOTENCY_KEY="quickstart-$(date +%Y%m%d)-note-001"
curl -sS -X POST "$GAVANA_BASE_URL/canvases/$CANVAS_ID/operations" \
-H "Authorization: Bearer $GAVANA_AGENT_TOKEN" \
-H 'Content-Type: application/json' \
-d "$(jq -n --arg rev "$BASE_REVISION" --arg key "$IDEMPOTENCY_KEY" '{
baseRevision: $rev,
idempotencyKey: $key,
operations: [
{
type: "node.create",
clientId: "client:quickstart-note",
node: {
type: "sticky",
title: "API quickstart",
position: { x: 0, y: 0 },
width: 240,
height: 160,
metadata: { content: "Written from the cURL quickstart." }
}
}
]
}')" | jq '{replayed, resolvedIds, changedNodeIds, revision: .canvas.revision}'{
"replayed": false,
"resolvedIds": { "client:quickstart-note": "node:1a2b3c4d5e6f" },
"changedNodeIds": ["node:1a2b3c4d5e6f"],
"revision": "2026-08-04T09:21:07.554213Z"
}Three things to notice:
resolvedIdsmaps theclientIdyou invented to the realnode:handle. Capture it — this is how you refer to the node from now on.replayed: falsemeans this actually executed. Run the identical command again and it comes backtrue, with the sameresolvedIdsand no second node.canvas.revisionhas moved. Use that value asbaseRevisionfor your next change; you do not need to re-read.
Verify it landed
curl -sS "$GAVANA_BASE_URL/canvases/$CANVAS_ID/nodes/node:1a2b3c4d5e6f" \
-H "Authorization: Bearer $GAVANA_AGENT_TOKEN" | jq '{node: .node.handle, title: .node.title, incoming: (.incoming | length), outgoing: (.outgoing | length)}'Or open the canvas in Gavana and look at it. The sticky note is there, attributed to the token’s label in the canvas activity.
What a failure looks like
Send a stale baseRevision — for example by making a change in the browser and
then re-running the apply step:
{
"error": {
"code": "conflict",
"message": "The canvas changed after it was read. Read the latest revision and retry the batch.",
"details": {
"currentRevision": "2026-08-04T09:24:11.008Z",
"canvas": { "id": "product-launch", "revision": "2026-08-04T09:24:11.008Z", "nodeCount": 15, "connectionCount": 9 }
}
}
}Nothing was written. Re-read the canvas, decide whether your change still makes
sense against the new graph, rebuild the batch, and retry. Do not just substitute
currentRevision into the old request — see
Revisions and baseRevision.
Send the same idempotencyKey with different operations and you get a
different 409, this one naming the key. Re-reading will not help; use a new key
for new intent.
Handy one-liners
# Scopes on the current token
curl -sS "$GAVANA_BASE_URL/auth/status" -H "Authorization: Bearer $GAVANA_AGENT_TOKEN" | jq -r '.scopes | join(", ")'
# Every accessible canvas handle
curl -sS "$GAVANA_BASE_URL/canvases?limit=25" -H "Authorization: Bearer $GAVANA_AGENT_TOKEN" | jq -r '.canvases[].handle'
# Runnable image models
curl -sS "$GAVANA_BASE_URL/models?capability=image.generate" -H "Authorization: Bearer $GAVANA_AGENT_TOKEN" | jq -r '.models[] | "\(.handle)\t\(.modelId)\t~\(.estimatedSeconds)s"'
# The deterministic Image Action catalogue
curl -sS "$GAVANA_BASE_URL/actions" -H "Authorization: Bearer $GAVANA_AGENT_TOKEN" | jq -r '.actions[] | "\(.id)\t\(.title)"'
# Render a canvas to SVG
curl -sS "$GAVANA_BASE_URL/canvases/$CANVAS_ID/render" -H "Authorization: Bearer $GAVANA_AGENT_TOKEN" -o canvas.svgGET /models and GET /providers need at least one of image:generate or
video:generate; a read-only token gets 403 there.
Next
- TypeScript quickstart — the same flow as runnable code
- Apply a revision-safe batch — larger batches, forward references, conflict recovery
- Generate and observe an image — the first flow that spends credit
- Errors and X-Request-ID — every code and what to do about it