Sume webhook handler status codes: when to return 401, 2xx or 5xx

Sume retries any non-2xx up to 10 times, 30 seconds apart. Return 2xx only after you store the event. A Python handler that refuses a bad signature.

5 min readSume
All posts

The status code your webhook endpoint returns is a control signal for Sume. A 2xx means the delivery is finished. Anything else, including a timeout, is retried. The docs give the budget: up to 10 attempts in total, a fixed 30 seconds between them (not exponential backoff), and 10 seconds for each attempt. Pick the code that says what you mean.

Which status for which situation

Webhook response codes and what Sume does next (read 2026-10-04)
SituationReturnSume then
Event stored durably200Stops retrying
Signature or timestamp invalid401Retries; it will fail again until the cause is fixed
Your store is down503Retries in 30 seconds
Handler slower than 10 seconds(timeout)Counts as a failed attempt and retries

The retry window

Ten attempts with 30 seconds between them cover about 270 seconds of spacing, which is a bit over four and a half minutes, plus up to 10 seconds of timeout for each attempt. So a database outage shorter than about four minutes recovers by itself if you return 503. A longer outage leaves a failed delivery, but the job still reached its terminal state, so poll its status_url or call POST /v1/jobs/{job_id}/webhook/redeliver once you are back.

Handler

The handler below verifies the signature over <timestamp>.<raw_body> with a 300-second tolerance, refuses an empty secret, and stores the event before it answers. It uses job_id as the key on your side, as the docs advise.

import hashlib, hmac, json, time
from http.server import BaseHTTPRequestHandler

SECRET = ""  # load SUME_COM_WEBHOOK_SIGNING_SECRET here
SEEN = {}

def verify(raw, ts, header, secret, tolerance=300):
    if not secret:
        raise RuntimeError("signing secret is empty")
    if abs(time.time() - int(ts)) > tolerance:
        return False
    digest = hmac.new(secret.encode(), f"{ts}.".encode() + raw, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(e.strip(), f"sume-v1={digest}") for e in header.split(","))

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        raw = self.rfile.read(int(self.headers.get("content-length", 0)))
        ts = self.headers.get("x-sume-webhook-timestamp", "0")
        if not verify(raw, ts, self.headers.get("x-sume-webhook-signature", ""), SECRET):
            return self.send_response(401) or self.end_headers()
        body = json.loads(raw)  # a webhook.test body has no job_id
        SEEN.setdefault(body.get("job_id") or body["request_id"], raw)  # store, then answer
        self.send_response(200)
        self.end_headers()

Do not answer early

Returning 200 before the write succeeds loses the event, because Sume believes the delivery is done. Store, then answer. If the work after storing is slow, queue it and return, since the 10-second limit counts against the attempt.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume