Sweep Format runs for failed webhook deliveries, then redeliver

Sweep in Python: read each run's webhook_delivery, redeliver only failed or exhausted ones, and leave the rest alone. Uses formats:read, formats:write.

5 min readSume
All posts

If your webhook receiver was down, Sume tried each terminal delivery ten times and then stopped, and the run stayed completed or failed. To catch up, read each run, look at webhook_delivery.status, and call POST /v1/format-runs/{run_id}/webhook/redeliver only for runs whose status is failed or exhausted. Redeliver re-sends the current receipt with a new timestamp and signature, and it does not use up one of the ten automatic attempts.

The script takes run ids from a file, one per line. You have them from your own records or from a bulk queue's items[].run_id.

The sweep

Set SUME_API_KEY, which needs formats:read for the reads and formats:write for redeliver. The reads run in a thread pool through asyncio.to_thread, so the reads overlap instead of running one after another.

import asyncio, json, os, urllib.error, urllib.request

BASE = os.environ.get("SUME_BASE", "https://api.sume.com/v1")
HEAD = {"Authorization": "Bearer " + os.environ["SUME_API_KEY"], "Content-Type": "application/json"}

def call(method, path):
    req = urllib.request.Request(BASE + path, method=method, headers=HEAD,
                                 data=b"{}" if method == "POST" else None)
    try:
        with urllib.request.urlopen(req, timeout=30) as r:
            return r.status, json.load(r)
    except urllib.error.HTTPError as e:
        return e.code, json.load(e)

def sweep(run_id):
    code, body = call("GET", f"/format-runs/{run_id}")
    if code != 200:
        return run_id, f"read {code}"
    state = (body["data"].get("webhook_delivery") or {}).get("status")
    if state not in ("failed", "exhausted"):
        return run_id, f"left alone ({state})"
    code, body = call("POST", f"/format-runs/{run_id}/webhook/redeliver")
    return run_id, "redelivered" if code < 300 else body["error"]["code"]

async def main():
    ids = [line.strip() for line in open("run_ids.txt") if line.strip()]
    for out in await asyncio.gather(*(asyncio.to_thread(sweep, i) for i in ids)):
        print(*out)

asyncio.run(main())

Why it only touches two states

not_armed, pending and retrying mean Sume is still working, and a manual redeliver on top of them only adds a duplicate. delivered means your endpoint answered with a 2xx. Only failed and exhausted mean Sume gave up, and last_status_code and last_error on the same block tell you why. Fix the receiver before you sweep: a redeliver to a broken endpoint fails the same way.

A run created without a webhook_url has no webhook_delivery block, so the script reports it as left alone. If you called redeliver on one anyway, the API would answer 409 webhook_not_configured. A run that is still in progress gives 409 run_not_terminal. Both come back as error.code and the script prints them.

Delivery status and what the sweep does (read 2026-10-07)
`webhook_delivery.status`MeaningSweep action
not_armedURL stored, nothing scheduled, run in progressLeave alone
pending, retryingArmed; next_attempt_at is setLeave alone
deliveredYour endpoint answered 2xxLeave alone
failed, exhaustedSume stopped after a refusal, a 10 s timeout, a redirect, or a failed URL checkRedeliver

Make the receiver safe to hit twice

A redelivered body is the current receipt, byte-identical to the data of GET /v1/format-runs/{run_id}, but with a fresh signature. Verify the sume-v1 signature against the raw body, and dedupe on the run id so a run you already stored is not stored again. A handler written this way is safe to sweep with whenever you like.

The sweep does not cancel anything and spends nothing: a webhook delivery never changes the run, and a redeliver is not a new run.

Where do the run ids come from? For a bulk queue, read items[].run_id from GET /v1/format-run-queues/{id}: queued items and children that never started have a null run id and nothing to redeliver. Skip those rows before writing run_ids.txt. A queue itself has no webhook, so a run webhook is something each item sets in its own body through communication.webhook_url.

Run the sweep after an outage or a deploy, not on a tight loop. The reads are cheap: they count against the read budget, which is forty times the write budget, while each redeliver is one write.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume