Quickstart
This walkthrough takes a public video URL to searchable scene data. You need an API key from the dashboard's API Keys tab; see Authentication.
Export it once so the examples below can use it:
export VUF_API_KEY="vuf_live_xxxxxxxx.yyyyyyyy"
- Submit a video
POST /api/v1/videostakes a publicly fetchablehttp/httpsURL and an optionaltitle. It returns202 Acceptedimmediately; indexing runs asynchronously through scene detection, captioning, embedding and moderation.- cURL
- Python
curl -X POST https://video.pyler.ai/api/v1/videos \-H "Authorization: Bearer $VUF_API_KEY" \-H "Content-Type: application/json" \-d '{"url": "https://example.com/clip.mp4", "title": "demo clip"}'import osimport requestsBASE = "https://video.pyler.ai/api/v1"HEADERS = {"Authorization": f"Bearer {os.environ['VUF_API_KEY']}"}r = requests.post(f"{BASE}/videos",headers=HEADERS,json={"url": "https://example.com/clip.mp4", "title": "demo clip"},)r.raise_for_status()submitted = r.json()video_id, job_id = submitted["video_id"], submitted["job_id"]Response:
{"video_id": "3f2c9d1e-...","job_id": "a81b7c44-...","created": true}createdisfalsewhen the same URL is already being indexed for you. In that case you get the existingvideo_idandjob_idback instead of a duplicate pipeline, still with status202.See
POST /api/v1/videos. - Poll the job
GET /api/v1/jobs/{job_id}returns the pipeline state.statusmovespending→running→doneorfailed.done_scenes/total_scenesreport progress while it runs.- cURL
- Python
curl https://video.pyler.ai/api/v1/jobs/$JOB_ID \-H "Authorization: Bearer $VUF_API_KEY"import timewhile True:job = requests.get(f"{BASE}/jobs/{job_id}", headers=HEADERS).json()print(job["status"], job.get("done_scenes", 0), "/", job.get("total_scenes", 0))if job["status"] == "done":breakif job["status"] == "failed":raise RuntimeError(f"indexing failed: {job.get('reason_code')} {job.get('error')}")time.sleep(5)Response while running:
{"id": "a81b7c44-...","video_id": "3f2c9d1e-...","status": "running","total_scenes": 42,"done_scenes": 18,"error": null,"reason_code": null,"reason": null,"started_at": "2026-06-04T10:23:01Z","completed_at": null}On
failed, branch onreason_codefor the stable cause;errorcarries the raw text. - Read the scenes and search
Once the job is
done, page through the video's scenes withGET /api/v1/videos/{video_id}/scenes. Each page is aScenesPage:items,offset,limit(1–200, default 50) andtotal.- cURL
- Python
curl "https://video.pyler.ai/api/v1/videos/$VIDEO_ID/scenes?offset=0&limit=50" \-H "Authorization: Bearer $VUF_API_KEY"page = requests.get(f"{BASE}/videos/{video_id}/scenes",headers=HEADERS,params={"offset": 0, "limit": 50},).json()for scene in page["items"]:print(scene["scene_index"], scene["start_ms"], scene["end_ms"], scene.get("caption"))Each item is a
SceneLite:{"items": [{"id": "c0a4e2f7-...","video_id": "3f2c9d1e-...","scene_index": 0,"start_ms": 0,"end_ms": 4120,"caption": "A child in a red jacket runs across a snowy yard.","transcript": null,"text_on_screen": [],"valence": 4,"arousal": 3,"music_mood": "playful","audio_events": ["laughter"],"garm_categories": [],"brands": [],"product_categories_visible": [],"people": [],"locations": ["backyard"],"action": ["running"],"human_activity": ["playing"],"weather": "snow","keyframe_url": "https://...","clip_url": "https://..."}],"offset": 0,"limit": 50,"total": 42}warningAn unknown
video_idreturns an empty page (total: 0), not a404. UseGET /api/v1/videos/{video_id}if you need to tell "no such video" from "a video with no scenes".Now search the corpus.
POST /api/v1/searchtakes a natural-languagequery, amodality(caption,transcript,omni,bothorall) andk(1–100, default 20). Addvideo_idto restrict the search to one video's scenes.- cURL
- Python
curl -X POST https://video.pyler.ai/api/v1/search \-H "Authorization: Bearer $VUF_API_KEY" \-H "Content-Type: application/json" \-d '{"query": "child playing in snow", "modality": "both", "k": 10}'results = requests.post(f"{BASE}/search",headers=HEADERS,json={"query": "child playing in snow", "modality": "both", "k": 10},).json()for match in results["caption"]:print(match["score"], match["video"]["id"], match["video"]["title"])for hit in match["scenes"]:s = hit["scene"]print(" ", hit["score"], s["start_ms"], s["end_ms"], s.get("caption"))The response groups scene matches by video, one column per modality. With
modality: "both"thecaptionandomnilists are populated andtranscriptis empty.{"caption": [{"score": 0.81,"video": {"id": "3f2c9d1e-...","title": "demo clip","duration_ms": 183000},"scenes": [{"score": 0.79,"scene": {"id": "c0a4e2f7-...","video_id": "3f2c9d1e-...","scene_index": 0,"start_ms": 0,"end_ms": 4120,"caption": "A child in a red jacket runs across a snowy yard.","keyframe_url": "https://...","clip_url": "https://..."}}]}],"transcript": [],"omni": []}See
GET /api/v1/videos/{video_id}/scenesandPOST /api/v1/search.
Where to go next
Submit many URLs in one call with POST /api/v1/videos/batch, or upload a CSV/XLSX sheet.
Narrow by iab_t1 and garm_min_level, or opt into the AI judge with judge: "text".
Get a GARM brand-safety verdict for an ad before it runs.
The error envelope, the 422 array form and retry guidance.