Sume run webhook 3xx redirect: a failed attempt, not a delivery
Sume does not follow redirects on run webhooks, so a trailing-slash 301 fails every attempt. How to find it with a no-follow probe and register the final URL.

If your webhook URL answers with a redirect, Sume counts it as a failed delivery. The run webhooks page is blunt about it: redirects are not followed, and a 3xx is a failed attempt. The common cause is not an intentional redirect but a framework adding or removing a trailing slash, or a proxy upgrading HTTP to HTTPS, in front of a handler that works perfectly in your own tests. The fix is to register the final URL, and to test with a client that does not follow redirects.
Where the accidental redirects come from
Your own tests usually pass because curl with -L, a browser, or a test client follows the redirect. Production deliveries do not. Typical sources:
- A framework that canonicalises
/hooks/sumeto/hooks/sume/(or the reverse) with a 301 or 308 on a POST. - A CDN or load balancer rule that sends
http://tohttps://. Run webhook URLs must be HTTPS anyway, but a stale registered URL withhttpis rejected up front, while a redirectingwwwhost is not. - A domain move: the old host returns 301 to the new host and nobody updated the Format run's
communication.webhook_url. - An auth gateway that redirects unauthenticated requests to a login page.
Probe without following
The script starts a local server that behaves like a trailing-slash framework and probes both spellings with http.client, which never follows redirects, the same behaviour as the delivery worker. The first call shows a 301 and a verdict of failed attempt; the second shows the 204 of the canonical path. Swap the local server for your real URL and a signed test body to check a staging endpoint. It uses only the standard library.
import http.client, threading
from http.server import BaseHTTPRequestHandler, HTTPServer
class H(BaseHTTPRequestHandler):
def do_POST(self):
self.rfile.read(int(self.headers.get("content-length", 0)))
if self.path == "/hooks/sume": # no trailing slash
self.send_response(301)
self.send_header("location", "/hooks/sume/")
else:
self.send_response(204)
self.end_headers()
def log_message(self, *a): pass
srv = HTTPServer(("127.0.0.1", 0), H)
threading.Thread(target=srv.serve_forever, daemon=True).start()
def probe(path):
c = http.client.HTTPConnection("127.0.0.1", srv.server_port)
c.request("POST", path, body=b"{}")
r = c.getresponse() # http.client does not follow redirects
verdict = "delivered" if 200 <= r.status < 300 else "FAILED attempt"
return r.status, r.getheader("location"), verdict
print(probe("/hooks/sume"))
print(probe("/hooks/sume/"))What Sume does around it
The URL is re-validated as a public HTTPS URL at delivery time, not only when you submit. Each failed attempt is retried on the schedule in the docs, 10 attempts with a 10-second timeout, and the receipt's webhook_delivery object tells you what happened: its state (pending, retrying, delivered, failed, exhausted) and last_status_code. A last_status_code of 301 or 308 on a receipt is the signature of this bug.
Fixing the URL does not change runs that already carry the bad one: a redeliver re-sends to the URL the run was created with. For those runs, read the receipt from the status or result URL and act on it directly; new runs should be created with the corrected communication.webhook_url.
| Your endpoint answers | Sume treats it as | Fix |
|---|---|---|
| Redirect response (3xx) | Failed attempt, retried | Register the final URL |
| 200 to 299 | Delivered | Nothing |
| 401 from your verifier | Failed attempt | Check secret and raw body |
| Timeout over 10 s | Failed attempt | Acknowledge first, then work |
Make it a deploy check
Add the no-follow probe to the pipeline that deploys your receiver. Any 3xx on the registered path fails the build, and the check takes one request. Pair it with a periodic read of webhook_delivery.last_status_code on recent runs, so a proxy change made by someone else does not stay invisible until a customer asks where their video is.
Reading the receipt
The receipt's webhook_delivery block is the quickest diagnostic: status says pending, retrying, delivered, failed or exhausted, last_status_code shows what your endpoint answered, manual_redeliveries counts the times someone re-sent it, and signing_secret_fingerprint identifies the secret used. A run stuck in retrying with a 3xx status code is this bug, while a 401 is a signature problem and a timeout is your handler being slow. Delivery outcomes never change the run itself; the receipt remains the truth.
Sources
Related posts
More in Developers
- Scalar API Reference for the Sume OpenAPI JSON, with a Try It key
Scalar renders an OpenAPI document as an interactive reference with a test client. Point it at the Sume spec, and keep the Bearer key out of the page source.
- Sume SDK wait timeouts: 20 min, 10 min, and the 90-minute run
subscribeFormatRun waits 20 minutes, waitForRun 10, waitForJob 20, yet a run lives up to 90. Which clock fires first and how to resume after a timeout.
- Fetch a Sume video output with the content endpoint index query
GET /v1/videos/{jobId}/content takes an index that defaults to 0. When it matters, how it lines up with unsigned_urls, and the curl line that saves a file.
- 768p on seedance-2.5 returns 400: use 720p, or pin a MiniMax id
Sume's resolution list is per model. seedance-2.5 takes 480p, 720p and 1080p; 768p exists on the MiniMax H3 ids and H3 Max Recast, and kling-3 has no 480p.
Written by Sume