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

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) — The device_code from POST /v1/keys/request.

Responses

  • 200 — Current status of the device code. → DeviceCodeStatus
  • 404 — resource_not_found — code not found. → ErrorEnvelope
  • 410 — 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 raw api_key appears only in this response. → UpgradeKeyResponse
  • 403 — test_mode_only — must call with a pv_test_ key. → ErrorEnvelope
  • 429 — 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. → SandboxKey
  • 429 — sandbox_limit_exceeded — IP daily cap reached. Retry-After set. → ErrorEnvelope
  • 503 — 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). → VideoList
  • 400 — validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — 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 bodyCreateVideoRequest

Responses

  • 201 — Video created. Presigned mode → status awaiting_upload with upload instructions; URL mode → status processing (async fetch). → Video
  • 400 — validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope

GET /v1/videos/{id}

Fetch a video

Parameters

  • id (path, string) (required)

Responses

  • 200 — The video. → Video
  • 400 — validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — 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. → DeletedVideo
  • 400 — validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — 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. → Video
  • 400 — validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — 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). → AnalysisList
  • 400 — validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — 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 bodyCreateAnalysisRequest

Responses

  • 200 — Terminal resource (Prefer: wait satisfied, or test mode) — OR an AnalysisPreview when validate_only:true was sent. Discriminate on the object field. → Analysis | AnalysisPreview
  • 202 — Queued. Response echoes the compiled query and honest queue_position. → Analysis
  • 400 — validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 402 — insufficient_credits (details.meter = credit_seconds) → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
  • 503 — 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 bodyCreateAnalysisBatchRequest

Responses

  • 200 — Dry-run preview (validate_only: true). Prompts parsed; cost estimated; no credits reserved, no jobs created. → AnalysisBatchPreview
  • 202 — Batch queued. All analyses dispatched; poll each id individually. → AnalysisBatch
  • 400 — validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 402 — insufficient_credits / grant_exhausted → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
  • 503 — 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. → Analysis
  • 400 — validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — 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). → Analysis
  • 400 — validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — 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). → StreamList
  • 400 — validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — 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 bodyCreateStreamRequest

Responses

  • 201 — Stream created (queued or ready). Response carries signaling.url + ice_servers + limits. → Stream
  • 400 — validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 402 — insufficient_credits — positive balance required. → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — stream_already_active — one queued|ready|live stream per account (also the un-keyed-retry guard, §31-F2) / idempotency_key_reused. → ErrorEnvelope
  • 429 — rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope
  • 503 — 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. → Stream
  • 400 — validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — rate_limit_exceeded / concurrency_limit_exceeded / quota_exceeded (always with Retry-After) → ErrorEnvelope

POST /v1/streams/{id}/end

End a stream (idempotent)

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. → Stream
  • 400 — validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — 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. → WebhookEndpointList
  • 400 — validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — 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 bodyCreateWebhookEndpointRequest

Responses

  • 201 — Endpoint created. secret is returned exactly once. → WebhookEndpoint
  • 400 — validation_failed / webhook_endpoint_limit (10 per account). → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — 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. → WebhookEndpoint
  • 400 — validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — 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) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — 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). → WebhookDeliveryList
  • 400 — validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — 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. → WebhookDelivery
  • 400 — validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — 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. → WebhookEndpoint
  • 400 — validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — 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-time secret. → WebhookEndpoint
  • 400 — validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — 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 bodyCreateClientTokenRequest

