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.

3 min readSume
All posts

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.

Usage row status values (OpenAPI GET /v1/usage, read 2026-10-10)
statusMeaningTypical moment
reservedEstimate is held against the balance.Job accepted, queued or processing.
capturedReserved amount was charged.Job completed.
refundedReserved 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

All Developers posts

Written by Sume