Agent run webhook: created_at orders deliveries, request_id dedupes

An agent.run.terminal delivery has two ids that look alike. request_id dedupes retries; created_at orders deliveries. Includes a Python receiver.

5 min readSume
All posts

Dedupe on the envelope's request_id and order by created_at. Sume's run webhook docs say request_id equals the run id and is stable across retries, so it is the dedupe key. created_at is the time Sume built that delivery body, so it is the field for putting deliveries in order, which request_id cannot do because it never changes between retries (Run webhooks).

Two ids that look alike

The docs call out a trap. The envelope's request_id is the run id. The receipt inside, at payload.request_id, is a correlation id: in a webhook it is also the run id, but when you read the same receipt with a GET it is an HTTP req_ id. Dedupe on the envelope value, or on run_id, and ignore the nested one.

An Agent Completion fires exactly one terminal event, agent.run.terminal, when the run completes or fails. A canceled run sends nothing, so do not wait for a POST after you cancel.

Envelope fields and their use, read 2026-10-05
FieldUse it toStable across retries?
request_idDedupeYes
run_idLook up the run; equals request_idYes
created_atOrder deliveriesNo, it is the body build time
status / outcomeBranch: ok, degraded, errorYes
payload.request_idIgnore; correlation onlyDepends on transport

Verify first, then dedupe

Sume signs <timestamp>.<raw_body> with HMAC-SHA256 and sends x-sume-webhook-signature: sume-v1=<hex> and x-sume-webhook-timestamp. Verify against the raw body before you parse it, and reject a timestamp outside a replay window; five minutes is the documented default. The receiver below refuses an empty secret and runs offline with a self-signed test.

import hashlib, hmac, json, time

def verify(raw: bytes, ts: str, sig: str, secret: str) -> bool:
    if not secret:
        raise ValueError("empty webhook secret")
    if abs(time.time() - int(ts)) > 300:
        return False
    mac = hmac.new(secret.encode(), ts.encode() + b"." + raw,
                   hashlib.sha256).hexdigest()
    return hmac.compare_digest("sume-v1=" + mac, sig)

SEEN = set()

def first_time(event: dict) -> bool:
    rid = event["request_id"]
    if rid in SEEN:
        return False
    SEEN.add(rid)
    return True

if __name__ == "__main__":
    raw = json.dumps({"request_id": "agrun_1"}).encode()
    ts = str(int(time.time()))
    sig = "sume-v1=" + hmac.new(b"s", ts.encode() + b"." + raw,
                               hashlib.sha256).hexdigest()
    print(verify(raw, ts, sig, "s"), first_time(json.loads(raw)))

Process after you answer

Record the event durably, then return a 2xx quickly. Each attempt times out at 10 seconds, success is any 2xx, and a redirect counts as a failed attempt. Sume makes up to 10 attempts with backoff, then marks the delivery exhausted.

Do the real work after the response. A slow handler uses the whole attempt budget and invites a retry, which your dedupe then has to absorb.

Handling out-of-order deliveries

Retries mean a delivery can arrive late, after your system already moved on. Keep the created_at of the last event you applied for each run, and ignore any delivery whose created_at is older. Because a run has one terminal event, this rarely matters for a single run, but it matters if you also write run state from a poll or from a list call.

Store the event before you act on it. If your worker crashes between the write and the side effect, the retry arrives with the same request_id, and you can resume from the stored row instead of repeating a paid follow-up step. Keep the dedupe set in a database with a unique constraint rather than in memory, since a restart empties memory and the retry schedule can stretch over hours.

Checklist

Review your receiver against these points.

  • Verify the signature on the raw bytes, before parsing.
  • Branch on outcome, not only status.
  • Dedupe on the envelope request_id.
  • Keep a poll as a backup using status_url.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume