Skip to Content
APIPlatformAuthentication

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/json

The 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:

PropertyRule
LabelFree 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.
ScopesAn explicit list. At least one is required, and the dependency rules below are enforced at creation time.
ExpiryNever 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

ScopeGrants
canvas:readRead canvases, nodes, connections, revisions, and the SVG render
canvas:writeApply graph operations, create canvases, create workflows, fork Recipes
asset:readRead durable assets and list them
element:readList Elements and collections, and read exact immutable Element revisions
element:writeCreate, revise, organize, archive, restore, and delete Element collections
image:generateQueue image generation, edits, variations, and Recipe Runs
video:generateQueue video generation
job:manageRead 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 selectYou must also select
canvas:writecanvas:read
element:writeelement:read
image:generatecanvas:read, canvas:write, asset:read
video:generatecanvas: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:

TaskScopes
Inspect canvases, audit a graph, render an SVGcanvas:read
Read assets alongside canvasescanvas:read, asset:read
Edit a canvas graph — add, move, delete, connectcanvas:read, canvas:write
Inspect exact Element revisionselement:read
Create, revise, organize, archive, or restore Elementselement:read, element:write
Generate or edit images, or run Recipescanvas:read, canvas:write, asset:read, image:generate
Generate or edit images with saved Elementsadd element:read
Generate videocanvas:read, asset:read, video:generate
Poll or cancel any of the aboveadd 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}/operations requires canvas:read always, and canvas:write only when validateOnly is not true. A read-only token can therefore run the full operation engine and graph validator as a dry run. See Revisions and baseRevision.
  • POST /assets requires asset:read plus at least one of image:generate or video:generate, and additionally canvas:read and canvas:write when the canvasId query parameter is present, because the upload is then stored inside that canvas.
  • GET /models, GET /models/{modelKey}, and GET /providers require at least one of image:generate or video:generate and no other scope — model discovery is gated on being allowed to generate something, not on canvas access.
  • POST /images/generate, POST /images/edit, and POST /images/variations require their normal image scopes, and additionally require element:read only when the request supplies the optional elements array.

Reading a rejection

StatusCodeWhat it actually means
401unauthorizedThe 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.
403forbiddenThe 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.
404not_foundThe 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 401 with 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.

Last updated on