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://www.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://www.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 audience-bound to https://vmotif.com/api/mcp. The MCP server validates that token on every request.
claude mcp add --transport http vmotif https://vmotif.com/api/mcpAvailable MCP tools:
- get_usage
- Read the signed-in account's plan tier and current generation budget.
- 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.
- 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.
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://www.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://www.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://www.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:
{
"tier": "pro",
"token_budget_micros": 20000000,
"used_micros": 3450000,
"remaining_micros": 16550000,
"resets_at": "2026-06-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://www.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",
"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.
- 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.