Seedance 2.5 job failed: refund, new idempotency key, and rerun cost
A failed Seedance 2.5 job is refunded on Sume. Retry with a new Idempotency-Key; the old one replays the failed job. A Python handler and the rerun cost.

When a Seedance 2.5 job fails on Sume, the reserved amount can be refunded on failure, per the Sume workflow docs,, and a retry needs a new Idempotency-Key, because a replay with the old key returns the original job, not a new one. A 30-second 720p clip reserves $17.34 at submit, so the refund path matters. The pattern is to receive the job.failed webhook, verify it, then resubmit with a key such as <job_id>-retry1.
The refund and idempotency rules are in the video docs, and the signature scheme is in the webhooks guide. Seedance 2.5 limits are in the Video Router docs.
What a retry costs
A refunded failure costs nothing, so the retry is the only new spend. Dropping to 480p for the retry turns a long clip into a cheap check.
Where the numbers come from: Sume prices Seedance by video tokens, computed as width x height x seconds x 24 fps / 1024, at a provider list rate of $0.0214 per 1,000 tokens for 2.5 (the 1080p tier is $0.0234), times the 1.25 house margin that the Video Router docs describe, rounded up to the cent. A 16:9 720p frame is 1280 x 720, so one second is 21,600 tokens and a 30-second clip is 648,000 tokens, or $13.87 at list and $17.34 billed. Use the same arithmetic to predict a rerun before you submit it, and remember that a retry at a lower resolution is a different price, not a discount on the first attempt.
| Length | 480p | 720p | 1080p |
|---|---|---|---|
| 4 s | $1.08 | $2.32 | $5.69 |
| 15 s | $4.04 | $8.67 | $21.33 |
| 30 s | $8.07 | $17.34 | $42.65 |
The handler
This verifies the sume-v1 signature, rejects an empty secret and a timestamp more than 300 seconds off, then branches on the event. Run it as is: the demo at the bottom signs a failed event, then shows an empty secret being rejected.
import hashlib, hmac, json, time
def verify(raw: bytes, ts: str, sig: str, secret: str) -> bool:
if not secret or not ts or not sig:
return False
if abs(time.time() - int(ts)) > 300:
return False
mac = hmac.new(secret.encode(), ts.encode() + b"." + raw, hashlib.sha256)
want = "sume-v1=" + mac.hexdigest()
return any([hmac.compare_digest(want, c.strip()) for c in sig.split(",")])
def handle(raw: bytes, ts: str, sig: str, secret: str) -> str:
if not verify(raw, ts, sig, secret):
return "401 rejected"
evt = json.loads(raw)
if evt["event"] == "job.completed":
return "save " + evt["payload"]["artifacts"][0]["url"]
return "retry with key " + evt["job_id"] + "-retry1"
if __name__ == "__main__":
body = json.dumps({"event": "job.failed", "job_id": "job_1", "status": "ERROR"}).encode()
ts = str(int(time.time()))
sig = "sume-v1=" + hmac.new(b"s3cret", ts.encode() + b"." + body, hashlib.sha256).hexdigest()
print(handle(body, ts, sig, "s3cret"))
print(handle(body, ts, sig, ""))Why a new key
An Idempotency-Key makes retries safe: a network timeout on submit can be repeated without making two jobs. The same safety means that reusing the key after a failure returns the failed job. So derive the key from the job id and a retry counter, and stop at a small number of tries. If a clip fails twice at the same settings, change something: shorten it, lower the resolution or simplify the prompt.
Do not treat job.canceled as a failure to retry. A cancel is a deliberate stop, so branch on the event name and leave it alone unless you know why it happened.
Webhooks come from the callback_url field on POST /v1/videos, which must be HTTPS; this post covers it. For the shape of failed and canceled events, see the event-branch post, and for where failures send their webhook, the failure webhook post. Before a retry of a long clip, run the request against the catalog with the pre-submit check, since a value outside the catalog is refused rather than rerendered.
Cap the retries
Set a limit of two retries per job id and log each one with the key and the settings. If the third attempt would be the same request, stop and look at the failure reason in the payload's error object. A prompt that fails the same way twice is a prompt problem, not a transient one. Because failures are refunded, the cost of this discipline is time, not money. Because successes are not, the cost of an unbounded loop is real, since a loop that keeps producing finished clips you never read will spend credits on every pass.
Sources
Related posts
More in Developers
- Low-latency TTS without streaming: one job per sentence
Sume TTS has no streaming. To start playback early, split the script by sentence, submit the jobs in parallel and play each file as it finishes, in order.
- Shadow-run gpt-image-2.5 beside your current model before October 23
Render one prompt on a Sume image id and your current model with a separate idempotency key per model, then compare cost and output before gpt-image-1 ends.
- Shortest and longest values Sume accepts for a Short: one table
Minimum clip, slot, spine and fade values and the maximums for trim, transitions and soundtrack on Sume, in one table with an offline slot checker in Python.
- Shorts season webhooks: a Python receiver that counts episodes
Receive Sume job webhooks for a season of Shorts renders: verify sume-v1, reject an empty secret, de-dupe by job_id, and know when every episode is terminal.
Written by Sume