How to Generate AI Videos and Images with the Meta Muse API

16 min read • October 12, 2026 (October 30, 2026)

Independent third-party service, see Legal

Table of contents

  1. Introduction
  2. What it cost
  3. Before you start
  4. How it was built
    1. 1. Add the account
    2. 2. Upload the photos
    3. 3. Images from text
    4. 4. Three photos into one picture
    5. 5. The picture as a video, no re-upload
    6. 6. Restore and colorize an old photo
    7. 7. The portrait comes to life
    8. 8. Swap the rider in the video
    9. 9. Check the weekly allowance
  5. What we learned
  6. Make a 30-second narrated Short (script, three scenes, one join)
    1. 1. Write the script as JSON
    2. 2. Three scenes at once
    3. 3. Poll the jobs
    4. 4. Join the clips in one chat
    5. 5. Download the Short
    6. A fixed voice instead: text to speech as audio_1
  7. Frequently asked questions
  8. Conclusion

Introduction

The useapi.net Muse API drives Muse, Meta’s personal AI agent, from code: one POST makes an image or a 10-second video with spoken dialogue, and every file it makes is an asset id the next job can take as an input.

Muse here is Meta’s agent at muse.ai, not Skiv, the video-hosting company that used the muse.ai name until March 2026. The API is hosted, so unlike self-hosted tools such as muse2api there is nothing to deploy: the Meta Muse video API comparison puts Meta’s own API, muse2api and hosted APIs side by side.

The API runs on your Muse website subscription, and the free plan works. This walkthrough takes three ordinary photos, a vintage BMW motorcycle, a red-haired rider and the beach below the Golden Gate Bridge, and turns them into one picture of her riding that motorcycle through the surf. The picture then becomes a video in which she shouts a line, and in a third job a different woman, taken from a fourth photo, replaces her in that video while the ride stays the same. Along the way it makes images from text, and it restores and colorizes a hundred-year-old portrait that then looks up and speaks. Every request below is the one that made the media on this page.

The rider from a photo, swapped into a clip Muse made from a still image (step 8).

What it cost

useapi.net is an API, not an image or video service of its own. Every call runs on a Muse account you add through Setup Muse, and uses that account’s weekly Muse allowance, with no per-image or per-video charge from useapi.net. Muse reports the allowance as a whole percent used, so a single image often shows no change.

What each step used on the free plan, taken from percentUsed before and after it, and how long each job took:

Step Endpoint Output Allowance used Time
Text to image, 9:16 POST /jobs/image 1152×2048 not measured 30 s
Text to image, exactly 16:9 POST /jobs/image 2048×1152 0% 21 s
Rider + motorcycle + beach POST /jobs/image 2048×1152 0% 31 s
The ride as a video POST /jobs/video 1280×720, 10 s 3% 88 s
Restore an old photo POST /jobs/image 1344×1792 1% 34 s
The portrait speaks POST /jobs/video 832×1104, 10 s 2% 68 s
Swap the rider POST /jobs/video 1248×704, 10 s 3% 138 s

Uploads use no allowance. On the free plan a video used 2 to 3 percent of the week and an image 0 to 1 percent, so one free account makes roughly 30 to 50 videos a week, or several hundred images (step 9).

Before you start

You need a useapi.net API token and a Muse account added through Setup Muse. Before you add the account:

  • Muse is available in the US, Canada, Japan and Singapore (as of October 30, 2026), so create the account in one of them. Once it is added, you can call the API from anywhere.
  • Finish Muse’s onboarding at muse.ai, including Meta’s age check, and create one image there.
  • The account must have email + password sign-in, because the API signs in with the email and password. A Meta account that signs in only with an emailed code has no password yet: create one in Meta Accounts Center (Password and security), then add the account here. Without a password the add is refused with 400.
  • Use the account only for the API, with no other automation tools on it. Opening it at muse.ai in a browser is fine, but only from the US, Canada, Japan or Singapore (the regions Muse officially supports), or through a VPN in one of them: Meta can switch off video for an account it sees used from elsewhere.

