Price-match and curbside end card: trim the tail, overlay, join

Add a store-policy end card to a sale clip without re-editing it: trim the last 4 s ($0.02), overlay the card ($0.02), then join in Timeline ($0.10 a minute).

4 min readSume
All posts

Put a policy end card (price match, curbside pickup, return window) on only the last seconds of a sale clip in three jobs: trim the last 4 seconds ($0.02), overlay your card PNG on that tail ($0.02), then join the untouched start and the carded tail in Timeline 1.0 ($0.10 per started output minute). The compose overlay covers the whole clip it is given, so you shorten the clip first rather than trying to time the card.

Why three steps

Timeline compose puts one still on screen for the entire length of one video, and takes its length from the video. It has no start or end time for the still. To show the card only at the end, hand it only the end. Trimming the tail gives you that, and the source clip is never changed.

This also means the policy card is one small file. When your policy changes (a new price-match window, curbside closed on a day), you re-run the compose and render, not the whole creative.

The three jobs

Jobs, endpoints and public rates from the Sume docs, read 2026-10-05
StepEndpointKey fieldsRate
Trim the tailPOST /v1/video-trimvideo_url, start, duration (0.2-900 s)$0.02 per job
Overlay the cardPOST /v1/timeline-1.0/composeoperation overlay, image.url, video.url, layout.position$0.02 flat per job
JoinPOST /v1/timeline-1.0/renderaudio, video[] slots$0.10 per started minute

A runnable chain

The script submits each job, polls it, and feeds the result into the next one. Every URL must be your workspace's media.sume.com artifact or asset, which is why the trim result's video_url can go straight into compose. The idempotency keys carry a version, so a policy change is a new key.

import json, os, time, urllib.request

def call(path, body=None, key=None):
    h = {"Authorization": "Bearer " + os.environ["SUME_API_KEY"], "Content-Type": "application/json"}
    if key:
        h["Idempotency-Key"] = key
    data = json.dumps(body).encode() if body else None
    return json.load(urllib.request.urlopen(urllib.request.Request("https://api.sume.com" + path, data, h)))

def run(path, body, key):
    jid = call(path, body, key)["request_id"]
    while call(f"/v1/jobs/{jid}/status")["status"] not in ("completed", "failed", "canceled"):
        time.sleep(5)
    return call(f"/v1/jobs/{jid}/result")

M = "https://media.sume.com/artifacts/artf_demo/"
tail = run("/v1/video-trim", {"video_url": M + "hero.mp4", "start": 14, "duration": 4}, "ec-trim-v1")
card = run("/v1/timeline-1.0/compose", {"operation": "overlay", "image": {"url": M + "endcard.png"},
           "video": {"url": tail["video_url"]}}, "ec-compose-v1")
out = run("/v1/timeline-1.0/render", {"audio": {"mode": "silence", "duration_seconds": 18},
          "video": [{"source_url": M + "hero.mp4", "start": 0, "duration": 14},
                    {"source_url": card["video_url"], "start": 14, "duration": 4}]}, "ec-render-v1")
print(out["video_url"])

Check the seam

At the join, the first slot should end exactly where the second starts, and the audio spine must cover the full 18 seconds. A silent spine is declared with audio.mode set to silence. Pull a frame at the join with video frames to confirm there is no jump.

Limits

Stating a price-match or pickup offer is a commitment, and its details belong to your policy page. The card should name the offer and point to the terms, not paraphrase them. If the policy differs by store, make one card per variant. For a single short clip, a plain caption cue (captions, $0.20) is simpler than three jobs. Keep the card legible at phone width, with the offer name large and the terms on the page behind the link, and run a frame check at the first and last second of the card, because the tail is the part viewers who stay to the end will actually remember.

Sources

Related posts

More in Use cases

All Use cases posts

Written by Sume