Developers
API reference
A small, versioned REST API for generating images and code headlessly, plus an OAuth-protected MCP endpoint for agent clients. Use API keys for server integrations and MCP when an agent should act as a signed-in vmotif user.
Base URL & versioning#
All endpoints live under a single versioned prefix:
https://vmotif.com/api/v1The API is at v1. Changes within v1are additive only — new fields may appear, but existing fields and shapes won't change underneath you. Every response carries an x-request-id header; include it when contacting support.
A machine-readable OpenAPI 3.1 spec is served at https://vmotif.com/api/v1/openapi.json — point your codegen or API client at it. A typed TypeScript client generated from that spec lives in @vmotif/sdk.
import { createVmotifClient } from "@vmotif/sdk"
const vmotif = createVmotifClient({ apiKey: process.env.VMOTIF_API_KEY! })
const { data, error } = await vmotif.createGeneration({ type: "image", prompt: "a fox" })Authentication#
Every request needs a bearer token — an API key you create in Settings → API keys. Keys start with vmk_live_ and are shown once at creation, so store them somewhere safe.
Authorization: Bearer vmk_live_xxxxxxxxxxxx/api/v1. Treat it like a password: never commit it or expose it in client-side code. Revoke a leaked key from the same settings page and the API rejects it immediately.A missing, malformed, revoked, or unknown key returns a single 401 shape — the API never discloses which keys exist.
Each key carries scopes chosen at creation. A write key can generate and run workflows (and read, since write implies read); a read-only key can list and poll but never spends budget. Calling an endpoint your key isn't scoped for returns 403 insufficient_scope.
Agent clients over MCP#
Agent clients that support the Model Context Protocol can connect directly to vmotif over Streamable HTTP:
https://vmotif.com/api/mcpThe first connection opens an OAuth consent flow in the browser. After approval, the agent receives a Supabase-issued access token that is bound to vmotif's MCP resource. The MCP server validates that token on every request and accepts both production-host audience spellings for compatibility with existing clients.
claude mcp add --transport http vmotif https://vmotif.com/api/mcpcodex mcp add --url https://vmotif.com/api/mcp --oauth-resource https://vmotif.com/api/mcp vmotif
codex mcp login vmotifMCP connections are initialized when an agent session starts. After adding or repairing the server, open a new session before checking whether its tools are available.
Available MCP tools:
- get_usage
- Read the signed-in account's plan tier and current generation budget. Optionally pass a workspace_id to read a workspace's pooled Team budget instead.
- list_models
- List available generation models, optionally filtered to image or text/code models.
- generate_image
- Start an image generation job and return a pending generation id.
- generate_code
- Start a code, markup, SVG, JSON, or screen-HTML generation job.
- get_generation
- Read generation status, completed output, cost, or failure details.
- transform_image
- Convert, compress, or trim a stored image (e.g. a generation result) — free, synchronous, returns the stored result URL.
- add_generation_to_project
- After explicit user confirmation, add every image from a completed generation to an editable project.
- list_projects
- List accessible personal and workspace projects/canvases, optionally filtered to favorites.
- get_favorite_projects
- List favorite projects/canvases ordered by their latest update.
- read_canvas
- Read project metadata plus the hydrated canvas graph and tagged brand-asset URLs.
- list_premade_workflows
- List canonical workflow ids, names, descriptions, and graph sizes available for insertion.
- create_canvas
- Create an empty canvas or atomically seed a new canvas with a premade workflow.
- add_premade_workflow
- Append a canonical premade workflow to any editable existing canvas without replacing its graph.
- get_workflow
- Read workflow metadata plus its hydrated node/edge graph.
- list_workflows
- List saved workflow summaries with limit/offset pagination.
- run_workflow
- Start a headless run of a saved workflow.
- get_workflow_run
- Read workflow-run progress, per-output results, costs, and failure details.
Premade workflows compose with existing work. For example, after finishing a brand canvas, an agent can discover the maintained press-kit workflow and insert it into that same canvas:
1. list_projects
2. list_premade_workflows
3. add_premade_workflow {
"canvas_id": "<existing brand canvas UUID>",
"premade_workflow_id": "press-kit"
}
4. read_canvas { "project_id": "<existing brand canvas UUID>" }To start separately, call create_canvas with an optional premade_workflow_id. Creation and template seeding commit together, and adding a workflow preserves the existing graph, assigns fresh ids, and places the inserted flow without overlap. Neither tool forces browser navigation or a new app route.
Supabase OAuth currently issues standard OpenID Connect scopes such as openid, email, and profile. vmotif does not rely on custom OAuth scopes for generation permission; it enforces those checks server-side for each tool call.
Generate an image or code#
Generation is asynchronous. You POST a request, get back a job id immediately, then poll until it completes. This keeps the connection short even when a model takes a while.
curl -X POST https://vmotif.com/api/v1/generations \
-H "Authorization: Bearer vmk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"type": "image",
"prompt": "a minimalist mountain logo, single line",
"model": "google/gemini-3-pro-image",
"aspectRatio": "1:1"
}'The response returns right away with a job id:
{ "id": "0c2f…", "status": "pending" }Request fields:
- type
"image"or"code". Required. Must match the model's category.- prompt
- The instruction. Required, up to 50,000 characters.
- model
- A model id from
GET /models. Optional — defaults to a sensible image or code model based ontype. - images
- Up to 10 input images, each
{ url, mediaType }. URLs must behttpsordata:. - textInputs
- Up to 10 text inputs, each
{ content, label?, language? }, fed to the model as context. - aspectRatio
- For images: one of
1:1,16:9,9:16,4:3,3:4,3:2,2:3. - targetLanguage
- For code: the output language (e.g.
tsx,css,json). - screenHtml
- For code: render the output as a full HTML screen rather than a snippet. Optional, defaults to
false.
type (e.g. a text model with type: "image") is rejected with 400up front, so you never burn budget on a job that can't succeed.Poll for the result#
Poll the job until status is completed or failed. This endpoint is never cached.
curl https://vmotif.com/api/v1/generations/0c2f… \
-H "Authorization: Bearer vmk_live_xxx"An image result:
{
"id": "0c2f…",
"status": "completed",
"result": {
"type": "image",
"url": "https://…",
"mediaType": "image/png",
"aspectRatio": "1:1",
"costMicros": 39000
},
"error": null
}A code result returns the generated text instead:
{
"id": "1a9b…",
"status": "completed",
"result": {
"type": "code",
"code": "export function Button() { … }",
"structuredOutput": null,
"costMicros": 4200
},
"error": null
}result is null until the job completes; error holds the failure message when status is failed. A job id that isn't yours, doesn't exist, or wasn't created via the API returns 404— ids can't be probed across accounts.
Transform an image#
Once a generation completes, you can post-process the image without another model call: convert it to a different format, compress it to a target file size, or trim uniform edges. Transforms run synchronously — the response carries the stored result — and are free: they never draw on your AI budget.
curl -X POST https://vmotif.com/api/v1/images/transforms \
-H "Authorization: Bearer vmk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"source": "https://…/workflow-images/…/generation.png",
"op": "compress",
"options": { "targetBytes": 200000 }
}'The result is stored under your account and returned as a URL:
{
"url": "https://…/workflow-images/…/result.webp",
"path": "…/result.webp",
"width": 1024,
"height": 1024,
"bytes": 187342,
"format": "webp",
"withinTarget": true,
"trimmed": null
}Request fields:
- source
- A
workflow-imagesstorage path or public URL you own — a completed generation'sresult.urlqualifies. Required. - op
"convert","compress", or"trim". Required.- options.format
- Output format:
png,jpeg,webp, oravif. Required forconvert;compressdefaults towebp; ignored bytrim. - options.quality
- Encoder quality for
convert, between 0 (exclusive) and 1. Defaults to0.9. - options.targetBytes
- Target output size for
compress(10KB–10MB). The API binary-searches encoder quality;withinTarget: falsein the response means even the lowest quality couldn't fit and the smallest encode was stored instead. - options.maxDimension
- Bound the longest output edge in pixels, capped at 8192. Never enlarges.
trim that finds no uniform edges returns { "trimmed": false } with all other fields null and stores nothing. Sources over 10MB are rejected with 400; a source that isn't yours returns 404. Transforms share a dedicated rate limit with the in-app image tools, separate from generation limits.Read endpoints#
Three read-only endpoints help you build against the API:
GET /models— the catalog of models usable via/generations, with each model's id, category, provider, modalities, and the prices you're actually billed.GET /usage— your AI budget for the current period. Pre-flight with this before generating.GET /workflows— summaries of your workflows (id,name,updated_at). Supports?limit(≤100) and?offset.
/usage reports budget in microdollars (1_000_000 = $1); a null budget means unlimited. Personal fields are always present for backward compatibility. When the API key is bound to a workspace, the scope field shows "workspace" and the workspace_* fields describe the pooled Team budget the key actually charges to:
{
"tier": "pro",
"token_budget_micros": 20000000,
"used_micros": 3450000,
"remaining_micros": 16550000,
"resets_at": "2026-06-01T00:00:00Z",
"scope": "personal",
"workspace_id": null,
"workspace_budget_micros": null,
"workspace_used_micros": null,
"workspace_remaining_micros": null,
"workspace_resets_at": null
}{
"tier": "pro",
"token_budget_micros": 20000000,
"used_micros": 1200000,
"remaining_micros": 18800000,
"resets_at": "2026-06-01T00:00:00Z",
"scope": "workspace",
"workspace_id": "7f3a8b2c-1234-4abc-9def-0123456789ab",
"workspace_budget_micros": 75000000,
"workspace_used_micros": 12500000,
"workspace_remaining_micros": 62500000,
"workspace_resets_at": "2026-07-01T00:00:00Z"
}Run a workflow#
Beyond single generations, you can execute an entire saved workflow headlessly. The API resolves the graph, runs each prompt node in dependency order — parallelizing independent nodes — and feeds each node's output into the prompts downstream, the same way the canvas does when you press Run all.
curl -X POST https://vmotif.com/api/v1/workflows/7f3a…/runs \
-H "Authorization: Bearer vmk_live_xxx"It returns immediately with a run id; the work continues server-side:
{ "id": "b81c…", "status": "pending" }Poll the run for progress and per-output results:
{
"id": "b81c…",
"workflowId": "7f3a…",
"status": "running",
"progress": { "total": 3, "completed": 1 },
"results": [
{
"promptNodeId": "node-a",
"outputNodeId": "out-a",
"type": "image",
"status": "completed",
"label": "X Image",
"platform": "x",
"url": "https://…",
"mediaType": "image/png",
"costMicros": 39000
}
],
"error": null
}status moves pending → running → completed or failed. Each entry in results appears as that output settles, so you can stream progress. Per-output status is completed, failed, or skipped; image outputs carry url, while code, screen, and design outputs carry their text in code. Entries also carry the output node's label when one is set, and outputs created by a social campaign carry their target platform — so automations can route results without re-reading the canvas.
- Supported outputs: image, code, and screen (HTML) and design nodes. Video outputs are reported as
skippedin this version. - Stops on failure: if any output fails, nodes downstream of it are abandoned (they depend on the missing result) and the run is marked
failed— earlier results are still returned. - Time budget: a run executes within a single ~5 minute server window. Very large workflows may exceed it; split them or run nodes individually via
/generations.
Only workflows you own are runnable; an unknown or unowned id returns 404. Each generated output draws on the same budget as a single generation.
Errors#
Every error uses the same envelope, with a stable machine-readable type:
{ "error": { "type": "invalid_request", "message": "…" } }- invalid_request
- 400 — a body field is missing, malformed, or incompatible.
- unauthorized
- 401 — missing, malformed, revoked, or unknown API key.
- insufficient_scope
- 403 — the key lacks the scope this endpoint needs (e.g. a
read-only key callingPOST /generations). - rate_limited
- 429 — too many requests; honor the
retry-afterheader. - insufficient_quota
- 429 — the billing budget for the period is exhausted.
- payment_required
- 402 — the key bills to a workspace that can't be charged right now (its Team plan lapsed, or the key's owner left the workspace). Fix billing, then retry.
- not_found
- 404 — the resource doesn't exist or isn't yours.
- server_error
- 500 (or 503 when a dependency is briefly unavailable).
Rate limits & budget#
Generation calls draw on the same AI budget and generation limits as your plan in the app — the API isn't a separate allowance. Read endpoints have their own, more generous limit. When you hit a limit you get a 429 with a retry-after header (in seconds); back off and retry after that window.
Each key is rate-limited in its own bucket, so a busy key can't exhaust another. You can set an optional per-key limit (requests/minute) when creating a key — handy for sandboxing a shared or third-party integration. It only ever tightens the ceiling, never raises it above your plan's limits.
503 rather than letting an unmetered request through. Retry with backoff.Workspace billing.By default a key's generations draw on its owner's personal budget. A workspace owner or admin can instead bind a key to a workspace when creating it (Settings → API keys), so its generations draw from that team's shared Team budget. The binding is re-checked on every call: if the workspace's Team plan lapses or the owner leaves the workspace, generation requests return 402 payment_requiredrather than quietly falling back to the owner's personal budget.