Story0.API

Concepts

Jobs and results

How processing requests run in the background, how to wait for them, cancel them and recover after a disconnect.

Every processing request, POST /v1/segments or POST /v1/screenings, creates a job and returns it at once. The job does the work in the background. When it succeeds, it points to a typed, immutable result: a segmentation or a screening. Jobs are the only asynchronous part of the API, so once you know how to wait for one, you know how to use every operation.

The job object

json
{
  "id": "job_01m6gwhj05rk4v9c2x7m3d8f6p",
  "object": "job",
  "operation": "segment",
  "status": "succeeded",
  "livemode": true,
  "request_id": "req_01m6gwhj03t8wq2c5n7z3k9r4a",
  "asset_id": "asset_01m6gwgx34hq7d2m9ycv5k8b1e",
  "stage": null,
  "progress": 1,
  "result": {
    "id": "seg_01m6gwj2c15y8n3q7w2e9t4h6b",
    "object": "segmentation",
    "url": "/v1/segments/seg_01m6gwj2c15y8n3q7w2e9t4h6b"
  },
  "error": null,
  "charge": { "currency": "USD", "estimate": "0.05", "max_charge": "0.50", "total": "0.05" },
  "created_at": "2026-11-03T14:20:52.611Z",
  "started_at": "2026-11-03T14:20:53.140Z",
  "finished_at": "2026-11-03T14:21:09.377Z",
  "metadata": { "story_id": "park-0412" }
}
Field Description
id The job ID, job_….
operation segment or screen.
status queued, running, succeeded, failed or cancelled.
livemode true for live keys, false for test keys.
request_id The ID of the request that created the job.
asset_id The asset the job works on, including one Story0 created for an input.url.
stage What a running job is doing now. null unless running.
progress A number from 0 to 1 while running, 1 once succeeded, null otherwise.
result { "id", "object", "url" } once the job has succeeded, null before that.
error { "code", "title", "detail", … } when the job failed, otherwise null.
charge estimate (the full price, or null if the duration wasn't known when the job was accepted), max_charge (the cap applied), and total (what was charged, null until the job ends).
created_at, started_at, finished_at When the job was accepted, first started and ended.
metadata The key-value pairs you sent with the request.

Lifecycle

Status Meaning Final
queued Accepted and waiting to start. No
running In progress. stage and progress say where it is. No
succeeded The result exists, and result points to it. Yes
failed There is no result, and error says why. Nothing is charged. Yes
cancelled Stopped at your request before it finished. Nothing is charged. Yes

A job moves from queued to running and then to one of the three final states, or straight from queued to cancelled. Final states never change.

Stages

While a job is running, stage names its current step:

Stage What's happening
starting The job has been picked up.
ingesting Story0 reads the media, fetching it first for a URL input and reading its media facts if they aren't known yet.
sampling Frames are taken from the video. Screening reports this stage; segmentation samples and analyzes in overlapping chunks, so it goes straight to analysing.
analysing The frames are analyzed. Segmentation advances progress as each chunk finishes.
assembling The result is being stored and the charge settled.
retrying A step hit a temporary problem. Story0 runs the job again automatically, up to five attempts in all, before failing it.

Treat stage and progress as information for display. New stages can be added, progress isn't a time estimate, and a retried job starts again from the beginning.

Create a job

The response to a processing request is the job, with 202 Accepted, a Location header and a Retry-After hint in seconds:

http
HTTP/1.1 202 Accepted
Content-Type: application/json
Location: /v1/jobs/job_01m6gwhj05rk4v9c2x7m3d8f6p
Retry-After: 2

The Location path is relative to https://api.story0.com. If the request repeats an earlier one with the same Idempotency-Key, the response is 200 OK with the original job, in whatever state it's in now, instead of a new job. See Idempotency and retries.

Wait for a job

There are three ways to wait. Choose by where your code runs.

Long-poll

GET /v1/jobs/{id}?wait=N holds the request for up to N seconds (0 to 30) and answers as soon as the job reaches a final state, so you see completion within a fraction of a second without polling rapidly. If the job is still going when the wait runs out, you get its current state with Retry-After: 1; just call again.

bash
curl "https://api.story0.com/v1/jobs/job_01m6gwhj05rk4v9c2x7m3d8f6p?wait=20" \
  -H "Authorization: Bearer $STORY0_API_KEY"

Without wait, the request answers immediately. Set your HTTP client's timeout comfortably above the wait; the SDKs add 30 seconds to it. A long-poll counts as a single request against your rate limit however long it waits.

SDK helpers

const params = {
  input: { asset_id: "asset_01m6gwgx34hq7d2m9ycv5k8b1e" },
  mode: "extract" as const,
  instruction: "Moments where a person is jogging",
};

const result = await story0.segments.createAndWait(params, {
  idempotencyKey: "park-0412:jogging-progress:v1",
  waitTimeoutMs: 15 * 60_000,
  onProgress: (job) => console.log(job.stage, job.progress),
});
params = {
    "input": {"asset_id": "asset_01m6gwgx34hq7d2m9ycv5k8b1e"},
    "mode": "extract",
    "instruction": "Moments where a person is jogging",
}

result = client.segments.create_and_wait(
    **params,
    idempotency_key="park-0412:jogging-progress:v1",
    wait_timeout=15 * 60,
    on_progress=lambda job: print(job["stage"], job["progress"]),
)
  • createAndWait (create_and_wait) submits the request, waits, and returns the typed result. It raises JobFailedError if the job fails, JobCancelledError if it's cancelled, and WaitTimeoutError if the wait timeout (10 minutes by default) runs out first.
  • jobs.wait(id) waits for a job you already have and returns it in its final state, whatever that is. It doesn't raise for a failed or cancelled job, only WaitTimeoutError when the wait runs out.
  • Both long-poll in holds of up to 20 seconds, or 2 seconds when you pass a progress callback, so the callback sees each stage.

Webhooks

For background pipelines, subscribe an endpoint to job.succeeded, job.failed and job.cancelled. job.succeeded is the single completion event, and it fires only once the result exists. See Webhooks.

Get the result

A succeeded job's result says which kind of result it produced and where to fetch it:

Result result.object Endpoint SDK
Segmentation segmentation GET /v1/segments/{id} segments.retrieve(id)
Screening screening GET /v1/screenings/{id} screenings.retrieve(id)

Results are immutable: fetching one twice returns the same content. They're kept for 30 days (retention.result_expires_at). After that the endpoint answers 410 result_expired, and you can run the request again to produce a new result, so store what you need. Result URLs require your API key; they aren't share links.

When a job fails

A failed job looks like this (other fields omitted):

json
{
  "id": "job_01m6gwq8r2v5k9c3x7m1d4f6t8",
  "object": "job",
  "status": "failed",
  "error": {
    "code": "source_unavailable",
    "title": "The URL could not be fetched",
    "detail": "The server answered 404."
  },
  "charge": { "currency": "USD", "estimate": null, "max_charge": "0.50", "total": "0.00" }
}

A GET that returns a failed job is still 200 OK: the request worked, the job didn't. Branch on error.code; Errors lists every code. Failed jobs are never charged, and their hold is released.

A failed job is final. To try again, send a new request with a new idempotency key, because the old key returns the same failed job.

Cancel a job

const job = await story0.jobs.cancel("job_01m6gwhj05rk4v9c2x7m3d8f6p");
job = client.jobs.cancel("job_01m6gwhj05rk4v9c2x7m3d8f6p")
curl -X POST https://api.story0.com/v1/jobs/job_01m6gwhj05rk4v9c2x7m3d8f6p/cancel \
  -H "Authorization: Bearer $STORY0_API_KEY"
  • A queued job is cancelled at once and costs nothing.
  • A running job is asked to stop. It checks between stages: after the media is ingested, and again after the analysis, before the result is stored. The response still shows running, so wait for the final state: cancelled (not charged), or succeeded (charged normally) if the job reached its last step first.
  • Cancelling a job that has already finished changes nothing and returns it as it is.

Closing a connection or aborting an SDK wait doesn't cancel anything; the job keeps running. Only the cancel endpoint stops it.

List jobs

GET /v1/jobs returns jobs newest first, 20 per page by default (limit accepts 1 to 100). Filter with status, and page with cursor and next_cursor. The list includes jobs from both test and live keys, so check livemode.

const running = await story0.jobs.list({ status: "running", limit: 100 });
for (const job of running.data) console.log(job.id, job.stage, job.progress);
running = client.jobs.list(status="running", limit=100)
for job in running["data"]:
    print(job["id"], job["stage"], job["progress"])
curl "https://api.story0.com/v1/jobs?status=running&limit=100" \
  -H "Authorization: Bearer $STORY0_API_KEY"

Request IDs

Every response carries an x-request-id header, every error body a matching request_id, and every job the request_id of the request that created it. You can send your own ID in an X-Request-Id header (8 to 80 letters, digits, _ or -), and Story0 uses it instead of generating one, which makes it easy to follow a request through your logs.

Recover after a disconnect

If your process restarts or a connection drops while you wait, the job keeps running. Pick up where you left off:

  • If you have the job ID, call jobs.wait(id) or long-poll GET /v1/jobs/{id}?wait=20.
  • If you don't, send the same request again with the same idempotency key. You get the same job back with 200 OK, not a new one, and nothing is charged twice.

WaitTimeoutError carries the job ID for exactly this case:

import Story0, { WaitTimeoutError } from "@story0/sdk";

const story0 = new Story0();

async function jogging(assetId: string) {
  const params = {
    input: { asset_id: assetId },
    mode: "extract" as const,
    instruction: "Moments where a person is jogging",
  };
  try {
    return await story0.segments.createAndWait(params, {
      idempotencyKey: `${assetId}:jogging:v1`,
      waitTimeoutMs: 60_000,
    });
  } catch (err) {
    if (!(err instanceof WaitTimeoutError)) throw err;
    const job = await story0.jobs.wait(err.jobId); // keep waiting; nothing is resubmitted
    if (job.status !== "succeeded" || !job.result) throw new Error(`Job ${job.id} ended ${job.status}`);
    return story0.segments.retrieve(job.result.id);
  }
}
import story0
from story0 import Story0

client = Story0()


def jogging(asset_id: str):
    params = {
        "input": {"asset_id": asset_id},
        "mode": "extract",
        "instruction": "Moments where a person is jogging",
    }
    try:
        return client.segments.create_and_wait(
            **params, idempotency_key=f"{asset_id}:jogging:v1", wait_timeout=60
        )
    except story0.WaitTimeoutError as err:
        job = client.jobs.wait(err.job_id)  # keep waiting; nothing is resubmitted
        if job["status"] != "succeeded":
            raise RuntimeError(f"Job {job['id']} ended {job['status']}")
        return client.segments.retrieve(job["result"]["id"])