Read /v1/balance: state, micros and the expiring-soon amount

What GET /v1/balance returns, why an empty state means a 402 is coming, and a Node check that compares available micros to a job's cost before you submit.

4 min readSume
All posts

Call GET /v1/balance, read data.balance.state, and compare data.balance.available_amount_usd_micros with the cost of the job you are about to send. If the state is empty, or the micros are lower than the cost, do not submit: the create call would answer 402 insufficient_credits. The balance is a USD figure for the workspace of the API key, and a workspace with no balance row yet reads as an explicit zero, not as an error.

The fields on data.balance

The OpenAPI schema lists seven top-level fields plus an expiration summary. Use the micros field for math, because it is an integer and never rounds.

data.balance fields, from the OpenAPI spec read 2026-10-08
FieldTypeMeaning
currencystringAlways USD
available_amount_usd_microsintegerSpendable balance in millionths of a dollar
available_amount_usd_centsintegerThe same balance in cents
available_creditsintegerLegacy rounded cent amount kept for compatibility
statefunded or emptyempty means no spendable balance, or no balance row yet
updated_atdate-time or nullWhen the balance last changed
expirationobjectSummary of spendable credit lots that expire

The expiration summary

The expiration object covers credits that are spendable right now. Amounts exclude expired, reserved, captured and depleted credits. next_expires_at is the earliest expiry, or null when nothing is spendable. next_expiring_amount_usd_micros says how much expires at that time. expiring_soon_days is the window, and expiring_soon_amount_usd_micros is the amount that falls inside it.

A Node preflight

Save this as preflight.mjs so top-level await works. It sends one credential header, x-api-key, because sending both Authorization and x-api-key returns 401. The cost is passed in micros: an 8-second Gemini Omni Flash 1.1 clip at 720p is 8 x $0.125 = $1.00, which is 1,000,000 micros.

const need = Number(process.argv[2] ?? 1_000_000); // micros
const res = await fetch("https://api.sume.com/v1/balance", {
  headers: { "x-api-key": process.env.SUME_API_KEY ?? "" },
});
if (!res.ok) {
  console.error("balance read failed", res.status);
  process.exit(2);
}
const { balance } = (await res.json()).data;
const usd = (m) => (m / 1_000_000).toFixed(2);
const exp = balance.expiration;
if (balance.state === "empty" || balance.available_amount_usd_micros < need) {
  console.error(`short: have $${usd(balance.available_amount_usd_micros)}, need $${usd(need)}`);
  process.exit(1);
}
if (exp.expiring_soon_amount_usd_micros > 0) {
  console.log(`$${usd(exp.expiring_soon_amount_usd_micros)} expires within ${exp.expiring_soon_days} days`);
}
console.log(`ok: $${usd(balance.available_amount_usd_micros)} available`);

What the check does not guarantee

Treat the preflight as a cheap way to stop a batch before the first request, and keep the 402 handler as the real guard.

For a batch, multiply: 20 clips of 8 seconds at 720p need 20 x 1,000,000 = 20,000,000 micros, which is $20.00. Compare that sum once, then submit in waves that respect your plan's queue capacity.

  • It is a read, not a hold. Another job can spend the balance between your check and your submit, so still handle 402 on the create call.
  • Sume reserves the cost at submit, so a queued job already lowers the balance you read.
  • Exit code 1 means short balance and 2 means the read failed, so a CI step can tell them apart.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume