Verify a canceled Sume job was refunded: read the usage ledger by job
After you cancel a queued job, check GET /v1/usage with job_id and read the row status: reserved, captured or refunded. A Python script prints the answer.

Call GET https://api.sume.com/v1/usage?job_id=<job id> and read the status of the row: reserved means the estimate is still held, captured means it was charged, and refunded means it was released. A job canceled before generation starts should end as refunded, and the script below checks that in one call.
This is how the docs describe the money path: Sume reserves the estimated amount when it accepts a request, a successful completion captures it, and failed jobs and failed queue admission release or refund it where applicable (read 2026-10-10, Generation admission).
What are the three ledger states?
Each usage row has a status from a closed set of three. The OpenAPI schema (read 2026-10-10) types it as an enum, so you can switch on it without a default branch that hides surprises.
| status | Meaning | Typical moment |
|---|---|---|
| reserved | Estimate is held against the balance. | Job accepted, queued or processing. |
| captured | Reserved amount was charged. | Job completed. |
| refunded | Reserved amount was released. | Job canceled before it started, failed, or failed queue admission. |
The script
It takes a job id on the command line, filters the ledger with job_id, and prints one line per row with the amount in dollars. billable_amount_usd_micros is an integer, so dividing by 1,000,000 gives dollars. Ledger rows expose Sume job and request ids and public billing amounts only; provider internals and idempotency keys are omitted.
import json, os, sys, urllib.parse, urllib.request
def get(path, **q):
url = "https://api.sume.com" + path + "?" + urllib.parse.urlencode(q)
req = urllib.request.Request(url, headers={"x-api-key": os.environ["SUME_API_KEY"],
"User-Agent": "sume-example/1.0"})
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)["data"]
job_id = sys.argv[1]
rows = get("/v1/usage", job_id=job_id, limit=20)["usage"]
for row in rows:
usd = row["billable_amount_usd_micros"] / 1_000_000
print(row["operation_type"], row["status"], "$%.4f" % usd)
if not rows:
sys.exit("no ledger row for this job id in this workspace")
if all(r["status"] == "refunded" for r in rows):
print("fully refunded")Why cancel first, then verify
Cancellation only works before generation starts. After that the API returns 409 job_generation_already_started with details.cancelable: false, and the job finishes normally and is billed like any other. Canceling a job that is already canceled is idempotent and returns the same canceled job, so running your cancel step twice is safe.
Check cancelable on the status envelope before you call cancel, then run the ledger check a moment later. If the row is still reserved right after a cancel, read the job again: the terminal status and the ledger are separate records and can be read a moment apart.
- A zero-row answer means the job id is not in this workspace, or not visible to this key.
- Do not sum
credit_amount; it is a legacy rounded cent field kept for compatibility. - Use the ledger for reconciliation, and the job record for outcome.
Reading the ledger rows
The usage route labels each row reserved, captured or refunded. A reserved row is credit held for a job that has not finished, a captured row is a charge for finished work, and a refunded row is credit returned. For a job you canceled, filter by job_id and expect the row to end as refunded.
If a canceled job still shows reserved a moment later, read the job itself first: GET /v1/jobs/{id} should report canceled before you judge the ledger. Check the balance afterward only as a sanity check, since other jobs move it too.
Sources
Related posts
More in Developers
- A video model row missing from Sume's list: why, and how to check
Veo and Genjutsu list in the Sume catalog only where their provider route is configured. How listing works, plus a Python check that fails on a missing id.
- Video-trim says unsupported_media_source: which Sume routes take URLs
Trim, filter and compose need a media.sume.com clip; upscale, STT, RMBG and captions take public HTTPS URLs. Imports take TikTok and Instagram. Read 2026-10-10.
- What to log from a Sume API error: request_id, code, no secrets
Log the status, error.code, error.request_id, retry-after and the path without its query. Keep keys, signed URLs and media URLs out. A 25-line Python logger.
- Which Sume timeout is which: sync, jobs_wait, waitForJob, webhooks
Sume's waits differ: 30 s sync cap, 50 s jobs_wait, a 20-minute SDK default in ms, 10 s per webhook attempt. A table of each unit and what expiry does.
Written by Sume