List Sume jobs: next_cursor, starting_after, and idempotency_key

Page through GET /v1/jobs with limit, next_cursor and starting_after, then recover a lost wave by joining your own key to each job's idempotency_key.

5 min readSume
All posts

GET /v1/jobs lists jobs newest first, at most 100 per page. When the response has data.next_cursor, pass it back as starting_after to read the next page; the last page has no next_cursor, and that absence is the loop terminator. To recover a lost wave, join on idempotency_key: each job row carries the key it was created with, and array position is not your submission order.

Everything here is from the listApiJobs operation in the API reference OpenAPI file, plus the idempotency rules on Jobs and results, read 2026-10-02.

Which query parameters does GET /v1/jobs take?

Seven parameters, all optional. An unrecognized parameter is a 400 unknown_parameter rather than a silently unfiltered page, so a typo cannot make a filter disappear.

Query parameters of GET /v1/jobs, from the Sume OpenAPI listApiJobs operation, read 2026-10-02.
ParameterValuesUse
limit1 to 100Page size; pages are capped at 100.
statusqueued, processing, completed, failed, canceledNarrow to one job status.
typestringNarrow to one job type.
starting_afteropaque cursorPass back the previous data.next_cursor.
scopethread or workspaceAn ordinary API key defaults to workspace.
run_idstringNarrow to one automation run.
thread_idstringOnly ever your own thread; never widens a bound credential.

How do I loop over every page?

Do not build a cursor from the final job; the schema says not to synthesize one. Pass next_cursor back unchanged and stop when it is absent. This generator also builds the key-to-job map used in the next section:

import os
import httpx

def all_jobs(status="processing"):
    params = {"limit": 100, "status": status}
    headers = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}
    with httpx.Client(base_url="https://api.sume.com", headers=headers) as c:
        while True:
            r = c.get("/v1/jobs", params=params)
            r.raise_for_status()
            data = r.json()["data"]
            yield from data["jobs"]
            if "next_cursor" not in data:  # absent on the last page
                return
            params["starting_after"] = data["next_cursor"]

by_key = {j["idempotency_key"]: j["id"] for j in all_jobs() if j["idempotency_key"]}
print(by_key)

Why join on idempotency_key instead of position?

Newest-first is not the order you submitted in once a wave retries. The schema describes idempotency_key on each job as the join column for recovery: it is the Idempotency-Key the job was created with, or null when none was sent. If your process died mid-wave, map each of your own business keys to a job id, resubmit only the keys with no job, and reuse the same key on any retry so it returns the original job instead of billing a second one.

Reuse a key only for the same operation and payload. The same key on a different payload returns 409 idempotency_conflict.

What can I not see in this list?

The list is a status view, with limits worth knowing before you build on it.

  • Per-job queue position or ETA; Sume exposes queue counts, not ranks.
  • Raw provider ids or URLs; public job records carry Sume-owned fields only.
  • Jobs from other workspaces: a key sees its own workspace.
  • Jobs created without an Idempotency-Key: their idempotency_key is null, so you cannot join on them.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume