Format run webhook payload is null: receipt over 1 MiB

A Sume format.run.terminal webhook carries payload null when the receipt is over 1 MiB. Read error.result_url, fetch the receipt, and guard your handler.

5 min readSume
All posts

When a Format run receipt is larger than 1 MiB, Sume still delivers the signed format.run.terminal webhook, but payload is null. In that case error.code is payload_too_large and error.result_url tells you where to fetch the full receipt.

A handler that does body["payload"]["id"] will throw on exactly your largest, most valuable runs. This post shows the envelope, the one branch you need, and a verifier that fetches the receipt when the payload is missing.

What does the envelope look like when payload is null?

The runs page defines the delivery body: event, request_id, run_id, object, status, outcome, created_at, payload and error. Normally payload is byte-identical to data from GET /v1/format-runs/{run_id}, so one handler serves both transports.

payload is null only when the receipt was over 1 MiB. The run did not fail because of that, so do not read the error object as a run failure. The docs do not say how status and outcome behave in this case, so fetch the receipt and read them there; error.code tells you why the payload is missing.

Webhook envelope fields to branch on, read 2026-10-02
FieldNormal deliveryReceipt over 1 MiB
eventformat.run.terminalformat.run.terminal
statusOK or ERRORRead it on the fetched receipt
payloadThe full receiptnull
errornull on OKcode payload_too_large, plus result_url
run_idStable dedupe keyStable dedupe key

How do I fetch the full receipt?

Follow error.result_url. It is the GET /v1/format-runs/{run_id}/result endpoint, which returns the full receipt once the run is terminal, and it needs a key with formats:read. The endpoint answers 409 run_not_completed while a run is still in flight, but a terminal webhook only fires after the run finishes, so you should not see that here.

Verify the signature first, on the raw bytes, then parse. The signature is HMAC-SHA256 over <timestamp>.<raw_body> using your workspace signing secret, the header is x-sume-webhook-signature: sume-v1=<hex>, and timestamps outside a five-minute window should be rejected.

import hashlib, hmac, json, time, urllib.request

def receipt(raw, headers, secret, api_key):
    if not secret:
        raise ValueError("empty signing secret")
    ts = headers["x-sume-webhook-timestamp"]
    if abs(time.time() - int(ts)) > 300:
        raise ValueError("stale timestamp")
    mac = hmac.new(secret.encode(), ts.encode() + b"." + raw, hashlib.sha256)
    given = headers["x-sume-webhook-signature"].removeprefix("sume-v1=")
    if not hmac.compare_digest(mac.hexdigest(), given):
        raise ValueError("bad signature")
    body = json.loads(raw)
    if body["payload"] is not None:
        return body["payload"]
    req = urllib.request.Request(body["error"]["result_url"],
        headers={"Authorization": "Bearer " + api_key})
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.load(r)["data"]

Why does the response need to be fast?

A delivery succeeds on any 2xx within 10 seconds, and Sume retries up to 10 times. Fetching a large receipt inside the request can push you past that limit. The documented advice is to record the event durably, answer, then do the work, so store run_id and result_url and fetch after you have responded.

Dedupe on request_id, which repeats across retries. If a delivery fails, the run is unchanged: ten refused attempts leave you with a failed delivery and a run that is still completed, readable from result_url.

Is a null payload the same as output null?

No, and mixing them up sends you to the wrong fix. A null payload means the envelope was too large to carry the receipt. A null output inside a receipt means nothing satisfied your schema, which the degraded outcome post covers. Check outcome on the receipt for the second case and error.code on the envelope for the first. For choosing between the status, result and receipt URLs, see which URL to poll.

Sources

Related posts

More in Formats

All Formats posts

Written by Sume