Sume webhook payload is null: receipt over 1 MiB, fetch result_url
A Sume format.run.terminal webhook with payload null means the receipt was over 1 MiB. Read error.result_url and fetch the receipt instead of failing the run.

A webhook whose payload is null is not a failed run. Sume sets payload to null only when the run receipt was more than 1 MiB, and in that case error.code is payload_too_large and error.result_url gives the address to fetch the full receipt. Your handler should fetch that URL rather than treat the empty payload as missing data (Sume docs: Format runs, read 2026-10-06).
This tends to happen when a run produces a big structured output or many artifacts, for example a season-length manifest.
Read the outcome first
The event carries an outcome of ok, degraded or error and an error object that is null on ok. A null payload together with outcome ok and an error code of payload_too_large is the large-receipt case. A null payload with a different error code is a real problem, so branch on the code, not on the null.
The payload, when present, is byte-identical to the data returned by GET /v1/format-runs/{run_id}. That is deliberate: one handler can serve both the webhook and a poll, because they return the same object.
A handler that copes with both
Verify the signature first. The webhook is signed with sume-v1 HMAC-SHA256 over the timestamp, a dot and the raw body, inside a five-minute window, and the verifier must refuse an empty secret. Then choose between the inline payload and a fetch.
import os, requests
API_KEY = os.environ["SUME_API_KEY"]
def receipt_from_event(event):
if event.get("payload") is not None:
return event["payload"]
err = event.get("error") or {}
if err.get("code") != "payload_too_large":
raise RuntimeError("no payload: " + str(err.get("code")))
r = requests.get(err["result_url"], timeout=60,
headers={"Authorization": "Bearer " + API_KEY})
r.raise_for_status()
return r.json()["data"]
event = {"payload": None,
"error": {"code": "payload_too_large",
"result_url": "https://api.sume.com/v1/format-runs/RUN_ID/result"}}
print("would fetch:", event["error"]["result_url"])Keep deliveries idempotent
Sume retries a failed delivery up to 10 times, starting at 30 seconds and doubling, capped at one hour. Dedupe on request_id and order by created_at. If your fetch of result_url fails, return a non-2xx and let the retry come back, or schedule your own fetch later. A run also keeps a redeliver endpoint you can POST to once you have fixed your receiver.
Do not log the fetched receipt wholesale. It can hold signed media URLs. Log the run id and the outcome.
| payload | error.code | What to do |
|---|---|---|
| Object | null | Use the payload |
| null | payload_too_large | Fetch error.result_url |
| null | Anything else | Treat as a real error |
Prevent it
Keep receipts small. Return URLs to files instead of file contents, bound arrays with maxItems, and avoid asking the run to paste large text into the output. A receipt under 1 MiB arrives inline, which makes your handler simpler.
Test the large-receipt path
You will rarely see a payload_too_large event in normal work, which is exactly why the branch breaks first when it finally happens. Write a test that feeds your handler an event with payload null and the error code set, and confirm it fetches the result and processes it the same as an inline one.
Also test the failure of the fetch itself. If the request to result_url times out, your handler should return a non-2xx so Sume retries the delivery, rather than swallowing the event and leaving the run unrecorded. The retry schedule gives you ten attempts, which is plenty of time to recover from a short outage.
Sources
Related posts
More in Formats
- Same Idempotency-Key on two Formats starts two runs: holiday A/B
A Sume Idempotency-Key is scoped to one Format. Reusing a key across two holiday Formats starts two runs, so make the key carry the variant.
- Which JSON Schema keywords a Sume Format output_schema accepts
Sume Format schemas use an allowlist: unknown keywords fail with unsupported_keyword. Number bounds are allowed; oneOf and allOf are not. Table inside.
- Which video model does a Sume Format use? Not the model field
The model field on a Sume Format run picks the orchestrating LLM, default gpt-6-sol. The Format's tools choose the image, video and audio models.
- Why did my Format run do that? Read the first message of its thread
A Format run's first thread message is the text the agent received: Format pointer, your instruction, unattended note and input file path. Read it first.
Written by Sume