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
- reqwest timeout versus read_timeout for Sume polls and downloads
reqwest has no timeout by default. timeout() is a total deadline including the body; read_timeout() resets per read. Which to use for Sume status polls.
- Same narrator every week: pin the voice, language and speed
Keep one narrator across a weekly series on Sume TTS: pin avatar_handle with voice.id, language, speed and format in one config, with one key per episode.
- 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.
- A Sume scheduled run is not in /v1/jobs: where to read it back
A scheduled Sume run never shows up in /v1/jobs. Read it from /v1/action-runs instead; the table maps start, read, status and overlap rules to each side.
Written by Sume