Redelivered job.completed must not start the trim twice

Redeliver re-sends the same job's terminal event with a fresh signature. Dedupe on job_id and derive the next step's Idempotency-Key from it.

4 min readSume
All posts

If your receiver starts the trim step when a render's job.completed arrives, a redelivery will start it again unless you stop it. The fix is small: treat job_id as the idempotency key, as the docs tell receivers to, and send the trim with an Idempotency-Key built from that job_id. Then a second delivery of the same event returns the original trim job and bills once.

What redeliver changes and what it keeps

POST /v1/jobs/{job_id}/webhook/redeliver needs jobs:write. Sume re-POSTs the real terminal event of that job (job.completed, job.failed or job.canceled) with a fresh timestamp and signature. It still works after all 10 automatic attempts are used, and it does not use one of them. It does not change the destination URL: a new URL is a new job.

Delivery fields across a redelivery (read 2026-10-05)
FieldSame as the first delivery?Use it for
job_idYesDedupe key
TimestampNo, freshReplay-window check only
sume-v1 signatureNo, freshVerify each delivery separately
Terminal event typeYesRoute to the next step

Derive the next key

Build the key from values that do not change. A key like trim-after-<job_id> is the same on every delivery of that event, so the platform's own idempotency does the deduping for you, even if your local table is lost. Keep a local job_id set too, so that you skip the work of a verification and an HTTP call when you know the step has already begun.

def trim_key(job_id: str) -> str:
    return f"trim-after-{job_id}"

# send as: Idempotency-Key: trim-after-job_123
# same job_id on redelivery -> same key -> same trim job

Pitfalls

Do not use the timestamp or signature as part of the key: both change on every redelivery, which would start a second trim. Do not skip signature verification on a redelivery because it looks like a repeat; each delivery is signed afresh and must be verified over <ts>.<raw_body>.

Only job.completed should start the next step. A job.failed or job.canceled event for the render means there is nothing to trim. When you redeliver by hand to test, send it against a job whose terminal state is the one your handler expects.

Testing the dedupe

Test the path with a real redeliver rather than a copy of a payload. Complete a render, let your handler start the trim, then call POST /v1/jobs/{job_id}/webhook/redeliver. You should see a second delivery with a new signature, your handler should verify it, and the trim submit should return the same job as before.

If the second delivery starts a second trim, the key is not derived from the job_id. Fix that before you rely on webhooks in production. The local dedupe table is an optimization; the derived key is the guarantee.

Remember that redeliver needs the jobs:write scope on the key you call it with.

  • Complete, redeliver, compare trim job ids.
  • Same id: pass.
  • Different id: the key is wrong.

When the receiver was down

The usual reason to redeliver is an outage. If your endpoint was unreachable, Sume tried up to 10 times, 30 seconds apart. After that, the automatic attempts are used up, and the event is not lost: you can still redeliver by hand, and the call does not take away from the 10.

Once the receiver is back, redeliver the jobs that missed their event in the order you want the chain steps to run. Each delivery goes through the same verified, deduplicated handler, so the result is the same as if the event had arrived the first time.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume