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.

5 min readSume
All posts

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.

CSV columns and where they come from (read 2026-10-04)
ColumnJob field
idid
typetype
statusstatus
modelmodel (may be null)
created_atcreated_at
captured_usd_microsusage_summary.captured_amount_usd_micros
finalusage_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

All Developers posts

Written by Sume