New: AI voiceover + auto-captions - script to captioned video in one API call. Read the announcement

How-to

Automated Sports Recap Video: One Card for Every Fixture

Render a full-time scorecard from a league feed, not from footage. One card per fixture, 26 fields, one credit, published before anyone opens an editor.

Automated Sports Recap Video: One Card for Every Fixture

A full-time card is a result, not a highlight reel. Map a league feed onto 26 named fields, POST one request when the final whistle fires, and the recap is published before an editor has opened the footage. Each 37-second card costs one credit.

The finished video

This guide renders Sports Match Recap - Full-Time Scorecard - a 37-second 16:9 card in seven scenes: a Full Time cold open with both crests, the scoreline, a goalscorers page, a head-to-head stats panel, a player of the match spotlight, a next-fixture tease, and a sign-off.

The card rendered from the payload in step 5.

Twenty-six fields change per fixture, driving 89 overlays. Some fields work harder than others: home_crest is used seven times across the seven scenes and match_photo five, so one URL each fills the whole card.

What you need

  • A Renderly API key. Create one in the dashboard under Settings → API keys.
  • A results feed or a match.finished webhook from whatever runs your fixtures.
  • A crest image per team and one venue photograph, at URLs that serve the file directly.

Step 1 - Decide whether you are cutting footage or publishing a result

Search this query and you get highlight detection: Pixellot, Spiideo, Magnifi, Eklipse. They point computer vision at match video, find the goals, and cut a reel. That is a real product and it is not this one.

The difference is what the pipeline consumes. A highlight tool consumes footage, which means a camera rig at the venue, an operator or an install per pitch, and a per-match cost that does not fall as you add fixtures. A card consumes the result, which arrives as JSON from the same system that already publishes your league table.

So the question is not which is better, but which one runs for every fixture. A federation with 400 matches a weekend has 400 results and maybe twelve camera rigs: the cards cover all 400, the reels cover twelve.

Step 2 - Map the league feed onto the fixture card

The 26 fields are grouped the way a feed row already is:

GroupFields
Fixturecompetition_label, match_round, venue_label, match_photo
Teamshome_team_name, away_team_name, home_crest, away_crest
Resulthome_score, away_score, home_scorers, away_scorers
Statsstat_1_label / _home / _away, and the same for 2 and 3
Spotlightmotm_name, motm_line
Nextnext_opponent_name, next_opponent_crest, next_match_label

Two of these need formatting rather than copying. home_scorers is one string carrying every goal, with a newline between them and the minute first. And motm_name is set at 150px in a 920x300 box, which is two lines, so break the name yourself: the sample ships "Vinícius\nJr" rather than trusting the layout to find the space.

Watch the team names too. They render at four sizes across the card, and the smallest box is 360px wide at 50px Anton on the stats scene. A club whose name fits the scoreline can still clip there, so test your longest name, not your first one.

Step 3 - Cap the goalscorer lines before the feed writes them

Each scorers panel is 760x360 at 52px Oswald on a 1.7 line height. That is 88 pixels a line and room for four:

{
  "home_scorers": "23'   Rodrygo\n45+2'   Raphinha\n78'   Vinícius Jr",
  "away_scorers": "67'   Mbappé"
}

Three lines is comfortable, four is the ceiling, and a fifth is sheared off while the render reports success. Decide the rule in the mapping function before a 6-1 decides it for you.

Step 4 - Make the stat bars tell the truth

This is the one that will embarrass you in public. The stats scene draws six dueling bars, and they are static shape overlays, not values. Their widths are baked to the sample fixture:

RowBar widthsImplied splitSample figures
1864 / 62658 / 42Possession 58% / 42%
2972 / 52265 / 35Shots 17 / 9
31077 / 42072 / 28Expected Goals 2.8 / 1.1

Send stat_1_home: "34%" and stat_1_away: "66%" and the printed figures change while the bars do not. The card then shows the away side dominating the number and the home side dominating the bar, in the same row:

The stats scene showing 34 percent against 66 percent possession while the longer bar still sits under the 34 percent figure
The same scene with the sample's bar widths and a different fixture's numbers.

Two honest ways out. Pick three stats whose ratios sit near the sample's and treat the bars as decoration, or send the composition as inputProps instead of replacements and set the six widths yourself. The API takes inputProps, projectId or templateId, so that escape hatch costs only the convenience of named fields.

