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 SDK: try/catch misses a 402, generated calls return { error }
Generated @sume-com/sdk operations return { data, error, response } instead of throwing, so a 402 or 429 slips past try/catch. Check error on every call.
- Sume SDK error.retryable: server flag first, status only as fallback
SumeApiError.retryable uses the error envelope's retryable flag when present and falls back to 408, 429 or 5xx otherwise. A 409 is never retried by status.
- Sume STT language_code: set a hint or omit it for auto-detect?
language_code is optional on Sume STT. Omit it to auto-detect, pass a BCP-47 hint like en or ko when you know the language. A quick way to choose.
- Sume STT takes 10 minutes per request: a 90-minute file is 9 jobs
Sume speech-to-text caps a request at 600 seconds of audio, so a 90-minute recording is nine requests at up to $0.10 each, $0.90 in total.
Written by Sume