Every call is also in the Postman collection.

Also in Chinese (中文教程) and Japanese (日本語チュートリアル). No code: the Old Photo to Talking Video n8n template restores a photo and makes it talk from a form, and the Narrated Faceless Shorts template turns a topic into a narrated Short. The whole walkthrough is also on GitHub as one script, in Node.js and Python: images-videos/ in useapi/muse-api, plus an agent skill for Claude Code and Codex.

The steps use curl, jq and these variables and helpers:

Expand the variables and helpers
export USEAPI_TOKEN="user:12345-..."
export EMAIL="[email protected]"
export MUSE_PASSWORD="..."   # the Muse account password, used once in step 1
API=https://api.useapi.net/v1/muse
AUTH="Authorization: Bearer $USEAPI_TOKEN"
enc() { jq -rn --arg v "$1" '$v|@uri'; }
post() { curl -sS -X POST "$API/$1" -H "$AUTH" -H "Content-Type: application/json" -d "$2"; }
get() { curl -sS "$API/$1" -H "$AUTH"; }
upload() { curl -sS -X POST "$API/assets?email=$(enc "$EMAIL")" -H "$AUTH" -H "Content-Type: $2" --data-binary @"$1" | jq -r .assetId; }
download() { curl -sSf "$API/assets/$(enc "$1")" -H "$AUTH" -o "$2"; }

# run a job async: submit it, poll GET /jobs/{jobid} every 10 s, save the final job record to $1
run() {
  local job id
  job=$(post "$2" "$(jq '. + {async: true}' <<< "$3")")
  id=$(jq -r .jobid <<< "$job")
  while [ "$(jq -r .status <<< "$job")" = started ]; do
    sleep 10
    job=$(get "jobs/$(enc "$id")")
  done
  echo "$job" > "$1"
  jq '{status, error, text: .result.text, asset: .result.assets[0].assetId}' "$1"
}

Every generation runs as a job. Without async, a POST waits up to about 100 seconds and returns the finished job record, or 202 with the job still running. A video here took 68 to 138 seconds, so run sends every job with async: true and polls GET /jobs/jobid. If run prints any status other than completed, read its error before going on.

To be called back instead of polling, add replyUrl: "https://example.com/hooks/muse" (and optionally replyRef) to any body below. The API then sends one POST of the final job record, as JSON, to that URL.

Ids contain : and @, so enc URL-encodes them whenever they go into a URL path. All inputs of one job must live on the same Muse account, so the uploads pin email, and a job runs on the account that holds its inputs.

How it was built

1. Add the account

POST /accounts takes the Muse account’s email and password. The API signs in once, keeps the 30-day session and renews it itself. Muse emails the account a login code at each sign-in. That email is only a notice, nothing needs to be entered.

curl — POST /accounts
post accounts "$(jq -n --arg email "$EMAIL" --arg password "$MUSE_PASSWORD" '{email: $email, password: $password}')" | jq '{health, maxJobs, quota}'

The answer shows health: "OK", maxJobs (10 by default) and the account’s plan and usage, here tier: "HATCH_FREE", the free plan. If it answers 596 instead, Meta wants a security check: sign in once at muse.ai in a browser, complete it, and post the same email and password again.

2. Upload the photos

POST /assets takes the raw file as the body, up to 50 MB, and writes it into the account’s Muse workspace. Nothing is stored at useapi.net. The answer’s assetId is what a job takes as image_N.

curl — POST /assets
BMW=$(upload bmw.jpeg image/jpeg)
RIDER=$(upload rider.jpeg image/jpeg)
BEACH=$(upload beach.jpeg image/jpeg)
OLD_PHOTO=$(upload old-photo.jpeg image/jpeg)
PINK=$(upload pink-hair.jpeg image/jpeg)
echo "$BMW"
# user:12345-email:6578616d706c6540676d61696c2e636f6d-image:7db53e7a1f0979cbab6ff46ec3f0a35b60e6ee579a0d738a9628cf46a0c7a0f5.jpg
<code>image_1</code> — the motorcycle
image_1 — the motorcycle
<code>image_2</code> — the rider
image_2 — the rider
<code>image_3</code> — the beach
image_3 — the beach
the old portrait
the old portrait
the second rider
the second rider

