Developers

API reference

A small, versioned REST API for generating images and code headlessly — no canvas required. Authenticate with an API key, enqueue a generation, and poll for the result.

Base URL & versioning#

All endpoints live under a single versioned prefix:

https://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.

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.

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, json).
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://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.

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://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.
rate_limited
429 — too many requests; honor the retry-after header.
insufficient_quota
429 — your AI budget for the period is exhausted.
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.

The limiter fails closed: if it's briefly unavailable the API returns 503 rather than letting an unmetered request through. Retry with backoff.