Responses

  • 201 — Token minted. The raw token appears only in this response. → ClientToken
  • 400 — validation_failed — scopes/ttl_s/binding violations, all listed at once. → ErrorEnvelope
  • 403 — insufficient_scope — requested scopes exceed the minting key’s, or the caller is itself a client token. → ErrorEnvelope
  • 404 — resource_not_found — video_id binding does not resolve to an owned video. → ErrorEnvelope
  • 429 — 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. → CreditsResponse
  • 400 — validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — 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). → UsageResponse
  • 400 — validation_failed / url_forbidden / prompt errors (§15) → ErrorEnvelope
  • 401 — invalid_api_key / key_revoked / key_expired → ErrorEnvelope
  • 404 — resource_not_found (account-scoped; no existence oracle) → ErrorEnvelope
  • 409 — resource_conflict / idempotency_key_reused / idempotency_unavailable → ErrorEnvelope
  • 429 — 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

List available 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 bodyParseRequest

Responses

  • 200 — The compiled query and which parser produced it. → ParseResponse
  • 400 — empty_prompt — prompt missing, not a string, or blank. → ErrorEnvelope
  • 422 — parse_failed — prompt could not be compiled into a query. → ErrorEnvelope
  • 429 — rate_limit_exceeded (30 req/min per IP). → ErrorEnvelope
  • 503 — parse_unavailable — parse service temporarily unavailable. → ErrorEnvelope

Object schemas

Every named object in the contract. Field types link to their schema.

Analysis

A video analysis (§13.2).

  • artifactsArtifacts (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)
  • errorErrorObject (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)
  • narrativeNarrative (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 value user is now api.)
  • parse_mode"llm" · "heuristic" · "client" (required)
  • progressProgress (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)
  • resultResult (required) — Analysis result — the product (§13.3). Reserved (always absent in v1): masks, bounding_boxes, events.
  • started_at — string,null (date-time) (required)
  • statusAnalysisStatus (required) — Analysis lifecycle state (§14.2). Clients MUST tolerate unknown future non-terminal values.
  • usageAnalysisUsage (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)
  • pricingAnalysisBatchPricing (required)
  • video_id — string (required) — Video resource id (prefixed ULID). Immutable.

AnalysisBatchPreview

- object"analysis_batch_preview" (required)

AnalysisBatchPreviewPrompt

- assessable — boolean (required) — True when the query type can produce a yes/no/count answer.

  • discount_pct0 | 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 — string
  • token — 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 — string
  • options — object
  • prompts — 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 — object
  • prompt — 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 — string
  • prompt — 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 — string
  • metadata — object — Free-form key/value metadata. ≤16 keys, key ≤40 chars [a-zA-Z0-9_.-], value ≤500 chars.
  • size_bytes — integer
  • url — string (uri) — URL-ingest mode: https-only, SSRF-guarded (§18.2). Mutually exclusive with filename/size_bytes.

CreateWebhookEndpointRequest

- description — string

  • 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

- deletedtrue (required)

  • 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 — string
  • object"device_code_request" (required)
  • poll_interval — integer (required) — Seconds between polls.

DeviceCodeStatus

- object"device_code_request" (required)

  • status"pending" · "consumed" (required)

ErrorEnvelope

- error — object (required)

ErrorObject

- code — string (required)

  • message — string (required)

ErrorRegistryEntry

- code — string (required)

  • 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

- entries — array (required)

  • 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,null
  • indeterminate_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_reasonStreamEndReason (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.
  • 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.
  • limitsStreamLimits (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).
  • signalingStreamSignaling (required) — Present while the stream is joinable; null once ended.
  • statusStreamStatus (required) — Stream lifecycle state (§4.8): queued → ready → live → ended. Terminal immutable.
  • usageStreamUsage (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) — The warning {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 — object
  • expires_at — string (date-time) (required)
  • grant_seconds — integer (required)
  • livemodetrue (required)
  • note — string
  • object"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

A source video (§13.1).

  • content_type"video/mp4" · "video/quicktime" (required)
  • created_at — string (date-time) (required)
  • duration_s — number,null (required)
  • errorErrorObject (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)
  • statusVideoStatus (required) — Video lifecycle state (§14.1).
  • uploadVideoUpload (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_typeWebhookEventType (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"

Prefer raw text? This page is available as markdown.