---
title: Make a birthday montage from photos and clips
description: A birthday montage, or birthday slideshow, cut to the beat of the music, with gentle zooms on the photos and a title card.
---
You get a birthday montage, also called a birthday slideshow or photo video, from your photos and phone clips. Each photo and clip changes on a beat of the song, the photos move with a gentle zoom, and a title card opens it with the name and the age.

Use invideo for this when you want the cuts to land on the beat without counting them by hand: your agent reads the beats of the song, places every cut on one, and you can still move any photo after on the timeline.

## Quickest path

Set up your agent first with [invideo for AI agents](/agents). Then give it this prompt.

```text
Make a birthday montage for Leo's 8th birthday from the photos and
clips in ./leo. Cut on the beat of a happy song from stock music,
add a slow zoom on each photo and open with "Happy 8th birthday, Leo".
16:9 for the TV. Show me frames before you export.
```

In a chat app, you drag your files into the project's Media panel in invideo, then name them in the chat. In a terminal, you name the folder and the agent uploads it.

## What you need

Pick the files in the order you like, or let the agent sort them by date.

- **Photos and clips.** JPG, PNG, HEIC, MP4 or MOV from a phone or camera.
- **The title text.** The name, the age, and a closing line if you want one.
- **The song or the mood.** A track from stock music, or your own file if you have the rights to it.
- **The shape.** 16:9 for a TV or a laptop, 9:16 for a phone story, 1:1 for a feed.
- **The length.** A song length, or a number of seconds.

## Steps

Your agent edits on a branch and tells you what changed at each step.

<Steps>
<Step title="Make the timeline and bring in the files">
The agent makes a project with `new_project`, a timeline with `new_timeline` in the shape you asked for, and a branch with `timeline_history`. For each local file it calls `start_upload`, sends the bytes, then calls `add_media` with the `upload_id` on the branch URL. Expected result: a branch URL, and a sequence URL and a media bin id for each file.
</Step>
<Step title="Pick the music">
The agent searches `stock_music` with `search_library`, for example "happy birthday" or "upbeat kids". You listen to the previews and pick one, and the agent adds it with `add_media` and `stock`. Expected result: the song in the media bin.
</Step>
<Step title="Make a song with the name in it, if you want one">
The agent runs `find_models` with the job in words, such as "birthday song that sings a name", and reads its pick with `get_model`. A good pick is `elevenlabs_music_v2_5_song`. Your agent can search for others with `find_models`. It calls `generate` with `prompt`, `duration` and `dry_run: true`, and tells you the price. For a price of 1 credit or more it waits for your yes. Then it calls `generate` with `max_credits` set to that price and an `idempotency_key`. Expected result: a thread URL, then the song's sequence URL, which the agent reads with `read`.
</Step>
<Step title="Cut on the beat">
The agent reads the beat times with `media` `audio.beats` and the big moments with `audio.sections`, and turns each time into a frame: seconds times the frame rate. Then it places each photo and clip with `insert_media` or `place_media` and moves each cut to a beat with `trim_clip`, all in one `edit_timeline` call. Expected result: every cut on a beat.
</Step>
<Step title="Add the zoom and the title card">
In one call, the agent adds the `dynamic_zoom` effect to each photo with `add_effect`, and the title with `add_text`. Then it reads the timeline with `get_timeline` for the new ids. In a second call it sets the end zoom with `set_effect_parameter` on the effect's id, for a slow push, and fades the title in with `set_keyframes` on `opacity` on the title clip's id, like `t1`. Expected result: the photos move gently and the title fades in.
</Step>
<Step title="Check before export">
The agent runs `capture` at the title, a few cuts and the end, and `check_timeline`. It checks that no face is cut off by the zoom. Expected result: frames where every face is in view.
</Step>
<Step title="Export">
The agent runs `export` with `format` video, then reads the export card until its status is `completed`. In a terminal, it saves the file with `invideo notebook show` on the card's URL. Expected result: a 1080p MP4.
</Step>
</Steps>

The same job from a terminal:

```bash
invideo project new --title "Leo turns 8"
invideo agent register <project-url> --name <name>
export INVIDEO_AS=<name>
invideo edit new <project-url> --name "Montage"
invideo asset upload <edit-url> ./leo/*
invideo library search <project-url> --library stock_music \
  --query "happy birthday"
invideo asset add <project-url> --stock <id>
invideo gen models <project-url> --query "birthday song that sings a name"
invideo gen model <project-url> --model elevenlabs_music_v2_5_song
invideo gen run <page-url> --model elevenlabs_music_v2_5_song --dry-run \
  -i prompt="upbeat kids pop song, sing happy birthday Leo" -i duration=60
invideo gen run <page-url> --model elevenlabs_music_v2_5_song \
  --max-credits <price> --idempotency-key leo-song-1 \
  -i prompt="upbeat kids pop song, sing happy birthday Leo" -i duration=60
invideo status <thread-url> --wait
invideo media audio.beats <sequence-url>
invideo edit show <edit-url>
invideo edit change <edit-url> --timeline T1 --token <state_token> \
  --operations @ops.json
invideo edit capture <edit-url> --at 0,4,12,24 --out ./frames
invideo edit check <edit-url>
invideo edit export <edit-url> --format video --quality 1080 --json
invideo status <card-url> --wait
invideo notebook show <card-url> --out ./out
```

Where each value comes from:

