API reference
Public API v1 for Primate Vision — video understanding for the agentic web. P1 (PRI-430) publishes resource schemas; endpoints ship from P3 onward.
Generated from the OpenAPI 3.1 contract (version 1.0.0-alpha). The machine-readable spec is the source of truth: https://api.primateintelligence.ai/v1/openapi.json.
- Base URL:
https://api.primateintelligence.ai - Auth:
Authorization: Bearer $PRIMATE_API_KEY(see Security model) - Errors: every non-2xx response carries the standard envelope — see the Error registry
Provisioning & keys
POST /v1/keys/request
Start a device-code claim flow for a full billed key
Stage 2 of the key lifecycle. Optional auth (test or live key accepted, or none). Returns a device_code and claim_url. Direct the user to claim_url to sign in; poll GET /v1/keys/request/{code} until status changes from pending. Key is delivered once in the claim response; subsequent polls return 410.
Responses
202— Device code issued. → DeviceCodeRequest503— db_unavailable → ErrorEnvelope
GET /v1/keys/request/{code}
Poll a device-code claim request
Returns {status: "pending"} until the user approves in the browser. After approval, returns status: "consumed" (key was delivered in the claim response). Expired or already-consumed codes return 410.
Parameters
code(path, string) (required) — Thedevice_codefrom POST /v1/keys/request.
Responses
200— Current status of the device code. → DeviceCodeStatus404— resource_not_found — code not found. → ErrorEnvelope410— device_code_expired — code consumed or expired. Start a new request. → ErrorEnvelope
POST /v1/keys/upgrade
Upgrade a test key to a live free-grant key (6,000s credit)
Stage 1 of the key lifecycle. Call with a pv_test_ key in Authorization; receive a pv_live_ key with tier: free_grant and a 6,000-second credit grant. No email, card, or human-in-the-loop required. IP-limited (3 upgrades / IP / 24h).
Responses
201— Live key issued. The rawapi_keyappears only in this response. → UpgradeKeyResponse403— test_mode_only — must call with a pv_test_ key. → ErrorEnvelope429— upgrade_limit_exceeded — per-IP or global daily cap reached. → ErrorEnvelope
POST /v1/sandbox
Provision an instant sandbox (pv_test_) key — no auth, no card
Anonymous zero-touch provisioning (A1). Issues a fixture-only test-mode key: analyses return deterministic canned results, never touch the GPU, and burn no credits. IP-limited; 7-day expiry. Live keys require signup (billing gate, not an integration gate).
Responses
201— Sandbox key issued. The raw key appears only in this response. → SandboxKey429— sandbox_limit_exceeded — IP daily cap reached. Retry-After set. → ErrorEnvelope503— db_unavailable — transient; retry with backoff. → ErrorEnvelope
Videos
GET /v1/videos
List videos (cursor pagination, created_at desc)
Parameters
limit(query, integer)starting_after(query, string) — Cursor — mutually exclusive with ending_before.ending_before(query, string)status(query, string)created_after(query, string (date-time))created_before(query, string (date-time))
Responses
200— List envelope (§13.4). → VideoList400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
POST /v1/videos
Create a video (presigned upload or URL ingest)
Parameters
Idempotency-Key(header, string) — §16.1: first request wins; 24h replay window; same key + different body → 409 idempotency_key_reused.
Request body — CreateVideoRequest
Responses
201— Video created. Presigned mode → status awaiting_upload with upload instructions; URL mode → status processing (async fetch). → Video400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
GET /v1/videos/{id}
Parameters
id(path, string) (required)
Responses
200— The video. → Video400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
DELETE /v1/videos/{id}
Delete a video (409 while analyses are non-terminal on it)
Parameters
id(path, string) (required)
Responses
200— Deleted. → DeletedVideo400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
POST /v1/videos/{id}/complete
Signal that the presigned S3 PUT finished
Parameters
id(path, string) (required)
Responses
200— Size verified (±5% of declaration); video is ready. → Video400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
Analyses
GET /v1/analyses
List analyses (filters: status, video_id, model, created_*)
Parameters
limit(query, integer)starting_after(query, string) — Cursor — mutually exclusive with ending_before.ending_before(query, string)created_after(query, string (date-time))created_before(query, string (date-time))status(query, string) — Filter by public analysis status (§13.2 vocabulary): queued | preparing | analyzing | rendering | completed | failed | canceled. E.g. status=analyzing returns currently-running analyses. Unknown values → 400 validation_failed.video_id(query, string)model(query, string)
Responses
200— List envelope (§13.4). → AnalysisList400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
POST /v1/analyses
Create an analysis (video × prompt × model)
Async by default: 202 + poll. Prefer: wait=<s> (≤120) blocks and returns the terminal resource when it finishes in time. Test-mode keys (pv_test_) return deterministic canned results with no GPU time and no credit burn. Set validate_only: true to dry-run a prompt — returns an AnalysisPreview with cost/assessability and creates nothing.
Parameters
Idempotency-Key(header, string) — §16.1: first request wins; 24h replay window; same key + different body → 409 idempotency_key_reused.Prefer(header, string) — wait=(cap 120)
Request body — CreateAnalysisRequest
Responses
200— Terminal resource (Prefer: wait satisfied, or test mode) — OR an AnalysisPreview when validate_only:true was sent. Discriminate on theobjectfield. → Analysis | AnalysisPreview202— Queued. Response echoes the compiled query and honest queue_position. → Analysis400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope402— insufficient_credits (details.meter = credit_seconds) → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope503— inference_unavailable / capacity_exhausted (with Retry-After) → ErrorEnvelope
POST /v1/analyses/batch
Submit 2–10 prompts against the same video (batch discount)
Pricing: first prompt billed at full price; each additional prompt billed at 50%. Credits are reserved per-prompt at the discounted rate so the ledger shows the discount directly. Returns an analysis_batch containing all analysis DTOs plus a pricing summary. Each analysis can be polled individually via GET /v1/analyses/{id}. Pass validate_only: true for a cost-estimate dry-run (HTTP 200, no credits reserved).
Request body — CreateAnalysisBatchRequest
Responses
200— Dry-run preview (validate_only: true). Prompts parsed; cost estimated; no credits reserved, no jobs created. → AnalysisBatchPreview202— Batch queued. All analyses dispatched; poll eachidindividually. → AnalysisBatch400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope402— insufficient_credits / grant_exhausted → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope503— capacity_exhausted / inference_unavailable → ErrorEnvelope
GET /v1/analyses/{id}
Fetch an analysis (live progress while running)
Parameters
id(path, string) (required)
Responses
200— The analysis. progress uses the canonical stage set queued→preparing→analyzing→rendering. → Analysis400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
POST /v1/analyses/{id}/cancel
Cancel an analysis (best-effort once analyzing)
Parameters
id(path, string) (required)
Responses
200— Canceled (or best-effort race documented in §14.2). → Analysis400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
Streams
GET /v1/streams
List streams (cursor pagination, created_at desc)
Parameters
limit(query, integer)starting_after(query, string) — Cursor — mutually exclusive with ending_before.ending_before(query, string)status(query, string)created_after(query, string (date-time))created_before(query, string (date-time))status(query,"queued"·"ready"·"live"·"ended")
Responses
200— List envelope (§13.4). → StreamList400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
POST /v1/streams
Create a real-time analysis stream
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, 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.
Parameters
Idempotency-Key(header, string) — §16.1: first request wins; 24h replay window; same key + different body → 409 idempotency_key_reused.
Request body — CreateStreamRequest
Responses
201— Stream created (queued or ready). Response carries signaling.url + ice_servers + limits. → Stream400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope402— insufficient_credits — positive balance required. → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— stream_already_active — one queued|ready|live stream per account (also the un-keyed-retry guard, §31-F2) / idempotency_key_reused. → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope503— capacity_exhausted (with Retry-After) — slot queue beyond the hard cap. → ErrorEnvelope
GET /v1/streams/{id}
Fetch a stream (usage + results summary once ended)
Parameters
id(path, string) (required)
Responses
200— The stream. Terminal streams carry usage {billed_seconds, credit_balance_after} + duration_s + results_summary. → Stream400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
POST /v1/streams/{id}/end
Graceful end: settles the credit reservation for the billed live-clock seconds (releases it entirely when the stream never went live). Ending an already-ended stream returns the terminal resource unchanged.
Parameters
id(path, string) (required)
Responses
200— The ended stream with final usage. → Stream400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
Webhook endpoints
GET /v1/webhook_endpoints
List webhook endpoints (secrets never included)
Responses
200— List envelope. → WebhookEndpointList400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
POST /v1/webhook_endpoints
Create a webhook endpoint (returns the one-time whsec_ secret)
Standard Webhooks signing (webhook-id / webhook-timestamp / webhook-signature). The secret appears ONLY in this response. Delivery contract: 10s timeout, retries 1m/5m/30m/2h/8h/24h with jitter, at-least-once, no ordering — dedupe on the evt_ id. Endpoints failing 100% for 72h are auto-disabled with an email notice. At most 10 endpoints per account.
Parameters
Idempotency-Key(header, string) — §16.1: first request wins; 24h replay window; same key + different body → 409 idempotency_key_reused.
Request body — CreateWebhookEndpointRequest
Responses
201— Endpoint created.secretis returned exactly once. → WebhookEndpoint400— validation_failed / webhook_endpoint_limit (10 per account). → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
GET /v1/webhook_endpoints/{id}
Fetch a webhook endpoint (secret never included)
Parameters
id(path, string) (required) — Webhook endpoint id (we_…).
Responses
200— The endpoint. → WebhookEndpoint400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
DELETE /v1/webhook_endpoints/{id}
Delete a webhook endpoint (effective immediately, §17)
Parameters
id(path, string) (required) — Webhook endpoint id (we_…).
Responses
200— {id, object, deleted: true}.400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
GET /v1/webhook_endpoints/{id}/deliveries
List recent deliveries (last 100, with response codes)
Dead-lettered deliveries stay visible for 30 days. Deliveries carry the evt_ id (stable across retries), attempt count, last response status/error, and next retry time.
Parameters
id(path, string) (required) — Webhook endpoint id (we_…).
Responses
200— List envelope (newest first). → WebhookDeliveryList400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
POST /v1/webhook_endpoints/{id}/deliveries/{delivery_id}/redeliver
Redeliver a delivery now (works on failed/dead rows)
Parameters
id(path, string) (required) — Webhook endpoint id (we_…).delivery_id(path, string) (required) — Delivery id (whd_…).
Responses
202— Requeued for immediate delivery. → WebhookDelivery400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
POST /v1/webhook_endpoints/{id}/enable
Re-enable a disabled endpoint (clears auto-disable state)
Parameters
id(path, string) (required) — Webhook endpoint id (we_…).
Responses
200— The re-enabled endpoint. → WebhookEndpoint400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
POST /v1/webhook_endpoints/{id}/rotate_secret
Rotate the signing secret (24h dual-secret overlap)
Returns the NEW secret exactly once. The previous secret keeps signing deliveries for 24h so receivers can migrate without dropped verifications — the signature header carries one signature per active secret.
Parameters
id(path, string) (required) — Webhook endpoint id (we_…).
Responses
200— Endpoint with the new one-timesecret. → WebhookEndpoint400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
Client tokens
POST /v1/client_tokens
Mint an ephemeral browser-safe client token (pvct_)
Server-side token exchange (§4.9): authenticate with a secret key (or first-party session), receive a short-lived scoped pvct_ bearer for the browser. The browser then calls Primate directly — your secret key never leaves your server. Tokens are revoked implicitly when the minting key is revoked (≤5s). Rate class: 120/min per key. Client tokens can never mint tokens.
Request body — CreateClientTokenRequest
Responses
201— Token minted. The raw token appears only in this response. → ClientToken400— validation_failed — scopes/ttl_s/binding violations, all listed at once. → ErrorEnvelope403— insufficient_scope — requested scopes exceed the minting key’s, or the caller is itself a client token. → ErrorEnvelope404— resource_not_found — video_id binding does not resolve to an owned video. → ErrorEnvelope429— rate_limit_exceeded — client_tokens.create is 120/min per key. → ErrorEnvelope
Billing
GET /v1/credits
Credit balance and transaction history
Agent-facing billing truth (PRI-480 A-5): remaining balance in billable seconds plus a paginated ledger. Check this before submitting work to avoid a 402. Requires the billing:read scope.
Parameters
limit(query, integer)before(query, string)
Responses
200— Balance plus one page of ledger entries. → CreditsResponse400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
Usage
GET /v1/usage
Usage meters (credit balance, period consumption)
Responses
200— Meters + legacy fields. Same numbers as the dashboard (shared service, reconciles with the credit ledger to the second). → UsageResponse400— validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope401— invalid_api_key / key_revoked / key_expired → ErrorEnvelope404— resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope409— resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope429— rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
Meta & platform
GET /v1/credit-pricing
Current credit pricing (public, no auth)
Responses
200— Pricing config. Billing unit is source-clock video-seconds; analysis cost = usage.billed_seconds × price_per_second_cents. → CreditPricing
GET /v1/errors
Machine-readable error-code registry (§15)
Responses
200— Closed registry — codes are append-only; SDK retry policy generates from retryable/idem flags. → object
GET /v1/models
Responses
200— Model catalog. Requests may say model:"latest"; responses always name the concrete version. → object
POST /v1/parse
Compile a free-text prompt into a CompiledQuery (no analysis created)
Stateless prompt compiler. Auth optional (anonymous allowed); rate-limited to 30 req/min per IP. Never touches the GPU and never burns credits — use POST /v1/analyses with validate_only:true when you also want cost/assessability estimates.
Request body — ParseRequest
Responses
200— The compiled query and which parser produced it. → ParseResponse400— empty_prompt — prompt missing, not a string, or blank. → ErrorEnvelope422— parse_failed — prompt could not be compiled into a query. → ErrorEnvelope429— rate_limit_exceeded (30 req/min per IP). → ErrorEnvelope503— parse_unavailable — parse service temporarily unavailable. → ErrorEnvelope
Object schemas
Every named object in the contract. Field types link to their schema.
Analysis
artifacts— Artifacts (required) — Signed URL TTL 1h; re-fetch GET analysis for a fresh URL.completed_at— string,null (date-time) (required)created_at— string (date-time) (required)error— ErrorObject (required)id— string (required) — Analysis resource id (prefixed ULID).livemode— boolean (required) — true when the request used a live key; false for test keys and sandbox. Stripe-style.metadata— object — Free-form key/value metadata. ≤16 keys, key ≤40 chars [a-zA-Z0-9_.-], value ≤500 chars.model— string (required)narrative— Narrative (required)object—"analysis"(required)options— object (required)origin—"api"·"console"·"system"(required) —api— submitted via the public API;console— uploaded via the dashboard;system— generated internally (e.g. auto-probe, fixture seeding, run_prompt). System analyses are never billed. Agents should normally only process api/console-origin analyses. (Renamed 2026-07-27: former valueuseris nowapi.)parse_mode—"llm"·"heuristic"·"client"(required)progress— Progress (required) — Non-terminal only. percent is monotonic non-decreasing per analysis.prompt— string,null (required)query— object (required) — CompiledQuery (schema at /v1/schemas/compiled-query.json). Always populated in responses.queue_position— integer,null (required)result— Result (required) — Analysis result — the product (§13.3). Reserved (always absent in v1): masks, bounding_boxes, events.started_at— string,null (date-time) (required)status— AnalysisStatus (required) — Analysis lifecycle state (§14.2). Clients MUST tolerate unknown future non-terminal values.usage— AnalysisUsage (required) — Terminal only. fps-independent clock seconds (pricing doc). credit_balance_after is the balance immediately after THIS analysis settled — an immutable point-in-time snapshot; it does not change as later analyses run. Null when the account row is unavailable (e.g. free-grant exhausted before settlement).video_id— string (required) — Video resource id (prefixed ULID). Immutable.
AnalysisBatch
- analyses — array<Analysis> (required)
id— string (required) — Stable batch identifier.object—"analysis_batch"(required)pricing— AnalysisBatchPricing (required)video_id— string (required) — Video resource id (prefixed ULID). Immutable.
AnalysisBatchPreview
- object — "analysis_batch_preview" (required)
pricing— AnalysisBatchPricing (required)prompts— array<AnalysisBatchPreviewPrompt> (required)video_id— string (required) — Video resource id (prefixed ULID). Immutable.
AnalysisBatchPreviewPrompt
- assessable — boolean (required) — True when the query type can produce a yes/no/count answer.
discount_pct—0|50(required) — 0 for the first (full-price) prompt; 50 for additional prompts.estimated_cost_usd— number,null (required) — Estimated cost in USD. Null when video has no known duration.estimated_seconds— integer,null (required) — Estimated credit-seconds for this prompt. Null when video has no known duration.index— integer (required) — Zero-based prompt index.parse_mode—"heuristic"·"llm"·"client"(required)query— object (required) — Compiled query structure.
AnalysisBatchPricing
- discount_pct — integer (required)
discounted_prompts— integer (required)estimated_total_cost_usd— number,null (required) — Estimated total cost in USD. Null when duration unknown.estimated_total_seconds— integer,null (required) — Estimated total credit-seconds for all prompts combined (full + discounted). Null when the video has no known duration.full_price_prompts— integer (required)
AnalysisList
- data — array<Analysis> (required)
has_more— boolean (required)object—"list"(required)
AnalysisPreview
- assessable — boolean (required) — False for open_ended queries — the model cannot score them, so detections will be empty.
estimated_cost_usd— number,null (required) — Estimated cost at current pricing. Null when duration is unknown.estimated_seconds— integer,null (required) — Billable seconds this analysis would consume. Null when duration is unknown.object—"analysis_preview"(required)parse_mode—"llm"·"heuristic"·"client"(required) — How the prompt was compiled. "client" means you supplied query directly.query— object (required) — The compiled query the analysis would run.video_duration_s— number,null (required) — Null when duration is not yet known (probe pending).
AnalysisStatus
Analysis lifecycle state (§14.2). Clients MUST tolerate unknown future non-terminal values.
Enum: "queued" · "preparing" · "analyzing" · "rendering" · "completed" · "failed" · "canceled"
AnalysisUsage
Terminal only. fps-independent clock seconds (pricing doc). credit_balance_after is the balance immediately after THIS analysis settled — an immutable point-in-time snapshot; it does not change as later analyses run. Null when the account row is unavailable (e.g. free-grant exhausted before settlement).
billed_seconds— integer (required)credit_balance_after— integer,null (required)
Artifacts
Signed URL TTL 1h; re-fetch GET analysis for a fresh URL.
annotated_video_url— string (uri) (required)expires_at— string (date-time) (required)
ClientToken
- expires_at — string (required)
id— string (required)object—"client_token"(required)scopes— array(required) stream_id— stringtoken— string (required) — 256-bit CSPRNG bearer, SHA-256 at rest — shown exactly once. Browser-safe: accepted on product-plane endpoints (CORS *).video_id— string — Video resource id (prefixed ULID). Immutable.
Clip
- confidence — number (required)
end_s— number (required)start_s— number (required)terms— object (required)
CreateAnalysisBatchRequest
- metadata — object — Free-form key/value metadata. ≤16 keys, key ≤40 chars [a-zA-Z0-9_.-], value ≤500 chars.
model— stringoptions— objectprompts— array(required) — 2–10 prompts to run against the video. First prompt is full price; each additional is 50%. validate_only— boolean — Dry-run mode. When true, parses all prompts and returns an analysis_batch_preview (HTTP 200) with per-prompt cost estimates. No credits are reserved and no jobs are created.video_id— string (required) — Video resource id (prefixed ULID). Immutable.
CreateAnalysisRequest
- metadata — object — Free-form key/value metadata. ≤16 keys, key ≤40 chars [a-zA-Z0-9_.-], value ≤500 chars.
model— string — Defaults to latest; response always echoes the concrete version.options— objectprompt— string — Free text — compiled server-side. Exactly one of prompt/query.query— object — Structured CompiledQuery JSON (schema at /v1/schemas/compiled-query.json).validate_only— boolean — Dry run (PRI-480). Compiles the prompt, reports whether the query is assessable, and estimates duration/cost — WITHOUT creating an analysis, touching the GPU, or burning credits. Returns an AnalysisPreview (object: "analysis_preview") with 200, not an Analysis. Use it to check a prompt before paying for it.video_id— string (required) — Video resource id (prefixed ULID). Immutable.webhook— object — Per-request webhook override (delivery ships P7). Write-only — never echoed.
CreateClientTokenRequest
- scopes — array<"videos:write" · "analyses:read" · "analyses:write" · "streams:create" · "streams:signal"> (required) — Non-empty subset of the §4.9 scope set. Must also be a subset of the minting key’s scopes.
stream_id— string — Optional resource binding to one stream (WS acceptance ships P6).ttl_s— integer — Token lifetime in seconds (60–900, default 900). Tokens are never refreshable — mint a new one.video_id— string — Optional resource binding: narrows the token to this one video. Mutually exclusive with stream_id.
CreateStreamRequest
Requires positive credit balance. One queued|ready|live stream per account by default (409 stream_already_active otherwise).
metadata— object — Free-form key/value metadata. ≤16 keys, key ≤40 chars [a-zA-Z0-9_.-], value ≤500 chars.model— stringprompt— string — Free text — compiled server-side. Exactly one of prompt/query.query— object — Structured CompiledQuery JSON.webhook— object — Per-request webhook override (delivery ships P7). Write-only.
CreateVideoRequest
Exactly one mode: presigned ({filename?, content_type, size_bytes?}) or ingest ({url}). Unknown fields are rejected.
content_type—"video/mp4"·"video/quicktime"— Required in presigned mode.filename— stringmetadata— object — Free-form key/value metadata. ≤16 keys, key ≤40 chars [a-zA-Z0-9_.-], value ≤500 chars.size_bytes— integerurl— string (uri) — URL-ingest mode: https-only, SSRF-guarded (§18.2). Mutually exclusive with filename/size_bytes.
CreateWebhookEndpointRequest
events— array<WebhookEventType> (required) — Subscribed event types — a non-empty subset of the closed v1 vocabulary.url— string (uri) (required) — HTTPS delivery URL.
CreditPricing
Current pricing configuration. Data-driven — a price change is a config update, not a schema change.
allowed_purchase_cents— array(required) — Preset purchase amounts, in cents. auto_refill_threshold_seconds— integer (required) — Balance below which auto-refill (if enabled) triggers.card_grant_seconds— integer (required) — Bonus credit-seconds granted when a card is added.custom_purchase_enabled— boolean (required)min_purchase_cents— integer (required)price_per_second_cents— number (required) — Price per billed video-second, in cents. Cost of an analysis = usage.billed_seconds × this value.signup_grant_seconds— integer (required) — Free credit-seconds granted on signup / free-grant upgrade.
CreditTransaction
- balance_after_seconds — number (required)
created_at— string (required)id— string (required)kind— string (required) — e.g. signup_grant, analysis_debit, stream_debit, purchase.seconds_delta— number (required) — Signed: negative debits, positive credits.source_id— string,null (required)source_type— string,null (required)
CreditsResponse
- balance_seconds — number (required) — Seconds of analysis remaining. The authoritative balance.
grant_seconds— number,null (required) — Original signup grant, when one exists.has_more— boolean (required)object—"credits"(required)transactions— array<CreditTransaction> (required)used_seconds— number,null (required) — Consumed against the signup grant (grant minus balance).
DeletedVideo
id— string (required) — Video resource id (prefixed ULID). Immutable.object—"video"(required)
DeviceCodeRequest
- claim_url — string (uri) (required) — Open in browser for the user to approve.
device_code— string (required) — Opaque code; use as path param when polling.expires_at— string (date-time) (required)instructions— stringobject—"device_code_request"(required)poll_interval— integer (required) — Seconds between polls.
DeviceCodeStatus
- object — "device_code_request" (required)
status—"pending"·"consumed"(required)
ErrorEnvelope
ErrorObject
message— string (required)
ErrorRegistryEntry
docs_url— string (uri) (required)idem— boolean (required)kind—"http"·"resource"(required)message— string (required)retryable— boolean (required)status— integer,null (required)
Meter
One named usage meter (§4.6). The list is data-driven — pricing changes add/change rows, never the schema.
balance— number — Balance-shaped meters (e.g. credit_seconds).limit— number,null — null = unlimited/PAYG.meter— string (required)resets_at— string (date-time) — Omitted for non-period meters.unit— string (required)used— number — Period-shaped meters.
Model
- capabilities — object (required)
created_at— string (required)default— boolean (required)id— string (required)object—"model"(required)status—"stable"·"preview"·"deprecated"(required)sunset_at— string,null (required)
Narrative
status—"generating"·"ready"·"failed"(required)
ParseRequest
- prompt — string (required) — Free-text video question. Must be a non-empty string.
ParseResponse
- compiled_query — object (required) — CompiledQuery — canonical JSON Schema served at /v1/schemas/compiled-query.json. Same structure accepted by POST /v1/analyses query.
parse_mode—"haiku"·"heuristic"(required) — How the prompt was compiled: "haiku" = LLM parser; "heuristic" = deterministic fallback (used when the LLM is unavailable).
Progress
Non-terminal only. percent is monotonic non-decreasing per analysis.
percent— number (required)stage— string (required)
Result
Analysis result — the product (§13.3). Reserved (always absent in v1): masks, bounding_boxes, events.
answer—"yes"·"no"·"indeterminate"(required)clips— array<Clip> (required)confidence— number (required)detected_count— integer,nullindeterminate_reason—"low_confidence"·"nothing_detected"·"unsupported_query_form"·"duration_mismatch"query_type—"object"·"action"·"compound"·"attribute"·"open_ended"(required)term_confidences— object (required)video_duration_s— number,null (required)
SandboxKey
- api_key — string (required) — Shown exactly once — store it now. Fixture-only test mode: no GPU, no credits.
docs_url— string (required)expected_answer— string (required) — Deterministic canned answer for the fixture prompt — assert this in CI.expires_at— string (required) — Keys expire 7 days after issuance.fixture_prompt— string (required)fixture_video_id— string,null (required) — Pre-seeded ready video for immediate POST /v1/analyses.mode—"test"(required)note— string (required)object—"sandbox_key"(required)
Stream
A real-time analysis stream (§4.8).
created_at— string (date-time) (required)duration_s— integer,null (required) — Live-clock duration. Terminal only.end_reason— StreamEndReason (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 nevercompleted.ended_at— string,null (date-time) (required)estimated_start_s— integer,null (required)failure_diagnostic— object,null (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".ice_servers— array,null (required)id— string (required) — Stream resource id (prefixed ULID). P6 §4.8.limits— StreamLimits (required)live_started_at— string,null (date-time) (required)metadata— object — Free-form key/value metadata. ≤16 keys, key ≤40 chars [a-zA-Z0-9_.-], value ≤500 chars.model— string (required)object—"stream"(required)prompt— string,null (required)query— object,null (required)queue_position— integer,null (required)results_summary— object,null (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).signaling— StreamSignaling (required) — Present while the stream is joinable; null once ended.status— StreamStatus (required) — Stream lifecycle state (§4.8): queued → ready → live → ended. Terminal immutable.usage— StreamUsage (required) — Terminal only. Same ledger as uploads — one balance.
StreamEndReason
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.
Enum: "completed" · "canceled" · "insufficient_credits" · "error" · "timeout" · "ice_failed" · "media_timeout"
StreamLimits
- max_session_s — integer (required) — Plan entitlement (max_session_s) — hard session cutoff.
warn_at_remaining_s— integer (required) — Thewarning {remaining_s}threshold surfaced mid-stream.
StreamList
- data — array<Stream> (required)
has_more— boolean (required)object—"list"(required)
StreamSignaling
Present while the stream is joinable; null once ended.
expires_at— string (date-time) (required)url— string (required) — Versioned signaling WS. Auth: ?token= accepts a client token (pvct_) or first-party Clerk JWT — NEVER a secret key.
StreamStatus
Stream lifecycle state (§4.8): queued → ready → live → ended. Terminal immutable.
Enum: "queued" · "ready" · "live" · "ended"
StreamUsage
Terminal only. Same ledger as uploads — one balance.
billed_seconds— integer (required) — LIVE-clock seconds (join/negotiation free). fps-independent by construction.credit_balance_after— integer,null (required)
UpgradeKeyResponse
- api_key — string (required) — The raw pv_live_ key — store securely, shown only once.
claim_flow— objectexpires_at— string (date-time) (required)grant_seconds— integer (required)livemode—true(required)note— stringobject—"api_key"(required)tier—"free_grant"(required)
UsageResponse
Usage meters + legacy dashboard fields (the flat fields are deprecated in favor of meters[]; they remain during the web-client migration).
meters— array<Meter> (required) — Data-driven meter list (§4.6 invariant #1). At launch: credit_seconds (balance) + seconds_processed.period (used/resets_at). Pricing changes add/change rows — never the schema. quota_exceeded errors carry details.meter naming which meter tripped.
Video
content_type—"video/mp4"·"video/quicktime"(required)created_at— string (date-time) (required)duration_s— number,null (required)error— ErrorObject (required)expires_at— string,null (date-time) (required)filename— string,null (required)fps— number,null (required)height— integer,null (required)id— string (required) — Video resource id (prefixed ULID). Immutable.livemode— boolean (required) — true when the request used a live key; false for test keys and sandbox. Stripe-style.metadata— object — Free-form key/value metadata. ≤16 keys, key ≤40 chars [a-zA-Z0-9_.-], value ≤500 chars.object—"video"(required)size_bytes— integer,null (required)source—"upload"·"url"(required)status— VideoStatus (required) — Video lifecycle state (§14.1).upload— VideoUpload (required) — Presigned upload instructions. Present only while awaiting_upload. URL TTL 1h.width— integer,null (required)
VideoList
- data — array<Video> (required)
has_more— boolean (required)object—"list"(required)
VideoStatus
Video lifecycle state (§14.1).
Enum: "awaiting_upload" · "processing" · "ready" · "failed"
VideoUpload
Presigned upload instructions. Present only while awaiting_upload. URL TTL 1h.
expires_at— string (date-time) (required)headers— object (required)method—"PUT"(required)url— string (uri) (required)
WebhookDelivery
One event→endpoint delivery record (§16.3). Dead-lettered rows visible 30 days.
attempt_count— integer (required)created_at— string (date-time) (required)delivered_at— string,null (date-time) (required)event_id— string (required) — Stable across retries — consumers dedupe on this.event_type— WebhookEventType (required) — Closed v1 event vocabulary (§4.5). Payload = the full resource DTO.id— string (required) — Webhook delivery id (prefixed ULID).last_attempt_at— string,null (date-time) (required)last_error— string,null (required)last_response_status— integer,null (required)next_attempt_at— string,null (date-time) (required) — Null once delivered or dead-lettered.object—"webhook_delivery"(required)status—"pending"·"delivering"·"delivered"·"failed"·"dead"(required)
WebhookDeliveryList
- data — array<WebhookDelivery> (required)
has_more— boolean (required)object—"list"(required)
WebhookEndpoint
A customer webhook endpoint (§4.5). Standard Webhooks signing.
created_at— string (date-time) (required)description— string,null (required)disabled_reason—"auto_failure"·"user"(required) — auto_failure = 72h of 100% delivery failure (§16.3). Re-enable via POST …/enable.events— array<WebhookEventType> (required)id— string (required) — Webhook endpoint id (prefixed ULID).last_success_at— string,null (date-time) (required)object—"webhook_endpoint"(required)secret— string — Signing secret — returned EXACTLY ONCE (create / rotate_secret), never readable again.status—"enabled"·"disabled"(required)url— string (uri) (required)
WebhookEndpointList
- data — array<WebhookEndpoint> (required)
has_more— boolean (required)object—"list"(required)
WebhookEventType
Closed v1 event vocabulary (§4.5). Payload = the full resource DTO.
Enum: "video.ready" · "video.failed" · "analysis.completed" · "analysis.failed" · "analysis.canceled" · "stream.ended"