Step 5 - Render one card per fixture on the final whistle

One finished fixture becomes one POST. The template's API id is football-recap; sports-match-recap is only the gallery URL.

curl -X POST https://renderly.video/api/v1/renders \
  -H "Authorization: Bearer $RENDERLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "templateId": "football-recap",
    "replacements": {
      "competition_label": "World Cup 2026",
      "match_round": "Group Stage · Matchday 3",
      "venue_label": "MetLife Stadium · New York / NJ",
      "home_team_name": "Brazil",
      "away_team_name": "France",
      "home_score": "3",
      "away_score": "1",
      "home_scorers": "23'\''   Rodrygo\n45+2'\''   Raphinha\n78'\''   Vinícius Jr",
      "away_scorers": "67'\''   Mbappé",
      "stat_1_label": "Possession",
      "stat_1_home": "58%",
      "stat_1_away": "42%",
      "motm_name": "Vinícius\nJr",
      "motm_line": "2 Goals · 1 Assist",
      "next_opponent_name": "Mexico",
      "next_match_label": "Saturday · July 4"
    },
    "webhookUrl": "https://your-app.com/hooks/renderly"
  }'

Any field left out keeps the template's default, which on this card means shipping the sample's crest or the sample's venue. That matters more here than elsewhere: a missing away_crest does not fail, it publishes the wrong flag. Validate the row before the request, not the file after it.

The call returns a job ID immediately and the finished URL arrives on the render.completed webhook, signed with HMAC-SHA256; the webhooks guide covers verifying it.

What this costs at scale

Renderly bills 1 credit per minute of 1080p output, rounded up to the nearest half credit. A 37-second card rounds to one credit.

Cards per monthCreditsPlan
8 - one club, two fixtures a week8Creator, $29/mo
40 - a ten-team league, every fixture40Creator, $29/mo
300 - 75 fixtures a weekend300Creator, $29/mo
1,500 - a federation at 375 a weekend1,500Business, $99/mo

A 20-team league playing 380 fixtures a season spends 380 credits on the whole season, spread across nine months, which never leaves the $29 plan. At roughly ten cents a card the rendering is not the constraint - the feed is.

Where to go next

The same shape appears wherever a feed fires an event and a card follows. The daily news brief runs it on a clock instead of a whistle, and the CSV guide has the resumable loop you want for backfilling a season that has already been played.

Frequently asked

Does Renderly cut highlights out of match footage?
No, and that is the line worth drawing before you choose a tool. Detecting goals in video is computer vision, and Pixellot, Spiideo, Magnifi and Eklipse all sell it. Renderly assembles a fixed layout from values you supply, so it produces the scorecard, the stats panel and the player spotlight from the result rather than from the video. The two are complements: one needs a camera rig per venue, the other needs a feed.
What happens when a team scores five?
The fifth line falls out of the box and the render still succeeds. Each scorers panel is 760x360 at 52px on a 1.7 line height, which is four lines exactly. Decide the rule in the mapping function - the usual answer is to keep the first three and write a fourth line reading something like "+2 more" - because a 6-1 will arrive eventually and it should not be the thing that finds this out.
Do the stat bars move with the numbers?
No. The six dueling bars are static shape overlays with widths baked to the sample fixture at 864/626, 972/522 and 1077/420 pixels, which encode 58/42, 65/35 and 72/28. Changing stat_1_home through stat_3_away changes the printed figures and nothing else, so a fixture with different ratios ships bars that contradict its own numbers unless you send the composition as inputProps and compute the widths yourself.
Is there a version for basketball?
Yes. Basketball Game Recap - Final Buzzer is the sibling, same seven scenes and same 37 seconds, with a violet accent, Archivo Black in place of Anton, and a quarter-by-quarter line score where the soccer card puts goalscorers. Its API id is basketball-recap. Watch the score fields there: three-digit totals are normal in basketball and the numerals are set large.
How do I backfill a season that has already been played?
The same request in a loop over historical rows. A 380-fixture season is 380 credits, and because rendering is asynchronous you can queue the whole archive and collect the URLs from webhooks rather than waiting on each one. Put the results in a CSV and the loop is the one in the CSV guide.