Verify a Sume agent.run.terminal webhook in Python

Python HMAC-SHA256 check for a Sume run webhook: sume-v1 signature over timestamp.raw_body, a five-minute window, and a verifier that refuses an empty secret.

5 min readSume
All posts

To verify a Sume run webhook in Python, compute HMAC-SHA256 over <timestamp>.<raw_body> with your signing secret, prefix the hex digest with sume-v1=, and compare it in constant time to the x-sume-webhook-signature header. Refuse an empty secret, and reject timestamps outside a five-minute window.

What Sume sends

The run webhook docs list the headers: x-sume-webhook-timestamp and x-sume-webhook-signature: sume-v1=<hex_signature>. An Agent Completion delivers the event agent.run.terminal with object agent.run. The check runs against the raw body, before any JSON parse or re-serialize.

The verifier

This function mirrors the scheme in the docs. It returns False for an empty secret, so a missing environment variable fails closed instead of accepting everything. Read the secret from SUME_COM_WEBHOOK_SIGNING_SECRET.

import hashlib, hmac, os, time

def verify(raw_body: bytes, timestamp: str, signature: str,
           secret: str, tolerance: int = 300) -> bool:
    if not secret:  # fail closed on an unset env var
        return False
    try:
        ts = int(timestamp)
    except ValueError:
        return False
    if abs(int(time.time()) - ts) > tolerance:
        return False
    digest = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body,
                      hashlib.sha256).hexdigest()
    return hmac.compare_digest(f"sume-v1={digest}", signature)

if __name__ == "__main__":
    secret = os.environ.get("SUME_COM_WEBHOOK_SIGNING_SECRET", "")
    body, ts = b'{"event":"agent.run.terminal"}', str(int(time.time()))
    sig = "sume-v1=" + hmac.new(b"test", f"{ts}.".encode() + body,
                                hashlib.sha256).hexdigest()
    print(verify(body, ts, sig, "test"), verify(body, ts, sig, secret))

Why the empty-secret guard matters

A signature made with an empty key is a valid HMAC. If your process starts without the variable and your code does hmac.new(b'', ...), an attacker who knows that can sign forged events. The guard turns that misconfiguration into a rejected request. The last line of the sample prints True for the test secret and False when the environment variable is unset.

After the check

The points that matter here, in the order you will hit them:

  • Record the event durably, then return a 2xx quickly. Sume times out each attempt at 10 seconds.
  • Dedupe on the envelope request_id, which equals run_id and is stable across retries.
  • Branch on outcome (ok, degraded, error), not on status alone.
  • Get the secret from the Webhooks tab of the dashboard or GET /v1/webhooks/signing-secret.

Using the SDK instead

If your receiver is TypeScript, `verifyWebhook` in `@sume-com/sdk` does this check for you. The Python function above exists for receivers in other languages. Both use the same scheme, as the run-webhook page notes.

Testing the receiver

Use the Send test control on the Webhooks page of the dashboard, or POST /v1/webhooks/test-deliveries, which needs account:write. It sends a dummy webhook.test payload. It is not a replay of a real run, so it proves your signature check and your secret, not your run handling. If a signature fails, compare the x-sume-webhook-secret-fingerprint header with the fingerprint shown next to the secret in the dashboard. The fingerprint is the only part of this data that is safe to paste into a ticket. Never log the secret itself, and rotate it if it might have leaked.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume