H3 Max Recast webhook: submit with mode webhook, verify the HMAC
Recast jobs run for a while. Submit h3-max-recast with mode webhook, then verify Sume's sume-v1 HMAC signature before you download the swapped video.

To get a webhook when an H3 Max Recast job finishes, submit with mode: webhook and a public HTTPS webhook_url. Sume returns the job id at once, then sends one terminal event: job.completed, job.failed or job.canceled. Check its HMAC signature before you act on the payload, and keep a status poll as a fallback.
Recast outlasts any request you would hold open. Sume's sync and subscribe modes block for 30 seconds at most, and a swap of up to 30 seconds of source is not going to finish in that time, so async polling or a webhook is the right shape.
How do I submit a Recast job with a webhook?
Same body as any Recast call, plus the two webhook fields. duration is your inspected source length in whole seconds rounded up, and the webhook URL must be public HTTPS: localhost, private networks and plain HTTP are rejected.
curl -X POST https://api.sume.com/v1/video-router/generate \
-H "Authorization: Bearer $SUME_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: recast-webhook-001" \
-d '{
"model": "h3-max-recast",
"video_url": "https://example.com/original.mp4",
"reference_image_urls": ["https://example.com/person.png"],
"resolution": "768p",
"duration": 15,
"mode": "webhook",
"webhook_url": "https://hooks.example.com/sume"
}'What does Sume send?
Per Sume's webhook docs, read 2026-10-03:
- During a signing-secret rotation the header carries several
sume-v1=entries, newest first. Accept the delivery if any one matches. - Your signing secret is on the Webhooks tab of the dashboard, or from
GET /v1/webhooks/signing-secretwith anaccount:readkey.
| Item | Value |
|---|---|
| Events | job.completed, job.failed, job.canceled (terminal only) |
| Progress events | None |
| Signature header | x-sume-webhook-signature: sume-v1=<hex> |
| Timestamp header | x-sume-webhook-timestamp |
| Signed string | <timestamp>.<raw_body>, HMAC SHA 256 |
| Failure payload | status ERROR plus an error object |
| Replay window | Reject outside about five minutes |
How do I verify the signature?
Verify against the raw bytes of the body, not a re-serialised copy, because any whitespace change breaks the HMAC. The function below refuses an empty secret, checks the timestamp window, and compares in constant time against every entry in the header.
import hashlib
import hmac
import time
def verify(raw_body: bytes, timestamp: str, signature_header: str,
secret: str, tolerance: int = 300) -> bool:
if not secret:
raise ValueError("webhook signing secret is empty")
try:
ts = int(timestamp)
except ValueError:
return False
if abs(int(time.time()) - ts) > tolerance:
return False
mac = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body,
hashlib.sha256).hexdigest()
expected = f"sume-v1={mac}"
entries = [e.strip() for e in signature_header.split(",")]
return any(hmac.compare_digest(expected, e) for e in entries)What does a minimal receiver look like?
A standard-library receiver is enough to see the flow. Save the verifier as verify.py, set the secret in the environment, and expose port 8080 through whatever public HTTPS front door you use. It answers 401 to anything that fails verification and 204 otherwise, and it refuses to start without a secret.
import json
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
from verify import verify # the function above, saved as verify.py
SECRET = os.environ.get("SUME_COM_WEBHOOK_SIGNING_SECRET", "")
if not SECRET:
raise SystemExit("set SUME_COM_WEBHOOK_SIGNING_SECRET")
class Hook(BaseHTTPRequestHandler):
def do_POST(self):
raw = self.rfile.read(int(self.headers.get("Content-Length", 0)))
ok = verify(raw, self.headers.get("x-sume-webhook-timestamp", ""),
self.headers.get("x-sume-webhook-signature", ""), SECRET)
self.send_response(204 if ok else 401)
self.end_headers()
if ok:
event = json.loads(raw)
print(event.get("event"), event.get("job_id"), event.get("status"))
HTTPServer(("", 8080), Hook).serve_forever()How do I test it before spending on a clip?
Use Sume's test delivery. The Webhooks tab has a Send test control, and POST /v1/webhooks/test-deliveries with an account:write key does the same: it sends a dummy signed webhook.test payload to a URL you type, and never replays a real job. That proves your verifier, your raw-body handling and your public URL for free, so the first paid Recast run is not also your first webhook test.
A test delivery has the event name webhook.test rather than a job event, which is why the receiver above reads the job id with get. After that, spend the minimum on a real run: a 5 second source at 768p is $1.88 on Sume.
Can the same event arrive twice?
Plan for it. Sume tracks each delivery with a status that includes retrying, failed and exhausted, so a delivery that your endpoint did not acknowledge in time can be attempted again, and POST /v1/jobs/{id}/webhook/redeliver can re-send one on request. Make the handler idempotent: key your own record on the job id, and ignore a terminal event for a job you already finished handling.
Also answer quickly and do slow work elsewhere. A handler that downloads a 30 second video before it replies invites a retry in the middle of its own work. Acknowledge with a 2xx, queue the download, and verify the file when it lands.
What should the handler do next?
Return a 2xx quickly and do the slow work after. On job.completed, read the result from the payload's artifacts or from GET /v1/jobs/{id}/result, then copy the file to your own storage; do not store a link as your only copy. On job.failed, read the error object and decide by its category: input errors need a fix, while queue errors are retried later with the same Idempotency-Key and generation_unavailable errors later, per Sume's error docs.
Keep a poll on /v1/jobs/{id}/status for jobs that have gone quiet. If a delivery never arrives, Sume can re-send it: POST /v1/jobs/{id}/webhook/redeliver with a jobs:write key. Never resubmit the paid Recast job just because your endpoint was down, because the first job is still billing and has a result waiting.
Sources
Related posts
More in Developers
- Ideogram 4 download: Hugging Face gate, login and first image
To run Ideogram 4 locally: accept the gate on Hugging Face, log in with hf, pip install the repo, run run_inference.py. The flags and the nf4 or fp8 choice.
- Is AI avatar video real time? How long a Sume job takes
A Sume avatar video is a job, not a live stream: it queues, renders, and you poll or take a webhook. What the sync wait caps at, and a Python polling loop.
- Latin American Spanish text to speech: es or es-MX on Sume?
Sume's TTS language is a free string and its voice library tags voices with plain es. What that means for Mexican, Argentine or Spain Spanish, and how to test.
- Live AI avatar API: a Tavus conversation vs a Sume job
A live avatar API creates a room you join. Sume's Avatar API creates a job you poll. Field-by-field map of Tavus create conversation and Sume talking-video.
Written by Sume