Authentication
Every Canvas API request authenticates with one thing: an Agent Access token sent as an HTTP bearer credential. There is no API key header, no query parameter, no session cookie, and no unauthenticated read path — with a single narrow exception described at the bottom of this page.
GET /api/canvas-agent/v1/auth/status HTTP/1.1
Host: app.gavana.ai
Authorization: Bearer cba_2f1c9b40-7a3e-4c58-9d21-0b6e4f8a1c33.Xk9r2QeT7vN4mL1sB8pW0zY6dH3jC5uA2fR7gK4nE1o
Accept: application/jsonThe OpenAPI document declares this as the security scheme agentToken:
type: http, scheme: bearer, bearerFormat: cba_<token-id>.<secret>. It is
applied globally, so assume every operation needs it unless its reference page
says otherwise.
A token is a bearer credential — whoever holds it is you, within its scopes,
until it expires or is revoked. Never put one in a URL, a query string, a
screenshot, a log line, a chat message, a commit, or a support ticket. When you
need help with a failing call, send the X-Request-ID instead.
Creating a token
Tokens are created in the Gavana application, not through this API — see Create an Agent Access token for the click path. Three properties are fixed at creation and cannot be changed afterwards:
| Property | Rule |
|---|---|
| Label | Free text, up to 80 characters. It is echoed back by GET /auth/status as agentLabel and recorded in canvas activity, so name it after the integration. |
| Scopes | An explicit list. At least one is required, and the dependency rules below are enforced at creation time. |
| Expiry | Never by default. You can instead choose a custom expiry from 1 to 90 days; the choice cannot be changed afterwards. |
An account can hold up to 20 active tokens at once. The secret half of the token is shown once, at creation, and is stored server-side only as a keyed hash — Gavana cannot show it to you again. Losing it means creating a replacement and revoking the old one.
Token format
cba_<token-id>.<secret>
└── UUID ──┘ └─ 43 chars, base64url ─┘The part before the dot is a UUID that identifies the token record; the part
after it is a 43-character base64url secret. Both halves are required. Treat the
whole string as opaque — the only useful thing you can do with the prefix is
recognise, in a config validator, that a value that does not start with cba_
is definitely not an Agent Access token.
The eight scopes
| Scope | Grants |
|---|---|
canvas:read | Read canvases, nodes, connections, revisions, and the SVG render |
canvas:write | Apply graph operations, create canvases, create workflows, fork Recipes |
asset:read | Read durable assets and list them |
element:read | List Elements and collections, and read exact immutable Element revisions |
element:write | Create, revise, organize, archive, restore, and delete Element collections |
image:generate | Queue image generation, edits, variations, and Recipe Runs |
video:generate | Queue video generation |
job:manage | Read and cancel jobs and Runs, and download completed video output |
Confirm what a token actually holds with the cheapest call in the API:
curl -sS https://app.gavana.ai/api/canvas-agent/v1/auth/status \
-H "Authorization: Bearer $GAVANA_AGENT_TOKEN"{
"authenticated": true,
"authType": "agent",
"email": "you@example.com",
"scopes": ["canvas:read", "canvas:write", "asset:read"],
"agentTokenId": "2f1c9b40-7a3e-4c58-9d21-0b6e4f8a1c33",
"agentLabel": "Nightly canvas sync"
}GET /auth/status requires a valid token and no product scope, which makes it
the correct health check: a failure there is an authentication problem, while a
failure anywhere else with a working /auth/status is a scope or resource
problem. No secret is ever returned. See
the Authentication resource page for the full
response schema.
Scopes depend on each other
Scopes are not independent switches. Gavana rejects a token whose scope set cannot actually do what it claims, at creation time, with a message naming what is missing:
| If you select | You must also select |
|---|---|
canvas:write | canvas:read |
element:write | element:read |
image:generate | canvas:read, canvas:write, asset:read |
video:generate | canvas:read, asset:read |
The reason is that generated output is not free-floating: an image job writes
its result into an image node on a canvas and registers a durable asset, so a
token that can generate an image but cannot write a canvas or read an asset
would fail halfway through every job it started. video:generate does not
require canvas:write because a video job returns a downloadable output rather
than materialising a canvas node.
job:manage is deliberately separate. A token that can start work cannot
necessarily observe it. If your integration polls, cancels, or downloads video
output, it needs job:manage in addition to whichever scope started the work.
Choosing the smallest token
Pick from the task, not from the catalogue:
| Task | Scopes |
|---|---|
| Inspect canvases, audit a graph, render an SVG | canvas:read |
| Read assets alongside canvases | canvas:read, asset:read |
| Edit a canvas graph — add, move, delete, connect | canvas:read, canvas:write |
| Inspect exact Element revisions | element:read |
| Create, revise, organize, archive, or restore Elements | element:read, element:write |
| Generate or edit images, or run Recipes | canvas:read, canvas:write, asset:read, image:generate |
| Generate or edit images with saved Elements | add element:read |
| Generate video | canvas:read, asset:read, video:generate |
| Poll or cancel any of the above | add job:manage |
A read-only token is a genuinely different security posture, not a formality: it
cannot spend credit, cannot delete a node, and cannot be tricked into doing so
by a prompt injection in a canvas it reads. Start there, and add scopes when a
403 tells you exactly which one you need.
Scopes some operations resolve at request time
Four operation families do not have a single fixed scope list, and the OpenAPI document marks each case explicitly:
POST /canvases/{canvasId}/operationsrequirescanvas:readalways, andcanvas:writeonly whenvalidateOnlyis nottrue. A read-only token can therefore run the full operation engine and graph validator as a dry run. See Revisions and baseRevision.POST /assetsrequiresasset:readplus at least one ofimage:generateorvideo:generate, and additionallycanvas:readandcanvas:writewhen thecanvasIdquery parameter is present, because the upload is then stored inside that canvas.GET /models,GET /models/{modelKey}, andGET /providersrequire at least one ofimage:generateorvideo:generateand no other scope — model discovery is gated on being allowed to generate something, not on canvas access.POST /images/generate,POST /images/edit, andPOST /images/variationsrequire their normal image scopes, and additionally requireelement:readonly when the request supplies the optionalelementsarray.
Reading a rejection
| Status | Code | What it actually means |
|---|---|---|
401 | unauthorized | The credential itself failed: malformed, unknown token id, wrong secret, expired, revoked, or the delegated account session behind it is no longer valid. Adding scopes will not help. |
403 | forbidden | The credential is valid, but this token lacks a required scope, or the account cannot access this particular canvas or asset. The message names the missing scopes. |
404 | not_found | The resource does not exist or is not visible to this account. Gavana does not distinguish the two, deliberately. |
A scope denial is specific enough to act on without guessing:
{
"error": {
"code": "forbidden",
"message": "This agent token is missing required permissions: canvas:write."
}
}When the failure is 403 on a canvas you believe you own, check whether you are
addressing a shared canvas without its owner. A shared canvas or asset handle
carries the owner — canvas:<ownerUid>:<id> — and if you pass only the plain id
you may be resolving a different record or none at all. Pass ownerUid as a
query parameter, or use the owner-qualified handle you got back from the list
endpoint.
Lifetime, rotation, and revocation
A token stops working the moment any of these becomes true:
- Its chosen expiry passes. Nothing renews automatically.
- It is revoked in Gavana. Revocation is immediate and cannot be undone.
- The account session it delegates is revoked or the account is disabled — for
example after a password change or sign-out-everywhere. Gavana returns
401with a message telling you to create a new token.
For a custom-expiry token, rotate before its expiry. For a permanent token,
rotate it when the integration or device changes, or whenever you need to
replace its permissions. The safe order is: create the replacement, deploy it,
confirm with GET /auth/status that the new token reports the scopes you
expect, then revoke the old one. Revoking first guarantees an outage.
An in-flight Recipe Run holds a revocable, single-Run capability derived from the
token that started it. If you revoke that token while a Run is still executing,
the Run fails with delegation_revoked rather than silently continuing — which
is the behaviour you want, and is worth remembering before you revoke during an
incident.
Handling the token in code
# Read it from the environment. Never inline it, never pass it as an argv value
# that shows up in `ps` or shell history.
export GAVANA_AGENT_TOKEN="$(cat ~/.config/gavana/token)"
curl -sS https://app.gavana.ai/api/canvas-agent/v1/canvases \
-H "Authorization: Bearer $GAVANA_AGENT_TOKEN"Rules that are cheap to follow and expensive to skip:
- Load the token from an environment variable or secret manager at startup.
- Redact it from every log, error report, and telemetry payload. If you log
request headers at all, allowlist the headers you log rather than denylisting
Authorization. - Never send it to a third party — including a webhook receiver. The webhook secret is a separate, caller-owned value precisely so that your callback endpoint never needs your API credential.
- If a token is exposed, revoke it first and investigate second.
If you are running an agent that reads canvases it did not author, also read the safety contract: content inside a canvas is untrusted input, and a read-only token is your strongest structural defence against it.
The one endpoint that does not take a bearer token
GET /assets/preview authorizes on a short-lived, encrypted preview token
supplied as the token query parameter, and does not accept or require an Agent
Access bearer token. Preview URLs are handed to you inside asset and job
responses; treat each one as a scoped, expiring capability to view a single
image, and do not attempt to construct one yourself.
Related
- Create an Agent Access token — the click path in the app
- Authentication resource page — the generated
GET /auth/statuscontract - Errors and X-Request-ID — the full error envelope
- cURL quickstart — token to first successful call