The five inputs: the motorcycle, the rider, the beach, the old portrait and the second rider.

The asset id is built from the file’s content, so uploading the same file to the same account again returns the same id and writes nothing.

3. Images from text

Muse has no aspect-ratio parameter: the prompt says it. A vertical portrait first:

curl — POST /jobs/image
run portrait.json jobs/image "$(jq -n '{prompt: "Vertical 9:16 full-body portrait photo: a confident young woman biker with long wavy fiery red hair, green eyes and freckles, in a worn black leather jacket, dark jeans and boots, standing in a rain-soaked neon alley at dusk, looking straight at the camera, cinematic lighting, photorealistic."}')"
download "$(jq -r '.result.assets[0].assetId' portrait.json)" portrait.webp

Text to image at 9:16

open in new tab

That came back at exactly 9:16, 1152×2048. A landscape version asked as Cinematic 16:9 landscape photo: … came back 1920×1280, which is 3:2. Writing the ratio as in exactly 16:9 aspect ratio fixed it:

curl — POST /jobs/image
run bmw-noir.json jobs/image "$(jq -n '{prompt: "In exactly 16:9 aspect ratio (landscape), a cinematic photo of a vintage 1920s black BMW motorcycle with white pinstripes, spoked wheels and a brown leather saddle, parked on wet cobblestones at night in San Francisco, the Golden Gate Bridge glowing through fog in the distance, neo-noir moonlight, reflections on the wet street, shot on 35mm film."}')"
download "$(jq -r '.result.assets[0].assetId' bmw-noir.json)" bmw-noir.webp

Text to image at exactly 16:9

open in new tab

Muse answered “That 16:9 frame with the bridge ghosting through the fog is pure neo-noir. Enjoy.” Its reply is in result.text.

4. Three photos into one picture

Up to 9 images go into one job as image_1..image_9, and the prompt names them. Here the motorcycle is image_1, the rider image_2 and the beach image_3. The prompt asks Muse to use the photographed beach as it is, so the Golden Gate Bridge stays where it was in the photo.

curl — POST /jobs/image
run beach-ride.json jobs/image "$(jq -n --arg bmw "$BMW" --arg rider "$RIDER" --arg beach "$BEACH" \
  '{image_1: $bmw, image_2: $rider, image_3: $beach,
  prompt: "In exactly 16:9 aspect ratio (landscape), a cinematic photo: the woman from image_2 (her face, long wavy red hair, light blue top and jeans) riding the exact vintage motorcycle from image_1 along the shoreline of the beach in image_3, the front wheel cutting through the shallow surf and throwing spray, bright afternoon sun, her hair streaming in the wind, photorealistic. Use the beach, rocks and Golden Gate Bridge from image_3 as the background exactly as photographed: same position, same orientation, not mirrored or redrawn. Keep her face and the motorcycle'\''s design exactly as in the images."}')"
BEACH_RIDE=$(jq -r '.result.assets[0].assetId' beach-ride.json)
download "$BEACH_RIDE" beach-ride.webp

The rider, the motorcycle and the beach in one picture

open in new tab

31 seconds, and no visible change in the weekly allowance.

5. The picture as a video, no re-upload

BEACH_RIDE is the id of a file Muse made. It is already in the account’s workspace, so it goes straight into the next job as image_1, with no download and no upload. Animate image_1 makes a clip that starts from the picture and keeps its shape, and the line in quotes is spoken.

