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.

4 min readSume
All posts

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.

Sume run webhook fields to CloudEvents 1.0 attributes (read 2026-10-06)
CloudEvents attributeRequiredTake it from
idYesrequest_id, which equals the run id and is stable across retries
sourceYesA URI-reference you pick, such as https://api.sume.com/v1/agent-runs
specversionYesThe fixed string 1.0
typeYesevent: agent.run.terminal, format.run.terminal or action.run.terminal
timeNocreated_at, the time Sume built the delivery body
dataNoThe 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

All Developers posts

Written by Sume