Thin vs full webhook payloads: how Sume's job events work

Stripe made thin events generally available for API v1. Sume's job webhooks carry the result in the payload, are keyed by job_id, and can be redelivered.

4 min readSume
All posts

Stripe's changelog for 2026-09-30 says thin events for API v1 resources are generally available. Sume's job webhooks are not thin: a terminal event carries the job's result in payload, keyed by job_id. Your handler can act on the delivery, and a lost delivery can be redelivered or recovered by polling.

What Stripe announced

The changelog line is the only Stripe fact used here. In general, a thin event names the resource that changed and leaves you to fetch the current object. Check Stripe's documentation for the details of its model.

What a Sume event holds

Sume sends terminal job events only: job.completed, job.failed and job.canceled. There are no progress or partial deliveries. A completed event carries the result, such as the artifact list with id, url, type and content_type. Failed and canceled events use status: "ERROR" with an error object.

Action, Format and Agent Completion runs have their own events: action.run.terminal, format.run.terminal and agent.run.terminal. They share the signature scheme, so one verifier covers both.

Sume job webhook facts (docs) (read 2026-10-03)
ItemValue
Eventsjob.completed, job.failed, job.canceled
Idempotency key on your sidejob_id
AttemptsUp to 10
SpacingFixed, 30 s by default
Timeout10 s per attempt
ReplayPOST /v1/jobs/{job_id}/webhook/redeliver

Verify before you trust it

Sume signs the raw body with HMAC SHA-256 over <timestamp>.<raw_body>. The headers are x-sume-webhook-timestamp and x-sume-webhook-signature: sume-v1=<hex>. During a secret rotation the header holds one entry per live secret, so accept any match. Reject old timestamps; five minutes is a reasonable window. This version refuses an empty secret.

import hashlib, hmac, time

def verify(raw: bytes, ts: str, header: str, secret: str, tol: int = 300) -> bool:
    if not secret:
        return False
    try:
        t = int(ts)
    except ValueError:
        return False
    if abs(time.time() - t) > tol:
        return False
    digest = hmac.new(secret.encode(), f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
    want = "sume-v1=" + digest
    ok = False
    for entry in header.split(","):
        if hmac.compare_digest(entry.strip(), want):
            ok = True
    return ok

Redeliver and poll

Return any 2xx after you durably store the event. Ten refused attempts leave a failed delivery, while the job still reaches its real terminal state. POST /v1/jobs/{job_id}/webhook/redeliver re-posts that job's real terminal event with a fresh timestamp and signature, and it does not use up one of the automatic ten. Keep status_url polling as a fallback for events that never arrive.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume