Export every Sume job to CSV with next_cursor pagination in Python
Loop GET /v1/jobs?limit=100 and pass next_cursor back as starting_after until it is absent, then write id, status and captured cost to CSV with csv.DictWriter.

Call GET /v1/jobs?limit=100, write the rows in data.jobs, and if data.next_cursor exists pass it back as starting_after. Stop when it is absent. The schema calls the missing cursor the loop terminator and warns not to build one from the last job.
Columns
Each job carries id, type, status, model, created_at and a nullable usage_summary. The summary has amounts in USD micros and cents plus a final flag, so a job whose final is false still has money held.
| Column | Job field |
|---|---|
id | id |
type | type |
status | status |
model | model (may be null) |
created_at | created_at |
captured_usd_micros | usage_summary.captured_amount_usd_micros |
final | usage_summary.final |
Script
A usage summary is null when no ledger row exists, so the code writes empty cells for it. Micros are integers: 1,000,000 micros equals 1 US dollar.
import asyncio, csv, json, os, urllib.parse, urllib.request
FIELDS = ["id", "type", "status", "model", "created_at", "captured_usd_micros", "final"]
def fetch(cursor):
q = {"limit": "100"}
if cursor: q["starting_after"] = cursor
req = urllib.request.Request("https://api.sume.com/v1/jobs?" + urllib.parse.urlencode(q),
headers={"x-api-key": os.environ["SUME_API_KEY"]})
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)["data"]
def export(path):
n, cursor = 0, None
with open(path, "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=FIELDS)
w.writeheader()
while True:
page = fetch(cursor)
for j in page["jobs"]:
u = j.get("usage_summary") or {}
w.writerow({"id": j["id"], "type": j["type"], "status": j["status"], "model": j.get("model") or "",
"created_at": j["created_at"], "captured_usd_micros": u.get("captured_amount_usd_micros", ""),
"final": u.get("final", "")})
n += 1
cursor = page.get("next_cursor")
if not cursor: return n
async def main():
print("rows:", await asyncio.to_thread(export, "sume-jobs.csv"))
asyncio.run(main())Scope
An API key reads only the jobs its own member created in its workspace, so the export is per key. Use the status, type or thread_id filters to narrow it. Stored usage_summary amounts are the public billing view; see reserved, captured, refunded and final.
Failures
If a request fails halfway, rerun from the start; reading is free of side effects. For very large exports keep the file open and flush per page so a crash leaves usable rows.
Sources
Related posts
More in Developers
- Sume MCP 256 KiB output budget: oversize error is not a failed job
Sume's MCP tool results are capped at 256 KiB serialized. An oversize result returns an explicit error instead of a cut-off value, and the job may be fine.
- Sume MCP wait_busy 429: a retryable isError, paid creates queue 20 s
A busy Sume MCP server returns an isError result with http_status 429, retryable. The paid-create limits behind it and how to back off without double billing.
- Sume MCP token lasts 3600 s with no refresh grant: plan for it
Sume's hosted MCP OAuth issues one-hour access tokens and only the authorization code grant. What a long agent run should do at minute 61.
- Sume MCP rate limit: one write per run created, none per status poll
Over Sume's hosted MCP, a tool call spends one write for the run it creates, and a jobs_status poll spends none. The numbers per plan and a Python budget check.
Written by Sume