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.

4 min readSume
All posts

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.

Webhook payload cases, read 2026-10-06 against Sume docs
payloaderror.codeWhat to do
ObjectnullUse the payload
nullpayload_too_largeFetch error.result_url
nullAnything elseTreat 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

All Formats posts

Written by Sume