Concepts
Pricing and billing
What each operation costs, how credits, estimates and charge caps work, and how to read a receipt.
Story0 charges per minute of video processed, from prepaid credits. You can quote any request before you send it, cap what it may cost, and read an itemized receipt on every result. Failed and cancelled jobs cost nothing, and test keys never bill.
Prices
| Operation | Price per minute of video | Receipt lines |
|---|---|---|
| Screening | $0.02 | screen $0.02 |
Segmentation, partition or extract |
$0.10 | analysis $0.08 and segment $0.02 |
These are price version 2026-11-01, which every estimate and receipt names in price_version. There are no charges for uploading, for storing media during its retention period, for estimates, for webhooks, or for reading jobs and results.
How a charge is calculated
- Billable time is the length of video analyzed, rounded up to a whole second, with a minimum of 30 seconds per request: the whole video for screenings, and the requested
range(or the whole video) for segmentations. - Each line is its per-minute price multiplied by the billable seconds and divided by 60.
- Amounts are calculated in millionths of a dollar and returned as decimal strings with two to four decimal places, so receipt lines can differ from the total by rounding in the last place.
| Video length | Billable seconds | Segmentation | Screening |
|---|---|---|---|
| 8 seconds | 30 | $0.05 | $0.01 |
| 24 seconds | 30 | $0.05 | $0.01 |
| 45 seconds | 45 | $0.075 | $0.015 |
| 2 minutes 10.4 seconds | 131 | $0.2183 | $0.0437 |
| 10 minutes | 600 | $1.00 | $0.20 |
| 20 minutes, the longest supported | 1,200 | $2.00 | $0.40 |
Credits
You pay from a prepaid balance of credits. 100 credits are worth $1, so a $0.05 segmentation uses 5 credits.
- Free credit: every new account receives $10 (1,000 credits) once its email address is verified.
- Buying credit: choose $10, $25, $50, $100 or $250, or any whole-dollar amount from $10 to $1,000, on the Billing page of the console (dev.story0.com/console/billing). Payment goes through a hosted Stripe checkout and the credits appear as soon as it is confirmed.
- No expiry: credits don't expire.
If your available balance can't cover a request, Story0 refuses it with 402 insufficient_credits and nothing runs.
Estimates
Quote an operation for a ready asset, or for any duration you name:
const estimate = await story0.estimates.create({
operation: "segment",
input: { asset_id: "asset_01m6gwgx34hq7d2m9ycv5k8b1e" },
});
console.log(estimate.total); // "0.05"estimate = client.estimates.create(
operation="segment",
input={"asset_id": "asset_01m6gwgx34hq7d2m9ycv5k8b1e"},
)
print(estimate["total"]) # 0.05curl https://api.story0.com/v1/estimates \
-H "Authorization: Bearer $STORY0_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "operation": "segment", "input": { "asset_id": "asset_01m6gwgx34hq7d2m9ycv5k8b1e" } }'For the 24-second sample clip, the estimate is:
{
"object": "estimate",
"operation": "segment",
"billable_seconds": 30,
"currency": "USD",
"total": "0.05",
"items": [
{ "item": "analysis", "amount": "0.04", "unit_price_per_minute": "0.08" },
{ "item": "segment", "amount": "0.01", "unit_price_per_minute": "0.02" }
],
"price_version": "2026-11-01",
"expires_at": "2026-11-03T15:20:41.502Z"
}operation is segment or screen, and input takes either asset_id (the asset must be ready, so its duration is known) or duration_seconds, such as { "duration_seconds": 600 } to price a 10-minute video before you have it. An estimate is a quote, not a reservation: it's meant to be current for an hour (expires_at), and the price is fixed again when Story0 accepts the actual request. Estimates always quote the full price; if analysis reuse applies, the charge comes out lower.
Cap a request with max_charge
Every processing request accepts a ceiling on what it may cost:
"billing": { "max_charge": { "currency": "USD", "amount": "0.50" } }amount is a decimal string with up to six decimal places, and currency must be USD. Story0 never charges more than the cap:
- When the duration is known (the input is a
readyasset), Story0 prices the request before accepting it. If the price is above the cap, the request is refused with402 charge_cap_exceeded, which reports theestimateand yourmax_charge. Nothing is created or charged. - When the duration isn't known yet (an
input.url, or an asset that's stillprocessing), Story0 accepts the request and checks the price as soon as it has read the media. If the price is above the cap, the job fails withcharge_cap_exceededbefore any analysis, and nothing is charged.
Without max_charge, the cap is the estimate when the duration is known, and $2.00 otherwise, which is what segmenting a 20-minute video costs.
Holds and settlement
Story0 reserves money when it accepts a job, so concurrent jobs can never spend more than your balance:
- On acceptance, Story0 places a hold for the most the job can cost: the full price when the duration is known, or the whole cap when it isn't. If your available balance is smaller than the hold, the request is refused with
402 insufficient_credits, which reports what wasneededand what isavailable. - While the job runs, the hold appears in your balance's
heldamount and is excluded fromavailable. - When the job succeeds, Story0 charges the actual price, never more than the hold, and releases the rest.
- When the job fails or is cancelled, the whole hold is released and nothing is charged.
So a URL input with a $0.50 cap holds $0.50 while it runs, and if the video turns out to cost $0.05, $0.45 goes back to available when the job ends.
Tip: If your available balance is below $2.00 and you send URL inputs, set
max_charge. Otherwise the default $2.00 hold can be refused withinsufficient_creditseven though the video itself would cost much less.
Receipts
A job shows its charge in charge:
"charge": { "currency": "USD", "estimate": "0.05", "max_charge": "0.50", "total": "0.05" }estimateis the full price when the duration was known at acceptance, andnullotherwise, for example for a URL input.max_chargeis the cap that applied, whether you set it or it was the default.totalis what was charged:nullwhile the job is queued or running, the actual charge once it succeeds, and"0.00"when it fails or is cancelled.
Every result carries an itemized receipt:
"receipt": {
"currency": "USD",
"total": "0.05",
"items": [
{ "item": "analysis", "amount": "0.04", "unit_price_per_minute": "0.08" },
{ "item": "segment", "amount": "0.01", "unit_price_per_minute": "0.02" }
],
"billable_seconds": 30,
"price_version": "2026-11-01"
}Repeating a request with the same idempotency key returns the original job and charges nothing more.
Analysis reuse
When you send an identical segmentation request for identical media, recognized by the file's SHA-256 hash, Story0 can answer from your account's earlier analysis. The analysis line is then left out, so the result costs only the segment line: $0.02 per minute instead of $0.10. A reused result has provenance.analysis_reused: true. Segmentation lists exactly what has to match.
Screenings have no separate analysis line, so a reused screening costs the same $0.02 per minute as a new one.
Test mode never bills
Jobs created with a test key (s0_test_…) return fixtures and are never charged. Their charge.total and receipt.total are "0.00", and they don't check your balance or your cap. See Test mode.
Balance and usage
const balance = await story0.balance.retrieve();
const usage = await story0.usage.retrieve({ days: 30 });balance = client.balance.retrieve()
usage = client.usage.retrieve(days=30)curl https://api.story0.com/v1/balance -H "Authorization: Bearer $STORY0_API_KEY"
curl "https://api.story0.com/v1/usage?days=30" -H "Authorization: Bearer $STORY0_API_KEY"GET /v1/balance returns what you can spend now:
{ "object": "balance", "currency": "USD", "available": "9.43", "held": "0.50", "credits": 943 }available excludes holds for queued and running jobs, held is the sum of those holds, and credits is available expressed in credits.
GET /v1/usage summarizes live charges over the last days days (1 to 90, default 30), by day and operation. It counts succeeded live jobs only; test-mode work is never billed and isn't included.
{
"object": "usage",
"currency": "USD",
"period": { "start": "2026-10-04T15:02:11.000Z", "end": "2026-11-03T15:02:11.000Z" },
"totals": { "jobs": 14, "billable_seconds": 1840, "amount": "2.5173" },
"days": [
{ "date": "2026-11-03", "operation": "screen", "jobs": 6, "billable_seconds": 412, "amount": "0.1373" },
{ "date": "2026-11-03", "operation": "segment", "jobs": 8, "billable_seconds": 1428, "amount": "2.38" }
]
}A worked example
Take the 24-second sample clip from the Quickstart, uploaded as a ready asset, on a new account with $10.00 of free credit:
- Estimate a segmentation. 24 seconds rounds up to the 30-second minimum:
analysis$0.04 plussegment$0.01, $0.05 in all. - Submit it with
max_charge$0.50. The duration is known and $0.05 is under the cap, so Story0 accepts the job and holds $0.05. The balance showsavailable$9.95 andheld$0.05. - The job succeeds. Story0 charges $0.05 (5 credits) and the hold closes:
available$9.95,held$0.00. - Screen the same clip. 30 billable seconds at $0.02 per minute: $0.01.
- Send the same segmentation again with a new idempotency key. The analysis is reused, so only the
segmentline applies: $0.01.
That's $0.07, or 7 credits, for the whole session, leaving $9.93. Had you passed the clip's URL instead of the asset in step 2, Story0 would have held the full $0.50 until the job finished, then charged the same $0.05 and released $0.45.