Story0.API

Concepts

Idempotency and retries

Retry processing requests safely, so a lost response never runs or bills the same work twice.

Networks fail at the worst moments. A request reaches Story0, the job starts, and the response never makes it back. If you simply send the request again, you might start, and pay for, a second job. Idempotency keys prevent that: send the same key with the same request, and Story0 returns the job it already created.

Send a key

Add an Idempotency-Key header to any request that creates something. It matters most for the processing requests, POST /v1/segments and POST /v1/screenings, because those run and bill work:

bash
curl https://api.story0.com/v1/screenings \
  -H "Authorization: Bearer $STORY0_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: feed-42:clip-118:screen:v1" \
  -d '{
    "input": { "asset_id": "asset_01m6gwgx34hq7d2m9ycv5k8b1e" },
    "categories": [{ "key": "animals", "description": "Animals doing something fun or surprising" }]
  }'

A key is 1 to 255 characters drawn from A–Z, a–z, 0–9, _, ., : and -. Anything else is refused with 422 invalid_request.

Keys belong to your account. They're shared by every endpoint and by your test and live keys, so one key always identifies exactly one request: a key you used to create an asset can't also start a segmentation.

What happens when you repeat a key

For processing requests:

You send Story0 answers
A key it hasn't seen 202 Accepted with a new job. The key is now tied to that request.
The same key with the same request 200 OK with the original job, in its current state. Nothing new starts and nothing is charged again.
The same key with a different request 422 idempotency_key_reused. Nothing is created.
The same key more than 7 days later A new job, because Story0 remembers keys for 7 days.

A request that's rejected, for example with 422 invalid_request, 402 insufficient_credits or 429 rate_limited, stores nothing under its key. Fix the problem and send the request again with the same key.

What counts as the same request

Story0 compares:

  • the operation, segmentation or screening;
  • input: the same asset_id, or exactly the same url string;
  • the request with defaults applied: for segmentation the mode, the instruction (ignoring leading and trailing spaces), the labels in order, the uncertainty label, sampling, range and constraints; for screening the categories in order;
  • billing.max_charge, compared as an amount, so "0.5" and "0.50" match; and
  • whether a live or a test key sent it.

The order of properties in your JSON doesn't matter. metadata isn't compared: a repeat with different metadata returns the original job, with the original metadata.

Tip: If your inputs are signed URLs that change every time you generate them, a retry with a fresh URL is a different request. Create an asset first and pass its asset_id, which stays the same.

Repeating a request that already finished

A repeat returns the job as it is now. If the job succeeded, the response points at its result. If it failed, you get the failed job: a repeat never runs the work again. To try a failed request again, send it with a new key.

Which endpoints use keys

The other create endpoints, POST /v1/uploads, POST /v1/assets, POST /v1/webhook_endpoints and POST /v1/feedback, honor keys too. The same key with the same JSON body returns the first response again, exactly as it was then: the same upload session, the same asset, the same webhook endpoint (including its signing secret) or the same feedback record. Because it's the original response, an asset replayed this way can still show processing; fetch it to see its current state. The same key with a different body is refused with 422 idempotency_key_reused, and a request that failed stores nothing under its key.

Here's how every endpoint behaves when repeated:

Endpoint When repeated
POST /v1/segments, POST /v1/screenings With the same key, returns the original job in its current state, as described above.
POST /v1/uploads, POST /v1/assets, POST /v1/webhook_endpoints, POST /v1/feedback With the same key, returns the first response. Without a key, each call creates something new: another upload session, another asset (fetching the URL again), another endpoint with its own secret, or another feedback record.
POST /v1/uploads/{id}/complete Safe with or without a key: completing the same upload again returns the same asset.
POST /v1/jobs/{id}/cancel Safe: cancelling again has no further effect.
POST /v1/estimates Safe: an estimate changes nothing.
POST /v1/webhook_endpoints/{id}/test Sends another ping.
DELETE /v1/webhook_endpoints/{id} Safe: returns the same disabled endpoint.
DELETE /v1/assets/{id} The repeat returns 404 not_found, because the asset is already gone.

Keys in the SDKs

Both SDKs add a key to every processing request and to upload, asset and webhook-endpoint creation, a random UUID unless you pass your own, and keep that key across their own automatic retries. A dropped connection inside one call therefore never creates a second job, asset or endpoint. Feedback is the exception: the SDKs send a key, and retry, only when you pass one.

A generated key only lives as long as that call. To stay safe across process restarts, crashes and task queues that re-run work, pass your own key:

const job = await story0.segments.create(
  {
    input: { asset_id: assetId },
    mode: "partition",
    instruction: "Partition the video by whether a dog is visible.",
    labels: [
      { key: "dog_visible", description: "At least one dog is visible." },
      { key: "no_dog_visible", description: "No dog is visible." },
    ],
  },
  { idempotencyKey: `story-${storyId}:dog-timeline:v3` },
);
job = client.segments.create(
    input={"asset_id": asset_id},
    mode="partition",
    instruction="Partition the video by whether a dog is visible.",
    labels=[
        {"key": "dog_visible", "description": "At least one dog is visible."},
        {"key": "no_dog_visible", "description": "No dog is visible."},
    ],
    idempotency_key=f"story-{story_id}:dog-timeline:v3",
)

The same applies to createAndWait and create_and_wait: if your worker dies while waiting and the task runs again, the same key returns the same job, and the helper simply waits for it.

Choose good keys

Build keys from your own identifiers plus a version of the question, so the same logical request always produces the same key:

  • story-8812:dog-timeline:v3
  • feed-42:clip-118:screen:v1
  • quickstart-asset_01m6gwgx34hq7d2m9ycv5k8b1e

When you change the instruction, labels or cap, bump the version part. Don't put timestamps or random values in keys that should deduplicate retries, and never reuse a key for a different request.

Retry the right failures

Failure Retry? How
Network error or timeout Yes Same key. The SDKs do this for you.
429 rate_limited Yes, after a pause Back off exponentially; same key. If you hit the limit of 20 active jobs, wait for some to finish first.
500 internal_error or another 5xx Yes Same key, with backoff.
402 insufficient_credits or 402 charge_cap_exceeded After fixing Add credits or raise billing.max_charge. The same key still works, because nothing was stored.
422 invalid_request, unsupported_input, asset_failed After fixing Correct the request first.
422 idempotency_key_reused No The key belongs to a different request. Use a new key.
401, 403 or 404 No Fix the key, the permissions or the ID.
A job that ended failed With a new key Read job.error.code. If the cause is temporary (capacity_exhausted, model_failed, model_unavailable, processing_failed), send the request again later with a new key.

The SDKs retry network errors, 429 and 5xx responses up to 2 times by default (maxRetries in TypeScript, max_retries in Python), with exponential backoff and jitter, and they honor a Retry-After header of up to 20 seconds. They retry only reads and requests that carry an idempotency key, so a request that might have created something is never sent twice under different keys.