curl — POST /jobs/video
run surf.json jobs/video "$(jq -n --arg img "$BEACH_RIDE" '{image_1: $img,
  prompt: "Animate image_1 into a cinematic video in exactly 16:9 aspect ratio (landscape): she rides the vintage motorcycle along the shoreline, the front wheel slices through the shallow waves and sends spray flying, the old engine roars, she raises one fist and shouts with a laugh: \"Nothing beats this view!\" The camera tracks alongside her. The beach, the rocks and the Golden Gate Bridge stay exactly as in image_1 (same orientation). Keep her face and the motorcycle exactly as in image_1."}')"
SURF=$(jq -r '.result.assets[0].assetId' surf.json)
download "$SURF" surf.mp4

Also on YouTube: 3 photos in, 1 talking video out: Meta Muse API from REST.

10 seconds at 1280×720 with sound. Muse said “Done. 10 seconds, 16:9 landscape, straight from your image — no edits, no upscaling.”

The first try of this exact job failed after 37 seconds: Muse answered that “the video worker glitched out before it rendered anything, so no clip was made”. The API reports an answer like that as 503, nothing made, and the same request worked when run again. A refusal on content grounds is a 403 instead, and needs a different prompt.

6. Restore and colorize an old photo

An image job can also repair an image. The old portrait is a faded sepia photo of 685×900 pixels, 3:4. The prompt lists what to fix and insists on the same frame.

curl — POST /jobs/image
run restored.json jobs/image "$(jq -n --arg img "$OLD_PHOTO" '{image_1: $img,
  prompt: "Restore the old photograph image_1 as a high-quality modern photo: remove the scratches, dust, stains, fading and grain, recover sharp detail in the face, hair and fabric, and colorize it naturally (warm natural skin tones, dark brown hair, ivory tulle shawl, a soft pink rose with green leaves, a muted warm background). Keep the exact same person, expression, pose, framing and crop, and the same portrait aspect ratio as image_1 (3:4, 685×900): do not crop, extend or reframe it."}')"
RESTORED=$(jq -r '.result.assets[0].assetId' restored.json)
download "$RESTORED" restored.webp
Before: the original, 685×900
Before: the original, 685×900
After: restored and colorized, 1344×1792
After: restored and colorized, 1344×1792

The result is 1344×1792, the same 3:4 frame at about twice the size, with the same pose and crop. Muse said “Done — restored, de-grained, and colorized, same framing and pose.”

7. The portrait comes to life

The restored image goes straight into a video job. The clip keeps the portrait’s shape, so it came back 832×1104.

curl — POST /jobs/video
run portrait-video.json jobs/video "$(jq -n --arg img "$RESTORED" '{image_1: $img,
  prompt: "Animate image_1 into a video with the same portrait frame: the young woman slowly lifts her eyes to the camera, her smile widens, she gently touches the rose on her shoulder and says softly: \"It has been a hundred years... and you still remember me.\" A light breeze moves a few strands of her hair, soft window light, the camera stays still with a very slow push-in. Keep her face exactly as in image_1."}')"
download "$(jq -r '.result.assets[0].assetId' portrait-video.json)" portrait.mp4

Also on YouTube: This old portrait just spoke: one API call (Meta Muse API, third-party).

8. Swap the rider in the video

A video job also takes videos. video_1 is the surf clip from step 5, and image_1 is the photo of a second woman with pink hair in two buns. The prompt asks Muse to replace the rider and keep everything else, and gives her a new line.

curl — POST /jobs/video
run swap.json jobs/video "$(jq -n --arg vid "$SURF" --arg img "$PINK" '{video_1: $vid, image_1: $img,
  prompt: "Edit video_1: replace the rider with the woman from image_1 (her face, pink hair in two buns, colorful makeup and tattoos), and keep everything else the same: the motorcycle, the beach, the waves and spray, the Golden Gate Bridge, the light, the camera move and the timing. Instead of the original line she shouts: \"Catch me if you can, San Francisco!\" Keep the 16:9 frame and the same length."}')"
download "$(jq -r '.result.assets[0].assetId' swap.json)" swap.mp4
Before: the original rider
After: the rider from the photo

