Basin Pipelines: log Sume webhook deliveries and dedupe by job_id
Cloudflare Basin Pipelines streams ingest up to 1 GB/s. Log each signed Sume webhook delivery as one record and dedupe on job_id; Python sketch included.

Cloudflare's October 1, 2026 changelog says Basin is generally available and that each Basin Pipelines stream can now ingest up to 1 GB/s, up from 5 MB/s. A Sume webhook receiver sends nothing close to that, so the useful part is the record: log each verified delivery with its event, job_id and request_id, and treat job_id as the dedupe key.
The facts used
The first row is from the Cloudflare changelog; the rest are from Sume's webhooks guide. I am not showing Cloudflare's ingestion API, because I did not read it.
| Source | Fact |
|---|---|
| Cloudflare changelog, Oct 1, 2026 | Basin GA; each Basin Pipelines stream ingests up to 1 GB/s, up from 5 MB/s |
| Sume webhooks | Terminal events only: job.completed, job.failed, job.canceled |
| Sume webhooks | Up to 10 delivery attempts, 30 s apart by default, 10 s timeout per attempt |
| Sume webhooks | Use job_id as the idempotency key on your side |
One record per delivery
Verify the signature first. Sume signs the timestamp, a dot and the raw body with HMAC SHA 256, and sends it as sume-v1= in x-sume-webhook-signature; during a secret rotation the header holds several entries, and any match is valid. Reject an empty secret and a stale timestamp. Then build a record and drop repeats of the same job_id and event. Return a 2xx only after the record is stored.
The sketch keeps seen ids in memory; in production use your store's unique key.
import hashlib, hmac, json, time
SEEN: set[str] = set()
def handle(raw: bytes, ts: str, sig_header: str, secret: str):
if not secret:
raise ValueError('empty webhook secret')
if abs(time.time() - int(ts)) > 300:
return None
want = 'sume-v1=' + hmac.new(secret.encode(), f'{ts}.'.encode() + raw, hashlib.sha256).hexdigest()
if not any(hmac.compare_digest(e.strip(), want) for e in sig_header.split(',')):
return None
body = json.loads(raw)
key = f"{body['job_id']}:{body['event']}"
if key in SEEN:
return None
SEEN.add(key)
return {'event': body['event'], 'job_id': body['job_id'],
'request_id': body.get('request_id'), 'status': body.get('status'), 'received_at': int(time.time())}
if __name__ == '__main__':
s, ts = 'test-secret', str(int(time.time()))
raw = json.dumps({'event': 'job.completed', 'job_id': 'job_1', 'request_id': 'job_1', 'status': 'OK'}).encode()
sig = 'sume-v1=' + hmac.new(s.encode(), f'{ts}.'.encode() + raw, hashlib.sha256).hexdigest()
print(handle(raw, ts, sig, s))
print(handle(raw, ts, sig, s))Keep polling as a fallback
Ten refused attempts leave you with a failed delivery and a job that still finished. Sume says delivery is an optimization and not the only recovery path, so keep status_url polling for events that never arrive. The Redeliver action re-sends a job's real terminal event with a fresh signature and does not use up one of the automatic attempts.
Sources
Related posts
More in Developers
- Blind-test Sonic 3.6 against 3.5 on your own script
A vendor's blind-test percentage is not yours. Render the same lines with two catalog versions through the TTS Router, shuffle them, and let listeners vote.
- Browser voice app that starts Sume jobs: keep the key on your server
Voice apps run in the browser over WebRTC, but Sume keys belong on a server. A route handler that holds the key, allowlists models, reuses idempotency keys.
- Sume bulk queue 404 format_run_queue_not_found: three causes
A Sume bulk queue poll returned 404 format_run_queue_not_found. The id is wrong or the queue is another owner's. How to tell which, and what to do next.
- Bulk run 409 idempotency_conflict: find the first queue by queue_id
A Sume bulk create that reuses a key with a different payload returns 409 with details.queue_id. How to read the original queue and decide what to resend.
Written by Sume