Run webhook payload is null: payload_too_large means fetch result_url
Sume cannot send a receipt over 1 MiB inline. The webhook arrives with payload null and error.code payload_too_large. The run did not fail. Fetch result_url.

A payload: null webhook with error.code of payload_too_large does not mean the run failed. Sume's run webhook docs say a receipt over 1 MiB (1,048,576 bytes) cannot be delivered inline, so the envelope goes out with a null payload and an error that carries a result_url. The top-level status still reports the real outcome, so a succeeded run that was too big to ship is still a success (Run webhooks).
What the envelope looks like
The documented body for the oversized case has status: OK, outcome: ok, payload: null, and an error object with code, message and result_url. The message tells you to fetch the receipt from result_url instead.
This is easy to mishandle. A handler that treats any non-null error as a failure will mark a successful run failed. A handler that dereferences payload without a check will crash on exactly the large runs, which are often the expensive ones.
| Case | status | payload | error |
|---|---|---|---|
| Normal success | OK | Full receipt | null |
| Run failed | ERROR | Full receipt | code and message |
| Oversized receipt | Real outcome, e.g. OK | null | payload_too_large with result_url |
| Canceled run | No webhook | None | None |
A handler that fetches when it must
The function below returns the receipt from either the inline payload or the result_url. The fetch is injected so it can run offline with a stub. In production, make the fetch a GET with your API key; the run is not complete until you hold the receipt.
def receipt_for(event: dict, fetch) -> dict:
if event.get("payload") is not None:
return event["payload"]
err = event.get("error") or {}
if err.get("code") == "payload_too_large" and err.get("result_url"):
return fetch(err["result_url"])
raise ValueError("no payload and no result_url")
if __name__ == "__main__":
big = {"status": "OK", "payload": None,
"error": {"code": "payload_too_large",
"result_url": "https://api.sume.com/v1/x/result"}}
print(receipt_for(big, lambda url: {"fetched": url}))
print(receipt_for({"payload": {"id": "r1"}}, None))Why receipts get large
A receipt carries output, artifacts and usage. A run that makes many clips, or a structured output with long text, can grow past 1 MiB. Nothing about that size means the run misbehaved. It is a delivery limit, not a quality signal.
If you control the run, you can keep receipts small by asking for a tight output_schema and a primary_output_key, so the headline result is easy to find. Even so, always handle the null case.
Where this goes wrong in practice
The failure shows up as a quiet bug. A receiver written against a small test run works for weeks, then a run with many generated files produces a large receipt, the handler throws on a null payload, and the endpoint returns a 500. Sume then retries the same delivery up to 10 times, each one failing the same way, and the delivery ends as exhausted even though the run succeeded.
Return a 2xx once you have stored the event, including the oversized case. Then fetch the receipt from result_url in a separate job, so a slow download never eats the 10-second attempt timeout. Log the payload_too_large code as an informational event, not an error, so your alerts stay meaningful.
Checklist
Add a test for the oversized case before you go live.
- Never assume
payloadis non-null. - Do not treat
erroras failure whenstatusis OK. - Fetch
result_urlwith your API key, then continue as normal. - Still dedupe on the envelope
request_id.
Sources
Related posts
More in Developers
- Missed an agent run webhook? Poll status_url, redeliver is Format-only
Sume's docs describe a webhook redeliver route for Format runs. For an Agent Completion you did not get a POST for, read the run from its status or result URL.
- Build an AI image progress UI: gate on supports_streaming false
Sume's image catalog reports supports_streaming false on every row. Build the progress UI from job states and gate a live preview on that field.
- AI image to CMYK for print: Pillow convert and what it misses
Image.convert('CMYK') makes a print-mode TIFF from a Sume image in two lines, but it is not color-managed. The code, the gamut risk, and when to ask for an ICC.
- AI video API: rate limit or concurrency, which stops a batch first
Concurrency binds long before request rate on Sume. Free allows 120 writes a minute but 1 running job; Pro 300 and 4. How to size a batch against both.
Written by Sume