---
title: "Create a real-time analysis stream"
method: POST
path: "/v1/streams"
tags: ["streams"]
---

# Create a real-time analysis stream

`POST /v1/streams`

## Request

- Base URL: `https://api.primateintelligence.ai`
- URL: `https://api.primateintelligence.ai/v1/streams`
- Auth: HTTP bearer

Returns the signaling WS URL, ephemeral ICE servers, and session limits. Signaling protocol v1: join → ready|queued(position) → offer/answer/ice → live; mid-stream update_prompt {prompt} (client→server; server confirms with prompt_updated), metering (5s ticks), warning, end {reason}. WS auth: ?token= accepts a client token (pvct_) or first-party session JWT — never a secret key. Billed per second of live clock time (join/negotiation free) from the same credit ledger as uploads. options.narrative: true adds (a) a narrative_update object ({t_s, text}) to result-frame detections whenever the engine emits a new sentence — event-driven, absent otherwise — and (b) an additive server→client status event {type: "status", status: "prompt_context"|"combined_prompt"|"recalculating", message?} for ordering/annotating narrative entries (closed vocabulary; non-opted-in streams never receive status events). No extra cost.

## Headers

- `Idempotency-Key` string

## Request body

- CreateStreamRequest: Requires positive credit balance. One queued|ready|live stream per account by default (409 stream_already_active otherwise).
  - `prompt` string: Free text — compiled server-side. Exactly one of prompt/query.
  - `query` object: Structured CompiledQuery JSON.
  - `model` string
  - `options` object: narrative: true — opt into live narrative. Result-frame detections then carry a narrative_update object ({t_s, text}) whenever the engine produces a new sentence (event-driven, not per frame; absent on frames without one). Included in the metered price — no surcharge.
    - `narrative` boolean
  - `recording` boolean: PRI-496: true — opt into retrieval of the server-side session recording. The ended stream then carries recording: {status} and GET /v1/streams/{id}/recording returns a signed 1h playback URL (raw H.264 elementary stream; lossless remux: ffmpeg -i in.h264 -c copy out.mp4). Default false.
  - `metadata` object: Free-form key/value metadata. ≤16 keys, key ≤40 chars [a-zA-Z0-9_.-], value ≤500 chars.
  - `webhook` object: Per-request webhook override (delivery ships P7). Write-only.
    - `url` string, uri, required
    - `events` string[], required

## Response `201`

Stream created (queued or ready). Response carries signaling.url + ice_servers + limits.

- Stream: A real-time analysis stream (§4.8).
  - `id` string, required: Stream resource id (prefixed ULID). P6 §4.8.
  - `object` 'stream', required
  - `status` 'queued' | 'ready' | 'live' | 'ended', required: Stream lifecycle state (§4.8): queued → ready → live → ended. Terminal immutable.
  - `metering_rule` 'per_second' | 'per_frame': PRI-552: settlement rule pinned at create time. Streams created before the cutover report per_second.
  - `end_reason` 'completed' | 'canceled' | 'insufficient_credits' | 'error' | 'timeout' | 'ice_failed' | 'media_timeout', nullable, required: Why the stream ended. completed — normal end after live. canceled — ended before going live (client action). insufficient_credits — balance exhausted. error — server-side failure. timeout — max_session_s cutoff. ice_failed — WebRTC ICE never connected (see failure_diagnostic). media_timeout — connected but no media/results within the first-frame window. A stream that never went live is never `completed`.
  - `prompt` string, nullable, required
  - `query` object, nullable, required
  - `model` string, required
  - `queue_position` integer, nullable, required
  - `estimated_start_s` integer, nullable, required
  - `signaling` StreamSignaling, nullable, required: Present while the stream is joinable; null once ended.
    - `url` string, required: Versioned signaling WS. Auth: ?token= accepts a client token (pvct_) or first-party Clerk JWT — NEVER a secret key.
    - `expires_at` string, date-time, required
  - `ice_servers` object[], nullable, required
  - `limits` StreamLimits, nullable, required
    - `max_session_s` integer, required: Plan entitlement (max_session_s) — hard session cutoff.
    - `warn_at_remaining_s` integer, required: The `warning {remaining_s}` threshold surfaced mid-stream.
  - `duration_s` integer, nullable, required: Live-clock duration. Terminal only.
  - `results_summary` object, nullable, required: Terminal summary: {result_frames, frames (deprecated alias of result_frames — removed ~2026-08-28), last_detections}. result_frames counts result EVENTS emitted (results are sampled, not per-source-frame). Detection rows use the same enums/casing as the file-API result contract (answer: yes|no|indeterminate, lowercase; prompt echoed exactly as submitted).
  - `failure_diagnostic` object, nullable, required: Present when end_reason is ice_failed (and some media_timeout cases): ICE candidate summary from the server side — {local_candidates: string[], hint}. Use it to distinguish "server advertised only private/host candidates" from "client could not reach relay".
  - `recording` StreamRecordingState, nullable: PRI-496: recording state — null unless the stream opted in with recording: true at create.
    - `status` 'recording' | 'available' | 'failed' | 'none', required
  - `usage` StreamUsage, nullable, required: Terminal only. Same ledger as uploads — one balance.
    - `billed_seconds` integer, required: Settled ledger amount. per_frame streams (PRI-552, the default): derived from the processed-frame count. Historical per_second streams: LIVE-clock derived (join/negotiation free).
    - `credit_balance_after` integer, nullable, required
    - `metering_rule` 'per_second' | 'per_frame': PRI-552: the rule this stream settled under. Mirrors the top-level metering_rule for a self-contained terminal summary.
    - `frames` integer, nullable: PRI-552: processed-frame count (results_summary.result_frames). Present for both metering rules — frame counting predates the billing cutover; null when unknown.
  - `metadata` object: Free-form key/value metadata. ≤16 keys, key ≤40 chars [a-zA-Z0-9_.-], value ≤500 chars.
  - `created_at` string, date-time, required
  - `live_started_at` string, date-time, nullable, required
  - `ended_at` string, date-time, nullable, required

## Other responses

- `400`: validation_failed / url_forbidden / prompt errors (§15)
- `401`: invalid_api_key / key_revoked / key_expired
- `402`: insufficient_credits — positive balance required.
- `404`: resource_not_found (account-scoped; no existence oracle)
- `409`: stream_already_active — one queued|ready|live stream per account (also the un-keyed-retry guard, §31-F2) / idempotency_key_reused.
- `429`: rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After)
- `503`: capacity_exhausted (with Retry-After) — slot queue beyond the hard cap.

---

[API](https://skmtc.dev/primateintelligence/apis/primate-vision-api.md) · [All operations](https://skmtc.dev/primateintelligence/apis/primate-vision-api/llms.txt) · [OpenAPI document](https://skmtc.dev/primateintelligence/apis/primate-vision-api/revisions/aa51b01dc8ad?raw)
