Guides

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.

Phil Duong

Founder

Wrapped-Style Year-in-Review Video API

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/renders call 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 replacements object 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:

CardFixed text
HookYOUR YEAR, WRAPPED, here's your, with, here's how it went →
Stat onein 2026 alone.
Stat twoThat's 33 days. Wow.
PersonaYOUR 2026 PERSONA
ShareWrapped, 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:

FieldHook or stat cardShare card
first_name (170px, then 96px)about 7 charactersabout 7
period_labelabout 7about 5
stat_1_value, stat_2_value (340px)about 6about 14
stat_1_label, stat_2_labelabout 41about 27
persona_labelabout 22about 24
stat_3_value / stat_3_labelabout 18 / 76not shown
top_item_nameabout 39, on 2 linesnot 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 a maxCharacters for 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:

PlanRequests per minuteTime to submit 100,000 recaps
No plan (pay-as-you-go)60about 27.8 hours
Creator120about 13.9 hours
Business300about 5.6 hours
Hours to submit 100,000 one-user render requestsPay-as-you-go (60/min)27.8 hCreator (120/min)13.9 hBusiness (300/min)5.6 hSubmission time at the plan's API rate limit. Render time is extra. Source: Renderly API rate limits. Linear scale.

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 the jobId. Store the pair as each POST returns, then look the user up when render.completed arrives.
  • Back off on 429. A request over the limit returns HTTP 429 with a Retry-After header. Successful responses also carry X-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 jobId map, or Dashboard → Renders, first.
  • Re-queue on render.failed. The payload carries an errorMessage. In a recap, check the top_item_image URL 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.

RecapsCreditsPay-as-you-go ($0.20/credit)Business ($99 for 1,000, then $0.12)
1,000500$100$99, included
10,0005,000$1,000$579
100,00050,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 inputProps if 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?
Yes. Send POST /api/v1/renders with a project or template id and one replacements object per user: the first name, the period, two stats with labels, a persona, a top item and a logo. Renderly renders a 25-second 1080x1920 MP4 and sends its URL to your webhook. Each user is one request.
What does one recap cost?
The Year-in-Review template runs 25 seconds. Renderly bills 1 credit per minute of 1080p output, rounded up to the nearest half credit, so each recap costs 0.5 credits. That is $0.10 on pay-as-you-go. On the Business plan, 100,000 recaps cost about $5,979, or about 6 cents each.
What happens when a user's name is too long?
The render succeeds and the end of the name is hidden. The name is set at a fixed 170px, so the hook holds about 7 characters. There is no error. Check lengths before you send, use a shorter display name for long ones, and run the free preview endpoint on the outliers. It flags lines that are likely to clip.
Can the big numbers count up from zero for each user?
Not through replacements. A counter layer reads its target number from the layer's style, and replacements only change a layer's text or media URL. A template counter would show the same number to everyone. To count up to each user's own figure, send the full composition as inputProps with the counter target set per user.
How long does it take to submit 100,000 recaps?
The API takes 300 requests a minute on the Business plan, 120 on Creator and 60 without a plan. Submitting 100,000 one-user requests takes about 5.6 hours on Business and about 13.9 hours on Creator. That is submission time only. Render the batch days before launch, not on launch day.