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.

Connect Vmotif to other apps without code. Join the Zapier beta for an invitation and setup guide.

Base URL & versioning#

All endpoints live under a single versioned prefix:

https://vmotif.com/api/v1

The API is at v1. Changes within v1 are 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.

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 connect to Vmotif over Streamable HTTP at one endpoint, signing in as you through OAuth rather than with an API key:

https://vmotif.com/api/mcp
Claude Code
claude mcp add --transport http vmotif https://vmotif.com/api/mcp
Codex
codex mcp add --url https://vmotif.com/api/mcp --oauth-resource https://vmotif.com/api/mcp vmotif
codex mcp login vmotif

MCP has its own manual: what agents can do, install steps for every client, how authentication and budgets work, and the full tool reference are on the MCP server page, with a real end-to-end session on the Claude Code walkthrough. MCP tools spend the same budget as this API and the canvas.

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://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, svg, glsl, 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 400 up front, so you never burn budget on a job that can't succeed.

targetLanguage: "glsl" returns a fragment shader in the Shadertoy convention — a bare mainImage body, with iResolution, iTime, iTimeDelta, iFrame and iMouse supplied by the harness, so the shader must not redeclare them or emit a #version directive. It also declares 3–5 of its own tunable uniforms, annotated on the same line as the declaration:

An annotated uniform
uniform float uSpeed; // @range 0.1 4.0 @default 1.0 @label Speed

Those annotations are what the app turns into a control panel. They are plain comments, so a shader you take elsewhere still compiles — and a shader you bring back keeps its knobs.

Poll for the result#

Poll the job until status is completed or failed. This endpoint is never cached.

GET /generations/{id}
curl https://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://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. 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:

GET /usage → 200 OK (personal key)
{
  "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
}
GET /usage → 200 OK (workspace-bound key)
{
  "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.

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

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",
      "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.

A run executes an immutable snapshot of the workflow's graph taken at enqueue time — edits made while it executes don't change what runs or what you're charged. Completed outputs are written back onto their output nodes, so opening the canvas afterwards shows exactly what the run produced.
  • 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 GPU wallet as your plan in the app — the API isn't a separate allowance, and there is no hourly cap on how many generations you start: spend is bounded by the wallet and load by your plan's parallel slots. Read endpoints carry a generous per-key request limit to blunt runaway polling. When a read is limited you get a 429 with a retry-after header (in seconds); back off and retry after that window.

Each key has 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 — and that limit applies to the key's generation calls too. It only ever tightens; it is your ceiling to set, not ours.

The per-key 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_required rather than quietly falling back to the owner's personal budget.