List every Format run: no GET /v1/format-runs, page per Format
GET /v1/format-runs does not exist. List runs per Format with limit, next_cursor and has_more, or keep your own index of the data.id you stored at create.

You cannot list every Format run in one call, because there is no GET /v1/format-runs. The docs say so directly: list the runs of each Format, or keep your own index keyed by the data.id you stored when you created the run. The list route is GET /v1/formats/{handle}/{slug}/runs, and it pages with limit, next_cursor and has_more.
Developers often hit this the first time they build an admin screen or a reconciliation job and guess a path. This page lists the three guesses and the correct reads.
Routes people guess, and what exists
| Guess | Result | Use instead |
|---|---|---|
GET /v1/format-runs | Does not exist | List per Format, or keep your own index |
GET /v1/formats/{handle}/{slug}/runs/{run_id} | Does not exist | GET /v1/format-runs/{run_id} |
GET /v1/format-runs/{run_id}/messages | Not published | events_url for the phase timeline; output and artifacts[] for the result |
The Format path only creates runs and lists them. Reading one run always goes through /v1/format-runs/{run_id}. If your key's workspace has never started a run of that Format over the API, the list call returns an empty list, not a 404.
How the paging works
The list is newest first. limit is 1 to 100 and defaults to 20. The response is a page: send next_cursor back as cursor until has_more is false. The cursor is opaque and is a keyset over (created_at, id), so runs created while you page do not shift rows between pages. A cursor that Sume did not issue returns 400 invalid_request, so never build one by hand.
The Formats list at GET /v1/formats uses the same has_more and next_cursor fields with a data array, and the sample below reads the runs list the same way.
const key = process.env.SUME_API_KEY;
const base = process.env.SUME_API_BASE_URL ?? "https://api.sume.com/v1";
if (!key) throw new Error("set SUME_API_KEY");
// There is no GET /v1/format-runs. List the runs of one Format, newest first.
export async function* runsOf(formatPath) {
let cursor;
do {
const qs = new URLSearchParams({ limit: "100" });
if (cursor) qs.set("cursor", cursor);
const res = await fetch(`${base}/formats/${formatPath}/runs?${qs}`, {
headers: { "x-api-key": key },
});
if (!res.ok) throw new Error(`list failed: ${res.status}`);
const page = await res.json();
yield* page.data;
cursor = page.has_more ? page.next_cursor : undefined;
} while (cursor);
}
let n = 0;
for await (const run of runsOf(process.argv[2] ?? "acme/product-promo")) n += 1;
console.log("runs listed:", n);Notes on the sample
- Each page is one read, and reads draw on the read budget, which is 40 times the write budget. A full sweep over a Format with many runs is therefore safe next to your creates. Still watch
ratelimit-remaining, and on a429waitretry-afterseconds. - The key needs
formats:read. A key without it gets403 insufficient_scope. - Runs from another workspace's key are invisible. A run you cannot see gives the same
404 format_run_not_foundas one that does not exist. - Pass the Format as
handle/slug, for example the pair in the Format'svanity_invoke_url. The opaque twinGET /v1/formats/{format_id}/runstakes theskl_id instead.
Build the index at create time
Listing is for audits. For day-to-day lookups, store the run id the moment the create call returns. Keep one row per business object: your order id, the Idempotency-Key you sent, the Format, and data.id. The docs recommend this for another reason as well: a value you send in input does not come back in output unless the run repeats it, so identifiers belong on your side, keyed by data.id or by your key.
When a webhook arrives, run_id and request_id are equal and stable across retries, so they join cleanly to that table. See the Runs and results page for the full list of read routes.
Sources
Related posts
More in Developers
- How do I add a listen-to-this-page audio version with TTS?
Turn each article into an audio file with one async TTS job per page: a 9,000-character article costs 43 cents on Sume. What it does not replace.
- Mandarin Chinese speech to text API: Sume STT language_code zh
Transcribe Mandarin audio with Sume STT using language_code zh, then check the result and timings. $0.01 per audio minute and no accuracy claim without a test.
- Measure softening and colour creep across AI edit passes in Pillow
Test the no-drift claim on your own photos: edge sharpness and a white-patch colour reading for each pass of an edit chain, in short Python with Pillow.
- Migrate a real-time avatar prototype to Sume async jobs: what changes
Moving from a live avatar session to Sume means replacing a stream with submit, poll and fetch. The code changes, the UX changes, and a Node example that runs.
Written by Sume