At 138 seconds this was the slowest job of the walkthrough, which is why every job here runs async. The clip is the same ride with the other woman on the motorcycle, shouting the new line. Muse said “Done. She’s in, 10 seconds, 16:9, same ride, same bridge, same waves.”

Also on YouTube: Replace the person in a video, keep everything else: Meta Muse API.

9. Check the weekly allowance

GET /accounts/email reads the account’s plan and usage live:

curl — GET /accounts/email
get "accounts/$(enc "$EMAIL")" | jq '{session, quota}'

quota.percentUsed is the share of the week used, in whole percent, and quota.resetsAt is when it starts over, a week after it began. The three videos above used 2 to 3 percent each and the images 0 or 1. Over the whole test session, which also ran two jobs not shown here, the free account went from 16 to 28 percent. When an account runs out, new jobs without email go to your other accounts, and a job pinned to it answers 412 until resetsAt.

What we learned

  • Write the aspect ratio as in exactly 16:9 aspect ratio. Cinematic 16:9 landscape photo came back 3:2 once.
  • Every video is about 10 seconds. A prompt asking for 5 seconds still got 10.
  • An image-to-video clip keeps the shape of its image unless the prompt asks for another one: in exactly 9:16 aspect ratio turned our 16:9 beach picture into a vertical 720×1280 clip, re-framed rather than stretched.
  • Lines written in quotes in a video prompt are spoken.
  • Pass generated files on by assetId. They stay in the account’s workspace, so a chain of edits needs no downloads in between.
  • Say what must stay the same. “Same position, same orientation, not mirrored or redrawn” kept the real beach in the composite.
  • A 503 with Muse’s own words such as “glitched out” is worth running again. A 403 is a refusal and needs a different prompt.
  • Send videos with async: true. A sync request waits about 100 seconds, and the swap took 138.

Make a 30-second narrated Short (script, three scenes, one join)

A Muse video is always about 10 seconds, so a 30-second vertical Short with a narrator takes three video jobs and two chats. One chat writes the script as JSON, three video jobs run side by side, one per scene, each speaking its line in the same narrator’s voice, and a last chat joins the clips with ffmpeg in the account’s own workspace. The Short below, about the 1815 eruption of Mount Tambora and the “year without a summer” that followed, went from a one-line topic to an MP4 in about 2 minutes.

Also on YouTube: A narrated 30-second Short made only with Meta Muse API calls.

Three 10-second clips joined into one 720×1280 Short of 30 seconds, from the topic “how a single volcano cooled the whole planet in 1816”.

The script took 11 seconds, the three clips 75, 75 and 87 seconds running at the same time, and the join 20 seconds, so a Short takes about 2 to 3 minutes. This one was made on a paid Muse plan. There, the four videos of that test session (these three and one more) moved the weekly reading by 2 points in all, and the chats by none. On the free plan a video uses 2 to 3 percent of the week, so a three-scene Short costs about 6 to 9 percent there.

No code: the same flow is an n8n app, the Narrated Faceless Shorts template, with a form, a script page and Google Drive save. A Short it made, published as it came out: The Great Pyramid was once bright white.

The whole workflow is also one script, in Node.js and Python: narrated-short/ in useapi/muse-api. It takes a topic, and optionally the number of scenes and your own narrator description, and saves the Short to output/.

The steps use the helpers above plus chat, which starts a chat and reads GET /chats/chatId until Muse is done:

Expand the chat helper
# start a chat, poll GET /chats/{chatId} every 8 s until Muse is done, save the chat to $1
chat() {
  local id c
  id=$(post chats "$2" | jq -r .chatId)
  while sleep 8; do
    c=$(get "chats/$(enc "$id")")
    jq -e '.pendingApprovals | length > 0' <<< "$c" > /dev/null &&
      echo "Muse asks for permission: answer it with POST /chats/{chatId}/approvals/{approvalId}"
    jq -e '(.status | IN("completed", "interrupted", "failed")) and (.pendingApprovals | length == 0)' <<< "$c" > /dev/null && break
  done
  echo "$c" > "$1"
  jq '{status, reply: ([.messages[] | select(.role == "assistant" and .text != "")][-1].text)}' "$1"
}

Every request below pins email. A chat that takes files must run on the account that holds them, so the three clips and the join all stay on one account.

1. Write the script as JSON

The first chat asks for exactly three scenes and spells out the JSON shape. Each scene has a line, what the narrator says, kept to 25 words so it fits in 10 seconds, and a visual, one sentence describing the shot. The prompt also tells Muse not to make any files, so the chat stays text only.

curl — POST /chats
TOPIC="how a single volcano cooled the whole planet in 1816"
chat script.json "$(jq -n --arg email "$EMAIL" --arg topic "$TOPIC" '{email: $email,
  message: "Write a script for a 30-second faceless YouTube Short about: \"\($topic)\". Exactly 3 scenes. Reply with ONLY a JSON object, no prose, no markdown code fences, in this exact shape: {\"title\": string, \"scenes\": [{\"line\": string, \"visual\": string}, {\"line\": string, \"visual\": string}, {\"line\": string, \"visual\": string}]}. \"line\" is what the off-screen narrator says (at most 25 words, about 9 seconds spoken). \"visual\" is a one-sentence description of the 10-second shot, no people talking on camera. Do not create any files, images or videos."}')"
jq -r '[.messages[] | select(.role == "assistant" and .text != "")][-1].text' script.json | sed '/^```/d' > scenes.json
jq . scenes.json

Muse answered in 11 seconds with the JSON alone:

{
  "title": "How One Volcano Cooled the Whole Planet in 1816",
  "scenes": [
    {
      "line": "In April 1815, Mount Tambora in Indonesia erupted, the largest explosion in recorded history, blasting ash forty kilometers into the sky.",
      "visual": "A massive volcanic eruption at dawn, with a towering ash column rising into the stratosphere above a dark island."
    },
    {
      "line": "That ash veil circled Earth, blocking sunlight and dropping global temperatures, turning summer into winter across North America and Europe.",
      "visual": "An animated globe wrapped in a gray haze as sunlight dims and frost spreads across continents."
    },
    {
      "line": "1816 became the Year Without a Summer: crops failed, snow fell in July, and famine spread as the planet shivered.",
      "visual": "Snow falling over green farmland in summer, covering withered crops and empty village fields under a pale sun."
    }
  ]
}

