Agency cost ledger: what each client's Sume jobs cost, by job id

Charge clients for AI video from real numbers: store each Sume job id per client, read /v1/usage?job_id and quote debited_usd_micros. Python with a markup line.

5 min readSume
All posts

To bill clients for AI video from real numbers, save the Sume job id of every job you run for a client, then read GET /v1/usage?job_id=... and quote summary.debited_usd_micros. The Usage docs call that field the figure to quote: what the wallet deducted, with open holds and refunds kept out of it.

Sume has one wallet per workspace, with no per-client sub-balances in the pages cited here, so the client split is yours to keep. The job id is the key that makes it possible.

Which field is the cost of a job?

A scoped usage read adds a summary over every ledger row the job caused. Three fields matter for a client statement.

Fields of the usage summary (Sume Usage docs, read 2026-10-03)
FieldWhat it isPut it on a client statement?
debited_usd_microsWhat the wallet deducted, captured rows of every operation typeYes, this is the cost
held_usd_microsHolds still openNo, not spend yet
refunded_usd_microsHolds given back after a failure or cancellationNo, not spend
finaltrue once no hold is openCheck it before you quote

How do you keep the client link?

Write the job id next to the client at submit time. Every Sume submit returns one, and a Format or bulk run returns run ids as well; the same endpoint takes run_id and thread_id. Use an idempotency key that carries the client and item, such as clienta-spring-001, so a retried submit reuses the same job instead of creating a second one.

Do not rebuild client costs by summing /v1/usage rows yourself. The docs warn that a refunded row keeps its hold amount, so a hand-rolled sum overstates what you paid.

What does the ledger script look like?

The script reads one scoped summary per job and refuses to quote a job whose final flag is false. The multiplier is a placeholder for your own cost-plus rule, not a Sume rate.

import os
import requests

KEY = os.environ["SUME_API_KEY"]
MARKUP = 1.30  # your own cost-plus multiplier, not a Sume number

CLIENTS = {
    "client-a": ["job_aaa", "job_bbb"],
    "client-b": ["job_ccc"],
}

def debited_micros(job_id: str) -> int:
    r = requests.get("https://api.sume.com/v1/usage",
                     params={"job_id": job_id, "limit": 50},
                     headers={"Authorization": f"Bearer {KEY}"}, timeout=30)
    r.raise_for_status()
    s = r.json()["data"]["summary"]
    if not s["final"]:
        raise RuntimeError(f"{job_id} still has an open hold")
    return s["debited_usd_micros"]

def main() -> None:
    for client, jobs in CLIENTS.items():
        cost = sum(debited_micros(j) for j in jobs) / 1_000_000
        print(f"{client}: cost ${cost:.4f}, bill ${cost * MARKUP:.2f}")

main()

What should the markup cover?

The wallet figure is Sume's price to you, which for router models is the provider list rate times 1.25 according to the video router docs. It is not your cost to produce the ad. A client price usually has to cover more, and only you know the numbers:

  • Retakes that you generated but did not deliver. Their jobs still debited the wallet, so decide whether the client pays for them or you absorb them.
  • Failed jobs. Sume releases the hold on a failed or canceled job, so these do not appear as debited cost.
  • Your plan fee, which is separate from per-call usage on the Sume pricing page.
  • Review time, music or voice choices you made outside Sume, and delivery.

What can this not tell you?

Usage rows carry job, thread and run ids, not client names, and the ledger has no tag you can set. If you forget to store an id, you cannot recover the client from Sume. The date and operation type on each row can narrow a guess, but they cannot prove it. Store the map from day one, and keep it with your own invoices.

Sources

Related posts

More in Pricing

All Pricing posts

Written by Sume