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 A/B test of two image edits: reviewer sheet and key in Python
Pick between GPT Image 2.5 and Nano Banana 2 edits without bias: shuffle left and right, hide model names, save an answer key. Pillow script, 25 lines.
- 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.
- Bluesky avatar and banner: 1,000,000 bytes, PNG or JPEG
Bluesky's profile record caps avatar and banner at 1,000,000 bytes each, PNG or JPEG only. How to size a GPT Image 2.5 output and check the bytes in Pillow.
- Bluesky link card thumbnail: 1,000,000 bytes, any image type
The thumb on a Bluesky link card is optional, accepts image/* and is capped at 1,000,000 bytes. Make a 1280x720 preview with Sume and shrink it under the cap.
Written by Sume