Sume webhook URL localhost returns 400: test with a tunnel
Sume refuses localhost, private ranges, and plain HTTP webhook URLs with a 400 at create. Use a public HTTPS tunnel and verify the signature.

If your Sume webhook_url points at localhost, a private address, or plain HTTP, the create call returns 400. Expose your local handler through a public HTTPS tunnel, put that URL in the request, and verify the signature in the handler.
Sume rejects those URLs at create time rather than failing quietly during delivery, so you find the mistake in seconds. The rule is on the Run webhooks page: the URL must be public HTTPS and at most 2048 characters. Redirects are not followed, so the tunnel URL must answer directly.
That fast failure is a kindness. A webhook that silently never delivers is a long afternoon, while a 400 at create tells you the URL class is wrong before any money moves.
How other vendors handle local development
Stripe's webhook guide, read 2026-10-10, tells you to use a tunnelling tool such as ngrok for a temporary public HTTPS URL, or to forward events with the Stripe CLI, which listens and forwards to a local port. Stripe's CLI path works because Stripe controls the sender and the forwarder.
Sume does not ship a forwarding listener for webhooks, so a tunnel you run yourself is the route. A hosted tunnel, a reverse proxy on a small server, or a preview deployment all satisfy the public HTTPS requirement.
| Question | Stripe | Sume |
|---|---|---|
| Localhost URL accepted at registration | Registered endpoints must be public HTTPS | No, 400 at create |
| Forward events to a local port | stripe listen --forward-to | No listener, use your own tunnel |
| Signature header | Stripe-Signature | x-sume-webhook-signature |
| Default replay tolerance | 5 minutes in libraries | 5 minutes |
The test loop
Start your handler and the tunnel. Send a test delivery with POST /v1/webhooks/test-deliveries, which sends a webhook.test event to a URL you choose, so you can check signature handling without paying for a generation. Then run a real, cheap Format run with a low generation_spend_cap_usd and watch the terminal delivery arrive.
If a delivery never arrives, the run receipt carries webhook_delivery.status. The values are not_armed, pending, retrying, delivered, failed, and exhausted. not_armed means no webhook was attached to that run.
A cheap run still costs something, so keep the cap small on test runs. generation_spend_cap_usd must be above zero and at most 500, and a run that would exceed it ends failed with format_run_failed rather than spending past the limit.
Verify before you parse
Compute HMAC-SHA256 over the string timestamp, a dot, and the raw request body, and compare it to the sume-v1 value in x-sume-webhook-signature. Reject anything older than five minutes and refuse to run at all when the secret is empty.
Your framework must not re-serialize the body before you hash it. In Node, read the raw bytes; in Python frameworks, read the request body as bytes. Dedupe on request_id, which equals the run id, because a delivery can arrive more than once.
Return a 2xx status once the event is stored. Delivery has a 10 second timeout per attempt and up to 10 attempts, so a handler that takes 12 seconds will look like a failure and trigger retries even though your code finished.
import hashlib, hmac, os, time
def verify(raw: bytes, ts: str, header: str) -> bool:
secret = os.environ.get("SUME_COM_WEBHOOK_SIGNING_SECRET", "")
if not secret:
raise RuntimeError("signing secret is empty")
if abs(time.time() - int(ts)) > 300:
return False
mac = hmac.new(secret.encode(), ts.encode() + b"." + raw, hashlib.sha256)
return hmac.compare_digest("sume-v1=" + mac.hexdigest(), header)
Before you go to production
Swap the tunnel for your real HTTPS host, fetch the signing secret from the dashboard Webhooks tab or GET /v1/webhooks/signing-secret, and return 2xx quickly. Do slow work after you have saved the event, as Stripe also advises.
Sources
Related posts
More in Developers
- Suno v6-wild is less predictable: how to keep a track on Sume
Suno describes v6-wild as less predictable. Sume Music has no seed, so you cannot rerun a track; keep the job result and its URL as the only master.
- Tailscale Funnel for Sume webhooks: a public HTTPS URL for localhost
Sume webhook URLs must be public HTTPS, so localhost is rejected. Tailscale Funnel gives your dev machine one, plus a receiver that refuses an empty secret.
- TTS output_format: choose wav or mp3 for joins, captions and clips
Sume TTS returns mp3 by default. Choose wav when you will join, slice per sentence or feed lip-sync; mp3 is smaller but adds padding at every edge.
- Twenty image jobs, one webhook: mode webhook plus a Python verifier
Submit 20 image requests with mode webhook, receive signed job.completed callbacks, and verify them in Python. Retries, replay window and the poll fallback.
Written by Sume