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/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

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 or card required. GitHub-gated (PRI-479): calling without a github_token returns 403 github_verification_required whose details carry the OAuth client_id and device-flow URLs — complete GitHub's device flow and retry with {"github_token": "..."}. One free grant per GitHub account, ever (duplicate → 409 github_account_already_used). IP caps (3 / IP / 24h) remain as secondary defense.

Request bodyUpgradeKeyRequest

Responses

  • 201 — Live key issued. The raw api_key appears only in this response. → UpgradeKeyResponse
  • 401 — github_token_invalid — the supplied github_token failed verification with GitHub. → ErrorEnvelope
  • 403 — test_mode_only — must call with a pv_test_ key. github_verification_required — GitHub gate enabled and no github_token supplied; details carry the device-flow bootstrap (client_id, URLs). → ErrorEnvelope
  • 409 — github_account_already_used — this GitHub account has already claimed its free grant. → ErrorEnvelope
  • 429 — upgrade_limit_exceeded — per-IP or global daily cap reached. → ErrorEnvelope

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

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

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

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

GET /v1/videos/{id}/analyses

List analyses for a video (result history)

PRI-496: resource-nested spelling of GET /v1/analyses?video_id={id}. Same list envelope, same filters (status, limit, starting_after), ordered created_at desc. Use it to rebuild a video's full analysis/session history.

Parameters

  • id (path, string) (required)
  • 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 of analyses referencing this video (§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

GET /v1/demo-videos

Public sample videos with precomputed results (no auth)

PRI-496 GAP-8: onboarding samples — curated videos with precomputed answers for a set of example prompts. Unauthenticated. Use these to see real result shapes before uploading your own video. The documented subset above is the stable contract; extra fields may appear and change without notice.

Responses

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

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}/rerun

Free re-run of a failed analysis (never billed)

PRI-493: when a failed analysis carries rerun_eligible: true (it failed during a platform incident, or was flagged by ops), POST here creates a FRESH analysis — same video, prompt/query, model, and options — at no charge (usage stays null on the re-run). One free re-run per failed analysis. 409 rerun_not_eligible otherwise. Normal capacity gates still apply.

Parameters

  • id (path, string) (required)

Responses

  • 202 — Fresh analysis queued (new id). Never billed. → 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
  • 503 — inference_unavailable / capacity_exhausted (with Retry-After) — the free re-run is NOT consumed on platform failure. → 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

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% (a 50% discount — config-driven; read the live value from GET /v1/credit-pricing batch_discount_pct). 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

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

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

GET /v1/streams/{id}/recording

Fetch the session recording (signed URL, sign-on-read)

PRI-496: returns a signed playback URL for the server-side session recording. Requires the stream to have been created with recording: true and to have ended with a stored recording. 404 resource_not_found when recording was not enabled; 409 resource_conflict while the stream is still active, when capture failed, or when no recording was stored. URL TTL 1h — re-fetch for a fresh one.

Parameters

  • id (path, string) (required)

Responses

  • 200 — Signed recording URL. → StreamRecording
  • 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

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

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

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

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/models

List available models

Responses

  • 200 — Model catalog. Requests may say model:"latest"; responses always name the concrete version. → object

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

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

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

Object schemas

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

VideoStatus

Video lifecycle state (§14.1).

Enum: "awaiting_upload" · "processing" · "ready" · "failed"

VideoUpload

Presigned upload instructions. Present only while awaiting_upload. URL TTL 1h.

  • method"PUT" (required)
  • url — string (uri) (required)
  • headers — object (required)
  • expires_at — string (date-time) (required)

VideoMedia

Signed 1h playback URL for the source video (sign-on-read — re-fetch for a fresh URL). Null while not ready or when no stored source object exists.

  • url — string (uri) (required)
  • expires_at — string (date-time) (required)

ErrorObject

- code — string (required)

  • message — string (required)

Video

A source video (§13.1).

  • id — string (required) — Video resource id (prefixed ULID). Immutable.
  • object"video" (required)
  • livemode — boolean (required) — true when the request used a live key; false for test keys and sandbox. Stripe-style.
  • statusVideoStatus (required) — Video lifecycle state (§14.1).
  • filename — string,null (required)
  • size_bytes — integer,null (required)
  • duration_s — number,null (required)
  • width — integer,null (required)
  • height — integer,null (required)
  • fps — number,null (required)
  • content_type"video/mp4" · "video/quicktime" (required)
  • source"upload" · "url" (required)
  • uploadVideoUpload (required) — Presigned upload instructions. Present only while awaiting_upload. URL TTL 1h.
  • mediaVideoMedia — Signed 1h playback URL for the source video (sign-on-read — re-fetch for a fresh URL). Null while not ready or when no stored source object exists.
  • errorErrorObject (required)
  • metadata — object — Free-form key/value metadata. ≤16 keys, key ≤40 chars [a-zA-Z0-9_.-], value ≤500 chars.
  • expires_at — string,null (date-time) (required)
  • created_at — string (date-time) (required)

