Story0.API

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:

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

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

json
"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;
  • partition with fewer than two labels, extract with labels, or a label key equal to the uncertainty label;
  • both or neither of input.asset_id and input.url;
  • range.end not after range.start, or min_seconds greater than max_seconds;
  • a currency other than USD in billing.max_charge;
  • a test key with a live asset, or the reverse;
  • an Idempotency-Key with characters outside A–Z a–z 0–9 _ . : -, or a malformed cursor;
  • wait above 30 on GET /v1/jobs/{id};
  • an estimate for an asset that isn't ready yet;
  • 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}")