Guides
Screening
Check a whole video against up to 20 categories at once and route it on match, no_match or uncertain.
Screening answers a quick question about a whole video: which of these categories does it show? For each category you get match, no_match or uncertain, a short observation, the frames that support a match, and a review flag. At $0.02 per minute, a fifth of the price of segmentation, it's built for the first pass: routing, triage and deciding what deserves a closer look.
Create a screening
const screening = await story0.screenings.createAndWait({
input: { asset_id: "asset_01m6gwgx34hq7d2m9ycv5k8b1e" },
categories: [
{ key: "animals", description: "Animals doing something fun or surprising" },
{ key: "breaking_news", description: "Breaking news, emergencies or police activity" },
{ key: "people", description: "People visible on screen" },
],
billing: { max_charge: { currency: "USD", amount: "0.10" } },
});
console.log(screening.matched); // ["animals", "people"]screening = client.screenings.create_and_wait(
input={"asset_id": "asset_01m6gwgx34hq7d2m9ycv5k8b1e"},
categories=[
{"key": "animals", "description": "Animals doing something fun or surprising"},
{"key": "breaking_news", "description": "Breaking news, emergencies or police activity"},
{"key": "people", "description": "People visible on screen"},
],
billing={"max_charge": {"currency": "USD", "amount": "0.10"}},
)
print(screening["matched"]) # ['animals', 'people']curl https://api.story0.com/v1/screenings \
-H "Authorization: Bearer $STORY0_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: clip-118:screen:v1" \
-d '{
"input": { "asset_id": "asset_01m6gwgx34hq7d2m9ycv5k8b1e" },
"categories": [
{ "key": "animals", "description": "Animals doing something fun or surprising" },
{ "key": "breaking_news", "description": "Breaking news, emergencies or police activity" },
{ "key": "people", "description": "People visible on screen" }
],
"billing": { "max_charge": { "currency": "USD", "amount": "0.10" } }
}'Like segmentation, the request returns a job (202 Accepted); the SDK helpers wait for it and return the screening. You can use input.url instead of input.asset_id, and metadata is echoed on the job. See Jobs and results.
Categories
- Send between 1 and 20 categories, each a
{ "key", "description" }object. - Keys follow the same rules as segmentation labels: a lowercase letter, then lowercase letters, digits or underscores, up to 40 characters in all. Keys must be unique.
- Descriptions are 1 to 500 characters and say what counts.
- Categories are independent. A video can match several of them, or none.
Read the result
{
"id": "scr_01m6gwn3a8x2v6m9k4t7c1r5q8",
"object": "screening",
"job_id": "job_01m6gwmtz4c7n2k9w4r8t3d6x1",
"asset_id": "asset_01m6gwgx34hq7d2m9ycv5k8b1e",
"livemode": true,
"engine": "screen-2026-11",
"time_unit": "seconds",
"duration_seconds": 24,
"categories": [
{
"key": "animals",
"outcome": "match",
"score": { "value": 1, "kind": "model_estimate", "calibrated": false },
"observation": "A golden retriever dog is running and catching a frisbee in the park.",
"evidence": [
{ "kind": "seen", "start": 6, "end": 8.4 },
{ "kind": "seen", "start": 8.4, "end": 10.8 },
{ "kind": "seen", "start": 10.8, "end": 13.2 },
{ "kind": "seen", "start": 13.2, "end": 15.6 }
],
"review": { "required": false, "reasons": [] }
},
{
"key": "breaking_news",
"outcome": "no_match",
"score": { "value": 0, "kind": "model_estimate", "calibrated": false },
"observation": "Peaceful park setting with no breaking news or emergencies.",
"evidence": [],
"review": { "required": false, "reasons": [] }
},
{
"key": "people",
"outcome": "match",
"score": { "value": 1, "kind": "model_estimate", "calibrated": false },
"observation": "A person is walking along the path in the park.",
"evidence": [
{ "kind": "seen", "start": 15.6, "end": 18 },
{ "kind": "seen", "start": 18, "end": 20.4 }
],
"review": { "required": false, "reasons": [] }
}
],
"matched": ["animals", "people"],
"coverage": { "sampled_frames": 10, "modalities": ["visual"] },
"config": {
"categories": [
{ "key": "animals", "description": "Animals doing something fun or surprising" },
{ "key": "breaking_news", "description": "Breaking news, emergencies or police activity" },
{ "key": "people", "description": "People visible on screen" }
],
"engine": "screen-2026-11"
},
"provenance": { "engine": "screen-2026-11", "analysis_reused": false, "api_version": "2026-11-01" },
"receipt": {
"currency": "USD",
"total": "0.01",
"items": [{ "item": "screen", "amount": "0.01", "unit_price_per_minute": "0.02" }],
"billable_seconds": 30,
"price_version": "2026-11-01"
},
"retention": {
"result_expires_at": "2026-12-03T14:22:51.730Z",
"media_expires_at": "2026-11-10T14:20:31.204Z"
},
"created_at": "2026-11-03T14:22:51.730Z"
}| Field | Description |
|---|---|
categories |
One entry per category, in the order you sent them. |
categories[].outcome |
match, no_match or uncertain. |
categories[].score |
{ "value", "kind": "model_estimate", "calibrated": false }; see below. |
categories[].observation |
A short note about what decided the outcome, up to 200 characters. |
categories[].evidence |
For a match, up to 4 frames that support it, as { "kind": "seen", "start", "end" }. Empty otherwise. |
categories[].review |
required: true for an uncertain outcome, with the reason. |
matched |
The keys whose outcome is match, in request order. |
coverage |
sampled_frames (how many frames were inspected) and modalities (["visual"]). |
duration_seconds, config, engine, provenance, receipt, retention |
As for segmentation. fixture: true appears on test-mode results. |
Outcomes
match: the frames clearly show the category.evidencelists frames that support it.no_match: the frames clearly don't show it.uncertain: the frames aren't enough to decide either way.review.requiredistruewith the reasoninsufficient_evidence. If the analysis returns no answer for a category at all, the outcome is alsouncertain, with the reasonnot_answeredand a score of 0.5. A missing answer never becomes a silentno_match.
Route uncertain to a person rather than treating it as no_match.
Scores are estimates
score.value is a number from 0 to 1: the analysis's own estimate that the category applies, rounded to two decimals. The other two fields say what it is: kind: "model_estimate" and calibrated: false. It isn't a probability, it hasn't been calibrated against your footage, and it shouldn't be compared across categories or engine versions. Values of exactly 0 or 1 are common and don't mean certainty. Make decisions on outcome; use score at most to sort videos within one category.
How screening looks at a video
Screening inspects between 6 and 16 frames spread evenly across the whole video: about one every 2.5 seconds, so a 24-second clip gets 10 frames, clips under 15 seconds get 6, and videos of 40 seconds or more get 16. That's what makes it quick and cheap, and it also means brief moments between those frames can be missed. Screening always covers the whole video (there's no range), and like segmentation it's visual only.
Each evidence entry covers one frame's slot of the timeline: start is the time of the frame and end is the start of the next slot. Seek a player to start to show a reviewer the frame behind a match.
If you need to know where in the video something happens, or you can't afford to miss brief moments, use segmentation.
Screen first, then segment
A typical pipeline screens everything cheaply, segments only what matched, and sends anything uncertain to a person:
const screening = await story0.screenings.createAndWait(
{
input: { asset_id: assetId },
categories: [
{ key: "animals", description: "Animals doing something fun or surprising" },
{ key: "breaking_news", description: "Breaking news, emergencies or police activity" },
],
},
{ idempotencyKey: `${assetId}:screen:v1` },
);
if (screening.matched.includes("animals")) {
const moments = await story0.segments.createAndWait(
{
input: { asset_id: assetId },
mode: "extract",
instruction: "Moments where an animal is doing something fun or surprising",
constraints: { min_seconds: 3 },
},
{ idempotencyKey: `${assetId}:animal-moments:v1` },
);
await queueForEditors(assetId, moments.segments);
} else if (screening.categories.some((c) => c.review.required)) {
await queueForReview(assetId, screening);
}screening = client.screenings.create_and_wait(
input={"asset_id": asset_id},
categories=[
{"key": "animals", "description": "Animals doing something fun or surprising"},
{"key": "breaking_news", "description": "Breaking news, emergencies or police activity"},
],
idempotency_key=f"{asset_id}:screen:v1",
)
if "animals" in screening["matched"]:
moments = client.segments.create_and_wait(
input={"asset_id": asset_id},
mode="extract",
instruction="Moments where an animal is doing something fun or surprising",
constraints={"min_seconds": 3},
idempotency_key=f"{asset_id}:animal-moments:v1",
)
queue_for_editors(asset_id, moments["segments"])
elif any(c["review"]["required"] for c in screening["categories"]):
queue_for_review(asset_id, screening)queueForEditors and queueForReview stand in for your own code. Creating the asset once and reusing its asset_id means the video is uploaded or fetched only once for both requests.
Cost
Screening costs $0.02 per minute of video, with a 30-second minimum per video, rounded up to the whole second: $0.01 for anything up to 30 seconds, $0.20 for 10 minutes, and $0.40 for 20 minutes, the longest video supported. A repeat of an identical screening on the same media can be answered from your earlier analysis (provenance.analysis_reused: true), but screenings are always charged the full $0.02 per minute; reuse lowers the price of segmentation only. See Pricing and billing.
Write good categories
- Describe what a viewer would see. "Police cars, fire engines or ambulances at a scene" works better than "emergency".
- Keep each category to one idea. Split "animals or sport" into two categories; you can send up to 20 at once.
- Put exclusions in the description. "Pets at home count. Zoo signage on its own does not."
- Keep keys stable. Routing rules and dashboards depend on them; refine the description instead.
- Don't rely on sound or small text. Screening is visual and inspects reduced-size frames, so audio and fine print aren't part of the decision.