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/imagesandPOST /v1/videosaccept a prompt and return a job. Onlypromptis 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.
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.
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:
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:
{
"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.
{
"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": {
"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 ignoredresolutonwould return200for a render you did not ask for. Content-Type: application/jsonis 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-RemainingandX-RateLimit-Resetare 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 as429withRetry-After. Write your client to read the headers if they are there and to honourRetry-After, and nothing changes for you on the day they switch on.