Wrapped-Style Year-in-Review Video API
Render a personalized Wrapped-style recap video for every user from one stats row. The request, the fields that clip, the fixed lines, and the batch math.
Founder

A Wrapped-style recap is one stats row per user, poured into the same short video. With Renderly, each user is one API request. You send their name, two headline numbers, a persona and their top item to the Year-in-Review "Wrapped" Recap template, and get back a 25-second 1080x1920 MP4 by webhook. It costs 0.5 credits per user. The hard parts are not the request. They are the copy that is fixed in the design, the names that do not fit, and how many hours a large batch takes to submit.
Key Takeaways
- One
POST /api/v1/renderscall per user returns a 25-second 9:16 recap. It costs 0.5 credits, because billing rounds up to the nearest half credit.- Some lines are fixed text in the template. One of them, "That's 33 days. Wow.", is the sample user's own maths. Clone the template and make those lines dynamic first.
- The first name holds about 7 characters and each headline number about 6. Longer values clip without an error.
- A counter's target lives in the layer's style, so replacements cannot make a number count up to each user's own value.
- At the Business limit of 300 requests a minute, 100,000 recaps take about 5.6 hours to submit. Render before launch and serve the files from your own storage.
What does Spotify's own engineering say about building one?
Spotify's 2025 Wrapped had "more than 300 million engaged users" and "more than 630 million shares on social media globally in 56 languages" (Spotify, Q4 2025 earnings, 2026). Your recap will be smaller. The way Spotify built theirs still applies.
Three lessons from Spotify's engineering posts carry over to a video recap:
- Compute every stat first, into one row per user. For the 2019 decade
recap, Spotify stored the output of each data story job "to that same row but
separate column families," one row per user, so the stories could be computed
in parallel
(Spotify Engineering, 2020).
That row is exactly the
replacementsobject a render needs. - Generate everything before launch. For 2025's Wrapped Archive, Spotify had "around 350 million eligible users, each getting up to five reports," and "by the end of pre-generation, over a billion reports were stored and ready to be served" (Spotify Engineering, 2026).
- Expect a spike, not a ramp. The same post says "Wrapped launches globally at a single moment" and "Wrapped doesn't ramp, it spikes!" Your recap videos should already be files when that moment comes.
The data window matters too. Spotify's 2025 Wrapped covered listening "from January until mid November, just a few weeks prior to this year's launch on December 3" (Spotify, How your Wrapped is made, 2025). That gap of about two and a half weeks is where the rendering happens. Close your data window early enough to compute, preview and render the whole batch.
What does one recap look like as a request?
The template is a 25-second 1080x1920 video in six cards: a name-drop hook, two big stat cards, a persona badge with an optional third stat, a top-item card, and a share card that repeats the name, the stats and the persona. It takes 12 fields. This is one user:
curl -X POST https://renderly.video/api/v1/renders \
-H "Authorization: Bearer $RENDERLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"projectId": "cmgb7x2k40001la08n3q9v6rd",
"replacements": {
"first_name": "Priya",
"period_label": "2026",
"stat_1_value": "312",
"stat_1_label": "Workouts logged",
"stat_2_value": "41.2K",
"stat_2_label": "Minutes moved",
"stat_3_value": "Top 5%",
"stat_3_label": "of all members",
"persona_label": "Dawn Patrol",
"top_item_name": "Saturday Hill Repeats",
"top_item_image": "https://cdn.example.com/classes/hill-repeats.jpg",
"brand_logo": "https://cdn.example.com/brand/logo.png"
}
}'The response returns a jobId, status: "PROCESSING" and creditsUsed: 0.5
at once. The video arrives later through the render.completed event on a
webhook you register once with POST /api/v1/webhooks. Its data.outputUrl is
the MP4. Registered webhooks are signed with HMAC-SHA256, so you can verify each
delivery. A per-request webhookUrl also works, but those one-off callbacks are
not signed, so put an unguessable token in the URL path if you use one.
Every value is a finished string. Nothing reads, rounds or formats your numbers, so "41.2K" shows as "41.2K". Format the figures in the job that computes them.
The example renders a project, not the template. The next section is why.
Which lines are fixed in the template?
The 12 fields do not cover every word on screen. The sample user, Maya, has 48,210 minutes watched, and the second stat card says "That's 33 days. Wow." under it. That line is fixed text. 48,210 minutes is 33.5 days, so the line is right for Maya and wrong for everyone else.
The other fixed lines name the year or the product:
| Card | Fixed text |
|---|---|
| Hook | YOUR YEAR, WRAPPED, here's your, with, here's how it went → |
| Stat one | in 2026 alone. |
| Stat two | That's 33 days. Wow. |
| Persona | YOUR 2026 PERSONA |
| Share | Wrapped, Persona, Share your wrapped → |
Fix this once. Click Use this template on the
template page to clone it into a project.
In the editor, give each line that changes per user or per year a name, such as
stat_2_context, and tick Dynamic. Then compute that line in your stats
job, next to the number it describes: "That's 28 days." for 40,320 minutes.
Type fixed product words, such as your own name for the recap, straight into
their layers.
Send every field. An omitted field keeps the sample value, so a user with no third stat would see Maya's "Top 3%". An empty string blanks the text, but the badge shape behind it stays on screen. For users without a third stat, clone a second project with the badge deleted and route those users to it.
What breaks when a name is too long?
Every text field in this template has a fixed font size. Renderly's renderer does not shrink fixed-size text to fit. It clips it, and the render still succeeds. These are the approximate capacities, from the same estimate that the variables endpoint and the preview warnings use:
| Field | Hook or stat card | Share card |
|---|---|---|
first_name (170px, then 96px) | about 7 characters | about 7 |
period_label | about 7 | about 5 |
stat_1_value, stat_2_value (340px) | about 6 | about 14 |
stat_1_label, stat_2_label | about 41 | about 27 |
persona_label | about 22 | about 24 |
stat_3_value / stat_3_label | about 18 / 76 | not shown |
top_item_name | about 39, on 2 lines | not shown |
Three things follow from this table:
- Names are the real risk. "Priya" fits. "Alexandrina" does not. Use a display name or nickname field, and send a shorter form for names past 7 characters.
- Shorten big numbers in your job. "41.2K" fits the stat card. "41,204" fits too. "1,204,310" does not.
- The share card is the tighter one for labels and years. The variables
endpoint,
GET /api/v1/projects/{projectId}/variables, reports amaxCharactersfor each field from its first layer. A stat label reports 41, but the share card holds about 27. Keep labels to the smaller number.
Check lengths in code against these numbers, then preview the outliers. The preview endpoint takes the same body, costs no credits, and checks every layer, not only the first one:
curl -X POST https://renderly.video/api/v1/previews \
-H "Authorization: Bearer $RENDERLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "projectId": "cmgb7x2k40001la08n3q9v6rd", "replacements": { "first_name": "Alexandrina" } }'A value that will clip returns a TEXT_MAY_CLIP warning, and a media URL that
does not answer returns MEDIA_UNREACHABLE. The clip check is an estimate from
average character width, so open the previewUrl for anything it flags.
Previews count against the same per-minute request limit as renders, so
preview a sample of the longest values, not every user.
Spotify sets data thresholds too. A user gets ranked top songs only "if you've streamed at least 30 tracks" (Spotify, 2025). Decide your own minimum before you render. A recap that says "3 workouts logged" in 340px type does not help anyone.
Can the numbers count up for each user?
Renderly's text layers support a counter mode that counts from one number to
another. The counter reads its from and to values from the layer's style,
styles.counterMode, not from the layer's text. Replacements change only text
and media URLs. So a counter in a template or project shows the same final
number to every user.
The Year-in-Review template does not use counters. Its stat values scale in as text, so each user's own number shows correctly.
If the count-up matters, use direct mode. Send the full composition as
inputProps, with styles.counterMode.to set to that user's number. You then
build and send the whole overlays array yourself; the
overlays reference has the schema. For most recaps the
scale-in is enough, and it keeps the request at 12 fields.
How do you render 100,000 recaps?
Spotify's pre-generation for Wrapped Archive alone meant "sustaining thousands of requests per second for days" (Spotify Engineering, 2026). A batch that big runs for days, before launch. Plan yours the same way. The limit to plan around is the API's request rate:
| Plan | Requests per minute | Time to submit 100,000 recaps |
|---|---|---|
| No plan (pay-as-you-go) | 60 | about 27.8 hours |
| Creator | 120 | about 13.9 hours |
| Business | 300 | about 5.6 hours |
That is the time to queue the jobs, not to finish them. Renders run after the request returns, and Business renders get a higher queue priority. Start a large batch days before launch.
Build the batch job around four rules:
- Keep your own map from user to
jobId. The render request has no field for your own user id, and the webhook payload returns thejobId. Store the pair as each POST returns, then look the user up whenrender.completedarrives. - Back off on 429. A request over the limit returns HTTP 429 with a
Retry-Afterheader. Successful responses also carryX-RateLimit-Remaining, so you can pace the loop before you hit the limit. - Do not blind-retry a timeout. There are no idempotency keys, so a retry
after a timeout can render and charge for the same user twice. Check your
jobIdmap, or Dashboard → Renders, first. - Re-queue on
render.failed. The payload carries anerrorMessage. In a recap, check thetop_item_imageURL first: a link that expired or needs a login cannot be fetched.
When a recap is done, copy the MP4 from outputUrl to your own storage and
CDN. The URL is public and does not expire, but files are kept according to
your account's retention policy, and your launch-day traffic should hit your
CDN, not a render output bucket. The
webhooks guide covers the
signature check on each delivery to a registered webhook.
What does a batch cost?
Renderly bills 1 credit per minute of 1080p output, rounded up to the nearest half credit. 1080x1920 has the same pixel count as 1920x1080, so it bills at the 1080p rate. The 25-second recap is 0.5 credits.
| Recaps | Credits | Pay-as-you-go ($0.20/credit) | Business ($99 for 1,000, then $0.12) |
|---|---|---|---|
| 1,000 | 500 | $100 | $99, included |
| 10,000 | 5,000 | $1,000 | $579 |
| 100,000 | 50,000 | $10,000 | $5,979 |
The rounding works in your favour here. Anything from 1 to 30 seconds costs the same 0.5 credits, so a sixth card added inside the 30-second mark costs nothing extra. A 31-second recap costs 1 credit, twice as much.
A 25-second 1080x1920 Wrapped-style recap costs 0.5 Renderly credits. 100,000 recaps cost 50,000 credits: $10,000 on pay-as-you-go, or about $5,979 on the Business plan.
How is this different from a monthly metrics recap?
A year-in-review recap goes to a consumer once a year, in 9:16, to be shared. A monthly client report goes to a business, in 16:9, to be read next to a dashboard. The second job has its own template and its own guide: automated business metrics videos, one per client. The batch rules above apply to both.
If the stats come from a spreadsheet rather than a database, the 1,000 personalized videos guide shows the same one-row-one-request loop from a CSV.
The short version
- One user, one request. Twelve fields to the Year-in-Review template, a 25-second 1080x1920 MP4 back by webhook, 0.5 credits.
- Clone first. "That's 33 days. Wow." and the year lines are fixed text. Make them dynamic in a project and compute them per user.
- Fit the fields. About 7 characters for a name and 6 for a headline number. Shorten in your job, preview the outliers for free.
- No per-user counters through replacements. Use direct
inputPropsif the count-up matters. - Render early. 100,000 recaps take about 5.6 hours to submit on Business. Finish before launch, and serve the files from your own CDN.
Frequently asked
Can one API call render a personalized Wrapped-style video?
What does one recap cost?
What happens when a user's name is too long?
Can the big numbers count up from zero for each user?
How long does it take to submit 100,000 recaps?
Related Articles

How to Generate 1,000+ Personalized Videos with API Automation (2026 Update)
A 30-second 1080p render costs $0.10 on a template API and $1.50+ on a generative one. Build the bulk pipeline: 1,000 personalized videos queued in under 20 minutes.
May 18, 2026

Personalized Video at Scale: The Complete Guide (2026)
83% of consumers can spot AI video and 36% trust those brands less. Here's how to scale personalized video in 2026 without falling into the avatar trap.
June 4, 2026

How to Let Your AI Agent Preview Videos Before Rendering (Free, No Credits)
66% of developers say their top AI frustration is output that's almost right but not quite. Renderly's free preview endpoint lets your agent see the video first.
August 11, 2026

How to Set Up Video Rendering Webhooks (Production Guide)
Video rendering is asynchronous — polling wastes 90%+ of requests. This guide covers HMAC SHA-256 signature verification, retry semantics, idempotency, and the raw-body pitfall that breaks most webhook handlers.
May 28, 2026

Template-Based Video vs AI-Generated Video: Which Should You Choose?
Template video vs AI video in 2026: a 1080p minute costs $0.10-$0.20 to render from a template and $4.80-$24.00 to generate with Veo 3.1. Here's the decision rule.
April 16, 2026