Sume /v1/usage summary.final is false: a hold is open, not spent
Read GET /v1/usage?job_id= and book cost only when summary.final is true. held_usd_micros and refunded_usd_micros are not spend. Code to poll it.

Book a Sume job's cost only when summary.final is true. GET /v1/usage with a job_id, run_id or thread_id adds a summary object, and final becomes true when no hold is open. While it is false, part of the money is still a reservation. debited_usd_micros is the amount the wallet really deducted. held_usd_micros and refunded_usd_micros are explicitly not spend.
A cost dashboard that sums every row of the ledger, or reads the figure the moment a job completes, can show a number that later moves. The summary is the supported fold, and the docs say never to sum the rows yourself. Two systems that disagree about one job's cost usually disagree about whether a hold counted, so make that rule explicit in your own reporting: spend is debited_usd_micros, and nothing else in the response is.
The summary fields that matter for billing
Add thread_id, run_id or job_id to the request and the response gives the cost of one thread, one run or one generation job. The limit parameter only caps the listed rows. The summary still folds over every row the scope caused.
| Field | Meaning | Count it as spend? |
|---|---|---|
debited_usd_micros, debited_usd | What the wallet deducted: captured rows of every operation type, including the agent's own turns | Yes. This is the figure to quote |
held_usd_micros | Holds that are still open, parked pending_* rows included | No |
refunded_usd_micros | Holds Sume gave back after a failure, a cancellation or queue_full | No |
final | true when no hold is open | Gate for booking |
Ledger row statuses
Usage rows move through three statuses. reserved means Sume reserved the estimated usage before provider execution. captured means Sume captured the billable usage after a successful completion. refunded means Sume released the reservation after a failure or a cancellation before capture. A refunded row keeps its hold amount in billable_amount_usd_micros, so that field is not a cost either.
Rows carry thread_id, run_id, turn_job_id, script_run_id, script_run_call_index and settle_state. job_id also accepts the id of a turn. Then the sum includes the turn's own row and every job that turn commissioned.
Wait for final, then record
The sample polls the summary every two seconds, up to twenty tries, and returns debited_usd_micros only when final is true. If a hold is still open at the end it throws instead of guessing. It reads the key from the environment and uses the x-api-key header only.
const base = process.env.SUME_API_BASE_URL ?? "https://api.sume.com/v1";
const key = process.env.SUME_API_KEY;
if (!key) throw new Error("SUME_API_KEY is not set");
async function settledCost(jobId) {
for (let i = 0; i < 20; i++) {
const res = await fetch(`${base}/usage?job_id=${jobId}&limit=50`, {
headers: { "x-api-key": key },
});
if (!res.ok) throw new Error(`usage ${res.status}`);
const { summary } = await res.json();
if (summary.final) return summary.debited_usd_micros;
await new Promise((r) => setTimeout(r, 2000));
}
throw new Error("a hold is still open; do not book this as spend yet");
}
const micros = await settledCost(process.argv[2] ?? "job_123");
console.log(`debited: $${(micros / 1e6).toFixed(6)}`);What to store
- Store
debited_usd_microsas integer micros, and convert to dollars only for display. The field names end in_usd_microsfor that reason. - Store the
job_idorrun_idnext to the figure, so a later correction can re-read the same scope. - Do not treat
usage.billable_amount_usd_microson a Format run receipt as cost. It counts reserved plus captured generation rows against the run's spend cap and leaves out the agent's own model turn. - If
finalnever turnstrue, keep the row as pending and look at the job. A job that is stillqueuedorprocessingcan legitimately keep a hold open.
The ledger reference is on the Usage dashboard page. Pair it with the job status from Jobs and results when a hold stays open.
Sources
Related posts
More in Developers
- Sume webhooks: 10 attempts 30 seconds apart for a video receiver
A Sume job webhook is tried up to 10 times, 30 seconds apart by default, with a 10 s timeout each. What that means for a video receiver, plus a Python verifier.
- Can a Sume webhook arrive twice? Build an idempotent receiver
Sume retries failed webhook deliveries up to 10 times and Redeliver replays a real event, so one terminal event can reach you twice. Dedupe on job_id or run_id.
- Sweep Format runs for failed webhook deliveries, then redeliver
Sweep in Python: read each run's webhook_delivery, redeliver only failed or exhausted ones, and leave the rest alone. Uses formats:read, formats:write.
- 10 hooks by 10 endings: a 100-variant grid in one Sume bulk queue
A 10 by 10 hook and ending grid is exactly 100 items, the bulk queue maximum. How to build the items array, pick concurrency up to 16, and read the result.
Written by Sume