- `<name>` is the name you choose in `invideo agent register`. `INVIDEO_AS` passes it to every later command.
- `<project-url>` comes from `invideo project new`, and `<edit-url>` from `invideo edit new`. `<id>` comes from `library search`.
- The `gen` lines and the `status` line are only for a custom song; skip them for stock. `<page-url>` is a notebook page; `invideo notebook show <project-url>` lists them. `<price>` is the dry run's `.estimated_credits`.
- `<thread-url>` is the `url` the paid run returns. `<sequence-url>` is the song's sequence URL: the one `asset add` returns, or the one `invideo notebook show <thread-url>` lists once the song lands.
- `<state_token>` comes from `invideo edit show`. `ops.json` holds a bare list of operations, like the first call's `operations` list below.
- `<card-url>` is `.sequences[0]` of the export result. `invideo status` waits and saves nothing. `invideo notebook show` saves the file as `./out/export.mp4`; rename it after.

The key MCP call is the full call. It trims a photo to the next beat, adds the zoom and adds the title. At 30 fps, a beat at 2.4 seconds is frame 72. `<branch-url>` is the `branch_url` that `timeline_history` returns, and `<state_token>` comes from `get_timeline` on that branch.

```json
{
  "url": "<branch-url>",
  "timeline_id": "T1",
  "expected_state_token": "<state_token>",
  "operations": [
    { "op": "trim_clip", "clip": "c2", "edge": "tail", "from_f": 90,
      "to_f": 72, "mode": "close_gap", "linked": true },
    { "op": "add_effect", "clip": "c2", "effect": "dynamic_zoom" },
    { "op": "add_text", "track": "V2", "start_f": 0, "end_f": 90,
      "text": "Happy 8th birthday, Leo",
      "style": { "font_size": 96, "align": "center", "shadow": true } }
  ]
}
```

The end zoom needs the effect's id, which only `get_timeline` returns. So the agent reads the timeline, finds the effect's id and the title clip's id, like `t1`, and sends the second call with the new `state_token`. The fade on the title is in it:

```json
{
  "url": "<branch-url>",
  "timeline_id": "T1",
  "expected_state_token": "<state_token>",
  "operations": [
    { "op": "set_keyframes", "clip": "t1", "property": "opacity",
      "points": [ { "at_f": 0, "value": 0, "ease": "ease_out" },
                  { "at_f": 20, "value": 1 } ] }
  ]
}
```

## Cost and time

Edits and exports are free. Generation and transcripts cost credits. Paid stock costs a small credit charge, or one of your workspace's iStock licenses. Your agent runs a dry run first and tells you the price before it spends anything.

- **Free:** uploads, the cuts, the zoom, the title, frame checks and the export.
- **Tool ops:** `audio.beats` and `audio.sections`. The dry run shows its price before anything starts.
- **Costs credits:** a paid stock song, or a song made with `elevenlabs_music_v2_5_song`. Your own music file costs nothing.

A montage takes a few minutes for the agent. Picking the photos and the song takes you longer. See [Credits and what they cost](/account/credits) for prices.

## If something goes wrong

Most problems come from beats that do not suit the photos, or a zoom that crops a face.

| Symptom | Fix |
| --- | --- |
| `get_model` or `generate` refuses the model as not found | Models retire. Run `find_models` with the job in words, use the best match, and tell the person which model you used. |
| Exit code 6, or `status` is `held` | A safety review, not a refusal. Wait, then run `invideo status <url> --wait` again. Do not resend. If it ends `failed`, change the input the review flagged. |
| The cuts feel too fast | Ask for a cut on every second or fourth beat. The agent skips beats in the list. |
| A face is cut off by the zoom | Lower the end zoom of `dynamic_zoom`, or move its end point onto the face, then `capture` again. |
| `trim_clip` refused as stale | The edge moved. Read with `get_timeline` and send the trim with the new `from_f`. |
| `audio.beats` finds few beats | The song has a soft beat. Read `audio.markers` for tempo, or pick a song with a clear beat. |
| The music ends before the video | Trim the last photo, or ask for a longer song. |
| CLI exit 3 | The timeline changed since your read. Run `invideo edit show` and rebuild the ops. |
| Avoid: the agent edits main | Instead: it works on a branch from `timeline_history`, and you accept the change. |
| Avoid: saying done without looking | Instead: `capture` frames at the title, the cuts and the end. |

## Prompts to try

Each prompt is a real variation of the job.

```text
Make a 60 second vertical birthday montage for Grandma's 90th from the
old photos in ./grandma. Slow, warm music, a cut on every second beat,
and "Happy 90th, Grandma Rose" at the start and the end.
```

```text
Cut my son's year in clips to this song, ./song.mp3. Put the cuts on
the strong beats, the fun clips on the chorus, and his name and age on
a title card. 16:9.
```

```text
Make a 30 second Instagram story montage of the party clips in ./party.
Fast cuts on every beat, a flash transition on the drop, and
"Best night ever" at the end.
```

## When to use something else

invideo does not print photo books or cards. Use a print service for those, with stills from the montage made by `export` in `mode` image.

If you want the song to react to faces or dance moves, like a CapCut template, see [Make a trend edit like CapCut: slow-mo, meme, freeze frame](/how-to/personal/trend-edit). Templates you bought for CapCut or After Effects do not open in invideo. Your agent rebuilds the look on the timeline instead.

## Related

- [Make a photo slideshow for any occasion: anniversary, graduation, retirement, baby's first year, family recap](/how-to/personal/occasion-slideshow)
- [Make a video invitation for a birthday, wedding, baby shower or gender reveal](/how-to/personal/video-invitation)
- [Make a trend edit like CapCut: slow-mo, meme, freeze frame](/how-to/personal/trend-edit)
- [Add music and sound effects that clear rights](/how-to/creators/music-and-sound-effects)
- [Check your video before you say it is done](/how-to/check-your-video)