CloudEvents envelope for a Sume run webhook: field mapping
Map Sume's agent.run.terminal webhook to CloudEvents 1.0: request_id to id, event to type, created_at to time, with a runnable Python wrapper.

Sume's run webhook can be wrapped as a CloudEvents 1.0 event with four required attributes and one optional one. Use request_id for id, the event string for type, a URI you choose for source, and 1.0 for specversion. Add time from created_at. Do the wrapping after you verify the signature, never before.
CloudEvents describes itself as a specification for describing event data in a common way, so that consumers do not need unique handling logic for each source. That is the reason to bother: if your bus already routes by type, Sume's three terminal events slot in beside events from other systems with no custom branch, and the same consumer can handle Action, Format and Agent Completion runs.
The mapping
The spec requires time, when present, to follow RFC 3339. Sume's created_at is an ISO 8601 UTC timestamp with a trailing Z, as in the documented example, which is a valid RFC 3339 form. Check your own receiver's parser before assuming that.
| CloudEvents attribute | Required | Take it from |
|---|---|---|
id | Yes | request_id, which equals the run id and is stable across retries |
source | Yes | A URI-reference you pick, such as https://api.sume.com/v1/agent-runs |
specversion | Yes | The fixed string 1.0 |
type | Yes | event: agent.run.terminal, format.run.terminal or action.run.terminal |
time | No | created_at, the time Sume built the delivery body |
data | No | The whole webhook body, or payload for the receipt only |
A wrapper that runs
This reads a webhook body, checks it has the fields, and prints a CloudEvents-shaped object. Run it after the HMAC check.
import json
def to_cloudevent(body: dict) -> dict:
for key in ("event", "request_id", "created_at"):
if not body.get(key):
raise ValueError("missing " + key)
return {
"specversion": "1.0",
"id": body["request_id"],
"source": "https://api.sume.com/v1/agent-runs",
"type": body["event"],
"time": body["created_at"],
"datacontenttype": "application/json",
"data": body,
}
sample = {"event": "agent.run.terminal", "request_id": "agrun_demo",
"created_at": "2026-10-06T09:00:00.000Z", "outcome": "ok"}
print(json.dumps(to_cloudevent(sample), indent=2))Two traps
Retries reuse the id. Sume makes up to ten attempts and request_id is the same each time, which is what you want for dedupe but means your bus will see repeated ids from one source unless you drop duplicates. Treat the first accepted id as the event and ignore the rest.
Do not put the signing secret or the x-sume-webhook-secret-fingerprint anywhere in data that downstream consumers can read. The fingerprint is for comparing secrets, not for forwarding.
If you forward events onward, keep the original signature check at the first hop. The HMAC covers <timestamp>.<raw_body>, so any re-serialisation of the JSON before verification will fail the check. Verify the raw bytes, then parse.
What stays Sume's
Branch on outcome (ok, degraded, error) inside the data, because a completed run can still have no structured output. Payloads over 1 MiB arrive with payload: null and a payload_too_large code, with result_url to fetch the receipt, so a consumer should be ready to follow that URL. Canceled and skipped runs send no webhook, so no event will appear for them.
Sources
More in Developers
- Cloudflare Quick Tunnel --allowed-mail: do Sume webhooks get through?
Cloudflare's --allowed-mail gate asks visitors for an email PIN, which a Sume webhook POST cannot answer. Test with an open tunnel plus a signature check.
- AGENTS.md rules for a coding agent that calls Sume over hosted MCP
Six AGENTS.md lines that stop a coding agent double-billing Sume video jobs: idempotency_key, jobs_wait slices, dry_run, plus a CI lint script.
- curl and jq script to test a new Sume image model id in one command
A 6-line shell script that posts one prompt to Sume POST /v1/images for any model id and prints the url, cost and status, to vet a gpt-image-1 replacement fast.
- Cursor mcp.json for the hosted Sume server: env interpolation or OAuth
Cursor reads .cursor/mcp.json with url and headers and supports ${env:NAME}. Pass the Sume key from the environment, or omit headers and use OAuth. Validated.
Written by Sume