Skip to content
MV2API docsStudioDownload for agentsGet an API key

Musavir API

The studio, as an API.

Start an image or a film with one POST, poll the job, download the result. The same models, credits and trained characters the MV2 studio runs on — behind a key your server holds.

What you can build

Everything under /v1 falls into four groups, and each one has a guide of its own:

  • Generation. POST /v1/images and POST /v1/videos accept a prompt and return a job. Only prompt is required — the server picks a sensible resolution and aspect ratio when you send nothing else.
  • Reference uploads. Presign a target, PUT the bytes straight to storage, attach the returned key to a generation. Media never travels through this API.
  • Characters. Create a draft, upload selfies into it, start training, then cast the trained character in any number of later generations.
  • Account and capabilities. Balance, usage, and the live list of what the configured models accept.

Authentication

Every request carries a key in an Authorization header: Authorization: Bearer mk_live_…. There are no other credentials, no signatures, and no query-string tokens.

A key acts as the account that owns it, bounded by its scopes. Create one in the developer dashboard, give it only the scopes the integration needs, and keep it on a server — the API does not enable wildcard CORS, and a key in a browser bundle is a key you have published. You may hold up to 25 live keys, so a separate key per integration costs nothing.

Set the key and confirm it works
export MUSAVIR_API_KEY="mk_live_your_key_here"

curl https://api.musavir.ai/v1/capabilities \
  -H "Authorization: Bearer $MUSAVIR_API_KEY"

Five-minute quickstart

Start a job, then poll it. That is the whole loop — the quickstart walks through it properly, including downloading the result.

Start an image generation
curl -X POST https://api.musavir.ai/v1/images \
  -H "Authorization: Bearer $MUSAVIR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"prompt":"A lone figure on a rooftop at dusk, 35mm, cinematic"}'

The response is 202 Accepted with the job, a Location header pointing at it, and a Retry-After telling you when to look first. Poll until status is succeeded or failed:

Poll the job
curl https://api.musavir.ai/v1/jobs/JOB_ID \
  -H "Authorization: Bearer $MUSAVIR_API_KEY"

Response shapes

Three envelopes, and nothing else. A single resource always arrives under data:

Single resource
{
  "data": {
    "id": "8f1c…",
    "object": "generation",
    "kind": "image",
    "status": "queued",
    "createdAt": "2026-08-22T10:15:30.000Z"
  }
}

A collection adds pagination. Pass nextCursor back as the cursor query parameter for the next page; the cursor is opaque and must not be parsed or constructed.

List resource
{
  "data": [ … ],
  "pagination": { "hasMore": true, "nextCursor": "eyJ…" }
}

A failure replaces data with error. The code is wire-stable — branch on it, never on the message — and requestId is the value support needs to find your request.

Error
{
  "error": {
    "code": "insufficient_credits",
    "message": "You need 1.250000 more credits to start this job.",
    "requestId": "0d1f…",
    "details": { "reason": "buy_credits", "requiredCredits": "2.000000" }
  }
}

Timestamps are ISO-8601 UTC strings. Field names are camelCase. Credit amounts are decimal strings with six decimal places, such as "1.250000" — never JSON numbers, because a balance that goes through a double does not come back the same. Samples on these pages are abridged; a … marks a value your own response will carry.

Everything is a job

Rendering takes longer than an HTTP request should, so no endpoint returns a picture. Generation and character training answer 202 Accepted with a resource to poll. Poll at about two seconds and back off toward ten; a job that is still moving tells you what to wait in pollAfterSeconds.

A finished job carries result.url and result.downloadUrl, both signed for that response only and both stamped with result.expiresAt. Store the job id, never the URL — re-read the job when you need a fresh link.

Limits and headers

  • JSON bodies are capped at 256 KB by default. Nothing you send should approach it: media goes to storage through a presigned upload, and a generation carries keys, not bytes.
  • Unknown fields are a 400, at every depth. A silently ignored resoluton would return 200 for a render you did not ask for.
  • Content-Type: application/json is required on every request with a body.
  • Every response carries X-Request-Id. Log it; it is what turns a support conversation into a lookup.
  • Rate limits are not currently enforced. The limiter exists and is switched off, so no ceiling is being applied to your key today. While it is off, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset are deliberately absent rather than advertising a budget nothing checks — when limiting is turned on, those headers appear on every authenticated response and a refusal arrives as 429 with Retry-After. Write your client to read the headers if they are there and to honour Retry-After, and nothing changes for you on the day they switch on.

Where to go next