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

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

TypeScript
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.

Header
Authorization: Bearer vmk_live_xxxxxxxxxxxx
The key is the only credential — there's no cookie or session auth on /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/mcp

The 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 Code
claude mcp add --transport http vmotif https://vmotif.com/api/mcp

Available 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.
MCP generation and workflow tools spend the same budget as the REST API and app canvas. Access is checked against your account entitlement, optional Team workspace billing scope, ownership, rate limits, and remaining budget.

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.

POST /generations
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:

202 Accepted
{ "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 on type.
images
Up to 10 input images, each { url, mediaType }. URLs must be https or data:.
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.
A model that can't produce the requested 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.

GET /generations/{id}
curl https://www.vmotif.com/api/v1/generations/0c2f… \
  -H "Authorization: Bearer vmk_live_xxx"

An image result:

200 OK
{
  "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:

200 OK
{
  "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.

POST /images/transforms
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:

200 OK
{
  "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-images storage path or public URL you own — a completed generation's result.url qualifies. Required.
op
"convert", "compress", or "trim". Required.
options.format
Output format: png, jpeg, webp, or avif. Required for convert; compress defaults to webp; ignored by trim.
options.quality
Encoder quality for convert, between 0 (exclusive) and 1. Defaults to 0.9.
options.targetBytes
Target output size for compress (10KB–10MB). The API binary-searches encoder quality; withinTarget: false in 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.
A 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:

GET /usage → 200 OK
{
  "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.

POST /workflows/{id}/runs
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:

202 Accepted
{ "id": "b81c…", "status": "pending" }

Poll the run for progress and per-output results:

GET /workflows/{id}/runs/{runId} → 200 OK
{
  "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 runningcompleted 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.

Runs are read-only against your workflow: outputs are returned as ephemeral artifacts and the saved canvas is never modified. A run is a pure function of the workflow's current graph and its inputs.
  • Supported outputs: image, code, and screen (HTML) and design nodes. Video outputs are reported as skipped in 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 calling POST /generations).
rate_limited
429 — too many requests; honor the retry-after header.
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.

The limiter fails closed: if it's briefly unavailable the API returns 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.