We ran the same prompt for three topics and got clean JSON every time, in 11 to 29 seconds, with no code fences and no extra text. That is a small sample, so the sed drops a stray ``` fence line anyway, and the GitHub script asks once more when the reply does not parse.

2. Three scenes at once

Each scene becomes one async POST /jobs/video. All three prompts carry the same narrator description, word for word, followed by The narrator says exactly: "<line>". Muse speaks a quoted line as written, and the shared description keeps the voice consistent from clip to clip. Vertical 9:16 video came back 720×1280.

curl — POST /jobs/video
NARRATOR="Narration: one off-screen narrator only, the same voice throughout: a calm, deep, middle-aged British male documentary narrator, measured pace, warm and authoritative, like a nature documentary. Nobody on screen speaks. No music."
for i in 0 1 2; do
  post jobs/video "$(jq --arg email "$EMAIL" --arg narrator "$NARRATOR" --argjson i $i '.scenes[$i] | {email: $email, async: true,
    prompt: "Vertical 9:16 video, about 10 seconds. \(.visual) Slow cinematic camera move, photoreal. \($narrator) The narrator says exactly: \"\(.line)\""}' scenes.json)" | jq -r .jobid
done > jobs.txt

Each POST answers 201 at once with a jobid. The three jobs take three of the account’s maxJobs slots (10 by default), so they run side by side. A 429 here means every slot is busy and nothing was started: send that one again in 30 seconds.

3. Poll the jobs

GET /jobs/jobid answers status: "started" until the clip is ready. The jobs run in parallel, so reading them one after another adds no time:

curl — GET /jobs/jobid
for n in 1 2 3; do
  id=$(sed -n "${n}p" jobs.txt)
  until job=$(get "jobs/$(enc "$id")"); [ "$(jq -r .status <<< "$job")" != started ]; do sleep 10; done
  echo "$job" > clip$n.json
  jq '{status, error, text: .result.text, asset: .result.assets[0].assetId}' clip$n.json
done

All three completed. Muse said “Here it is — vertical 9:16, photoreal volcanic eruption at dawn with the narration as specified” about the first. Each clip speaks its line word for word, and every line, 20 to 21 words long, fit inside its 10 seconds. A job that failed with 503 made nothing and can run again. Any other failure, such as a 403 refusal, needs a look at error.message first.

4. Join the clips in one chat

The three clip ids go into one chat as files, where Muse sees them as video_1, video_2 and video_3. The message asks for ffmpeg, the order, the audio and a file name, and ends with No public links. With that line, Muse did not stop to ask for permission.

curl — POST /chats
CLIPS=$(jq -s '[.[].result.assets[0].assetId]' clip1.json clip2.json clip3.json)
chat join.json "$(jq -n --arg email "$EMAIL" --argjson files "$CLIPS" '{email: $email, files: $files,
  message: "Concatenate video_1, video_2, video_3 in order into one MP4 with ffmpeg, keep the audio, save it to workspace/your_files/short.mp4 and attach it here. No public links."}')"
SHORT=$(jq -r '[.messages[] | select(.role == "assistant") | .files[] | select(.type == "video")][-1].assetId' join.json)
echo "$SHORT"
# user:12345-email:6578616d706c6540676d61696c2e636f6d-video:ws~your_files~short.mp4

20 seconds later Muse said “Concatenated all three in order — 30 seconds, 720x1280, with audio kept.” and attached the file as a message of its own, one video in its files. Its asset id is named after the workspace path, so give each Short its own file name. ffmpeg copied the streams without re-encoding them: the Short is exactly 30.0 seconds, and all three lines are in it, in order.

5. Download the Short

GET /assets/assetId returns the MP4:

curl — GET /assets/assetId
download "$SHORT" tambora-short.mp4

The clips and the Short also stay in the account’s Muse workspace, where the muse.ai app shows them, and the Short’s asset id can go into the next job as video_1.

A fixed voice instead: text to speech as audio_1

A shared description gives the same kind of voice, not a locked one. In the three Tambora clips the narrator’s median pitch was 105 to 117 Hz, less than two semitones apart, but Muse voices each clip anew. For one voice for certain, have Muse speak each line with its text to speech first, then pass that MP3 to the video job as audio_1. The text to speech chat took 20 seconds in our test.

curl — POST /chats and POST /jobs/video
chat voice1.json "$(jq -n --arg email "$EMAIL" --arg line "$(jq -r '.scenes[0].line' scenes.json)" '{email: $email,
  message: "Generate one mp3 file with text to speech: single speaker, voice Aria (avocado_v2:MAI_01), speed 100, language en, clean narration only: no music, no sound effects. Read exactly this text, do not change any word:\n\"\($line)\"\nSave it directly in workspace/your_files/ (not in a subfolder) as line-1.mp3 and attach it here. No public links and no uploads to remote storage."}')"
VOICE1=$(jq -r '[.messages[] | select(.role == "assistant") | .files[] | select(.type == "audio")][-1].assetId' voice1.json)

run clip1.json jobs/video "$(jq --arg email "$EMAIL" --arg voice "$VOICE1" '.scenes[0] | {email: $email, audio_1: $voice,
  prompt: "Vertical 9:16 video, about 10 seconds, no people on screen and nobody speaking on camera. \(.visual) Slow cinematic camera move, photoreal. Use audio_1 as the narration soundtrack exactly as it is: the off-screen narrator'\''s voice in audio_1, word for word, with no other voice and no new speech. Quiet ambient sound under it is fine."}' scenes.json)"
jq -r '.result.assets[] | select(.origin == "made") | .assetId' clip1.json

Muse makes its clip and then lays audio_1 onto it with ffmpeg. The job lists both files: the finished copy first, with origin: "made", and Muse’s raw clip after it, with origin: "generated". Take the made one. The raw clip carries a voice Muse made up instead of yours. The video job took 108 seconds in our test. Repeat it for each scene and join the made clips as in step 4.

Frequently asked questions

Is there an API for Meta Muse (muse.ai)? Through useapi.net, yes: the Muse API is a third-party API that runs Muse on your Muse website subscription. Make images with POST /jobs/image and videos with POST /jobs/video. Meta’s official Model API sells the Muse Image model at $0.01 per image, but as of October 2026 it has no video generation and no API for the Muse agent at muse.ai, where the videos are made. Our Meta Muse video API comparison covers muse2api too.

Does it work on the free Muse plan? Through useapi.net, yes: this walkthrough was made on the free plan (HATCH_FREE), which has a weekly allowance. A video used 2 to 3 percent of it and an image 0 to 1 percent, so one free account makes roughly 30 to 50 videos a week. The paid Power ($20 a month) and Maximum ($100 a month) plans raise the allowance (as of October 2026).

Can Muse make a video longer than 10 seconds? Through useapi.net, not in one job: every video is about 10 seconds. Make one clip per scene and have a chat join them with ffmpeg, as in Make a 30-second narrated Short, where three clips became one Short in about 2 to 3 minutes. One chat takes up to 3 videos.

How do I set the aspect ratio, length or resolution? Through useapi.net, in the prompt, because Muse has no parameters for them. The aspect ratio is followed when written as in exactly 16:9 aspect ratio, and an image-to-video clip keeps the shape of its image unless the prompt asks for another. Length and resolution stay fixed: every video comes back at about 720p with sound, whatever the prompt asks.

Can I use my own photos and videos? Through useapi.net, yes: upload them with POST /assets, up to 50 MB each, and pass the assetId as image_1..image_9, the same way you pass a result from an earlier job. Video jobs also take video_1..video_3 and audio_1..audio_3. All inputs of one job must be on the same account.

How do I make a photo talk? Through useapi.net, pass the photo as image_1 to POST /jobs/video with a prompt like Animate image_1: … she says softly: "…". Muse keeps the photo’s shape and speaks the line in quotes, as in step 7, where a restored hundred-year-old portrait looks up and talks.

Can it replace a person in a video? Through useapi.net, yes: pass the clip as video_1 and a photo of the new person as image_1, and ask Muse to replace the person and keep everything else, as in step 8. Say what must stay the same, such as the ride, the bridge and the waves, and give the new person a line in quotes if they should speak.

Why did my sync request return 202? Through useapi.net, a 202 means the job was still running after about 100 seconds, and it keeps running on our side. Fetch it with GET /jobs/jobid, send long jobs with async: true and poll, or add a replyUrl to have the finished job record posted to you. In our runs, a video took 68 to 138 seconds.

Why did Muse email me a login code? Through useapi.net, the API signed in to your Muse account, which happens when you add it and when the API renews the 30-day session, before it expires or whenever Muse asks for a new sign-in. Muse emails a login code on each sign-in, and nothing needs to be done with it. See Setup Muse.

How is this different from muse2api? Through useapi.net, the Muse API is hosted, so there is nothing to deploy: you add each account with its email and password and we keep it signed in. Results chain by asset id, and a finished job can call your webhook. You can also chat with the Muse agent, with its answers streamed live. muse2api is a project you deploy, host and keep running yourself.

Conclusion

Visit our Discord Server or Telegram Channel for any support questions and concerns.

Check our GitHub repo with code examples.

The same calls are available as tools on the hosted Muse MCP server for Claude Code, Cursor and VS Code.

Cross posted

Also published on dev.to, discussed on Reddit, shared on Bluesky, and on YouTube as a 2:49 demo of the whole API.