Canvas API Reference
The Canvas API is the backend contract used by the Gavana CLI and MCP adapter. Most people should use the CLI; use the raw API when you are building your own integration, client library, or automation.
Machine-readable contract
The document is the machine-readable contract for every stable V1 path, required input, common output shape, permission scope, error code, and request ID. Tools such as Postman, Bruno, Insomnia, and OpenAPI client generators can import it directly.
For tools that read Markdown or llms.txt, use the
compact Markdown reference ,
LLM index , or
complete LLM context . Production
builds fail when these generated files drift from the OpenAPI contract.
The production base URL is:
https://app.gavana.ai/api/canvas-agent/v1Authentication
Create an Agent Access Token, then send it as a bearer token:
Authorization: Bearer cba_...
X-Gavana-Agent-Surface: apiNever put a token in a URL, screenshot, log, or support message.
Safe retries
Mutations to an existing canvas require both:
baseRevisionfrom the latest canvas read- an
idempotencyKeycontaining 8–200 characters
If somebody changes the canvas first, the API returns 409 conflict. Read the
canvas again, update baseRevision, and retry with a key that represents the
same intended change. Creating a new canvas is a separate operation; follow its
exact OpenAPI schema rather than adding these fields by assumption.
Errors and support IDs
Every response includes X-Request-ID. Errors also have a stable code and may
identify the exact input fields to fix:
{
"error": {
"code": "input_validation_error",
"message": "baseRevision is required.",
"fields": [
{
"field": "baseRevision",
"message": "Read the canvas and pass its current revision."
}
]
}
}Share the request ID—not the token—when asking for support.
Paginating lists
Canvas, Recipe, asset, Action, model, and provider list responses keep their existing named array and also include page metadata:
{
"canvases": [],
"page": {
"limit": 25,
"hasMore": true,
"nextCursor": "opaque-value"
}
}Pass limit to choose the page size. When hasMore is true, send
nextCursor back as the next request’s cursor. The cursor is opaque and is
valid only with the same endpoint and filters. Do not decode it or construct
one yourself.
Canvas and asset pages are newest first, with stable handles breaking ties. Owned and shared canvases are merged into the same recent-first order while each request stays bounded for large accounts. Canvas pages contain at most 25 summaries because each summary currently reads its complete graph document.
Recipe Runs and shared Runs
Recipe discovery, forking, and execution are separate. POST /recipes/{recipeId}/fork creates one private, editable Recipe instance and
never executes it. POST /recipes/{recipeId}/runs is the explicit operation
that binds typed inputs, materializes declared outputs, and starts sequential
text or image work:
{
"canvasId": "canvas:OWNER_UID:CANVAS_ID",
"baseRevision": "LATEST_CANVAS_REVISION",
"idempotencyKey": "product-direction-001",
"inputs": {
"product-context": "A matte black travel bottle for a quiet premium campaign"
}
}Pass instanceNodeId to run an already connected private Recipe. Otherwise,
Gavana creates a private instance from the requested catalog version.
Each value must match a named port returned by GET /recipes/{recipeId}.
Image ports require a durable image node: or asset: handle. Written ports
accept plain text, a text/sticky node: handle, or an image node:/asset:
handle when the Recipe asks for a reference image or written note. Terminal
outputs include typed durable node: handles plus asset: handles for images.
The current Recipe start route requires image:generate and its dependent
scopes for every Recipe, including a text-only Recipe.
Every Recipe, image, and Action start returns a shared run: handle and
pollUrl. Use GET /runs/{runId} until the status is terminal, or DELETE
that route to cancel. The older /jobs/{jobId} route and job: handle remain
an image and Action compatibility alias; Recipe Runs never have a job:
handle. Recipe Run records currently persist without the temporary image-job
TTL described below.
Deterministic Image Actions
Image Actions make exact raster changes without invoking an AI model or
spending AI-generation credits. GET /actions lists the stable catalog and
GET /actions/{actionKey} returns each Action’s ordered image inputs, typed
parameters, defaults, bounds, and output contract.
The first stable catalog contains:
action:resizeaction:cropaction:change-aspect-ratioaction:side-by-side-compositeaction:add-text-to-imageaction:overlay-imageaction:color-gradeaction:rotate
Run one with POST /actions/{actionKey}/runs:
{
"canvasId": "canvas:OWNER_UID:CANVAS_ID",
"baseRevision": "LATEST_CANVAS_REVISION",
"idempotencyKey": "resize-product-card-001",
"targetNodeId": "node:OUTPUT_IMAGE_NODE",
"inputs": ["node:SOURCE_IMAGE_NODE"],
"params": {
"width": 1080,
"height": 1350,
"fit": "cover"
}
}The raw API requires an existing empty image target; create it with a
revision-safe canvas operation first. The CLI and browser do that setup
automatically. Action runs require canvas:read, canvas:write, and
asset:read, but not image:generate. They return the same shared run:
state, signed webhook option, durable asset: result, and durable node:
result as AI image work.
That scope statement applies to the raw Action request, which accepts existing
node: and asset: handles. The CLI can also upload a local file, stdin, or
the macOS clipboard before starting the Action; that separate private asset
upload currently requires image:generate.
In the browser, an editor can right-click an existing image, choose Image Actions…, select one of the eight transformations, and receive a new result beside the untouched original.
Discovering image models
GET /models returns only models Gavana can run through the managed image
runtime or your saved provider connections. Each entry names its exact
connection, supported operations, typed parameters, allowed values, and an
estimated duration:
{
"handle": "model:opaque-key",
"modelId": "gpt-image-1",
"provider": "openai",
"capabilities": ["image.generate", "image.edit", "image.variations"],
"estimatedSeconds": 35,
"parameters": [{ "name": "count", "type": "integer", "min": 1, "max": 4 }]
}Use the returned opaque model: handle with GET /models/{modelKey}. Do not
decode or construct model keys. Image start requests may pass the same handle
as model; Gavana resolves both the saved connection and provider model
without a second lookup. Duration is an operational estimate, not a
billing or delivery guarantee; Gavana does not invent credit costs for
bring-your-own provider keys.
Understanding image and Action job retention
Image and Action starts return a shared run: handle and a legacy job: alias
for the same temporary queue record. Both observation handles expire together;
neither is permanent history. Read the Run until status is succeeded,
failed, canceled, or expired. Every state includes an
estimatedSeconds value from the selected model or deterministic Action
catalog, plus observed timing:
{
"id": "job:...",
"run": "run:...",
"kind": "image",
"pollUrl": "/api/canvas-agent/v1/runs/...",
"status": "failed",
"estimatedSeconds": 35,
"durationMs": 42118,
"timing": {
"queueDurationMs": 812,
"executionDurationMs": 41306,
"totalDurationMs": 42118
},
"failure": {
"code": "provider_rate_limited",
"message": "The image provider is rate limiting requests. Wait briefly, then retry.",
"retryable": true,
"providerStatus": 429
}
}A provider failure is a normal terminal job response, not an authentication
error from Gavana. Use failure.retryable instead of guessing from the
message. For potentially paid image, workflow, or video work, ask for explicit
approval before retrying. Starting the same intended request with the same
idempotency key replays its job; use a new key only when intentionally starting
a new attempt.
By default, an unacknowledged job result expires after 24 hours. After the first
server finalization—either an authenticated Run GET or the worker finalization
that precedes a signed callback—its temporary job record expires after 15
minutes. The expired tombstone remains for seven days before the handle
returns 404. Operators may override these retention windows. Fetched
asset: and node: outputs remain durable; store those handles rather than
treating either the image/Action run: or job: resource as permanent
history. Recipe run: records currently do not use this temporary TTL.
Signed completion webhooks
Polling is optional. Signed webhooks require an Agent Access token. Add a caller-owned webhook to a Recipe, image, or Action start request:
{
"webhook": {
"url": "https://automation.example.com/hooks/gavana",
"secret": "a-caller-owned-secret-containing-at-least-32-characters"
}
}Gavana accepts only public HTTPS destinations, encrypts the secret at rest,
never returns it, and sends one stable terminal event. Delivery is at least
once: Gavana makes at most four total attempts. Retryable network failures,
408, 425, 429, and 5xx responses back off for 10 seconds, 60 seconds,
then 5 minutes. Redirects and other 4xx responses stop delivery. With
job:manage, inspect run.webhook to see pending, delivering, delivered,
or failed without exposing the endpoint or secret. A start-only callback
token cannot query later delivery state. For a Recipe, Gavana stores a revocable
one-Run worker capability and refreshes the original Agent Access delegation,
so --no-wait execution continues independently of the starting HTTP or CLI
process. If that delegation expires or is revoked before completion, the Run
fails with delegation_revoked and Gavana sends the signed failed callback.
Verify the raw request body before parsing it. The signature is the lowercase hex HMAC-SHA256 of:
<Gavana-Webhook-Timestamp>.<raw request body>using your secret. Compare it to the v1 value in
Gavana-Webhook-Signature with a constant-time comparison. Use
Gavana-Webhook-Id plus the event id to ignore duplicate deliveries.
The event says which Run became terminal. Recipe events include typed output
statuses and durable handles. Successful Image and Action callbacks are not
sent until Gavana has finalized the provider response into durable canvas,
node:, and asset: results; those successful event payloads include the
images, so a callback-only integration does not need an extra GET to make them
durable.
Actions use the same image_job.* event family as generated images; inspect
data.run.operation and data.run.actionId to identify an Action.
If a provider succeeds but Gavana cannot durably store its output after
internal retries, the callback type is image_job.finalization_failed,
data.run.status is failed, and
data.run.failure.code is output_finalization_failed with retryable: true.
If the caller also has job:manage, it can inspect the shared Run; otherwise,
start a new intended attempt with a new idempotency key after correcting the
storage or access problem.
Stable and deprecated surfaces
Canvas, Recipe, asset, Action, image, video, model, provider, Run, and job-alias
endpoints are the supported V1 surface. Campaign endpoints remain in the
contract only for compatibility and
are marked deprecated. Campaign records remain readable, but campaign mutations
return 410 Gone by default unless a server operator explicitly enables legacy
compatibility. New integrations should use explicit Recipe Runs, shared Runs,
and revision-safe canvas operations instead.