Reference
Errors
The problem document every failed request returns, every error code Story0 uses, and what to do about each one.
Story0 reports errors in two places. A request that fails gets an HTTP error status and a problem document. A job that fails after it was accepted is still retrieved with 200 OK, with status: "failed" and the reason in job.error. Both use the same stable code values, so you can branch on code everywhere.
The problem document
Every HTTP error has the content type application/problem+json and follows RFC 9457:
{
"type": "https://dev.story0.com/docs/errors#insufficient_credits",
"title": "Not enough credits",
"status": 402,
"code": "insufficient_credits",
"detail": "Add credits in the console, or lower `billing.max_charge`.",
"request_id": "req_01m6gwhj03t8wq2c5n7z3k9r4a",
"needed": "2.00",
"available": "1.35",
"currency": "USD"
}| Field | Description |
|---|---|
type |
A link to this page, at the section for the code. |
title |
A short summary for people. |
status |
The HTTP status, repeated in the body. |
code |
The stable, machine-readable reason. Branch on this. |
detail |
An explanation of this occurrence, when there is one. |
request_id |
The same value as the x-request-id response header. Log it. |
Some codes add extension members: errors for validation failures, needed, available and currency for insufficient_credits, and estimate, max_charge and currency for charge_cap_exceeded. Amounts are decimal strings in US dollars. title and detail are written for people and their wording can change, so don't parse them.
Validation errors list every problem with a JSON pointer into your request:
{
"type": "https://dev.story0.com/docs/errors#invalid_request",
"title": "The request is not valid",
"status": 422,
"code": "invalid_request",
"detail": "/labels/1/key: Use lowercase letters, digits and underscores, starting with a letter.",
"request_id": "req_01m6gwk7t3c9v2n5q8x1r4m6d0",
"errors": [
{ "pointer": "/labels/1/key", "detail": "Use lowercase letters, digits and underscores, starting with a letter." }
]
}Job errors
When a job fails, job.error has the same code, title and detail, plus any extension members:
"error": {
"code": "charge_cap_exceeded",
"title": "The work would cost more than `max_charge`",
"detail": null,
"estimate": "0.75",
"max_charge": "0.50",
"currency": "USD"
}Failed jobs are never charged. A failed job is final: to try again, send a new request with a new idempotency key. Assets that fail record the same kind of object in asset.error, and a job on such an asset fails with the asset's code.
All codes
| Code | HTTP status | Where it appears | Meaning |
|---|---|---|---|
unauthorized |
401 | Response | The API key is missing, malformed, unknown or revoked. |
forbidden |
403 | Response | The account is suspended, or the endpoint is for the console only. |
not_found |
404 | Response | The object doesn't exist or isn't visible to this account. |
route_not_found |
404 | Response | The method and path aren't part of the API. |
invalid_request |
422 | Response | The request failed validation. See errors. |
idempotency_key_reused |
422 | Response | The key was already used for a different request. |
unsupported_input |
422 | Response, job, asset | The input's type, size, length or address isn't supported. |
upload_missing |
422 | Response, job, asset | The upload was completed before its bytes arrived. |
asset_failed |
422 | Response | The request referenced an asset whose processing failed. |
source_unavailable |
422 | Response, job, asset | A URL couldn't be resolved or fetched. |
insufficient_credits |
402 | Response | The available balance can't cover the hold. |
charge_cap_exceeded |
402 | Response, job | The work would cost more than billing.max_charge. |
result_expired |
410 | Response | The result has passed its 30-day retention. |
rate_limited |
429 | Response | Too many requests in a minute. Retry-After says when to try again. |
concurrency_limit |
429 | Response | 20 jobs are already queued or running. Retry-After: 5. |
range_outside_media |
422 | Job | The segmentation range has no video in it. |
internal_error |
500 | Response | Something went wrong on Story0's side. |
billing_unavailable |
502, 503 | Console | Credit purchases are temporarily unavailable. |
media_unreadable |
None | Job, asset | The file isn't a readable video. |
asset_deleted |
None | Job | The asset's media was deleted before the job read it. |
capacity_exhausted |
None | Job | Processing capacity for the day is used up. |
model_unavailable |
None | Job | Analysis is temporarily unavailable. |
model_failed |
None | Job | The analysis didn't produce a usable answer. |
processing_failed |
None | Job | Processing failed and retries didn't help. |
Request errors
unauthorized
401. The Authorization header is missing, isn't in the form Bearer <key>, or the key is unknown or revoked. Check that STORY0_API_KEY is set and current. If the key was revoked or lost, create a new one in the console.
forbidden
403. The key is valid but the request isn't allowed: the account is suspended, or an API key was sent to an endpoint that only the console can use. Don't retry.
not_found
404. The ID doesn't exist, belongs to another account, or refers to something no longer available, such as a deleted or expired asset. A result ID sent to the wrong endpoint, such as a screening ID to /v1/segments/{id}, is also not_found. Check the ID and that you're using a key from the right account.
route_not_found
404. No endpoint matches the method and path. Check for typos and the /v1 prefix; the API reference lists every route.
invalid_request
422. The body, query string or headers failed validation. errors lists each problem with a JSON pointer and a detail, and the top-level detail repeats the first one. Common causes:
- a missing required field, or a value outside its allowed range;
- a label or category key that isn't lowercase letters, digits and underscores starting with a letter, or duplicate keys;
partitionwith fewer than two labels,extractwith labels, or a label key equal to the uncertainty label;- both or neither of
input.asset_idandinput.url; range.endnot afterrange.start, ormin_secondsgreater thanmax_seconds;- a currency other than
USDinbilling.max_charge; - a test key with a live asset, or the reverse;
- an
Idempotency-Keywith characters outsideA–Z a–z 0–9 _ . : -, or a malformedcursor; waitabove 30 onGET /v1/jobs/{id};- an estimate for an asset that isn't
readyyet; - a webhook URL that doesn't use
https, or an eleventh enabled webhook endpoint.
Fix the request and send it again. Nothing was stored, so the same idempotency key still works.
idempotency_key_reused
422. You sent an Idempotency-Key that was already used, within the last 7 days, for a different request. Nothing was created. Use a new key for the new request. See Idempotency and retries for what counts as the same request.
unsupported_input
422. The input is well-formed but not something Story0 can process. On a request it means an upload content type other than video/* or application/octet-stream, an upload over 2 GB, or a webhook URL that points at a private address, has credentials in it, or isn't http or https at all. As a job or asset error it means a video longer than 20 minutes, a file over 2 GB, a platform page (such as a YouTube link) or a web page instead of a video file, a private or local address, a URL that isn't http or https, or a URL with credentials. Change the input; retrying the same one won't help.
upload_missing
422. You completed an upload before all of its bytes arrived, or the uploaded file wasn't found when a job needed it. Finish sending the bytes to upload_url, then complete the upload again. See Uploading media.
asset_failed
422. The input.asset_id refers to an asset whose processing failed. detail repeats the asset's own error, and GET /v1/assets/{id} shows its error.code. Fix the source file and create a new asset.
source_unavailable
422. A URL couldn't be used: the host didn't resolve, the server answered with an error status, it redirected more than 5 times, or the download didn't finish within 10 minutes. On POST /v1/webhook_endpoints it means the webhook host didn't resolve. As a job or asset error it means an input URL couldn't be fetched. Check that the URL is a direct, publicly reachable link to the file, or upload the file instead.
insufficient_credits
402. Your available balance is less than the hold this request needs. needed and available give both amounts. Buy credits on the Billing page, wait for running jobs to release their holds, or lower billing.max_charge; for URL inputs without a cap the hold is $2.00. Nothing was created, so the same idempotency key still works.
charge_cap_exceeded
402. The work would cost more than billing.max_charge. estimate is the full price and max_charge is your cap. On a request, nothing was created; raise the cap or don't process this video. As a job error, the video's duration only became known during the job; the job failed before any analysis and nothing was charged. See Pricing and billing.
result_expired
410. The result has passed its 30-day retention, and its content has been deleted; the job record is still there. To get a new result, run the request again with a new idempotency key. Media is kept for only 7 days, so upload it again first.
rate_limited
429. You sent more than 1,200 reads or 300 writes in a minute. The response carries Retry-After with the seconds until the window resets; wait that long and retry with the same idempotency key. The SDKs do this for you.
concurrency_limit
429. 20 of your jobs are already queued or running. Wait for some to finish (Retry-After: 5), then submit the next one. This limit protects your balance from a runaway loop as much as it protects the service.
range_outside_media
422, on the job. The segmentation's range contains less than a quarter of a second of video, usually because range.start is past the end. Nothing was charged. Check the asset's media.duration_seconds and send a range inside it.
internal_error
500. Something unexpected went wrong on Story0's side. Retry with backoff and the same idempotency key; nothing is charged twice. If it keeps happening, note the request_id.
billing_unavailable
502 or 503. Buying credits in the console is temporarily unavailable. Processing isn't affected. Try the purchase again later.
Job errors by code
The codes in this section appear only in job.error and asset.error, never as an HTTP status. unsupported_input, upload_missing, source_unavailable and charge_cap_exceeded can also appear there, as described above.
media_unreadable
The file couldn't be read as a video: it has no video stream, or no measurable duration. If the file plays elsewhere, re-encode it to a common format such as H.264 in an MP4 container, upload it again, and send a new request.
asset_deleted
The asset's media was deleted with DELETE /v1/assets/{id} after the job was accepted but before the job read it. Nothing was charged. Upload the media again and send a new request.
capacity_exhausted
Story0's processing capacity for the day was used up. Nothing was charged. Send the request again later with a new idempotency key.
model_unavailable
Analysis was temporarily unavailable when the job ran. Nothing was charged. Send the request again later with a new idempotency key.
model_failed
The analysis didn't produce a usable answer, even after Story0 retried it. The error carries retryable: true. Nothing was charged. Send the request again with a new idempotency key; if the same video fails repeatedly, simplify the instruction or the labels.
processing_failed
Processing failed for an unexpected reason, and Story0's automatic retries didn't help. Nothing was charged. Check that the media and any range are valid, for example that range.start lies inside the video, then send the request again with a new idempotency key.
Retrying
| Situation | What to do |
|---|---|
Network error, 429, 5xx |
Retry with backoff and the same idempotency key. The SDKs do this automatically. |
402, 422, 401, 403, 404 |
Fix the cause first. Retrying the same request gives the same answer. |
Job failed with a temporary cause (capacity_exhausted, model_unavailable, model_failed, processing_failed) |
Send a new request, with a new idempotency key, later. |
Job failed because of the input (media_unreadable, unsupported_input, source_unavailable, asset_deleted, charge_cap_exceeded) |
Fix the input or the cap, then send a new request with a new key. |
SDK error classes
The SDKs turn problem documents into typed errors. Every error has status (0 when no response arrived), code, the request ID (requestId in TypeScript, request_id in Python), detail, and raw, the full problem document.
| Situation | TypeScript | Python |
|---|---|---|
| Base class of every SDK error | Story0Error |
Story0Error |
| 401 | AuthenticationError |
AuthenticationError |
| 403 | PermissionError |
PermissionDeniedError |
| 404 or 410 | NotFoundError |
NotFoundError |
| 400 or 422 | InvalidRequestError, with errors |
InvalidRequestError, with errors |
402 insufficient_credits |
InsufficientCreditsError |
InsufficientCreditsError |
402 charge_cap_exceeded |
ChargeCapExceededError |
ChargeCapExceededError |
| 429 | RateLimitError |
RateLimitError |
| 5xx | APIError |
APIError |
| No response: network failure or timeout | ConnectionError |
APIConnectionError |
| A waited-for job failed | JobFailedError, with job |
JobFailedError, with job |
| A waited-for job was cancelled | JobCancelledError, with job |
JobCancelledError, with job |
| A wait ran out of time | WaitTimeoutError, with jobId |
WaitTimeoutError, with job_id |
| A webhook failed verification | WebhookVerificationError |
WebhookVerificationError |
For JobFailedError, code is the job's error code, such as media_unreadable.
import Story0, {
ChargeCapExceededError,
InsufficientCreditsError,
InvalidRequestError,
JobFailedError,
} from "@story0/sdk";
const story0 = new Story0();
try {
const result = await story0.segments.createAndWait(params);
console.log(result.segments.length);
} catch (err) {
if (err instanceof ChargeCapExceededError) {
console.log(`Would cost ${err.raw?.estimate}; the cap is ${err.raw?.max_charge}`);
} else if (err instanceof InsufficientCreditsError) {
console.log(`Needs ${err.raw?.needed}; ${err.raw?.available} available`);
} else if (err instanceof InvalidRequestError) {
for (const e of err.errors) console.log(e.pointer, e.detail);
} else if (err instanceof JobFailedError) {
console.log(`Job ${err.job.id} failed: ${err.code}`);
} else {
throw err;
}
}import story0
from story0 import Story0
client = Story0()
try:
result = client.segments.create_and_wait(**params)
print(len(result["segments"]))
except story0.ChargeCapExceededError as err:
print(f"Would cost {err.raw['estimate']}; the cap is {err.raw['max_charge']}")
except story0.InsufficientCreditsError as err:
print(f"Needs {err.raw['needed']}; {err.raw['available']} available")
except story0.InvalidRequestError as err:
for e in err.errors:
print(e["pointer"], e["detail"])
except story0.JobFailedError as err:
print(f"Job {err.job['id']} failed: {err.code}")