AnalysisStatus

Analysis lifecycle state (§14.2). Clients MUST tolerate unknown future non-terminal values.

Enum: "queued" · "preparing" · "analyzing" · "rendering" · "completed" · "failed" · "canceled"

Progress

Non-terminal only. percent is monotonic non-decreasing per analysis.

  • stage — string (required)
  • percent — number (required)

Clip

- start_s — number (required)

  • end_s — number (required)
  • confidence — number (required)
  • terms — object (required)

Result

Analysis result — the product (§13.3). Reserved (always absent in v1): masks, bounding_boxes, events.

  • answer"yes" · "no" · "indeterminate" (required)
  • confidence — number (required)
  • clips — array<Clip> (required)
  • term_confidences — object (required)
  • query_type"object" · "action" · "compound" · "attribute" · "open_ended" (required)
  • video_duration_s — number,null (required)
  • detected_count — integer,null
  • indeterminate_reason"low_confidence" · "nothing_detected" · "unsupported_query_form" · "duration_mismatch"

Narrative

- status"generating" · "ready" · "failed" (required)

  • entries — array (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)

    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)

    Analysis

    A video analysis (§13.2).

    • id — string (required) — Analysis resource id (prefixed ULID).
    • object"analysis" (required)
    • livemode — boolean (required) — true when the request used a live key; false for test keys and sandbox. Stripe-style.
    • 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.)
    • statusAnalysisStatus (required) — Analysis lifecycle state (§14.2). Clients MUST tolerate unknown future non-terminal values.
    • video_id — string (required) — Video resource id (prefixed ULID). Immutable.
    • prompt — string,null (required)
    • query — object (required) — CompiledQuery (schema at /v1/schemas/compiled-query.json). Always populated in responses.
    • parse_mode"llm" · "heuristic" · "client" (required)
    • model — string (required)
    • options — object (required)
    • progressProgress (required) — Non-terminal only. percent is monotonic non-decreasing per analysis.
    • queue_position — integer,null (required)
    • resultResult (required) — Analysis result — the product (§13.3). Reserved (always absent in v1): masks, bounding_boxes, events.
    • narrativeNarrative (required)
    • artifactsArtifacts (required) — Signed URL TTL 1h; re-fetch GET analysis for a fresh URL.
    • errorErrorObject (required)
    • 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).
    • rerun_eligible — boolean — PRI-493: present on failed analyses — true when this analysis qualifies for ONE free re-run (it failed during a platform incident, or was flagged by ops). POST /v1/analyses/{id}/rerun creates a fresh analysis at no charge. Absent on non-failed analyses.
    • metadata — object — Free-form key/value metadata. ≤16 keys, key ≤40 chars [a-zA-Z0-9_.-], value ≤500 chars.
    • created_at — string (date-time) (required)
    • started_at — string,null (date-time) (required)
    • completed_at — string,null (date-time) (required)

    StreamStatus

    Stream lifecycle state (§4.8): queued → ready → live → ended. Terminal immutable.

    Enum: "queued" · "ready" · "live" · "ended"

    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"

    StreamSignaling

    Present while the stream is joinable; null once ended.

    • url — string (required) — Versioned signaling WS. Auth: ?token= accepts a client token (pvct_) or first-party Clerk JWT — NEVER a secret key.
    • expires_at — string (date-time) (required)

    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.

    StreamRecordingState

    PRI-496: recording state — null unless the stream opted in with recording: true at create.

    • status"recording" · "available" · "failed" · "none" (required)

    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)

    Stream

    A real-time analysis stream (§4.8).

    • id — string (required) — Stream resource id (prefixed ULID). P6 §4.8.
    • object"stream" (required)
    • statusStreamStatus (required) — Stream lifecycle state (§4.8): queued → ready → live → ended. Terminal immutable.
    • 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.
    • prompt — string,null (required)
    • query — object,null (required)
    • model — string (required)
    • queue_position — integer,null (required)
    • estimated_start_s — integer,null (required)
    • signalingStreamSignaling (required) — Present while the stream is joinable; null once ended.
    • ice_servers — array,null (required)
    • limitsStreamLimits (required)
    • duration_s — integer,null (required) — Live-clock duration. Terminal only.
    • 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).
    • 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".
    • recordingStreamRecordingState — PRI-496: recording state — null unless the stream opted in with recording: true at create.
    • usageStreamUsage (required) — Terminal only. Same ledger as uploads — one balance.
    • metadata — object — Free-form key/value metadata. ≤16 keys, key ≤40 chars [a-zA-Z0-9_.-], value ≤500 chars.
    • created_at — string (date-time) (required)
    • live_started_at — string,null (date-time) (required)
    • ended_at — string,null (date-time) (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"

    WebhookEndpoint

    A customer webhook endpoint (§4.5). Standard Webhooks signing.

    • id — string (required) — Webhook endpoint id (prefixed ULID).
    • object"webhook_endpoint" (required)
    • url — string (uri) (required)
    • events — array<WebhookEventType> (required)
    • description — string,null (required)
    • status"enabled" · "disabled" (required)
    • disabled_reason"auto_failure" · "user" (required) — auto_failure = 72h of 100% delivery failure (§16.3). Re-enable via POST …/enable.
    • secret — string — Signing secret — returned EXACTLY ONCE (create / rotate_secret), never readable again.
    • last_success_at — string,null (date-time) (required)
    • created_at — string (date-time) (required)

    WebhookDelivery

    One event→endpoint delivery record (§16.3). Dead-lettered rows visible 30 days.

    • id — string (required) — Webhook delivery id (prefixed ULID).
    • object"webhook_delivery" (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.
    • status"pending" · "delivering" · "delivered" · "failed" · "dead" (required)
    • attempt_count — integer (required)
    • last_response_status — integer,null (required)
    • last_error — string,null (required)
    • next_attempt_at — string,null (date-time) (required) — Null once delivered or dead-lettered.
    • last_attempt_at — string,null (date-time) (required)
    • delivered_at — string,null (date-time) (required)
    • created_at — string (date-time) (required)

    Meter

    One named usage meter (§4.6). The list is data-driven — pricing changes add/change rows, never the schema.

    • meter — string (required)
    • unit — string (required)
    • balance — number — Balance-shaped meters (e.g. credit_seconds).
    • limit — number,null — null = unlimited/PAYG.
    • used — number — Period-shaped meters.
    • resets_at — string (date-time) — Omitted for non-period meters.

    CreditPricing

    Current pricing configuration. Data-driven — a price change is a config update, not a schema change. Batch discount included (PRI-508) — read it from here instead of hardcoding.

    • 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.
    • card_grant_seconds — integer (required) — Bonus credit-seconds granted when a card is added.
    • auto_refill_threshold_seconds — integer (required) — Balance below which auto-refill (if enabled) triggers.
    • allowed_purchase_cents — array (required) — Preset purchase amounts, in cents.
    • custom_purchase_enabled — boolean (required)
    • min_purchase_cents — integer (required)
    • batch_discount_pct — integer (required) — Batch analyses (POST /v1/analyses/batch): percent discount applied to each prompt after the first. The first prompt is always full price.
    • batch_min_prompts — integer (required) — Minimum prompts accepted per batch request.
    • batch_max_prompts — integer (required) — Maximum prompts accepted per batch request.

    ErrorEnvelope

    - error — object (required)

    CreateVideoRequest

    Exactly one mode: presigned ({filename?, content_type, size_bytes?}) or ingest ({url}). Unknown fields are rejected.

    • filename — string
    • content_type"video/mp4" · "video/quicktime" — Required in presigned mode.
    • size_bytes — integer
    • url — string (uri) — URL-ingest mode: https-only, SSRF-guarded (§18.2). Mutually exclusive with filename/size_bytes.
    • metadata — object — Free-form key/value metadata. ≤16 keys, key ≤40 chars [a-zA-Z0-9_.-], value ≤500 chars.

    VideoList

    - object"list" (required)

    • data — array<Video> (required)
    • has_more — boolean (required)

    AnalysisList

    - object"list" (required)

    • data — array<Analysis> (required)
    • has_more — boolean (required)

    DeletedVideo

    - id — string (required) — Video resource id (prefixed ULID). Immutable.

    • object"video" (required)
    • deletedtrue (required)

    AnalysisPreview

    - object"analysis_preview" (required)

    • query — object (required) — The compiled query the analysis would run.
    • parse_mode"llm" · "heuristic" · "client" (required) — How the prompt was compiled. "client" means you supplied query directly.
    • assessable — boolean (required) — False for open_ended queries — the model cannot score them, so detections will be empty.
    • video_duration_s — number,null (required) — Null when duration is not yet known (probe pending).
    • estimated_seconds — integer,null (required) — Billable seconds this analysis would consume. Null when duration is unknown.
    • estimated_cost_usd — number,null (required) — Estimated cost at current pricing. Null when duration is unknown.

    CreateAnalysisRequest

    - video_id — string (required) — Video resource id (prefixed ULID). Immutable.

    • prompt — string — Free text — compiled server-side. Exactly one of prompt/query.
    • query — object — Structured CompiledQuery JSON (schema at /v1/schemas/compiled-query.json).
    • model — string — Defaults to latest; response always echoes the concrete version.
    • options — object — narrative: true — generate a narrative (timestamped event sentences) for the analysis. Populates the narrative object on the completed analysis; generation runs asynchronously after completion (poll the GET — status generating → ready|failed). Included in the analysis price.
    • metadata — object — Free-form key/value metadata. ≤16 keys, key ≤40 chars [a-zA-Z0-9_.-], value ≤500 chars.
    • webhook — object — Per-request webhook override (delivery ships P7). Write-only — never echoed.
    • 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.

    DemoVideoResult

    Precomputed result for one prompt. Documented subset — additional fields (result_json, clips) exist but are undocumented and may change.

    • answer — string,null (required)
    • confidence — number,null (required)
    • query_type — string,null (required)
    • video_url — string,null (required) — Playable annotated result video (CDN).
    • computed_at — string (required)

    DemoVideo

    - id — string (required)

    • display_name — string (required)
    • duration_seconds — number,null (required)
    • thumbnail_url — string,null (required)
    • results_by_prompt — object (required) — Map of prompt text → precomputed result.

    DemoVideoList

    - videos — array<DemoVideo> (required)

    AnalysisBatchPreviewPrompt

    - index — integer (required) — Zero-based prompt index.

    • query — object (required) — Compiled query structure.
    • parse_mode"heuristic" · "llm" · "client" (required)
    • assessable — boolean (required) — True when the query type can produce a yes/no/count answer.
    • estimated_seconds — integer,null (required) — Estimated credit-seconds for this prompt. Null when video has no known duration.
    • estimated_cost_usd — number,null (required) — Estimated cost in USD. Null when video has no known duration.
    • discount_pct0 | 50 (required) — 0 for the first (full-price) prompt; the configured batch discount (GET /v1/credit-pricing batch_discount_pct) for additional prompts.

    AnalysisBatchPricing

    - full_price_prompts — integer (required)

    • discounted_prompts — integer (required)
    • discount_pct — integer (required) — Config-driven batch discount percent — the same value GET /v1/credit-pricing serves as batch_discount_pct.
    • estimated_total_seconds — integer,null (required) — Estimated total credit-seconds for all prompts combined (full + discounted). Null when the video has no known duration.
    • estimated_total_cost_usd — number,null (required) — Estimated total cost in USD. Null when duration unknown.

    AnalysisBatchPreview

    - object"analysis_batch_preview" (required)

    AnalysisBatch

    - object"analysis_batch" (required)

    • id — string (required) — Stable batch identifier.
    • video_id — string (required) — Video resource id (prefixed ULID). Immutable.
    • analyses — array<Analysis> (required)
    • pricingAnalysisBatchPricing (required)

    CreateAnalysisBatchRequest

    - video_id — string (required) — Video resource id (prefixed ULID). Immutable.

    • prompts — array (required) — 2–10 prompts to run against the video. First prompt is full price; each additional is billed at 50% (default — the live discount is served by GET /v1/credit-pricing as batch_discount_pct).
    • model — string
    • options — object
    • metadata — object — Free-form key/value metadata. ≤16 keys, key ≤40 chars [a-zA-Z0-9_.-], value ≤500 chars.
    • 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.

    Model

    - id — string (required)

    • object"model" (required)
    • status"stable" · "preview" · "deprecated" (required)
    • default — boolean (required)
    • capabilities — object (required)
    • sunset_at — string,null (required)
    • created_at — string (required)

    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).

    ParseRequest

    - prompt — string (required) — Free-text video question. Must be a non-empty string.

    ErrorRegistryEntry

    - code — string (required)

    • kind"http" · "resource" (required)
    • status — integer,null (required)
    • retryable — boolean (required)
    • idem — boolean (required)
    • message — string (required)
    • docs_url — string (uri) (required)

    ClientToken

    - object"client_token" (required)

    • id — string (required)
    • token — string (required) — 256-bit CSPRNG bearer, SHA-256 at rest — shown exactly once. Browser-safe: accepted on product-plane endpoints (CORS *).
    • scopes — array (required)
    • expires_at — string (required)
    • video_id — string — Video resource id (prefixed ULID). Immutable.
    • stream_id — string

    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.

    • 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.
    • stream_id — string — Optional resource binding to one stream (WS acceptance ships P6).

    CreateStreamRequest

    Requires positive credit balance. One queued|ready|live stream per account by default (409 stream_already_active otherwise).

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

    StreamList

    - object"list" (required)

    • data — array<Stream> (required)
    • has_more — boolean (required)

    StreamRecording

    - object"stream_recording" (required)

    • stream_id — string (required) — Stream resource id (prefixed ULID). P6 §4.8.
    • url — string (uri) (required) — Signed playback URL — fresh on every GET, 1h TTL. Re-fetch for a new one.
    • expires_at — string (required) — RFC 3339 expiry of the signed URL.
    • content_type"video/h264" (required)
    • container"h264-annex-b" (required) — Raw H.264 Annex-B elementary stream. Lossless remux to MP4: ffmpeg -i in.h264 -c copy out.mp4.

    SandboxKey

    - object"sandbox_key" (required)

    • api_key — string (required) — Shown exactly once — store it now. Fixture-only test mode: no GPU, no credits.
    • mode"test" (required)
    • expires_at — string (required) — Keys expire 7 days after issuance.
    • fixture_video_id — string,null (required) — Pre-seeded ready video for immediate POST /v1/analyses.
    • fixture_prompt — string (required)
    • expected_answer — string (required) — Deterministic canned answer for the fixture prompt — assert this in CI.
    • docs_url — string (required)
    • note — string (required)

    UpgradeKeyResponse

    - object"api_key" (required)

    • livemodetrue (required)
    • api_key — string (required) — The raw pv_live_ key — store securely, shown only once.
    • tier"free_grant" (required)
    • expires_at — string (date-time) (required)
    • grant_seconds — integer (required)
    • note — string
    • claim_flow — object

    UpgradeKeyRequest

    - github_token — string — PRI-479: GitHub OAuth access token from the device flow. Required when the GitHub gate is enabled (calling without one returns 403 github_verification_required with device-flow bootstrap details). Verified against GitHub and discarded — never stored. One free grant per GitHub account, ever.

    DeviceCodeRequest

    - object"device_code_request" (required)

    • device_code — string (required) — Opaque code; use as path param when polling.
    • claim_url — string (uri) (required) — Open in browser for the user to approve.
    • expires_at — string (date-time) (required)
    • poll_interval — integer (required) — Seconds between polls.
    • instructions — string

    DeviceCodeStatus

    - object"device_code_request" (required)

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

    CreateWebhookEndpointRequest

    - url — string (uri) (required) — HTTPS delivery URL.

    • events — array<WebhookEventType> (required) — Subscribed event types — a non-empty subset of the closed v1 vocabulary.
    • description — string

    WebhookEndpointList

    - object"list" (required)

    • data — array<WebhookEndpoint> (required)
    • has_more — boolean (required)

    WebhookDeliveryList

    - object"list" (required)

    • data — array<WebhookDelivery> (required)
    • has_more — boolean (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.

    CreditTransaction

    - id — string (required)

    • kind — string (required) — e.g. signup_grant, analysis_debit, stream_debit, purchase.
    • seconds_delta — number (required) — Signed: negative debits, positive credits.
    • balance_after_seconds — number (required)
    • source_type — string,null (required)
    • source_id — string,null (required)
    • created_at — string (required)

    CreditsResponse

    - object"credits" (required)

    • balance_seconds — number (required) — Seconds of analysis remaining. The authoritative balance.
    • grant_seconds — number,null (required) — Original signup grant, when one exists.
    • used_seconds — number,null (required) — Consumed against the signup grant (grant minus balance).
    • transactions — array<CreditTransaction> (required)
    • has_more — boolean (required)
    Prefer raw text? This page is available as markdown.