Uploading video
There are three paths to get video into Primate Vision. All of them end with a video resource in status ready, at which point you can create analyses against it.
Formats: video/mp4 (H.264) and video/quicktime. Max size: 2 GiB. Retention: source videos are deleted 30 days after upload (see security model).
Choosing a path
| Path | Use when |
|---|---|
| URL ingest | The video is already at a public https URL (CDN, S3, YouTube-dl output) |
| Presigned upload | You have the bytes (server file, browser file input) — bytes go straight to S3, never through our API servers |
SDK upload() helper |
You use an SDK and want the presigned dance done for you |
URL ingest
One call. The API fetches the video asynchronously:
curl -s -X POST https://api.primateintelligence.ai/v1/videos \
-H "Authorization: Bearer $PRIMATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/clip.mp4"}'
The video starts processing and transitions to ready (or failed with error.code = url_fetch_failed / url_forbidden / video_unreadable). Poll GET /v1/videos/{id} or subscribe to the video.ready webhook.
URL rules (SSRF policy): https only, port 443, public hosts only, max 3 redirects, 2 GiB cap. Private/internal addresses are rejected with url_forbidden.
Presigned upload
Three steps — create, PUT, complete:
# 1. Create — declares filename/type/size, returns a presigned S3 PUT URL
curl -s -X POST https://api.primateintelligence.ai/v1/videos \
-H "Authorization: Bearer $PRIMATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filename": "clip.mp4", "content_type": "video/mp4", "size_bytes": 12345678}'
# 2. PUT the bytes directly to S3 (the upload.url + upload.headers from step 1)
curl -s -X PUT "$UPLOAD_URL" -H "Content-Type: video/mp4" --data-binary @clip.mp4
# 3. Tell the API the PUT finished
curl -s -X POST https://api.primateintelligence.ai/v1/videos/$VIDEO_ID/complete \
-H "Authorization: Bearer $PRIMATE_API_KEY"
The presigned URL expires (see upload.expires_at, typically 1h). If the declared size_bytes doesn't match what landed in S3, complete fails with upload_incomplete — re-create the video.
SDK helper
import Primate from '@primate-intelligence/sdk';
import { readFile } from 'fs/promises';
const client = new Primate();
const video = await client.videos.upload(await readFile('clip.mp4'), {
filename: 'clip.mp4',
content_type: 'video/mp4',
});
from primate_intelligence import Primate
client = Primate()
with open("clip.mp4", "rb") as f:
video = client.videos.upload(f.read(), filename="clip.mp4")
Both run create → PUT → complete and return the video resource.
Browser uploads
Never put a secret key in a browser. Mint an ephemeral client token server-side with the videos:write scope, then run the presigned flow from the browser with the pvct_ token. Working example: the browser SPA sample.
Lifecycle
awaiting_upload → uploading → processing → ready
↘ failed
DELETE /v1/videos/{id} removes the video (409 resource_conflict while analyses are still running against it). Deletion propagates from S3 + CDN within 72h; signed URLs expire within 1h regardless.