Push or poll for a finished render: listen, webhook or jobs_wait

MCP 2026-07-28 adds subscriptions/listen. For a render that takes minutes, compare a listen stream, a signed webhook and jobs_wait, with a Python verifier.

5 min readSume
All posts

Use a signed webhook when your server owns the workflow, jobs_wait slices when an agent is waiting in a conversation, and treat the new MCP listen stream as a notification channel, not as the record of a job. MCP 2026-07-28 replaces the HTTP GET endpoint and resources/subscribe with subscriptions/listen: one long-lived POST response stream for opted-in listChanged and resource-subscription notifications, per the changelog read 2026-10-03.

Three ways to learn a render finished

They differ in who holds a connection and what survives a restart. The Sume figures come from its webhooks and jobs pages.

Finish-notification options (read 2026-10-03)
OptionWho holds the connectionLimits to plan around
MCP listen streamClient, one long-lived streamNotifications only reach a connected client
Sume webhookSume calls your HTTPS endpointTerminal events only; up to 10 attempts, 30 s apart, 10 s timeout
Sume jobs_wait (MCP)Client, one call at a timeAt most 55 s per call; 1 to 20 job ids

Why the listen stream is not the record

A subscription tells a connected client that something changed. It cannot tell a client that was offline. For a paid render that may finish in minutes, the durable record is the job itself: Sume's docs say delivery is an optimization and never the only recovery path, and they recommend keeping status polling in place for events that never arrive.

Sume sends terminal events only (job.completed, job.failed, job.canceled), with no progress deliveries. If you want progress, poll the status.

Verify the webhook

Sume signs <timestamp>.<raw_body> with HMAC SHA 256 and sends x-sume-webhook-timestamp and x-sume-webhook-signature: sume-v1=<hex>. During a secret rotation the header carries one entry per live secret, so accept any match. Reject timestamps outside your tolerance window; the docs suggest five minutes. The verifier below refuses an empty secret and compares in constant time.

import hashlib, hmac, time


def verify(raw_body: bytes, timestamp: str, header: str, secret: str, tolerance: int = 300) -> bool:
    if not secret:
        return False
    try:
        ts = int(timestamp)
    except ValueError:
        return False
    if abs(int(time.time()) - ts) > tolerance:
        return False
    signed = f"{ts}.".encode() + raw_body
    expected = "sume-v1=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    ok = False
    for entry in header.split(","):
        if hmac.compare_digest(entry.strip().encode(), expected.encode()):
            ok = True
    return ok

A practical split

Use the webhook for the system of record: store the event, return a 2xx, and key your handling on job_id. Use jobs_wait for the agent, and when it returns wait_slice_expired, call it again with the same ids rather than resubmitting. If your client supports the listen stream, use it only to refresh a UI; do not let a missed notification decide whether a render is lost.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume