Story0.API

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.05
curl 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:

json
{
  "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:

json
"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 ready asset), Story0 prices the request before accepting it. If the price is above the cap, the request is refused with 402 charge_cap_exceeded, which reports the estimate and your max_charge. Nothing is created or charged.
  • When the duration isn't known yet (an input.url, or an asset that's still processing), 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 with charge_cap_exceeded before 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:

  1. 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 was needed and what is available.
  2. While the job runs, the hold appears in your balance's held amount and is excluded from available.
  3. When the job succeeds, Story0 charges the actual price, never more than the hold, and releases the rest.
  4. 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 with insufficient_credits even though the video itself would cost much less.

Receipts

A job shows its charge in charge:

json
"charge": { "currency": "USD", "estimate": "0.05", "max_charge": "0.50", "total": "0.05" }
  • estimate is the full price when the duration was known at acceptance, and null otherwise, for example for a URL input.
  • max_charge is the cap that applied, whether you set it or it was the default.
  • total is what was charged: null while 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:

json
"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:

json
{ "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.

json
{
  "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:

  1. Estimate a segmentation. 24 seconds rounds up to the 30-second minimum: analysis $0.04 plus segment $0.01, $0.05 in all.
  2. 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 shows available $9.95 and held $0.05.
  3. The job succeeds. Story0 charges $0.05 (5 credits) and the hold closes: available $9.95, held $0.00.
  4. Screen the same clip. 30 billable seconds at $0.02 per minute: $0.01.
  5. Send the same segmentation again with a new idempotency key. The analysis is reused, so only the segment line 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.