# Analyze Uploaded File
Source: https://docs.vidnavigator.com/api-reference/endpoint/analyze-file
POST /analyze/file
Analyze an uploaded file and return comprehensive analysis results with intelligent caching.
**Behavior:**
- If no query provided: Returns summary analysis (cached if available)
- If query provided: Returns both summary (from cache if available) and fresh question analysis
**Billing:** `analysis_request` is token-based — `quantity = ceil(total_tokens / 15000)`. One unit is charged up front as a credit gate; additional units are topped up at the end if the LLM call exceeded 15,000 tokens. The file's transcript is read locally from your storage — no proxy fetch is involved. Set `include_usage=true` to receive the per-charge breakdown.
Optional: set `transcript_text=true` to return the transcript as a single text string instead of an array of segments.
Run AI-powered analysis on a previously uploaded audio/video file.
## Overview
Similar to **Analyze Video**, but operates on files in your library.
## Request Body
* `file_id` (string, required): ID of the uploaded file to analyze
* `query` (string, optional): Ask a specific question about the content
## Billing
File analysis consumes `analysis_request` units based on context size. One unit covers up to **15,000 total tokens**, and longer contexts are billed with `ceil(total_tokens / 15000)`. For example, an analysis that uses 17,000 total tokens consumes 2 `analysis_request` units. One unit is charged up front as a credit gate; additional units are topped up at the end if the LLM call exceeded 15,000 tokens.
The file's transcript is read locally from your storage — no proxy fetch is involved, so no `standard_request` / `residential_request` is charged.
Set `include_usage: true` to receive a [`usage`](/api-reference/usage-costs#per-call-usage-disclosure) block with the per-charge breakdown.
## Example Request
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/analyze/file" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"file_id": "file_abc123",
"query": "Summarize the key points"
}'
```
# Analyze Video
Source: https://docs.vidnavigator.com/api-reference/endpoint/analyze-video
POST /analyze/video
Analyze an online video and return comprehensive analysis results with intelligent caching.
**Behavior:**
- If no query provided: Returns summary analysis (cached if available)
- If query provided: Returns both summary (from cache if available) and fresh question analysis
**Caching:**
- Summary analyses are cached and reused
- Question/query analyses are not cached (always fresh)
- Transcripts are cached for optimization
**Billing:** `analysis_request` is token-based — `quantity = ceil(total_tokens / 15000)`. One unit is charged up front as a credit gate; additional units are topped up at the end if the LLM call exceeded 15,000 tokens. When the transcript isn't cached, one additional `residential_request` or `standard_request` is charged for the fetch (residential for YouTube/Instagram/Facebook-watch/Rumble, standard otherwise). Set `include_usage=true` to receive the per-charge breakdown — the consolidated `analysis_request` entry carries a nested `tokens` object with `prompt_tokens`/`completion_tokens`/`total_tokens`.
Optional: set `transcript_text=true` to return the transcript as a single text string instead of an array of segments.
Get comprehensive AI-powered analysis of online videos including summaries, key insights, and structured data extraction.
## Overview
Transform any online video into structured insights with our advanced AI analysis. Extract summaries, identify people and places, generate key topics, and ask specific questions about the content.
### Analysis Capabilities
AI-generated summaries highlighting key points and themes
Identify people, places, organizations, and concepts mentioned
Extract main topics, themes, and discussion points
Ask specific questions about the video content
### Intelligent Caching
* **Summary analyses** are cached and reused for efficiency
* **Question/query analyses** are always fresh and never cached
* **Transcripts** are cached to optimize performance
## Billing
Analysis consumes `analysis_request` units based on context size. One unit covers up to **15,000 total tokens**, and longer contexts are billed with `ceil(total_tokens / 15000)`. For example, an analysis that uses 17,000 total tokens consumes 2 `analysis_request` units. One unit is charged up front as a credit gate; additional units are topped up at the end if the LLM call exceeded 15,000 tokens.
When the transcript isn't cached, one additional `residential_request` or `standard_request` is charged for the fetch (residential for YouTube/Instagram/Facebook-watch/Rumble, standard otherwise).
Set `include_usage: true` to receive a [`usage`](/api-reference/usage-costs#per-call-usage-disclosure) block with the per-charge breakdown — the consolidated `analysis_request` entry carries a nested `tokens` object with `prompt_tokens` / `completion_tokens` / `total_tokens`.
## Example Usage
### Basic Analysis (Summary Only)
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/analyze/video" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://youtube.com/watch?v=dQw4w9WgXcQ"
}'
```
```python Python theme={null}
import requests
url = "https://api.vidnavigator.com/v1/analyze/video"
headers = {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json"
}
data = {
"video_url": "https://youtube.com/watch?v=dQw4w9WgXcQ"
}
response = requests.post(url, headers=headers, json=data)
analysis = response.json()
print("Summary:", analysis['data']['transcript_analysis']['summary'])
print("Key Topics:", analysis['data']['transcript_analysis']['key_topics'])
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.vidnavigator.com/v1/analyze/video', {
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
video_url: 'https://youtube.com/watch?v=dQw4w9WgXcQ'
})
});
const analysis = await response.json();
```
### Analysis with Custom Question
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/analyze/video" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://youtube.com/watch?v=dQw4w9WgXcQ",
"query": "What is the main topic discussed and who are the key speakers?"
}'
```
```python Python theme={null}
import requests
url = "https://api.vidnavigator.com/v1/analyze/video"
headers = {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json"
}
data = {
"video_url": "https://youtube.com/watch?v=dQw4w9WgXcQ",
"query": "What is the main topic discussed and who are the key speakers?"
}
response = requests.post(url, headers=headers, json=data)
analysis = response.json()
# Get both summary and question-specific analysis
summary = analysis['data']['transcript_analysis']['summary']
question_answer = analysis['data']['transcript_analysis']['question_analysis']
```
## Response Example
### Basic Analysis Response
```json theme={null}
{
"status": "success",
"data": {
"video_info": {
"title": "How to Build a Startup",
"description": "A comprehensive guide to building a startup from scratch.",
"thumbnail": "https://example.com/thumbnail.jpg",
"url": "https://youtube.com/watch?v=example123",
"channel": "TechTalks",
"duration": 1530.0,
"views": 150000,
"likes": 4500,
"published_date": "2023-05-10",
"keywords": ["startup", "entrepreneurship", "business"],
"category": "Education"
},
"transcript": [
{
"text": "Welcome to today's discussion on building startups...",
"start": 0.0,
"end": 4.2
}
],
"transcript_analysis": {
"summary": "This video covers essential startup building strategies including market validation, team building, and funding approaches. The speaker emphasizes the importance of customer feedback and iterative development.",
"key_topics": [
"Market validation",
"Team building",
"Funding strategies",
"Customer feedback",
"Product development"
],
"people": [
{
"name": "Sarah Johnson",
"context": "CEO and founder discussing her startup journey"
}
],
"places": [
{
"name": "Silicon Valley",
"context": "Mentioned as a startup hub"
}
],
"sentiment": "positive",
"category": "business_education"
}
}
}
```
### Analysis with Question Response
```json theme={null}
{
"status": "success",
"data": {
"transcript_analysis": {
"summary": "...",
"question_analysis": {
"question": "What is the main topic discussed and who are the key speakers?",
"answer": "The main topic is startup building strategies with focus on market validation and customer development. The key speaker is Sarah Johnson, CEO of TechCorp, who shares insights from her 5-year entrepreneurial journey.",
"confidence": 0.92,
"relevant_segments": [
{
"text": "Hi, I'm Sarah Johnson, CEO of TechCorp...",
"start": 15.0,
"end": 25.0
}
]
}
}
}
}
```
## Use Cases
Quickly understand video content without watching entire videos
Extract structured data from educational or training content
Analyze content for compliance and safety requirements
Create summaries and study materials from video lectures
# Cancel File Processing
Source: https://docs.vidnavigator.com/api-reference/endpoint/cancel-file
POST /file/{file_id}/cancel
Cancel an ongoing file upload/processing
Cancel an ongoing file upload or processing job.
## Path Parameter
* `file_id` (string, required)
## Example Request
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/file/file_abc123/cancel" \
-H "X-API-Key: YOUR_API_KEY"
```
## Success Response (200 OK)
Returns a confirmation that the file processing has been cancelled.
```json theme={null}
{
"status": "success",
"data": {
"file_id": "file_abc123",
"file_name": "processing_video.mp4",
"message": "File cancelled successfully"
}
}
```
# Create Namespace
Source: https://docs.vidnavigator.com/api-reference/endpoint/create-namespace
POST /namespaces
Create a new namespace for organizing files. Maximum 5 namespaces per user. The name 'default' is reserved and cannot be used.
Create a new namespace to organize your uploaded files.
## Overview
Each user can have up to **5 namespaces**. The name `default` is reserved and cannot be used when creating a new namespace.
## Example Request
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/namespaces" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Client Calls"
}'
```
```python Python theme={null}
import requests
url = "https://api.vidnavigator.com/v1/namespaces"
headers = {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json"
}
data = {"name": "Client Calls"}
response = requests.post(url, headers=headers, json=data)
result = response.json()
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.vidnavigator.com/v1/namespaces', {
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'Client Calls' })
});
const result = await response.json();
```
## Success Response (201 Created)
```json theme={null}
{
"status": "success",
"data": {
"id": "64a1b2c3d4e5f6789abc0003",
"name": "Client Calls",
"created_at": "2025-03-20T09:15:00Z",
"updated_at": "2025-03-20T09:15:00Z"
}
}
```
# Delete File
Source: https://docs.vidnavigator.com/api-reference/endpoint/delete-file
DELETE /file/{file_id}/delete
Delete a file and its associated data from the database and cloud storage
Delete a file and all associated data from storage.
## Path Parameter
* `file_id` (string, required)
## Example Request
```bash cURL theme={null}
curl -X DELETE "https://api.vidnavigator.com/v1/file/file_abc123/delete" \
-H "X-API-Key: YOUR_API_KEY"
```
## Success Response (200 OK)
Returns a confirmation that the file and its associated data have been successfully deleted.
```json theme={null}
{
"status": "success",
"data": {
"file_id": "file_abc123",
"file_name": "video_to_delete.mp4",
"message": "File deleted successfully"
}
}
```
# Delete Namespace
Source: https://docs.vidnavigator.com/api-reference/endpoint/delete-namespace
DELETE /namespaces/{namespace_id}
Delete a namespace and remove its association from all files. The 'default' namespace cannot be deleted.
Delete a namespace. The `default` namespace cannot be deleted. Files previously assigned to this namespace will have the association removed but are **not** deleted.
## Example Request
```bash cURL theme={null}
curl -X DELETE "https://api.vidnavigator.com/v1/namespaces/64a1b2c3d4e5f6789abc0003" \
-H "X-API-Key: YOUR_API_KEY"
```
```python Python theme={null}
import requests
namespace_id = "64a1b2c3d4e5f6789abc0003"
url = f"https://api.vidnavigator.com/v1/namespaces/{namespace_id}"
headers = {"X-API-Key": "YOUR_API_KEY"}
response = requests.delete(url, headers=headers)
result = response.json()
```
```javascript JavaScript theme={null}
const namespaceId = '64a1b2c3d4e5f6789abc0003';
const response = await fetch(`https://api.vidnavigator.com/v1/namespaces/${namespaceId}`, {
method: 'DELETE',
headers: { 'X-API-Key': 'YOUR_API_KEY' }
});
const result = await response.json();
```
## Success Response
```json theme={null}
{
"status": "success",
"message": "Namespace deleted successfully"
}
```
# Extract Data from File
Source: https://docs.vidnavigator.com/api-reference/endpoint/extract-file
POST /extract/file
Extract structured data from an uploaded file's transcript using a custom schema.
Provide a `file_id` and a `schema` describing the fields to extract. The file must be processed and have a transcript available. Optionally include `what_to_extract` to guide the extraction.
**Schema format:** Each field must have `type` and `description`. Supported types: `String`, `Number`, `Boolean`, `Integer`, `Object`, `Array`, `Enum`. Max 10 root fields, max 3 nesting levels.
**Content-Type:** Accepts `application/json`, YAML (`application/yaml`, `application/x-yaml`, `text/yaml`), or `multipart/form-data`. For multipart requests, send `file_id` as a form field and `schema` as either a JSON/YAML form value or an uploaded JSON/YAML file. If the uploaded schema file contains a full request body with a nested `schema` property, the nested schema is used and the form `file_id` takes precedence.
**Token usage:** Set `include_usage=true` to include prompt/completion token counts in the response.
**Billing:** Each extraction consumes at least 1 analysis credit. For longer transcripts, billing scales as `ceil(total_tokens / 15000)` credits. All charges are reverted if the request fails.
Extract structured data from an uploaded file's transcript using a custom schema you define.
## Overview
Extract structured data from files you have previously uploaded to your VidNavigator library. The file must be fully processed and have a transcript available.
### How It Works
1. Provide a `file_id` and a `schema` describing the structured fields you want
2. VidNavigator reads the transcript from the uploaded file
3. VidNavigator runs AI extraction against your schema
4. You receive structured JSON matching your schema definition, plus `file_info`
### Schema Rules
* Each field must have `type` and `description`
* Supported types: `String`, `Number`, `Boolean`, `Integer`, `Object`, `Array`, `Enum`
* Maximum **10 root-level fields**
* Maximum **3 nesting levels**
You can also send the request body as YAML by setting `Content-Type` to `application/x-yaml` or `text/yaml`.
This endpoint does **not** support a `transcribe` parameter. It works only on files that already have a transcript available.
## Example Usage
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/extract/file" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"file_id": "file_abc123",
"schema": {
"action_items": {
"type": "Array",
"description": "Action items and tasks mentioned in the meeting",
"items": {
"type": "Object",
"description": "An action item",
"properties": {
"task": { "type": "String", "description": "The task description" },
"assignee": { "type": "String", "description": "Person assigned" },
"deadline": { "type": "String", "description": "Deadline if mentioned" }
}
}
},
"decisions_made": {
"type": "Array",
"description": "Key decisions made during the meeting",
"items": { "type": "String", "description": "A decision" }
}
},
"what_to_extract": "Extract action items and deadlines from this meeting",
"include_usage": true
}'
```
```python Python theme={null}
import requests
url = "https://api.vidnavigator.com/v1/extract/file"
headers = {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json"
}
data = {
"file_id": "file_abc123",
"schema": {
"action_items": {
"type": "Array",
"description": "Action items and tasks mentioned in the meeting",
"items": {
"type": "Object",
"description": "An action item",
"properties": {
"task": {"type": "String", "description": "The task description"},
"assignee": {"type": "String", "description": "Person assigned"},
"deadline": {"type": "String", "description": "Deadline if mentioned"}
}
}
},
"decisions_made": {
"type": "Array",
"description": "Key decisions made during the meeting",
"items": {"type": "String", "description": "A decision"}
}
},
"what_to_extract": "Extract action items and deadlines from this meeting"
}
response = requests.post(url, headers=headers, json=data)
result = response.json()
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.vidnavigator.com/v1/extract/file', {
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
file_id: 'file_abc123',
schema: {
action_items: {
type: 'Array',
description: 'Action items and tasks mentioned in the meeting',
items: {
type: 'Object',
description: 'An action item',
properties: {
task: { type: 'String', description: 'The task description' },
assignee: { type: 'String', description: 'Person assigned' },
deadline: { type: 'String', description: 'Deadline if mentioned' }
}
}
},
decisions_made: {
type: 'Array',
description: 'Key decisions made during the meeting',
items: { type: 'String', description: 'A decision' }
}
},
what_to_extract: 'Extract action items and deadlines from this meeting'
})
});
const result = await response.json();
```
## Response Example
```json theme={null}
{
"status": "success",
"data": {
"action_items": [
{
"task": "Update the product roadmap with Q2 priorities",
"assignee": "Sarah",
"deadline": "Friday"
},
{
"task": "Schedule customer interviews for user research",
"assignee": "Mike",
"deadline": "next week"
}
],
"decisions_made": [
"Proceed with option B for the pricing model",
"Delay the mobile app launch to Q3"
]
},
"file_info": {
"id": "file_abc123",
"name": "weekly-product-meeting.mp4",
"size": 248517632,
"type": "video/mp4",
"duration": 3720.0,
"status": "completed",
"created_at": "2026-04-10T09:30:00Z",
"updated_at": "2026-04-10T09:36:12Z",
"has_transcript": true,
"namespace_ids": ["ns_team_notes"],
"namespaces": [
{
"id": "ns_team_notes",
"name": "Team Notes"
}
]
},
"usage": {
"prompt_tokens": 3200,
"completion_tokens": 120,
"total_tokens": 3320
}
}
```
The `usage` field is only included when `include_usage=true` in the request. `file_info` is included in the response metadata.
## Billing
* AI extraction consumes `analysis_request` units in blocks of **15,000 total tokens**
* Formula: `ceil(total_tokens / 15000)`
* Examples:
* `14,000` tokens -> `1` `analysis_request` unit
* `17,000` tokens -> `2` `analysis_request` units
* `31,000` tokens -> `3` `analysis_request` units
* There is no speech-to-text billing on this endpoint
* Failed requests are not charged
* Set `include_usage: true` to receive a [`usage`](/api-reference/usage-costs#per-call-usage-disclosure) block with the per-charge breakdown (the `analysis_request` entry carries a nested `tokens` object)
# Extract Data from Video
Source: https://docs.vidnavigator.com/api-reference/endpoint/extract-video
POST /extract/video
Extract structured data from an online video's transcript using a custom schema.
Provide a `video_url` and a `schema` describing the fields to extract. Optionally include `what_to_extract` to guide the extraction.
**Auto-transcription:** For non-YouTube videos without an existing transcript (e.g. Instagram, TikTok, Facebook), the API automatically transcribes the video audio when `transcribe` is `true` (the default). This uses speech-to-text credits (`transcription_hour` usage). YouTube videos rely on platform captions and cannot be auto-transcribed. Set `transcribe=false` to disable this behavior.
**Schema format:** Each field must have `type` and `description`. Supported types: `String`, `Number`, `Boolean`, `Integer`, `Object`, `Array`, `Enum`. Max 10 root fields, max 3 nesting levels.
**Content-Type:** Accepts `application/json`, YAML (`application/yaml`, `application/x-yaml`, `text/yaml`), or `multipart/form-data`. For multipart requests, send `video_url` as a form field and `schema` as either a JSON/YAML form value or an uploaded JSON/YAML file (for example, `schema=@fact-check.yaml`). If the uploaded schema file contains a full request body with a nested `schema` property, the nested schema is used and the form `video_url` takes precedence.
**Token usage:** Set `include_usage=true` to include prompt/completion token counts in the response.
**Billing:** Each extraction consumes at least 1 analysis_request unit. For longer transcripts, billing scales as `ceil(total_tokens / 15000)` analysis_request units. If auto-transcription is triggered, transcription_hour usage is also charged based on video duration. All charges are reverted if the request fails.
**Long videos:** this endpoint is synchronous and holds the connection open for the whole transcription. For media longer than ~10 minutes use `POST /extract/video/async` instead, which returns a `task_id` and can call your webhook when it finishes. Media longer than 10 minutes is rejected here with `video_too_long` (HTTP 400). The metadata fetch needed to read the video's duration is billed (`standard_request` or `residential_request`) and is **not** refunded: the limit is documented, so the proxy call was spent answering a request that could not be served. No `transcription_hour` is charged — no audio is processed. Send long media to the async endpoint, which has no cap. See https://docs.vidnavigator.com/guides/async-jobs
Extract structured data from an online video's transcript using a custom schema you define.
## Overview
The extraction endpoint lets you pull structured, typed data from any video transcript by providing a JSON schema describing the fields you need. This is ideal for building automated pipelines that need consistent, machine-readable output from video content.
### How It Works
1. Provide a `video_url` and a `schema` defining the fields to extract
2. VidNavigator fetches the platform transcript when available
3. For supported non-YouTube sources, it can auto-transcribe audio when no transcript exists
4. VidNavigator runs AI extraction against your schema
5. You receive structured JSON matching your schema definition, plus `video_info`
### Schema Rules
* Each field must have `type` and `description`
* Supported types: `String`, `Number`, `Boolean`, `Integer`, `Object`, `Array`, `Enum`
* Maximum **10 root-level fields**
* Maximum **3 nesting levels**
You can also send the request body as YAML by setting `Content-Type` to `application/x-yaml` or `text/yaml`.
### Automatic Transcription
The `transcribe` parameter controls whether VidNavigator should automatically fall back to speech-to-text when a transcript is not available.
* `transcribe=true` by default
* applies to non-YouTube videos only
* useful for platforms like Instagram, TikTok, Facebook, X, and similar sources
* YouTube extraction relies on platform captions and does not support speech-to-text fallback through this endpoint
If you set `transcribe=false`, the request will fail when no transcript is available instead of triggering speech-to-text processing.
## Synchronous or async?
The extraction itself is fast; the speech-to-text step in front of it is what takes time on long videos. This endpoint therefore comes in two modes, with the same request body and the same result:
| Mode | Endpoint | Media duration (with speech-to-text) | How you get the result |
| - | - | - | - |
| Synchronous | `POST /extract/video` | **Up to 10 minutes** | In the HTTP response |
| Async | [`POST /extract/video/async`](/api-reference/endpoint/extract-video-async) | **Any length** | Poll `GET /extract/video/{task_id}` or receive a [webhook](/webhooks) |
**When `transcribe` is on and the video is longer than 10 minutes, the synchronous endpoint rejects it** with `400 video_too_long`. Use the [async mode](/api-reference/endpoint/extract-video-async) whenever the video may run past 10 minutes — it also works for short videos.
## Example Usage
### Basic Extraction
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/extract/video" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://youtube.com/watch?v=dQw4w9WgXcQ",
"schema": {
"main_topics": {
"type": "Array",
"description": "List of main topics discussed",
"items": { "type": "String", "description": "A topic" }
},
"sentiment": {
"type": "Enum",
"description": "Overall sentiment of the video",
"enum": ["positive", "negative", "neutral"]
},
"key_takeaway": {
"type": "String",
"description": "The single most important takeaway"
}
}
}'
```
```python Python theme={null}
import requests
url = "https://api.vidnavigator.com/v1/extract/video"
headers = {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json"
}
data = {
"video_url": "https://youtube.com/watch?v=dQw4w9WgXcQ",
"schema": {
"main_topics": {
"type": "Array",
"description": "List of main topics discussed",
"items": {"type": "String", "description": "A topic"}
},
"sentiment": {
"type": "Enum",
"description": "Overall sentiment of the video",
"enum": ["positive", "negative", "neutral"]
},
"key_takeaway": {
"type": "String",
"description": "The single most important takeaway"
}
}
}
response = requests.post(url, headers=headers, json=data)
result = response.json()
print(result["data"])
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.vidnavigator.com/v1/extract/video', {
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
video_url: 'https://youtube.com/watch?v=dQw4w9WgXcQ',
schema: {
main_topics: {
type: 'Array',
description: 'List of main topics discussed',
items: { type: 'String', description: 'A topic' }
},
sentiment: {
type: 'Enum',
description: 'Overall sentiment of the video',
enum: ['positive', 'negative', 'neutral']
},
key_takeaway: {
type: 'String',
description: 'The single most important takeaway'
}
}
})
});
const result = await response.json();
```
### With Extraction Guidance
Use `what_to_extract` to provide additional context to the AI about what to focus on:
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/extract/video" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://youtube.com/watch?v=dQw4w9WgXcQ",
"what_to_extract": "Focus on product names and pricing mentioned in the video",
"schema": {
"products": {
"type": "Array",
"description": "Products mentioned in the video",
"items": {
"type": "Object",
"description": "A product",
"properties": {
"name": { "type": "String", "description": "Product name" },
"price": { "type": "String", "description": "Price if mentioned" }
}
}
}
},
"include_usage": true
}'
```
### Disable Auto-Transcription
Use `transcribe=false` when you want extraction to run only if a platform transcript already exists:
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/extract/video" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://www.instagram.com/reel/example/",
"transcribe": false,
"schema": {
"main_claim": {
"type": "String",
"description": "Main claim made in the video"
}
}
}'
```
## Response Example
```json theme={null}
{
"status": "success",
"data": {
"main_topics": ["machine learning", "neural networks", "data preprocessing"],
"sentiment": "positive",
"key_takeaway": "Start with clean data before choosing a model architecture"
},
"video_info": {
"title": "Machine Learning Fundamentals",
"description": "An introduction to practical machine learning workflows.",
"thumbnail": "https://example.com/thumbnail.jpg",
"url": "https://youtube.com/watch?v=example123",
"channel": "AI Academy",
"duration": 4520.0,
"views": 152340,
"likes": 8100,
"published_date": "2026-03-05",
"keywords": ["machine learning", "ai", "data science"],
"category": "Education"
},
"usage": {
"prompt_tokens": 2150,
"completion_tokens": 85,
"total_tokens": 2235
}
}
```
The `usage` field is only included when `include_usage=true` in the request. `video_info` is included in the response metadata.
## Billing
* AI extraction consumes `analysis_request` units in blocks of **15,000 total tokens**
* Formula: `ceil(total_tokens / 15000)`
* Examples:
* `14,000` tokens -> `1` `analysis_request` unit
* `17,000` tokens -> `2` `analysis_request` units
* `31,000` tokens -> `3` `analysis_request` units
* If `transcribe=true` triggers speech-to-text fallback, speech-to-text is charged separately as `transcription_hour` usage based on **video/audio duration**
* Failed requests are not charged
* Set `include_usage: true` to receive a [`usage`](/api-reference/usage-costs#per-call-usage-disclosure) block with the per-charge breakdown (the `analysis_request` entry carries a nested `tokens` object)
* The async mode is billed exactly the same way
* A `video_too_long` rejection on the synchronous endpoint still bills the metadata fetch that read the duration (`standard_request` or `residential_request`, not refunded), but no `transcription_hour` and no `analysis_request`
## Error Responses
| Status | `error` | Description |
| - | - | - |
| `400` | `missing_parameter`, `request_body_required`, `invalid_schema`, `input_too_large` | Invalid request or schema. |
| `400` | `video_too_long` | Speech-to-text is needed and the media is longer than 10 minutes. Use [`POST /extract/video/async`](/api-reference/endpoint/extract-video-async). |
| `402` | `limit_exceeded` | Not enough credits. |
| `404` | `video_not_found`, `video_unavailable`, `transcript_not_available` | The video can't be found or accessed, or no transcript is available. |
| `500` | `extraction_failed`, `transcription_failed`, `internal_server_error` | Processing failed. Charges are reverted. |
| `503` | `system_overload` | Temporarily overloaded. Retry later. |
See [Errors](/api-reference/errors) for the full list.
## Use Cases
Build automated pipelines that extract consistent structured data from video content at scale
Automatically tag and categorize videos with custom taxonomies
Extract product mentions, sentiment, and competitive insights from video reviews
Pull specific compliance-relevant data points from training or policy videos
# Extract Data from Video (Async)
Source: https://docs.vidnavigator.com/api-reference/endpoint/extract-video-async
POST /extract/video/async
Queue a structured-data extraction and return a `task_id` immediately.
**Use this instead of `POST /extract/video` whenever `transcribe` is left on and the video may run past ~10 minutes.** The extraction itself is fast; the speech-to-text step in front of it is what makes a synchronous request time out. This endpoint has no duration cap.
Accepts the same body as `POST /extract/video` — JSON, YAML and `multipart/form-data` all work here too — plus `webhook_url`. The schema is validated at submit time, so an invalid schema is rejected on the `POST` rather than after a transcription you would have been billed for.
**Billing** is identical to the synchronous endpoint and happens in the background. Pass `include_usage=true` on `GET /extract/video/{task_id}` for the breakdown.
See https://docs.vidnavigator.com/guides/async-jobs
**Submit-time credit gate:** the submit endpoint returns `402` instead of a `task_id` when the account has less than 60 seconds of transcription credit left. This is a spam gate, not a quote — the real, duration-accurate charge happens when the job runs, and a job whose video exceeds the remaining balance still fails on the same credit check a synchronous call would hit. An invalid or deactivated API key is rejected with `401`, and a key without the endpoint's permission with `403`; no task is created in either case.
Extract structured data from an online video of **any length**. The job runs in the background: you get a `task_id` immediately, then collect the result by polling or through a [webhook](/webhooks).
Use this endpoint instead of [`POST /extract/video`](/api-reference/endpoint/extract-video) when speech-to-text is needed (`transcribe=true`) and the video may run past **10 minutes** — the synchronous endpoint rejects it with `video_too_long`. The extraction itself is fast; the transcription in front of it is what takes time.
## How It Works
`POST /extract/video/async` with the **same body** as [`POST /extract/video`](/api-reference/endpoint/extract-video), plus an optional `webhook_url`. You get `202 Accepted` with a `task_id` and a `check_status_url`.
Poll `GET /extract/video/{task_id}` every few seconds while `task_status` is `processing` — polling is free — or receive a [webhook](/webhooks) when the job finishes.
On `completed`, `data.result` is identical to the synchronous response's `data` block, so the same parsing code works for both. On `failed`, `data.error` carries the same error code the synchronous endpoint would have returned.
Accepts the same body as the synchronous endpoint — JSON, YAML and `multipart/form-data` all work. The schema is validated at submit time, so an invalid schema is rejected on the `POST`, before any transcription is billed.
## Billing
Billed **exactly like** [`POST /extract/video`](/api-reference/endpoint/extract-video) — the async mode costs nothing extra. Charges are made in the background worker, so pass `include_usage=true` on the result request (not on the submit) to see them. A failed job has all of its charges reverted.
## Submit the job
`POST https://api.vidnavigator.com/v1/extract/video/async`
| Parameter | Type | Required | Description |
| - | - | - | - |
| `video_url` | string | Yes | URL of the video to extract data from. |
| `schema` | object | Yes | Extraction schema (same rules as the synchronous endpoint). |
| `what_to_extract` | string | No | Optional guidance for what to extract. |
| `transcribe` | boolean | No | Auto-transcribe when no platform transcript is available. Default `true`. |
| `webhook_url` | string | No | Where to POST the result when the job finishes. Overrides the account default set in [Studio → API](https://vidnavigator.com/studio/api); pass `""` to opt this job out of the default. Must be a public `https` URL. See [Webhooks](/webhooks). |
`include_usage` is not accepted on the submit request — pass it on the poll request instead.
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/extract/video/async" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://www.tiktok.com/@user/video/1234567890",
"schema": {
"main_topics": {
"type": "Array",
"description": "List of main topics discussed",
"items": { "type": "String", "description": "A topic" }
},
"key_takeaway": {
"type": "String",
"description": "The single most important takeaway"
}
},
"webhook_url": "https://example.com/hooks/vidnavigator"
}'
```
```python Python theme={null}
import time
import requests
BASE = "https://api.vidnavigator.com"
HEADERS = {"X-API-Key": "YOUR_API_KEY"}
task = requests.post(
f"{BASE}/v1/extract/video/async",
headers=HEADERS,
json={
"video_url": "https://www.tiktok.com/@user/video/1234567890",
"schema": {
"key_takeaway": {"type": "String", "description": "The single most important takeaway"}
},
},
).json()["data"]
while True:
job = requests.get(BASE + task["check_status_url"], headers=HEADERS).json()["data"]
if job["task_status"] != "processing":
break
time.sleep(5)
if job["task_status"] == "completed":
print(job["result"]) # the extracted fields
print(job["video_info"]["title"]) # the video's metadata
else:
print("Failed:", job["error"]["error"], job["error"]["message"])
```
```javascript JavaScript theme={null}
const BASE = 'https://api.vidnavigator.com';
const HEADERS = { 'X-API-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' };
const { data: task } = await (await fetch(`${BASE}/v1/extract/video/async`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({
video_url: 'https://www.tiktok.com/@user/video/1234567890',
schema: {
key_takeaway: { type: 'String', description: 'The single most important takeaway' }
}
})
})).json();
let job;
do {
await new Promise(r => setTimeout(r, 5000));
({ data: job } = await (await fetch(BASE + task.check_status_url, { headers: HEADERS })).json());
} while (job.task_status === 'processing');
console.log(job.task_status === 'completed' ? { data: job.result, video_info: job.video_info } : job.error);
```
The [Python](/sdks/python#structured-extraction) and [JavaScript](/sdks/javascript#structured-extraction) SDKs (v2.1.0) do the submit and polling for you: `client.extract_video_data(...)` / `vn.extractVideoData(...)` return the extracted `data` and the video's `video_info` in one call.
### Response (202 Accepted)
```json theme={null}
{
"status": "success",
"data": {
"task_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"task_status": "processing",
"job_type": "extract_video",
"expires_at": "2026-09-23T13:00:00Z",
"check_status_url": "/v1/extract/video/7c9e6679-7425-40de-944b-e07fc1f90ae7",
"webhook_url": "https://example.com/hooks/vidnavigator",
"message": "Extraction job accepted."
}
}
```
| Status | `error` | Description |
| - | - | - |
| `400` | `missing_parameter`, `request_body_required`, `invalid_schema`, `invalid_parameter` | Invalid request or schema. |
| `401` | — | Invalid or deactivated API key. |
| `402` | `limit_exceeded` | Less than 60 seconds of transcription credit left. No task is created. |
| `403` | — | The API key lacks permission for this endpoint. |
| `429` | `too_many_active_jobs` | Too many jobs already running for this account. |
## Get the result
The playground above only covers the submit request. Call the result endpoint with cURL or your HTTP client.
`GET https://api.vidnavigator.com/v1/extract/video/{task_id}`
| Parameter | In | Required | Description |
| - | - | - | - |
| `task_id` | path | Yes | The `task_id` returned by `POST /extract/video/async`. |
| `include_usage` | query | No | When `true` and `task_status=completed`, attaches a `usage` block with the final charges. |
Polling is **free**. Results are kept for **1 hour after the job finishes**, and reading a task doesn't delete it.
On `completed`, `data.result` is **identical** to the `data` block of a successful [synchronous `POST /extract/video`](/api-reference/endpoint/extract-video) response, and `data.video_info` holds the same `video_info` the synchronous endpoint returns beside it.
```json theme={null}
{
"status": "success",
"data": {
"task_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"task_status": "completed",
"job_type": "extract_video",
"created_at": "2026-09-23T12:00:00Z",
"started_at": "2026-09-23T12:00:01Z",
"completed_at": "2026-09-23T12:04:10Z",
"expires_at": "2026-09-23T13:04:10Z",
"check_status_url": "/v1/extract/video/7c9e6679-7425-40de-944b-e07fc1f90ae7",
"webhook": null,
"result": {
"main_topics": ["machine learning", "neural networks"],
"key_takeaway": "Start with clean data before choosing a model architecture"
},
"video_info": {
"title": "Machine Learning Fundamentals",
"channel": "Tech Academy",
"duration": 1843.0,
"url": "https://www.youtube.com/watch?v=abc123"
},
"error": null
}
}
```
`video_info` is only present once `task_status` is `completed`.
On `failed`, `data.result` is `null` and `data.error` carries the same code the synchronous endpoint would have returned (e.g. `transcript_not_available`, `http_status: 404`). All charges are reverted.
| Status | `error` | Description |
| - | - | - |
| `404` | `task_not_found` | Unknown `task_id`, expired, owned by another account, or not an extraction task. |
## Webhook
Pass `webhook_url` on the submit request, or configure a default endpoint in [Studio → API](https://vidnavigator.com/studio/api), to be called back when the job finishes. See [Webhooks](/webhooks) for the payload and signature verification, and [Async Jobs](/guides/async-jobs) for the full workflow.
# Get File Info
Source: https://docs.vidnavigator.com/api-reference/endpoint/file-info
GET /file/{file_id}
Retrieve detailed information about a specific file including its transcript if available
Retrieve detailed information about a specific uploaded file, including its transcript once processing is complete.
## Path Parameter
* `file_id` (string, required) – The ID of the uploaded file.
## Example Request
```bash cURL theme={null}
curl "https://api.vidnavigator.com/v1/file/file_abc123" \
-H "X-API-Key: YOUR_API_KEY"
```
## Success Response (200 OK)
Returns a `file_info` object (including namespace assignments) and, if processing is complete, a `transcript` object.
```json theme={null}
{
"status": "success",
"data": {
"file_info": {
"id": "file_abc123",
"name": "session.mp4",
"size": 15728640,
"type": "video/mp4",
"duration": 330.5,
"status": "completed",
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:00:00Z",
"has_transcript": true,
"namespace_ids": ["64a1b2c3d4e5f6789abc0001"],
"namespaces": [
{ "id": "64a1b2c3d4e5f6789abc0001", "name": "default" }
]
},
"transcript": [
{
"text": "Welcome to this tutorial...",
"start": 0.0,
"end": 3.5
}
]
}
}
```
# Get Temporary File URL
Source: https://docs.vidnavigator.com/api-reference/endpoint/file-url
GET /file/{file_id}/url
Returns a short-lived (~1 hour) signed storage URL pointing directly at the file bytes. Same model as AWS S3 / CloudFront pre-signed URLs: the URL itself is the capability, time-limited and resource-scoped, so it can be used from browsers, no-code tools, or any HTTP client without forwarding header auth. Call this endpoint again at any time to mint a fresh URL.
Generate a temporary, signed URL to directly access and read an uploaded file. This URL is valid for 1 hour.
## Path Parameter
* `file_id` (string, required)
## Example Request
```bash cURL theme={null}
curl "https://api.vidnavigator.com/v1/file/file_abc123/url" \
-H "X-API-Key: YOUR_API_KEY"
```
## Success Response (200 OK)
Returns a temporary, signed URL that can be used to access the file directly. The URL will expire after a short period.
```json theme={null}
{
"status": "success",
"data": {
"file_id": "file_abc123",
"file_url": "https://signed.aws.url/file_abc123?exp=1700000000"
}
}
```
# API Health Check
Source: https://docs.vidnavigator.com/api-reference/endpoint/health
GET /health
Check API health and get endpoint information
Check API health status and get a list of all available endpoints. This endpoint does **not** require authentication.
## Example Request
```bash cURL theme={null}
curl "https://api.vidnavigator.com/v1/health"
```
## Success Response
```json theme={null}
{
"status": "success",
"message": "API is healthy",
"version": "1.0.0",
"endpoints": [
{ "path": "/transcript", "method": "POST", "description": "Get transcript for any supported video (incl. YouTube)", "auth_required": true },
{ "path": "/transcribe", "method": "POST", "description": "Transcribe online videos", "auth_required": true },
{ "path": "/analyze/video", "method": "POST", "description": "Analyze online video", "auth_required": true },
{ "path": "/analyze/file", "method": "POST", "description": "Analyze uploaded file", "auth_required": true },
{ "path": "/extract/video", "method": "POST", "description": "Extract structured data from online video", "auth_required": true },
{ "path": "/extract/file", "method": "POST", "description": "Extract structured data from uploaded file", "auth_required": true },
{ "path": "/youtube/search", "method": "POST", "description": "Search YouTube for videos", "auth_required": true },
{ "path": "/tiktok/profile", "method": "POST", "description": "Submit a TikTok profile scrape", "auth_required": true },
{ "path": "/tiktok/profile/{task_id}", "method": "GET", "description": "Get TikTok profile scrape result", "auth_required": true },
{ "path": "/tiktok/search", "method": "POST", "description": "Submit a TikTok keyword search", "auth_required": true },
{ "path": "/tiktok/search/{task_id}", "method": "GET", "description": "Get TikTok keyword search result", "auth_required": true },
{ "path": "/search/file", "method": "POST", "description": "Search uploaded files", "auth_required": true },
{ "path": "/files", "method": "GET", "description": "List uploaded files", "auth_required": true },
{ "path": "/file/{file_id}", "method": "GET", "description": "Get file info and transcript", "auth_required": true },
{ "path": "/file/{file_id}/url", "method": "GET", "description": "Get temporary file URL", "auth_required": true },
{ "path": "/upload/file", "method": "POST", "description": "Upload a file", "auth_required": true },
{ "path": "/file/{file_id}/retry", "method": "POST", "description": "Retry failed file processing", "auth_required": true },
{ "path": "/file/{file_id}/cancel", "method": "POST", "description": "Cancel file processing", "auth_required": true },
{ "path": "/file/{file_id}/delete", "method": "DELETE", "description": "Delete a file", "auth_required": true },
{ "path": "/namespaces", "method": "GET", "description": "List namespaces", "auth_required": true },
{ "path": "/namespaces", "method": "POST", "description": "Create a namespace", "auth_required": true },
{ "path": "/namespaces/{namespace_id}", "method": "PUT", "description": "Rename a namespace", "auth_required": true },
{ "path": "/namespaces/{namespace_id}", "method": "DELETE", "description": "Delete a namespace", "auth_required": true },
{ "path": "/file/{file_id}/namespaces", "method": "PUT", "description": "Update file namespace assignments", "auth_required": true },
{ "path": "/usage", "method": "GET", "description": "Get usage statistics", "auth_required": true },
{ "path": "/health", "method": "GET", "description": "Health check", "auth_required": false }
]
}
```
# List Files
Source: https://docs.vidnavigator.com/api-reference/endpoint/list-files
GET /files
Retrieve a paginated list of all files uploaded by the authenticated user. Supports filtering by status and namespace.
Retrieve a paginated list of all audio/video files you have uploaded. Supports filtering by status and namespace.
## Overview
Use this endpoint to get basic information on every file you have uploaded, including status, metadata, and namespace assignments. Supports pagination with `limit` and `offset` query parameters.
## Query Parameters
* `limit` (integer, default `50`): Maximum number of files to return (1-100)
* `offset` (integer, default `0`): Number of files to skip for pagination
* `status` (string): Filter by processing status (`processing`, `completed`, `failed`, `cancelled`)
* `namespace_id` (string): Filter by namespace ID. Only returns files belonging to this namespace. If not set, returns files from all namespaces.
## Example Request
```bash cURL theme={null}
curl -G "https://api.vidnavigator.com/v1/files" \
-H "X-API-Key: YOUR_API_KEY" \
--data-urlencode "limit=25" \
--data-urlencode "offset=0" \
--data-urlencode "status=completed"
```
```bash cURL (filter by namespace) theme={null}
curl -G "https://api.vidnavigator.com/v1/files" \
-H "X-API-Key: YOUR_API_KEY" \
--data-urlencode "namespace_id=64a1b2c3d4e5f6789abc0002"
```
## Success Response
```json theme={null}
{
"status": "success",
"data": {
"files": [
{
"id": "file_abc123",
"name": "session.mp4",
"size": 15728640,
"type": "video/mp4",
"duration": 330,
"status": "completed",
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:00:00Z",
"has_transcript": true,
"namespace_ids": ["64a1b2c3d4e5f6789abc0001", "64a1b2c3d4e5f6789abc0002"],
"namespaces": [
{ "id": "64a1b2c3d4e5f6789abc0001", "name": "default" },
{ "id": "64a1b2c3d4e5f6789abc0002", "name": "Client Calls" }
]
}
],
"total_count": 1,
"limit": 25,
"offset": 0,
"has_more": false
}
}
```
# List Namespaces
Source: https://docs.vidnavigator.com/api-reference/endpoint/list-namespaces
GET /namespaces
Get all namespaces for the authenticated user. A 'default' namespace is auto-created if none exist. Files without a namespace are lazily migrated to the default.
Retrieve all namespaces for your account. Namespaces let you organize uploaded files into logical groups (like folders).
## Overview
Namespaces help you organize your uploaded files into categories such as "Client Calls", "Product Demos", or "Training Videos". A `default` namespace is automatically created if you don't have any yet, and files without a namespace are lazily migrated to it.
## Example Request
```bash cURL theme={null}
curl "https://api.vidnavigator.com/v1/namespaces" \
-H "X-API-Key: YOUR_API_KEY"
```
```python Python theme={null}
import requests
url = "https://api.vidnavigator.com/v1/namespaces"
headers = {"X-API-Key": "YOUR_API_KEY"}
response = requests.get(url, headers=headers)
namespaces = response.json()
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.vidnavigator.com/v1/namespaces', {
headers: { 'X-API-Key': 'YOUR_API_KEY' }
});
const namespaces = await response.json();
```
## Success Response
```json theme={null}
{
"status": "success",
"data": [
{
"id": "64a1b2c3d4e5f6789abc0001",
"name": "default",
"created_at": "2025-01-10T08:00:00Z",
"updated_at": "2025-01-10T08:00:00Z"
},
{
"id": "64a1b2c3d4e5f6789abc0002",
"name": "Client Calls",
"created_at": "2025-02-15T14:30:00Z",
"updated_at": "2025-02-15T14:30:00Z"
}
]
}
```
# Rename Namespace
Source: https://docs.vidnavigator.com/api-reference/endpoint/rename-namespace
PUT /namespaces/{namespace_id}
Rename an existing namespace. The 'default' namespace cannot be renamed.
Rename an existing namespace. The `default` namespace cannot be renamed.
## Example Request
```bash cURL theme={null}
curl -X PUT "https://api.vidnavigator.com/v1/namespaces/64a1b2c3d4e5f6789abc0003" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Product Demos"
}'
```
```python Python theme={null}
import requests
namespace_id = "64a1b2c3d4e5f6789abc0003"
url = f"https://api.vidnavigator.com/v1/namespaces/{namespace_id}"
headers = {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json"
}
data = {"name": "Product Demos"}
response = requests.put(url, headers=headers, json=data)
result = response.json()
```
```javascript JavaScript theme={null}
const namespaceId = '64a1b2c3d4e5f6789abc0003';
const response = await fetch(`https://api.vidnavigator.com/v1/namespaces/${namespaceId}`, {
method: 'PUT',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'Product Demos' })
});
const result = await response.json();
```
## Success Response
```json theme={null}
{
"status": "success",
"message": "Namespace renamed successfully"
}
```
# Retry File Processing
Source: https://docs.vidnavigator.com/api-reference/endpoint/retry-file
POST /file/{file_id}/retry
Retry processing for a file that previously failed or was cancelled
Restart processing for a file that previously failed or was cancelled.
## Path Parameter
* `file_id` (string, required)
## Example Request
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/file/file_abc123/retry" \
-H "X-API-Key: YOUR_API_KEY"
```
## Success Response (200 OK)
Returns a confirmation that the file processing has been restarted. The `file_status` will be updated to `processing`.
```json theme={null}
{
"status": "success",
"data": {
"file_id": "file_abc123",
"file_name": "failed_video.mp4",
"file_status": "processing",
"message": "File processing restarted successfully"
}
}
```
# Search Files
Source: https://docs.vidnavigator.com/api-reference/endpoint/search-file
POST /search/file
Search through user's uploaded files using vector similarity with AI reranking.
**Namespace filtering:** Pass `namespace_ids` to restrict the search to files in specific namespaces. If omitted, all namespaces are searched.
**Process:**
1. Vector similarity search using text embeddings
2. Initial ranking by semantic similarity
3. AI reranking for improved relevance
4. Rich metadata and signed URLs for file access
5. Optional per-file AI analysis for people, places, key subjects, and key moments
Each result includes the `namespace_ids` and resolved `namespaces` the file belongs to.
**Billing:** one `search_request` per call. The AI analysis pass (per-file + overall aggregation) bills `analysis_request` as `ceil(total_tokens / 15000)` units (minimum 1 when analysis ran). Set `include_usage=true` to receive the per-charge breakdown — the consolidated `analysis_request` entry carries a nested `tokens` object.
Search your uploaded files using vector similarity and AI reranking.
## Overview
This endpoint searches through your uploaded file library using OpenAI embeddings for semantic similarity, followed by AI reranking for improved relevance. Results include rich metadata, signed URLs, and optional per-file analysis.
## Billing
File search is a semantic (vector) search over already indexed content and consumes **one `search_request`** per call. 1 credit covers 1,000 semantic searches. If the AI analysis pass runs (per-file + overall aggregation), it also bills `analysis_request` as `ceil(total_tokens / 15000)` units (minimum 1 when analysis ran).
Set `include_usage: true` to receive a [`usage`](/api-reference/usage-costs#per-call-usage-disclosure) block with the per-charge breakdown — the consolidated `analysis_request` entry carries a nested `tokens` object.
### Search Process
1. Vector similarity search using OpenAI embeddings
2. Initial ranking by semantic similarity
3. AI reranking for improved relevance
4. Rich metadata and signed URLs for file access
5. Optional per-file AI analysis for people, places, key subjects, and key moments
## Request Body
| Parameter | Type | Required | Description |
| - | - | - | - |
| `query` | string | Yes | Search query |
| `namespace_ids` | string\[] | No | Restrict search to specific namespaces. If omitted, searches all files. |
## Example Request
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/search/file" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "What did the customer say about pricing?"
}'
```
```bash cURL (with namespace filter) theme={null}
curl -X POST "https://api.vidnavigator.com/v1/search/file" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "What did the customer say about pricing?",
"namespace_ids": ["64a1b2c3d4e5f6789abc0002"]
}'
```
```python Python theme={null}
import requests
url = "https://api.vidnavigator.com/v1/search/file"
headers = {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json"
}
data = {
"query": "What did the customer say about pricing?",
"namespace_ids": ["64a1b2c3d4e5f6789abc0002"]
}
response = requests.post(url, headers=headers, json=data)
results = response.json()
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.vidnavigator.com/v1/search/file', {
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: 'What did the customer say about pricing?',
namespace_ids: ['64a1b2c3d4e5f6789abc0002']
})
});
const results = await response.json();
```
## Success Response
```json theme={null}
{
"status": "success",
"data": {
"results": [
{
"id": "file_abc123",
"name": "client-call-march.mp4",
"duration": 1800,
"size": 52428800,
"type": "video/mp4",
"status": "completed",
"created_at": "2026-03-10T14:00:00Z",
"file_url": "https://storage.example.com/signed-url...",
"namespace_ids": ["64a1b2c3d4e5f6789abc0002"],
"namespaces": [
{ "id": "64a1b2c3d4e5f6789abc0002", "name": "Client Calls" }
],
"relevance_score": 0.92,
"timestamps": [245.5, 890.2],
"relevant_text": "The customer mentioned the pricing was competitive but wished for a monthly option.",
"query_answer": "The customer found pricing competitive but requested monthly billing.",
"transcript_summary": "A 30-minute client call discussing product features, pricing, and onboarding timeline.",
"people": [
{ "name": "John Smith", "context": "Client representative" }
],
"key_subjects": [
{ "name": "Pricing", "description": "Discussion about pricing tiers and billing options" }
]
}
],
"query": "What did the customer say about pricing?",
"total_found": 1,
"explanation": "Found 1 file matching your query about customer pricing feedback."
}
}
```
# TikTok Profile Scrape
Source: https://docs.vidnavigator.com/api-reference/endpoint/tiktok-profile
POST /tiktok/profile
Start an asynchronous TikTok profile scrape and return a `task_id` immediately.
The scrape runs in the background with shallow extraction so it can stop early when filters are satisfied. Results are retained for 1 hour and, when cloud storage is configured, also uploaded as a single JSON file accessible through `download_url`.
**Billing:** Each TikTok API page fetched is billed as one standard request. One page is pre-charged when the task is accepted; remaining pages are reconciled when the scrape completes. If the scrape fails after charging additional pages, all charged pages are reversed. TikTok currently returns ~15 videos per page; this is an internal estimate and may change.
**Filters:** All filters are applied as the scraper iterates. `max_posts` and `after_datetime` allow the scraper to stop fetching new pages early; the other filters reduce result size but do not stop pagination.
**Usage disclosure:** This endpoint does NOT accept `include_usage`. Because billing happens in the background worker after the 202 response is sent, there is nothing to disclose at submit time. Pass `include_usage=true` on `GET /tiktok/profile/{task_id}` instead — the GET endpoint replays the final charges once `task_status=completed`.
Use `GET /tiktok/profile/{task_id}` to poll status and retrieve paginated results, or follow `download_url` for the complete JSON.
Start an asynchronous scrape of a public TikTok profile and return a `task_id` immediately.
## Overview
This endpoint is designed for profile-level collection, not single-video analysis. It starts a background scrape job that iterates through a TikTok account, applies your filters as it goes, and stores the results temporarily so you can fetch them later.
Use it when you want to:
* collect recent posts from a TikTok creator
* filter videos by date or like count
* scrape large profiles without blocking on a long request
* export the full result set through a temporary `download_url`
## How the Async Flow Works
1. Send `POST /tiktok/profile` with a public TikTok `profile_url` and optional filters.
2. The API accepts the task and returns a `task_id` with `task_status: "processing"`.
3. Poll [`GET /tiktok/profile/{task_id}`](#get-the-result) until the task is `completed` or `failed`.
4. Once completed, either page through `videos` using cursors or download the full JSON from `download_url` when available.
Results are temporary. Tasks expire about 1 hour after creation, including `download_url` availability.
## Filters You Can Apply
All filters are optional except `profile_url`.
| Field | Type | Description |
| - | - | - |
| `profile_url` | string | Public TikTok profile URL, such as `https://www.tiktok.com/@username` |
| `max_posts` | integer | Maximum number of matching videos to return |
| `after_datetime` | string | Include only videos published on or after this boundary. Accepts `YYYY-MM-DD` or an ISO datetime with timezone, such as `2024-01-01T00:00:00Z`. |
| `before_datetime` | string | Include only videos published on or before this boundary. Accepts `YYYY-MM-DD` or an ISO datetime with timezone, such as `2024-12-31T23:59:59+02:00`. |
| `min_likes` | integer | Include only videos with at least this many likes |
| `max_likes` | integer | Include only videos with at most this many likes |
| `webhook_url` | string | Where to POST a notification when the task finishes. Overrides the account default set in [Studio → API](https://vidnavigator.com/studio/api); pass `""` to opt this task out of the default. Must be a public `https` URL. See [Webhooks](/webhooks). |
The TikTok webhook event is a **notification, not the payload**: a scrape can hold thousands of videos, so the event carries `stats` and tells you to read the results from `check_status_url`.
`max_posts` and `after_datetime` are the most useful filters for keeping scrapes smaller and faster because they can stop pagination early.
## What You Get Back Immediately
The submit endpoint now returns more than just a task ID. The accepted response includes:
* `task_id` to identify the scrape job
* `task_status` set to `processing`
* `profile_url` echoing the submitted profile
* `expires_at` so you know when the task record will expire
* `check_status_url` with the relative API path to poll
* `webhook_url`: the URL the completion notification will be POSTed to, or `null`
* `message` confirming the job was accepted
## Example Request
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/tiktok/profile" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"profile_url": "https://www.tiktok.com/@tiktok",
"max_posts": 50,
"after_datetime": "2024-01-01T00:00:00Z",
"min_likes": 10000
}'
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.vidnavigator.com/v1/tiktok/profile",
headers={
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"profile_url": "https://www.tiktok.com/@tiktok",
"max_posts": 50,
"after_datetime": "2024-01-01T00:00:00Z",
"min_likes": 10000,
},
)
print(response.json())
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.vidnavigator.com/v1/tiktok/profile', {
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
profile_url: 'https://www.tiktok.com/@tiktok',
max_posts: 50,
after_datetime: '2024-01-01T00:00:00Z',
min_likes: 10000
})
});
const result = await response.json();
```
## Success Response
The endpoint returns `202 Accepted` because the scrape runs in the background.
```json theme={null}
{
"status": "success",
"data": {
"task_id": "tt_profile_abc123",
"task_status": "processing",
"profile_url": "https://www.tiktok.com/@tiktok",
"expires_at": "2026-04-26T00:55:00Z",
"check_status_url": "/v1/tiktok/profile/tt_profile_abc123",
"webhook_url": null,
"message": "TikTok profile scrape accepted. Poll the status endpoint to retrieve results."
}
}
```
## Billing Notes
TikTok profile scraping is billed by pages consumed during the scrape:
* each TikTok API page is billed as one `standard_request` when no proxy is used
* each TikTok API page is billed as one `residential_request` when a residential proxy is used
* one page is pre-charged when the task is accepted
* final usage is reconciled when the scrape completes
* if the scrape fails after extra pages were charged, those charges are reversed
TikTok currently returns about 15 videos per page, but that is an implementation detail and may change.
## Tips for Large Profiles
* use `after_datetime` to avoid crawling older history you do not need
* use `max_posts` to cap the result size
* prefer `download_url` after completion if you expect a large result set
* poll the result endpoint while the task is processing instead of resubmitting the same scrape
## Common Errors
* `request_body_required`: the JSON body was missing
* `missing_parameter`: `profile_url` was not sent
* `invalid_url`: the submitted `profile_url` is malformed
* `invalid_parameter`: one of the filters is malformed
The OpenAPI spec no longer lists `unsupported_platform` for this endpoint.
## Get the result
`GET /tiktok/profile/{task_id}` — poll this endpoint with the `task_id` (or the `check_status_url`) returned above, or wait for the [webhook](/webhooks) notification. Polling does **not** consume credits.
The playground above only covers the submit request. Call the result endpoint with cURL or your HTTP client.
### Request Parameters
| Parameter | In | Required | Description |
| - | - | - | - |
| `task_id` | path | Yes | Task ID returned by `POST /tiktok/profile` |
| `cursor` | query | No | Opaque pagination cursor from a previous response |
| `limit` | query | No | Maximum number of videos to return, between `1` and `500`. Default is `50`. |
### How Pagination Works
Pagination is cursor-based, but the cursor encodes an absolute offset. That means you can safely change `limit` between requests without skipping or duplicating videos.
Typical flow:
1. Call the endpoint with no `cursor`.
2. Read `data.pagination.next_cursor`.
3. Pass that value back as the next request's `cursor`.
4. Stop when `has_next` is `false`.
### Polling Lifecycle
The `task_status` field will be one of:
* `processing`: the scrape is still running
* `completed`: the scrape finished and results are ready
* `failed`: the scrape ended with an error
While the task is still processing:
* `profile` may be `null`
* `videos` will be empty
* `download_url` will usually be `null`
Once completed:
* `videos` contains a cursor-paginated page of matching posts
* `pagination` tells you whether more pages exist
* `download_url` may contain a temporary signed URL for the full JSON result
### Example Requests
```bash cURL theme={null}
curl "https://api.vidnavigator.com/v1/tiktok/profile/tt_profile_abc123?limit=25" \
-H "X-API-Key: YOUR_API_KEY"
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.vidnavigator.com/v1/tiktok/profile/tt_profile_abc123",
headers={"X-API-Key": "YOUR_API_KEY"},
params={"limit": 25},
)
print(response.json())
```
```javascript JavaScript theme={null}
const response = await fetch(
'https://api.vidnavigator.com/v1/tiktok/profile/tt_profile_abc123?limit=25',
{
headers: {
'X-API-Key': 'YOUR_API_KEY'
}
}
);
const result = await response.json();
```
#### Fetch the Next Page
```bash cURL theme={null}
curl "https://api.vidnavigator.com/v1/tiktok/profile/tt_profile_abc123?limit=25&cursor=eyJvZmZzZXQiOjI1fQ==" \
-H "X-API-Key: YOUR_API_KEY"
```
### Example Completed Response
```json theme={null}
{
"status": "success",
"data": {
"task_id": "tt_profile_abc123",
"task_status": "completed",
"profile_url": "https://www.tiktok.com/@tiktok",
"profile": {
"uploader": "tiktok",
"follower_count": 12345678
},
"filters": {
"max_posts": 50,
"after_datetime": "2024-01-01T00:00:00Z",
"before_datetime": null,
"min_likes": 10000,
"max_likes": null
},
"stats": {
"videos_scanned": 120,
"videos_matched": 50,
"pages_consumed": 8
},
"videos": [
{
"id": "7351234567890123456",
"track": "Original sound",
"artists": ["Creator Name"],
"duration": 28,
"title": "Example TikTok",
"description": "Video caption text here",
"timestamp": 1712345678,
"published_at": "2024-04-05T19:34:38Z",
"views": 250000,
"likes": 22000,
"reposts": 180,
"comments": 950,
"thumbnails": [],
"url": "https://www.tiktok.com/@tiktok/video/7351234567890123456"
}
],
"pagination": {
"limit": 25,
"offset": 0,
"total_items": 50,
"has_next": true,
"has_prev": false,
"next_cursor": "eyJvZmZzZXQiOjI1fQ==",
"prev_cursor": null
},
"download_url": "https://storage.googleapis.com/bucket/api/tiktok_profiles/user/task.json?...",
"error_message": null,
"error": null,
"webhook": null,
"created_at": "2026-04-25T20:00:00Z",
"completed_at": "2026-04-25T20:01:12Z",
"expires_at": "2026-04-25T21:00:00Z"
}
}
```
### Understanding the Response
#### `profile`
Contains profile-level metadata returned by the scraper, such as uploader information and follower counts when available.
#### `stats`
Helps you understand what happened during the scrape:
* `videos_scanned`: total TikTok entries iterated before filtering
* `videos_matched`: entries that passed your filters
* `pages_consumed`: estimated number of TikTok API pages fetched during the scrape
#### `videos`
This is the current page of matched TikTok videos. Each item includes public metadata such as:
* `id`
* `title`
* `description`
* `timestamp`: Unix timestamp in seconds when the video was uploaded
* `published_at`: UTC datetime string derived from `timestamp`, in ISO 8601 format with timezone
* `views`
* `likes`
* `comments`
* `url`
#### `download_url`
When present, this is a short-lived signed URL pointing to the full scrape result as one JSON file. It is useful for large profiles, browser downloads, and no-code integrations.
If it is `null`, either:
* the task has not finished yet, or
* the full result wasn't archived, and you should paginate through `videos` instead
#### `error`
Present when `task_status` is `failed`: an object with `error` (a code you can branch on), `message` and `http_status`. See [Errors](/api-reference/errors).
`error_message` is **deprecated**. It is kept for existing integrations, but new code should read `error.error` / `error.message` instead.
#### `webhook`
Delivery state of the task's [webhook](/webhooks) notification (`status`, `attempts`, `response_status`, `last_error`, `delivered_at`), or `null` when no webhook applies.
### Error Cases
* `400 invalid_cursor`: the supplied `cursor` is invalid
* `404 task_not_found`: task does not exist, expired, or does not belong to the current user
### Tips
* poll every few seconds while `task_status` is `processing`
* switch to `download_url` for large completed jobs
* do not assume tasks live forever; they expire after about 1 hour
* call the endpoint again later if you need a freshly minted `download_url`
* use the `expires_at` value to avoid polling a task after it has already expired
# TikTok Keyword Search
Source: https://docs.vidnavigator.com/api-reference/endpoint/tiktok-search
POST /tiktok/search
Start an asynchronous TikTok keyword search task and return a `task_id` immediately. Poll `GET /tiktok/search/{task_id}` for cursor-paginated results. Results are retained for ~1 hour.
Results are returned sorted by `published_at` descending (newest first) across pagination, full retrieval, and the signed `download_url` payload, unless `sort_by` is set, in which case that order is kept.
**parallel_search_slices billing:** by default (`1`), the search runs a single paginated query chain and is naturally capped at roughly 115-140 items by TikTok. Setting `parallel_search_slices` to `2..4` runs that many concurrent chains in parallel and dedups by video id, lifting the effective ceiling. Cost scales linearly: each additional slice consumes up to ~1x the residential pages of a single search, so an N-slice request bills up to **Nx the residential pages**. Extra slices add the most on searches without a date window; with `published_within` set, every slice searches the same window and can return largely the same videos. `parallel_search_slices` is *not* a time filter — use `published_within`, `after_datetime` or `before_datetime` for time windows.
**Usage disclosure:** This endpoint does NOT accept `include_usage`. Billing happens in the background worker after the 202 response is sent, so there is nothing to disclose at submit time. Pass `include_usage=true` on `GET /tiktok/search/{task_id}` instead — the GET endpoint replays the final charges once `task_status=completed`.
Start an asynchronous TikTok keyword search and get a `task_id` back immediately.
## Overview
This endpoint runs an actual keyword search across TikTok (not a single profile) and returns multiple matching videos. It is asynchronous: the search runs in a background worker, and you poll [`GET /tiktok/search/{task_id}`](#get-the-result) for cursor-paginated results. Results are retained for \~1 hour.
Results are returned sorted by `published_at` descending (newest first) across pagination, full retrieval, and the signed `download_url` payload — unless you set `sort_by`, in which case that order is kept.
This is different from the [TikTok Profile scrape](/api-reference/endpoint/tiktok-profile), which walks a single account's videos. Use this endpoint to discover videos across TikTok by keyword.
## Billing
Charges happen in the background worker and are disclosed on the polling endpoint (see below), not here.
* Billing is `pages_fetched × residential_request`. **No `search_request`** is billed for TikTok keyword search.
* **`parallel_search_slices`** scales the cost linearly. By default (`1`) the search runs a single paginated query chain, naturally capped at roughly 115–140 items by TikTok. Setting it to `2..4` runs that many concurrent chains and dedups by video id, lifting the ceiling — but an N-slice request bills up to **N× the residential pages** of a single search. Extra slices help most on searches **without** a date window: with `published_within` set, every slice searches the same window and can return largely the same videos.
**Usage disclosure:** this endpoint does **not** accept `include_usage`. Because billing happens after the `202` response is sent, there is nothing to disclose at submit time. Pass `include_usage=true` on [`GET /tiktok/search/{task_id}`](#get-the-result) instead — it replays the final charges once `task_status=completed`.
## Request Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `query` | string | Yes | Keyword phrase to search on TikTok (min length 2). |
| `max_results` | integer | No | Upper bound on results the task will try to collect. `0` (default) or omitted means **unlimited** — pagination stops only when TikTok has no more results or the page cap is reached (\~115–140 items per slice: \~5 pages of \~30 items). No hard server-side cap is enforced; you are billed per residential page actually fetched. |
| `parallel_search_slices` | integer | No | How many concurrent paginated query chains to run and dedup (`1`–`4`, default `1`). Higher values lift the result ceiling at up to **\~N× the residential pages** of a single search. With `published_within` set, keep `1` unless you have measured a gain. Not a time filter — use `published_within`, `after_datetime` or `before_datetime` for time windows. |
| `sort_by` | string | No | Sort order applied by TikTok itself (same as the app's search Filters): `relevance`, `most_liked` or `newest`. See [Sorting and date windows](#sorting-and-date-windows). |
| `published_within` | string | No | Publication window applied by TikTok itself: `all`, `past_24_hours`, `this_week`, `this_month`, `last_3_months` or `last_6_months`. Windows are rolling (`this_week` = the last 7 days). |
| `after_datetime` | string | No | Only include videos published on or after this boundary. Accepts `YYYY-MM-DD` or an ISO datetime with timezone. Unless `sort_by` or `published_within` is set, it also makes the search run newest first and stop once it passes this boundary. |
| `before_datetime` | string | No | Only include videos published on or before this boundary. Accepts `YYYY-MM-DD` or an ISO datetime with timezone. |
| `min_likes` | integer | No | Only include videos with at least this many likes. |
| `max_likes` | integer | No | Only include videos with at most this many likes. |
| `min_views` | integer | No | Only include videos with at least this many views. |
| `max_views` | integer | No | Only include videos with at most this many views. |
| `webhook_url` | string | No | Where to POST a notification when the task finishes. Overrides the account default set in [Studio → API](https://vidnavigator.com/studio/api); pass `""` to opt this task out of the default. Must be a public `https` URL. The event carries `stats` and points you to `check_status_url` for the results. See [Webhooks](/webhooks). |
**Diminishing returns on slices:** on a representative query without a date window, 2 slices yield \~+43% unique items, 3 slices \~+22%, and 4 slices \~+13% — so 4 already saturates TikTok's natural \~480-item dedup ceiling for most queries.
## Sorting and date windows
`sort_by` and `published_within` are applied **by TikTok itself**, before VidNavigator's own filters (`after_datetime`, `min_likes`, …). Using them well returns more matching videos per billed page.
| `sort_by` | Order |
| - | - |
| `relevance` | TikTok's own ranking |
| `most_liked` | By like count |
| `newest` | By publish date, newest first |
**When `sort_by` is omitted**, results are returned newest first, and the search is ordered to fit your filters:
* `after_datetime` or `before_datetime` set → the search runs **newest first** and stops once it passes `after_datetime`
* only `min_likes` or `min_views` set → the search runs **most liked first**
**When `published_within` is omitted** and `after_datetime` is more than 24 hours ago, the smallest window that covers `after_datetime` is selected automatically. The window actually used is reported in `stats.published_within` on the [result endpoint](#get-the-result).
```json theme={null}
{
"query": "ai tools",
"sort_by": "most_liked",
"published_within": "this_week",
"min_views": 10000
}
```
## Example Usage
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/tiktok/search" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "ai tools",
"parallel_search_slices": 2,
"after_datetime": "2024-01-01T00:00:00Z",
"min_likes": 1000
}'
```
```python Python theme={null}
import requests
url = "https://api.vidnavigator.com/v1/tiktok/search"
headers = {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json"
}
data = {
"query": "ai tools",
"parallel_search_slices": 2,
"after_datetime": "2024-01-01T00:00:00Z",
"min_likes": 1000
}
response = requests.post(url, headers=headers, json=data)
task = response.json()
print("Task ID:", task["data"]["task_id"])
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.vidnavigator.com/v1/tiktok/search', {
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: 'ai tools',
parallel_search_slices: 2,
after_datetime: '2024-01-01T00:00:00Z',
min_likes: 1000
})
});
const task = await response.json();
console.log('Task ID:', task.data.task_id);
```
## Success Response (202 Accepted)
```json theme={null}
{
"status": "success",
"data": {
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"task_status": "processing",
"query": "ai tools",
"max_results": 0,
"parallel_search_slices": 2,
"filters": {
"sort_by": null,
"published_within": null,
"after_datetime": "2024-01-01T00:00:00Z",
"before_datetime": null,
"min_likes": 1000,
"max_likes": null,
"min_views": null,
"max_views": null
},
"expires_at": "2026-04-26T13:00:00Z",
"check_status_url": "/v1/tiktok/search/550e8400-e29b-41d4-a716-446655440000",
"webhook_url": null,
"message": "TikTok search task accepted and queued."
}
}
```
## Get the result
`GET /tiktok/search/{task_id}` — poll this endpoint with the `task_id` (or the `check_status_url`) returned above, or wait for the [webhook](/webhooks) notification. Polling does **not** consume credits.
The playground above only covers the submit request. Call the result endpoint with cURL or your HTTP client.
### Request Parameters
| Parameter | In | Required | Description |
| - | - | - | - |
| `task_id` | path | Yes | Task ID returned by `POST /tiktok/search` |
| `cursor` | query | No | Opaque pagination cursor from a previous response |
| `limit` | query | No | Maximum number of items to return, between `1` and `500`. Default is `50`. |
| `include_usage` | query | No | When `true` and `task_status=completed`, attach a `usage` block describing the charges the background worker recorded (`pages_fetched × residential_request`; no `search_request` for TikTok keyword search). |
### Usage Disclosure
Because billing happens in the background worker, the submit endpoint cannot disclose usage. Instead, pass `include_usage=true` here:
* Returned **only** when `task_status=completed` (processing tasks haven't finished billing; failed tasks have all charges refunded).
* Charges are `pages_fetched × residential_request`. No `search_request` is billed for TikTok keyword search.
### Polling Lifecycle
The `task_status` field will be one of:
* `processing`: the search is still running
* `completed`: the search finished and results are ready
* `failed`: the search ended with an error (all charges refunded)
While processing, `results` will be empty and `download_url` will usually be `null`.
### Example Requests
```bash cURL theme={null}
curl "https://api.vidnavigator.com/v1/tiktok/search/tt_search_abc123?limit=25&include_usage=true" \
-H "X-API-Key: YOUR_API_KEY"
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.vidnavigator.com/v1/tiktok/search/tt_search_abc123",
headers={"X-API-Key": "YOUR_API_KEY"},
params={"limit": 25, "include_usage": "true"},
)
print(response.json())
```
```javascript JavaScript theme={null}
const response = await fetch(
'https://api.vidnavigator.com/v1/tiktok/search/tt_search_abc123?limit=25&include_usage=true',
{
headers: {
'X-API-Key': 'YOUR_API_KEY'
}
}
);
const result = await response.json();
```
### Example Completed Response
```json theme={null}
{
"status": "success",
"data": {
"task_id": "tt_search_abc123",
"task_status": "completed",
"query": "ai tools",
"parallel_search_slices": 2,
"filters": {
"sort_by": null,
"published_within": null,
"after_datetime": "2024-01-01T00:00:00Z",
"before_datetime": null,
"min_likes": 1000,
"max_likes": null,
"min_views": null,
"max_views": null
},
"stats": {
"pages_fetched": 18,
"results_count": 172,
"next_search_cursor": null,
"sort_by": "relevance",
"published_within": "all"
},
"results": [
{
"id": "7351234567890123456",
"item_type": 1,
"description": "Top AI tools you should try",
"timestamp": 1712345678,
"published_at": "2024-04-05T19:34:38Z",
"author": {
"id": "6789",
"unique_id": "creator",
"nickname": "Creator Name",
"sec_uid": "MS4wLjAB..."
},
"stats": {
"views": 250000,
"likes": 22000,
"comments": 950,
"shares": 120,
"collects": 340
},
"music": {
"id": "1122",
"title": "Original sound",
"author_name": "creator",
"duration": 30
},
"duration": 28,
"hashtags": ["ai", "tools", "productivity"],
"url": "https://www.tiktok.com/@creator/video/7351234567890123456"
}
],
"pagination": {
"limit": 25,
"offset": 0,
"total_items": 172,
"has_next": true,
"has_prev": false,
"next_cursor": "eyJvZmZzZXQiOjI1fQ==",
"prev_cursor": null
},
"download_url": "https://storage.googleapis.com/bucket/api/tiktok_searches/user/task.json?...",
"error_message": null,
"error": null,
"webhook": null,
"created_at": "2026-04-26T12:00:00Z",
"completed_at": "2026-04-26T12:01:40Z",
"expires_at": "2026-04-26T13:00:00Z"
},
"usage": {
"charges": [
{ "service_type": "residential_request", "quantity": 18, "credits": 0.09 }
],
"total_credits": 0.09
}
}
```
The `usage` block appears only when `include_usage=true` and `task_status=completed`.
### Understanding the Response
#### `stats`
* `pages_fetched`: number of TikTok pages fetched (this is what you are billed for, as `residential_request`)
* `results_count`: total normalized items collected
* `next_search_cursor`: internal upstream cursor, or `null` when exhausted
* `sort_by`: sort order the search ran with (`relevance` when `sort_by` was omitted)
* `published_within`: date window the search ran with — shows the window chosen from `after_datetime` when `published_within` was omitted; `all` means no window
#### `results`
Each item is a normalized TikTok video with `id`, `description`, `timestamp`, `published_at` (UTC ISO 8601), `author`, `stats` (views/likes/comments/shares/collects), `music`, `duration`, `hashtags`, and `url`. Items are sorted by `published_at` descending (items without a timestamp go last), unless `sort_by` was set, in which case TikTok's order is kept.
#### `download_url`
When present, a short-lived signed URL pointing to the full search result as one JSON file (same shape as the paginated `results`, but unsliced). `null` when the task hasn't finished or the full result wasn't archived — paginate through `results` instead.
#### `error`
Present when `task_status` is `failed`: an object with `error` (a code you can branch on), `message` and `http_status`. See [Errors](/api-reference/errors).
`error_message` is **deprecated**. It is kept for existing integrations, but new code should read `error.error` / `error.message` instead.
#### `webhook`
Delivery state of the task's [webhook](/webhooks) notification (`status`, `attempts`, `response_status`, `last_error`, `delivered_at`), or `null` when no webhook applies.
### Error Cases
* `400`: invalid request (e.g. bad cursor)
* `404`: task does not exist, expired, or does not belong to the current user
### Tips
* poll every few seconds while `task_status` is `processing`
* switch to `download_url` for large completed jobs
* tasks expire after about 1 hour — use `expires_at` to avoid polling expired tasks
* call the endpoint again later if you need a freshly minted `download_url`
# Transcribe Online Video
Source: https://docs.vidnavigator.com/api-reference/endpoint/transcribe
POST /transcribe
Transcribe online videos (e.g., Instagram, TikTok) using speech-to-text models when a transcript is not available.
**Instagram Carousel Posts:**
- For posts with multiple videos, the URL can include `?img_index=N` to select a specific video
- Example: `https://www.instagram.com/p/ABC123/?img_index=2` transcribes the second video
- Without `img_index`, the first video is transcribed
- Set `all_videos=true` to transcribe ALL videos in a carousel post
**Options:**
- `transcript_text=true`: Returns transcript as a single text string instead of segments
- `all_videos=true`: For carousel posts, returns all videos with their transcripts
**Billing:** on cache miss, one `standard_request` or `residential_request` is charged for the metadata fetch (residential for Instagram, Facebook-watch, Rumble; standard otherwise), plus `transcription_hour` proportional to the audio duration. On cache hit (same URL transcribed before for any user), only `transcription_hour` is charged — the metadata fetch is skipped. Users with the `free_on_cache_hit` sponsorship flag get the cache-hit `transcription_hour` waived (`waived: true` in the per-charge entry). Set `include_usage=true` to see the breakdown.
**Long videos:** this endpoint is synchronous and holds the connection open for the whole transcription. For media longer than ~10 minutes use `POST /transcribe/async` instead, which returns a `task_id` and can call your webhook when it finishes. Media longer than 10 minutes is rejected here with `video_too_long` (HTTP 400). The metadata fetch needed to read the video's duration is billed (`standard_request` or `residential_request`) and is **not** refunded: the limit is documented, so the proxy call was spent answering a request that could not be served. No `transcription_hour` is charged — no audio is processed. Send long media to the async endpoint, which has no cap. See https://docs.vidnavigator.com/guides/async-jobs
Transcribe online videos from platforms like Instagram, TikTok, and more using speech-to-text models when a direct transcript is not available.
## Overview
This endpoint is designed for videos where a pre-existing transcript or caption file cannot be retrieved. It downloads the video content and processes it through an AI speech-to-text model to generate a new transcript.
This is particularly useful for social media platforms where direct transcript extraction is not possible, such as Instagram Reels that don't have captions.
**YouTube is not supported by this endpoint.** For YouTube videos, use the [/transcript](/api-reference/endpoint/transcript) endpoint instead, which retrieves existing transcripts/captions.
## Synchronous or async?
This endpoint comes in two modes, with the same request body and the same result:
| Mode | Endpoint | Media duration | How you get the result |
| - | - | - | - |
| Synchronous | `POST /transcribe` | **Up to 10 minutes** | In the HTTP response |
| Async | [`POST /transcribe/async`](/api-reference/endpoint/transcribe-async) | **Any length** | Poll `GET /transcribe/{task_id}` or receive a [webhook](/webhooks) |
**Media longer than 10 minutes is rejected by the synchronous endpoint** with `400 video_too_long`. Use the [async mode](/api-reference/endpoint/transcribe-async) for anything that may run past 10 minutes — it also works for short videos, so it is the safe default when you don't know the duration.
## Billing
Speech-to-text processing consumes `transcription_hour` usage. 1 credit covers 3 hours of video/audio transcription.
On a **cache miss**, one `standard_request` or `residential_request` is also charged for the metadata fetch (residential for Instagram, Facebook-watch, Rumble; standard otherwise), plus `transcription_hour` proportional to the audio duration. On a **cache hit** (same URL transcribed before, by any user), only `transcription_hour` is charged — the metadata fetch is skipped. Users with the `free_on_cache_hit` sponsorship flag get the cache-hit `transcription_hour` waived (`waived: true` in the per-charge entry).
Set `include_usage: true` to receive a [`usage`](/api-reference/usage-costs#per-call-usage-disclosure) block with the breakdown.
The async mode is billed exactly the same way. A `video_too_long` rejection on the synchronous endpoint still bills the metadata fetch that read the duration (`standard_request` or `residential_request`, not refunded) but no `transcription_hour`.
## Supported Platforms
VidNavigator can generate transcripts from these platforms using speech-to-text:
* Instagram
* X / Twitter (just copy the tweet video link)
* Facebook (public videos only)
* TikTok
* Dailymotion
* Loom
* Vimeo
## Request Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `video_url` | string | Yes | URL of the video to transcribe. For Instagram carousel posts, append `?img_index=N` to select a specific video. |
| `transcript_text` | boolean | No | When `true`, returns the transcript as a single plain-text string instead of an array of segments. |
| `all_videos` | boolean | No | For Instagram carousel posts only. When `true`, transcribes ALL videos in the post and returns them in an array. Ignored for non-carousel URLs (reels, TikTok, etc.). |
## Instagram Carousel Behavior
Instagram **carousel posts** (e.g. `https://www.instagram.com/p/.../`) may contain multiple videos and/or images.
* **Select a specific item**: Append `?img_index=N` to the URL to select a specific item (1-based).\
Example: `https://www.instagram.com/p/ABC123/?img_index=2` selects the **second** item.
* **Transcribe all videos**: Set `all_videos=true` to transcribe **all videos** in the carousel. The response format changes (see “Success Response”).
If a carousel post contains **only images** (no videos), the API returns `400` with `error=no_videos_found`.
## Example Usage
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/transcribe" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://www.instagram.com/reel/C86ZvEaqRmo/"
}'
```
```python Python theme={null}
from vidnavigator import VidNavigatorClient, VidNavigatorError
client = VidNavigatorClient()
try:
result = client.transcribe_video(
video_url="https://www.instagram.com/reel/C86ZvEaqRmo/"
)
print("Video Title:", result.data.video_info.title)
for segment in result.data.transcript:
print(f"[{segment.start:.2f}s - {segment.end:.2f}s] {segment.text}")
except VidNavigatorError as e:
print(f"An error occurred: {e.message}")
```
```javascript JavaScript theme={null}
const { VidNavigatorClient, VidNavigatorError } = require('vidnavigator');
const client = new VidNavigatorClient({
apiKey: process.env.VIDNAVIGATOR_API_KEY,
});
async function transcribeVideo() {
try {
const { video_info, transcript } = await client.transcribeVideo({
video_url: "https://www.instagram.com/reel/C86ZvEaqRmo/"
});
console.log("Video Title:", video_info.title);
transcript.forEach(segment => {
console.log(`[${segment.start.toFixed(2)}s - ${segment.end.toFixed(2)}s] ${segment.text}`);
});
} catch (error) {
if (error instanceof VidNavigatorError) {
console.error(`An error occurred: ${error.message}`);
} else {
console.error("An unexpected error occurred:", error);
}
}
}
transcribeVideo();
```
### Instagram Carousel (select a specific video with `?img_index=2`)
```bash theme={null}
curl -X POST "https://api.vidnavigator.com/v1/transcribe" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://www.instagram.com/p/ABC123/?img_index=2"
}'
```
### Instagram Carousel (transcribe all videos with `all_videos=true`)
```bash theme={null}
curl -X POST "https://api.vidnavigator.com/v1/transcribe" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://www.instagram.com/p/ABC123/",
"all_videos": true
}'
```
## Success Response (200 OK)
This endpoint has **two** success response shapes (a `oneOf`), depending on whether you set `all_videos=true`:
* **Single-video response (default)**: returns `data.video_info` + `data.transcript`
* **All-videos response (`all_videos=true`)**: returns `data.carousel_info` + `data.videos[]`
`transcript_text=true` affects the shape of each returned `transcript` (string vs segments) in both response types.
### Single-video response (default)
```json theme={null}
{
"status": "success",
"data": {
"video_info": {
"title": "A Reel Interesting Video",
"description": "An example description for the video.",
"thumbnail": "https://example.com/thumbnail.jpg",
"url": "https://www.instagram.com/reel/C86ZvEaqRmo/",
"channel": "Example Creator",
"duration": 58.5,
"views": 123456,
"likes": 7890,
"published_date": "2024-07-01",
"keywords": ["example", "social media", "video"],
"category": "Entertainment"
},
"transcript": [
{
"text": "This is the first sentence of the video.",
"start": 0.5,
"end": 3.2
},
{
"text": "And this is the second.",
"start": 3.5,
"end": 5.1
}
]
}
}
```
### All-videos response (`all_videos=true`)
```json theme={null}
{
"status": "success",
"data": {
"carousel_info": {
"total_items": 5,
"video_count": 2,
"image_count": 3,
"transcribed_count": 2,
"total_duration": 123.4
},
"videos": [
{
"index": 1,
"status": "success",
"video_info": {
"title": "Video 1",
"description": "Example description",
"thumbnail": "https://example.com/thumb1.jpg",
"url": "https://www.instagram.com/p/ABC123/?img_index=1",
"channel": "Example Creator",
"duration": 60.0,
"views": 123,
"likes": 45,
"published_date": "2024-07-01",
"keywords": ["example"],
"category": "Entertainment"
},
"transcript": [
{ "text": "First line", "start": 0.0, "end": 2.0 }
]
},
{
"index": 2,
"status": "error",
"error": "transcript_not_available",
"message": "Transcript could not be generated for this video."
}
]
}
}
```
## Error Responses
| Status | `error` | Description |
| - | - | - |
| `400` | `missing_parameter`, `invalid_parameter`, `invalid_url`, `unsupported_url`, `unsupported_platform`, `request_body_required` | Invalid request. |
| `400` | `no_videos_found` | The post carries no video (e.g. an image-only carousel). |
| `400` | `video_too_long` | The media is longer than 10 minutes. Use [`POST /transcribe/async`](/api-reference/endpoint/transcribe-async). |
| `402` | `limit_exceeded` | Not enough credits. |
| `403` | `content_restricted` | The content requires login or age verification. |
| `404` | `video_not_found`, `video_unavailable`, `transcript_not_available` | The video can't be found or accessed, or no transcript could be generated. |
| `451` | `geo_restricted` | The content is not available in this region. |
| `500` | `metadata_fetch_failed`, `audio_extraction_failed`, `transcription_failed`, `internal_server_error` | Processing failed. Charges are reverted. |
See [Errors](/api-reference/errors) for the full list.
# Transcribe Online Video (Async)
Source: https://docs.vidnavigator.com/api-reference/endpoint/transcribe-async
POST /transcribe/async
Queue a speech-to-text transcription and return a `task_id` immediately.
**Use this instead of `POST /transcribe` for anything longer than ~10 minutes.** A synchronous transcription holds the HTTP connection open for the whole download and speech-to-text pass; past roughly ten minutes of audio that starts failing against client timeouts, reverse proxies and marketplace gateways. This endpoint has no duration cap.
Accepts the same body as `POST /transcribe`, plus `webhook_url`.
**Billing** is identical to the synchronous endpoint and happens in the background worker. Because the `202` fires before any charge is made, `include_usage` is not accepted here — pass it on `GET /transcribe/{task_id}` instead, which replays the final charges once `task_status=completed`. A failed job has all of its charges reverted.
See https://docs.vidnavigator.com/guides/async-jobs
**Submit-time credit gate:** the submit endpoint returns `402` instead of a `task_id` when the account has less than 60 seconds of transcription credit left. This is a spam gate, not a quote — the real, duration-accurate charge happens when the job runs, and a job whose video exceeds the remaining balance still fails on the same credit check a synchronous call would hit. An invalid or deactivated API key is rejected with `401`, and a key without the endpoint's permission with `403`; no task is created in either case.
Transcribe an online video of **any length**. The job runs in the background: you get a `task_id` immediately, then collect the transcript by polling or through a [webhook](/webhooks).
Use this endpoint instead of [`POST /transcribe`](/api-reference/endpoint/transcribe) for media longer than **10 minutes**, which the synchronous endpoint rejects with `video_too_long`. It also works for short videos, so it is the safe default when you don't know the duration.
## How It Works
`POST /transcribe/async` with the **same body** as [`POST /transcribe`](/api-reference/endpoint/transcribe), plus an optional `webhook_url`. You get `202 Accepted` with a `task_id` and a `check_status_url`.
Poll `GET /transcribe/{task_id}` every few seconds while `task_status` is `processing` — polling is free — or receive a [webhook](/webhooks) when the job finishes.
On `completed`, `data.result` is identical to the synchronous response's `data` block, so the same parsing code works for both. On `failed`, `data.error` carries the same error code the synchronous endpoint would have returned.
## Billing
Billed **exactly like** [`POST /transcribe`](/api-reference/endpoint/transcribe) — the async mode costs nothing extra. Charges are made in the background worker, so pass `include_usage=true` on the result request (not on the submit) to see them. A failed job has all of its charges reverted.
## Submit the job
`POST https://api.vidnavigator.com/v1/transcribe/async`
| Parameter | Type | Required | Description |
| - | - | - | - |
| `video_url` | string | Yes | URL of the video to transcribe. For Instagram carousel posts, append `?img_index=N` to select a specific video. |
| `transcript_text` | boolean | No | When `true`, returns the transcript as a single plain-text string. |
| `all_videos` | boolean | No | For Instagram carousel posts only. When `true`, transcribes all videos in the post. |
| `webhook_url` | string | No | Where to POST the result when the job finishes. Overrides the account default set in [Studio → API](https://vidnavigator.com/studio/api); pass `""` to opt this job out of the default. Must be a public `https` URL. See [Webhooks](/webhooks). |
`include_usage` is not accepted on the submit request — pass it on the poll request instead.
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/transcribe/async" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://www.instagram.com/reel/C86ZvEaqRmo/",
"webhook_url": "https://example.com/hooks/vidnavigator"
}'
```
```python Python theme={null}
import time
import requests
BASE = "https://api.vidnavigator.com"
HEADERS = {"X-API-Key": "YOUR_API_KEY"}
task = requests.post(
f"{BASE}/v1/transcribe/async",
headers=HEADERS,
json={"video_url": "https://www.instagram.com/reel/C86ZvEaqRmo/"},
).json()["data"]
while True:
job = requests.get(BASE + task["check_status_url"], headers=HEADERS).json()["data"]
if job["task_status"] != "processing":
break
time.sleep(5)
if job["task_status"] == "completed":
for segment in job["result"]["transcript"]:
print(f'[{segment["start"]:.2f}s - {segment["end"]:.2f}s] {segment["text"]}')
else:
print("Failed:", job["error"]["error"], job["error"]["message"])
```
```javascript JavaScript theme={null}
const BASE = 'https://api.vidnavigator.com';
const HEADERS = { 'X-API-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' };
const { data: task } = await (await fetch(`${BASE}/v1/transcribe/async`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({ video_url: 'https://www.instagram.com/reel/C86ZvEaqRmo/' })
})).json();
let job;
do {
await new Promise(r => setTimeout(r, 5000));
({ data: job } = await (await fetch(BASE + task.check_status_url, { headers: HEADERS })).json());
} while (job.task_status === 'processing');
if (job.task_status === 'completed') {
job.result.transcript.forEach(s => console.log(`[${s.start}s] ${s.text}`));
} else {
console.error('Failed:', job.error.error, job.error.message);
}
```
### Response (202 Accepted)
```json theme={null}
{
"status": "success",
"data": {
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"task_status": "processing",
"job_type": "transcribe",
"expires_at": "2026-09-23T13:00:00Z",
"check_status_url": "/v1/transcribe/550e8400-e29b-41d4-a716-446655440000",
"webhook_url": "https://example.com/hooks/vidnavigator",
"message": "Transcription job accepted."
}
}
```
| Status | `error` | Description |
| - | - | - |
| `400` | `missing_parameter`, `invalid_parameter`, … | Invalid request. |
| `401` | — | Invalid or deactivated API key. |
| `402` | `limit_exceeded` | Less than 60 seconds of transcription credit left. No task is created. |
| `403` | — | The API key lacks permission for this endpoint. |
| `429` | `too_many_active_jobs` | Too many jobs already running for this account. |
## Get the result
The playground above only covers the submit request. Call the result endpoint with cURL or your HTTP client.
`GET https://api.vidnavigator.com/v1/transcribe/{task_id}`
| Parameter | In | Required | Description |
| - | - | - | - |
| `task_id` | path | Yes | The `task_id` returned by `POST /transcribe/async`. |
| `include_usage` | query | No | When `true` and `task_status=completed`, attaches a `usage` block with the final charges. |
Polling is **free**. Poll every few seconds while `task_status` is `processing`. Results are kept for **1 hour after the job finishes**, and reading a task doesn't delete it.
```bash cURL theme={null}
curl "https://api.vidnavigator.com/v1/transcribe/550e8400-e29b-41d4-a716-446655440000?include_usage=true" \
-H "X-API-Key: YOUR_API_KEY"
```
On `completed`, `data.result` is **identical** to the `data` block of the [synchronous response](/api-reference/endpoint/transcribe) (`video_info` + `transcript`, or `carousel_info` + `videos` when `all_videos=true`).
```json theme={null}
{
"status": "success",
"data": {
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"task_status": "completed",
"job_type": "transcribe",
"request": { "video_url": "https://www.instagram.com/reel/C86ZvEaqRmo/", "transcript_text": false, "all_videos": false },
"created_at": "2026-09-23T12:00:00Z",
"started_at": "2026-09-23T12:00:01Z",
"completed_at": "2026-09-23T12:00:40Z",
"expires_at": "2026-09-23T13:00:40Z",
"check_status_url": "/v1/transcribe/550e8400-e29b-41d4-a716-446655440000",
"webhook": { "status": "delivered", "attempts": 1, "response_status": 200, "last_error": null, "delivered_at": "2026-09-23T12:00:41Z" },
"result": {
"video_info": { "title": "A Reel Interesting Video", "duration": 58.5, "url": "https://www.instagram.com/reel/C86ZvEaqRmo/" },
"transcript": [
{ "text": "This is the first sentence of the video.", "start": 0.5, "end": 3.2 }
]
},
"error": null
}
}
```
On `failed`, `data.result` is `null` and `data.error` carries the same code the synchronous endpoint would have returned, e.g. `{ "error": "transcript_not_available", "message": "...", "http_status": 404 }`. All charges are reverted.
| Status | `error` | Description |
| - | - | - |
| `404` | `task_not_found` | Unknown `task_id`, expired, owned by another account, or not a transcription task. |
## Webhook
Pass `webhook_url` on the submit request, or configure a default endpoint in [Studio → API](https://vidnavigator.com/studio/api), to be called back when the job finishes. See [Webhooks](/webhooks) for the payload and signature verification, and [Async Jobs](/guides/async-jobs) for the full workflow.
# Get Video Transcript
Source: https://docs.vidnavigator.com/api-reference/endpoint/transcript
POST /transcript
Extract transcript from any supported online video (YouTube, Vimeo, Twitter/X, TikTok, Facebook, Dailymotion, Loom, etc.) with optional language selection. The endpoint auto-detects the platform from the URL and routes internally.
The URL inside `video_url` is what determines the upstream behavior.
**Note:** for Instagram videos, use `/transcribe` instead (speech-to-text).
Options:
- `transcript_text=true`: Returns the transcript as a single plain-text string instead of an array of segments.
- `metadata_only=true`: Returns only video metadata (no transcript).
- `fallback_to_metadata=true`: If transcript is unavailable, returns video metadata with an empty transcript instead of a 404 error (ignored when `metadata_only=true`).
**Billing** depends on the URL:
- YouTube (full transcript OR `metadata_only=true`) → 1× `residential_request` (the transcript fetch routes through the residential proxy)
- Non-YouTube → 1× `standard_request` (some platforms internally use residential and would be billed accordingly)
The charge applies whether or not a transcript is returned (mirrors the proxy hop we paid for). Set `include_usage=true` to receive a `usage` block in the response.
Extract accurate metadata and transcripts from any supported online video — YouTube, TikTok, Facebook, X/Twitter, Vimeo, Dailymotion, Loom, and more — through a single endpoint.
This is now the **single transcript endpoint** for every supported platform, including YouTube. The endpoint auto-detects the platform from `video_url` and routes internally. (The old `/youtube/transcript` endpoint has been merged into `/transcript`.)
**For Instagram videos**, use the [/transcribe](/api-reference/endpoint/transcribe) endpoint instead (speech-to-text).
## Billing
Billing depends on the URL:
* **YouTube** (full transcript **or** `metadata_only: true`) → 1× `residential_request` (the fetch routes through the residential proxy).
* **Non-YouTube** → 1× `standard_request` (some platforms internally require residential and are billed as `residential_request` accordingly).
The charge applies whether or not a transcript is returned, mirroring the proxy hop that was paid for. Set `include_usage: true` to receive a [`usage`](/api-reference/usage-costs#per-call-usage-disclosure) block in the response.
## Overview
This endpoint automatically extracts rich video metadata and transcripts from online videos with high accuracy.
## Supported Platforms
VidNavigator can retrieve transcripts from a wide variety of sources. Below is the list of platforms currently supported by this endpoint:
* YouTube (uses residential proxy infrastructure for reliable access)
* X / Twitter (just copy the tweet video link)
* Facebook (public videos only)
* TikTok
* Dailymotion
* Loom
* Vimeo
**For Instagram videos**, use the [/transcribe](/api-reference/endpoint/transcribe) endpoint instead, which runs speech-to-text.
## Use Cases
Extract transcripts for AI analysis, sentiment analysis, or content categorization
Enable text-based search across video content libraries
Generate captions and transcripts for accessibility compliance
Create summaries, articles, or repurpose video content
## Request Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `video_url` | string | Yes | URL of the video to retrieve the transcript for (YouTube or any other supported platform) |
| `language` | string | No | ISO2 language code (e.g., "en", "es", "fr") |
| `metadata_only` | boolean | No | When `true`, returns only video metadata without fetching the transcript. Takes precedence over `fallback_to_metadata`. Usage is still recorded. |
| `fallback_to_metadata` | boolean | No | When `true`, returns video metadata with an empty transcript if transcript is unavailable (returns 200 instead of 404). Ignored if `metadata_only` is `true`. |
| `transcript_text` | boolean | No | When `true`, returns the transcript as a single plain-text string instead of an array of segments. |
| `include_usage` | boolean | No | When `true`, the response includes a `usage` block listing every meter charged during this request, the total credits deducted, and the remaining balance. |
## Example Usage
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/transcript" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://youtube.com/watch?v=dQw4w9WgXcQ",
"language": "en"
}'
```
```python Python theme={null}
import requests
url = "https://api.vidnavigator.com/v1/transcript"
headers = {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json"
}
data = {
"video_url": "https://youtube.com/watch?v=dQw4w9WgXcQ",
"language": "en"
}
response = requests.post(url, headers=headers, json=data)
result = response.json()
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.vidnavigator.com/v1/transcript', {
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
video_url: 'https://youtube.com/watch?v=dQw4w9WgXcQ',
language: 'en'
})
});
const result = await response.json();
```
### Transcript as Plain Text (`transcript_text=true`)
Use `transcript_text=true` to return the transcript as a single plain-text string instead of an array of `{ text, start, end }` segments:
```bash theme={null}
curl -X POST "https://api.vidnavigator.com/v1/transcript" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://twitter.com/user/status/123456789",
"transcript_text": true
}'
```
### Getting Metadata Only
If you only need video metadata without the transcript (faster and still counts as usage):
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/transcript" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://www.tiktok.com/@user/video/123456789",
"metadata_only": true
}'
```
```python Python theme={null}
data = {
"video_url": "https://www.tiktok.com/@user/video/123456789",
"metadata_only": True
}
response = requests.post(url, headers=headers, json=data)
```
```javascript JavaScript theme={null}
const result = await fetch('https://api.vidnavigator.com/v1/transcript', {
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
video_url: 'https://www.tiktok.com/@user/video/123456789',
metadata_only: true
})
});
```
For YouTube, `metadata_only: true` still routes through the residential proxy and is billed as a `residential_request`.
### Getting Usage Disclosure (`include_usage=true`)
Set `include_usage: true` to receive a `usage` block describing exactly what was charged:
```json theme={null}
{
"status": "success",
"data": { "video_info": { ... }, "transcript": [ ... ] },
"usage": {
"charges": [
{ "service_type": "residential_request", "quantity": 1, "credits": 0.005 }
],
"total_credits": 0.005
}
}
```
## Limitations
* `https://fb.watch/` format is not supported, use the `https://www.facebook.com/watch/?v=` format instead.
## Troubleshooting
* **Invalid URL**: Ensure the video URL is complete and valid. This endpoint supports direct links from YouTube, X (a tweet), Vimeo, TikTok, etc.
* **Transcript Not Found**: Please ensure that the video has subtitles available (either manually added or auto-generated). If no subtitles are present, the transcript cannot be extracted. Consider using `fallback_to_metadata: true` to get metadata even when transcript is unavailable.
* **Language Not Supported**: Sometimes the selected subtitles language is not available. In which case, you will get a clear message stating that. Verify that the chosen language is available for the video. We recommend not using the language input if you are unsure.
* **For Instagram videos**: This endpoint does not transcribe Instagram. Please use the [/transcribe](/api-reference/endpoint/transcribe) endpoint instead.
If issues persist or need more help, please reach out to us at [support@vidnavigator.com](mailto:support@vidnavigator.com).
## Success Response (200 OK)
Returns a `video_info` object and a `transcript` object.
```json theme={null}
{
"status": "success",
"data": {
"video_info": {
"title": "A Really Cool Video",
"description": "An example description for the video.",
"thumbnail": "https://example.com/thumbnail.jpg",
"url": "https://twitter.com/user/status/123456789",
"channel": "Example Creator",
"duration": 45.5,
"views": 123456,
"likes": 9876,
"published_date": "2024-01-15",
"keywords": ["example", "twitter", "video"],
"category": "Entertainment"
},
"transcript": [
{
"text": "This is the first sentence of the video.",
"start": 0.5,
"end": 3.2
},
{
"text": "And this is the second sentence.",
"start": 3.5,
"end": 5.1
}
]
}
}
```
### Response with `transcript_text: true`
When you set `transcript_text: true`, the transcript is returned as a single string:
```json theme={null}
{
"status": "success",
"data": {
"video_info": { ... },
"transcript": "This is the first sentence of the video. And this is the second sentence."
}
}
```
### Response with `metadata_only: true`
When you set `metadata_only: true`, only the video metadata is returned:
```json theme={null}
{
"status": "success",
"data": {
"video_info": {
"title": "A Really Cool Video",
"description": "An example description for the video.",
"thumbnail": "https://example.com/thumbnail.jpg",
"url": "https://twitter.com/user/status/123456789",
"channel": "Example Creator",
"duration": 45.5,
"views": 123456,
"likes": 9876,
"published_date": "2024-01-15"
}
}
}
```
# Tweet Claim Analysis
Source: https://docs.vidnavigator.com/api-reference/endpoint/tweet-claim-analysis
POST /tweet/statement
Fetches the tweet content (including any quoted tweet and attached media), then uses AI to extract a rich structured analysis: the core claim, classification axes, topics, entities, and raw tweet data.
**Multimodal media handling:** attached media is processed by type. Videos are transcribed (platform captions or speech-to-text) and summarized; images are described by a vision model — transcribed text, chart/graph data points, recognisable people and logos, and any claims they make — and folded into the analysis.
**Classification axes** (`claim_type`, `intent`, `tone`, `emotion`, `authority`) reflect **only the original tweet text**, not quoted content, media, or images.
**Billing (consolidated, mirrors `/v1/analyze/video`):**
- exactly one `analysis_request` charge with `quantity = ceil(total_tokens / 15000)`, summing tokens across the optional per-media extraction calls, the optional image vision calls, AND the main statement-extraction call. A typical tweet+quoted tweet with one video or an attached image runs several LLM calls but produces just one billing entry when the combined tokens stay under 15k. Image analysis is billed through this same `analysis_request` meter.
- one `standard_request` per media URL whose transcript or metadata is fetched (mirrors `/v1/transcript`).
- `transcription_hour` proportional to the audio duration if speech-to-text runs for a media video that has no platform transcript. Waived for users with `users.free_on_cache_hit=true` when STT was served from cache (same waiver as `/v1/transcribe`).
**Long videos:** this endpoint is synchronous and holds the connection open for the whole transcription. For media longer than ~10 minutes use `POST /tweet/statement/async` instead, which returns a `task_id` and can call your webhook when it finishes. Media longer than 10 minutes is rejected here with `video_too_long` (HTTP 400). The metadata fetch needed to read the video's duration is billed (`standard_request` or `residential_request`) and is **not** refunded: the limit is documented, so the proxy call was spent answering a request that could not be served. No `transcription_hour` is charged — no audio is processed. Send long media to the async endpoint, which has no cap. See https://docs.vidnavigator.com/guides/async-jobs
Extract the real meaning behind an X/Twitter post by analyzing the original tweet, any quoted tweet, and media shared in both contexts.
## Overview
This endpoint turns a single X post into a machine-readable intelligence object. It does not just retrieve tweet text or metadata. It analyzes what the author actually meant by combining the original tweet, any quoted tweet, and any media attached to either one.
That means the output reflects the full communicative intent of the post, not just the visible text. If a tweet makes sense only when read together with a quoted post, an attached video, or media inside the quoted tweet, this endpoint includes that context in the analysis.
It is designed for workflows that need more than keyword monitoring or tweet retrieval, such as fact-checking pipelines, narrative tracking, social risk monitoring, and structured ingestion into dashboards or AI systems.
This is **not** a basic tweet retrieval API. Its core value is intelligence extraction: understanding the author's intent, the role of the quoted tweet, and the meaning conveyed by media attached to both the original and quoted post.
### What This Endpoint Returns
Extract a multi-sentence `final_statement` that reflects what the author actually meant, not just what the raw tweet text says in isolation.
Incorporate quoted tweet context when the original post depends on it for meaning, stance, or narrative framing.
Classify the original tweet by `claim_type`, `intent`, `tone`, `emotion`, and `authority`.
Analyze media attached to the tweet and the quoted tweet. Videos are transcribed (platform captions or speech-to-text) and summarized; images are described by a vision model — transcribed text, chart/graph data points, recognizable people and logos, and any claims they make.
### Why This Endpoint Is Different
Most tweet APIs stop at text retrieval, engagement metrics, or shallow metadata. This endpoint is built for intelligence extraction.
* It understands the **intent** behind the original tweet.
* It understands the **quoted tweet** as part of the message, not as unrelated metadata.
* It analyzes **shared media in the original tweet** — both videos (transcribed) and images (described by a vision model).
* It analyzes **shared media in the quoted tweet**.
* It returns a structured output that reflects the **combined meaning** of all of that context.
This is especially important for X posts where the author's real point is only clear when you interpret the text together with a quoted post, a reaction clip, a narrated video, or media evidence embedded in either layer.
### How It Works
1. Send a `tweet_id` for the target X post.
2. VidNavigator fetches the original tweet, any quoted tweet, and media attached to both.
3. If video or audio is present in either context, the system retrieves transcripts or falls back to speech-to-text when needed. Attached images are described by a vision model.
4. The combined context is analyzed to determine the real claim, intent, framing, and supporting media meaning.
5. The result is returned as structured JSON ready for applications and pipelines.
This endpoint accepts a **tweet ID**, not a full URL. For example, from `https://x.com/user/status/1912345678901234567`, the `tweet_id` is `1912345678901234567`.
## Synchronous or async?
Attached videos are transcribed in full before the claim can be extracted, so a tweet carrying a long video takes as long as that video. This endpoint therefore comes in two modes, with the same request body and the same result:
| Mode | Endpoint | Attached video duration | How you get the result |
| - | - | - | - |
| Synchronous | `POST /tweet/statement` | **Up to 10 minutes** | In the HTTP response |
| Async | [`POST /tweet/statement/async`](/api-reference/endpoint/tweet-claim-analysis-async) | **Any length** | Poll `GET /tweet/statement/{task_id}` or receive a [webhook](/webhooks) |
**When the tweet (or the tweet it quotes) carries a video longer than 10 minutes, the synchronous endpoint rejects it** with `400 video_too_long`. Use the [async mode](/api-reference/endpoint/tweet-claim-analysis-async) — it also works for text-only tweets and short clips.
## Classification Axes
The classification labels below are derived from the **original tweet text only**, not from quoted tweets or attached media:
* `claim_type`: `factual_claim`, `opinion`, `question`, `call_to_action`, `satire`, `news_sharing`, `personal_experience`
* `intent`: `educate`, `inform`, `analyze`, `persuade`, `entertain`, `inspire`, `challenge`
* `tone`: `serious`, `humorous`, `provocative`, `neutral`, `warm`, `skeptical`, `inspirational`
* `emotion`: `curiosity`, `urgency`, `outrage`, `fear`, `hope_inspiration`, `confidence_reassurance`, `empathy_warmth`, `awe_wonder`
* `authority`: `data_driven`, `expert_led`, `experience_based`, `speculative`
## Example Usage
### Best For
Use this endpoint when you need to understand what a post is really saying, especially when meaning depends on:
* the author's underlying intent
* a quoted tweet that changes the framing
* attached video or audio in the original tweet
* attached video or audio in the quoted tweet
If you only need raw tweet metadata, this is more powerful than necessary. This endpoint is for interpretation and intelligence extraction.
### Basic Request
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/tweet/statement" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tweet_id": "1912345678901234567"
}'
```
```python Python theme={null}
import requests
url = "https://api.vidnavigator.com/v1/tweet/statement"
headers = {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json"
}
data = {
"tweet_id": "1912345678901234567"
}
response = requests.post(url, headers=headers, json=data)
result = response.json()
print("Final statement:", result["data"]["final_statement"])
print("Claim type:", result["data"]["claim_type"])
print("Topics:", result["data"]["topics"])
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.vidnavigator.com/v1/tweet/statement', {
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
tweet_id: '1912345678901234567'
})
});
const result = await response.json();
console.log(result.data.final_statement);
```
### Example: Feeding Results into Search or Verification
When you want to take a tweet's interpreted meaning and use it for search, fact-checking, or evidence retrieval, the structured output is ready to plug into downstream pipelines:
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/tweet/statement" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tweet_id": "1912345678901234567"
}'
```
Once returned, you can use:
* `final_statement` for human review or as a search/verification query
* `detailed_analysis` for framing and stance context
* `topics` and `entities` for clustering, tagging, or analytics
## Response Example
```json theme={null}
{
"status": "success",
"data": {
"final_statement": "The author claims that a new city policy will ban gas-powered delivery scooters in the downtown area next month, arguing that the change is being rushed without enough support for affected workers.",
"detailed_analysis": "The tweet presents a concrete policy claim and frames it as both imminent and harmful to delivery workers. The author positions the decision as poorly planned and emphasizes the lack of transition support. The message appears intended to persuade the audience that the policy is unfair rather than merely to report it. The framing combines factual assertion with rhetorical pressure.",
"topics": [
"urban policy",
"delivery workers",
"transport regulation",
"labor impact"
],
"entities": [
"City Council",
"downtown"
],
"claim_type": "factual_claim",
"intent": "persuade",
"tone": "provocative",
"emotion": "urgency",
"authority": "speculative",
"tweet_text": "Next month the city is banning gas scooters downtown and nobody has explained how delivery workers are supposed to adapt this fast.",
"tweet_media_summary": "A short attached video shows several delivery riders speaking about costs, route changes, and reduced earnings if the rule takes effect.",
"quoted_tweet_text": "Official announcement: downtown clean transport rules begin next month.",
"quoted_media_summary": null
}
}
```
## Field Guide
| Field | Description |
| - | - |
| `final_statement` | Full normalized claim in the author's voice, informed by the full contextual analysis |
| `detailed_analysis` | 3-5 sentence explanation covering claim, framing, evidence, and stance |
| `topics` | Key subjects discussed in the tweet (3-8) |
| `entities` | Named entities such as people, organizations, or places (0-8) |
| `claim_type` / `intent` / `tone` / `emotion` / `authority` | Classification axes derived from the **original tweet text only** |
| `tweet_text` | Original text of the source tweet |
| `tweet_media_summary` | Full content summary of media attached to the original tweet: a video transcript summary **or** a vision-model description of attached image(s). `null` when no media is attached. |
| `quoted_tweet_text` | Text of a quoted or referenced tweet, when present |
| `quoted_media_summary` | Full content summary of media attached to the quoted tweet (video transcript or image description). `null` when no media is attached. |
## Intelligence Model
This endpoint should be thought of as a multi-layer interpretation pipeline:
1. Read the original tweet text.
2. Detect whether a quoted tweet changes or completes the meaning.
3. Analyze media attached to the original tweet.
4. Analyze media attached to the quoted tweet.
5. Produce a final claim and classification that reflect the author's intended message as completely as possible.
That is why the endpoint is effective for tweets where the visible text alone is incomplete, sarcastic, reactive, or dependent on embedded media.
## Billing Notes
Billing is **consolidated** and mirrors [`/analyze/video`](/api-reference/endpoint/analyze-video). A single call can involve several internal LLM calls (media extraction, image vision, and the main statement extraction) but produces **one** billing entry per meter:
* **Exactly one `analysis_request` charge** with `quantity = ceil(total_tokens / 15000)`, summing tokens across the optional per-media extraction calls, the optional image vision calls, and the main statement-extraction call. A typical tweet + quoted tweet with one video or one attached image runs several LLM calls but produces just one billing entry when the combined tokens stay under 15k. **Image analysis is billed through this same `analysis_request` meter.**
* **One `standard_request` per media URL** whose transcript or metadata is fetched (mirrors [`/transcript`](/api-reference/endpoint/transcript)).
* **`transcription_hour`** proportional to the audio duration if speech-to-text runs for a media video with no platform transcript. Waived for users with `free_on_cache_hit=true` when STT was served from cache.
For cost planning, a text-only tweet is typically simpler than a tweet with embedded media and a quoted post that also contains video or images.
The async mode is billed exactly the same way. A `video_too_long` rejection on the synchronous endpoint still bills the metadata fetch that read the duration (not refunded), but no `transcription_hour`.
## Use Cases
Convert noisy social posts into verification-ready claims that analysts can review quickly.
Group similar claims using `final_statement`, `topics`, and `entities` across many tweets.
Detect accusations, emotionally charged narratives, and emerging risk signals earlier.
Feed structured social intelligence into internal tools, LLM workflows, or search systems.
## Troubleshooting
* **Invalid tweet ID**: Make sure you are sending only the numeric post ID, not the full X URL.
* **Upstream fetch failure**: If X content cannot be retrieved or parsed, the endpoint may return a `502` response.
* **Missing media summary**: If no media is attached, or media could not be processed, `tweet_media_summary` and `quoted_media_summary` may be `null`.
* **Unexpected classification**: `claim_type`, `intent`, `tone`, `emotion`, and `authority` are based only on the original tweet text, while `final_statement` and media summaries reflect the broader contextual analysis.
* **`video_too_long`**: the tweet or its quoted tweet carries a video longer than 10 minutes. Use [`POST /tweet/statement/async`](/api-reference/endpoint/tweet-claim-analysis-async).
* **`402 limit_exceeded`**: not enough credits. See [Errors](/api-reference/errors) for the full list.
# Tweet Claim Analysis (Async)
Source: https://docs.vidnavigator.com/api-reference/endpoint/tweet-claim-analysis-async
POST /tweet/statement/async
Queue a tweet claim analysis and return a `task_id` immediately.
**Use this instead of `POST /tweet/statement` when the tweet (or the tweet it quotes) carries a long video.** That media is transcribed in full before the claim can be extracted, so the request inherits the same timeout problem as `/transcribe`.
Accepts the same body as `POST /tweet/statement`, plus `webhook_url`.
**Billing** is identical to the synchronous endpoint and happens in the background. Pass `include_usage=true` on `GET /tweet/statement/{task_id}` for the breakdown.
See https://docs.vidnavigator.com/guides/async-jobs
**Submit-time credit gate:** the submit endpoint returns `402` instead of a `task_id` when the account has less than 60 seconds of transcription credit left. This is a spam gate, not a quote — the real, duration-accurate charge happens when the job runs, and a job whose video exceeds the remaining balance still fails on the same credit check a synchronous call would hit. An invalid or deactivated API key is rejected with `401`, and a key without the endpoint's permission with `403`; no task is created in either case.
Analyze a tweet's claim when it carries a **long video**. The job runs in the background: you get a `task_id` immediately, then collect the result by polling or through a [webhook](/webhooks).
Use this endpoint instead of [`POST /tweet/statement`](/api-reference/endpoint/tweet-claim-analysis) when the tweet (or the tweet it quotes) may carry a video longer than **10 minutes** — attached media is transcribed in full before the claim is extracted, and the synchronous endpoint rejects it with `video_too_long`. It works for any tweet, with or without media.
## How It Works
`POST /tweet/statement/async` with the **same body** as [`POST /tweet/statement`](/api-reference/endpoint/tweet-claim-analysis), plus an optional `webhook_url`. You get `202 Accepted` with a `task_id` and a `check_status_url`.
Poll `GET /tweet/statement/{task_id}` every few seconds while `task_status` is `processing` — polling is free — or receive a [webhook](/webhooks) when the job finishes.
On `completed`, `data.result` is identical to the synchronous response's `data` block, so the same parsing code works for both. On `failed`, `data.error` carries the same error code the synchronous endpoint would have returned.
## Billing
Billed **exactly like** [`POST /tweet/statement`](/api-reference/endpoint/tweet-claim-analysis) — the async mode costs nothing extra. Charges are made in the background worker, so pass `include_usage=true` on the result request (not on the submit) to see them. A failed job has all of its charges reverted.
## Submit the job
`POST https://api.vidnavigator.com/v1/tweet/statement/async`
| Parameter | Type | Required | Description |
| - | - | - | - |
| `tweet_id` | string | Yes | The X/Twitter tweet ID. |
| `webhook_url` | string | No | Where to POST the result when the job finishes. Overrides the account default set in [Studio → API](https://vidnavigator.com/studio/api); pass `""` to opt this job out of the default. Must be a public `https` URL. See [Webhooks](/webhooks). |
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/tweet/statement/async" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tweet_id": "1912345678901234567",
"webhook_url": "https://example.com/hooks/vidnavigator"
}'
```
```python Python theme={null}
import time
import requests
BASE = "https://api.vidnavigator.com"
HEADERS = {"X-API-Key": "YOUR_API_KEY"}
task = requests.post(
f"{BASE}/v1/tweet/statement/async",
headers=HEADERS,
json={"tweet_id": "1912345678901234567"},
).json()["data"]
while True:
job = requests.get(BASE + task["check_status_url"], headers=HEADERS).json()["data"]
if job["task_status"] != "processing":
break
time.sleep(5)
if job["task_status"] == "completed":
print(job["result"]["final_statement"])
else:
print("Failed:", job["error"]["error"], job["error"]["message"])
```
```javascript JavaScript theme={null}
const BASE = 'https://api.vidnavigator.com';
const HEADERS = { 'X-API-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' };
const { data: task } = await (await fetch(`${BASE}/v1/tweet/statement/async`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({ tweet_id: '1912345678901234567' })
})).json();
let job;
do {
await new Promise(r => setTimeout(r, 5000));
({ data: job } = await (await fetch(BASE + task.check_status_url, { headers: HEADERS })).json());
} while (job.task_status === 'processing');
console.log(job.task_status === 'completed' ? job.result.final_statement : job.error);
```
### Response (202 Accepted)
```json theme={null}
{
"status": "success",
"data": {
"task_id": "9b2f1c3e-5d4a-4e8b-a1c2-3d4e5f6a7b8c",
"task_status": "processing",
"job_type": "tweet_statement",
"expires_at": "2026-09-23T13:00:00Z",
"check_status_url": "/v1/tweet/statement/9b2f1c3e-5d4a-4e8b-a1c2-3d4e5f6a7b8c",
"webhook_url": "https://example.com/hooks/vidnavigator",
"message": "Tweet analysis job accepted."
}
}
```
| Status | `error` | Description |
| - | - | - |
| `400` | `missing_parameter`, `invalid_parameter`, … | Invalid request. |
| `401` | — | Invalid or deactivated API key. |
| `402` | `limit_exceeded` | Less than 60 seconds of transcription credit left. No task is created. |
| `403` | — | The API key lacks permission for this endpoint. |
| `429` | `too_many_active_jobs` | Too many jobs already running for this account. |
## Get the result
The playground above only covers the submit request. Call the result endpoint with cURL or your HTTP client.
`GET https://api.vidnavigator.com/v1/tweet/statement/{task_id}`
| Parameter | In | Required | Description |
| - | - | - | - |
| `task_id` | path | Yes | The `task_id` returned by `POST /tweet/statement/async`. |
| `include_usage` | query | No | When `true` and `task_status=completed`, attaches a `usage` block with the final charges. |
Polling is **free**. Results are kept for **1 hour after the job finishes**, and reading a task doesn't delete it.
On `completed`, `data.result` is **identical** to the `data` block of the [synchronous response](/api-reference/endpoint/tweet-claim-analysis) (`final_statement`, `detailed_analysis`, `topics`, `entities`, classification axes and media summaries).
```json theme={null}
{
"status": "success",
"data": {
"task_id": "9b2f1c3e-5d4a-4e8b-a1c2-3d4e5f6a7b8c",
"task_status": "completed",
"job_type": "tweet_statement",
"created_at": "2026-09-23T12:00:00Z",
"started_at": "2026-09-23T12:00:01Z",
"completed_at": "2026-09-23T12:06:12Z",
"expires_at": "2026-09-23T13:06:12Z",
"check_status_url": "/v1/tweet/statement/9b2f1c3e-5d4a-4e8b-a1c2-3d4e5f6a7b8c",
"webhook": { "status": "delivered", "attempts": 1, "response_status": 200, "last_error": null, "delivered_at": "2026-09-23T12:06:13Z" },
"result": {
"final_statement": "The author claims that ...",
"detailed_analysis": "...",
"topics": ["urban policy"],
"entities": ["City Council"],
"claim_type": "factual_claim",
"intent": "persuade",
"tone": "provocative",
"emotion": "urgency",
"authority": "speculative",
"tweet_text": "...",
"tweet_media_summary": "...",
"quoted_tweet_text": null,
"quoted_media_summary": null
},
"error": null
}
}
```
On `failed`, `data.result` is `null` and `data.error` carries the same code and `http_status` the synchronous endpoint would have returned. All charges are reverted.
| Status | `error` | Description |
| - | - | - |
| `404` | `task_not_found` | Unknown `task_id`, expired, owned by another account, or not a tweet analysis task. |
## Webhook
Pass `webhook_url` on the submit request, or configure a default endpoint in [Studio → API](https://vidnavigator.com/studio/api), to be called back when the job finishes. See [Webhooks](/webhooks) for the payload and signature verification, and [Async Jobs](/guides/async-jobs) for the full workflow.
# Update File Namespaces
Source: https://docs.vidnavigator.com/api-reference/endpoint/update-file-namespaces
PUT /file/{file_id}/namespaces
Set the namespaces a file belongs to. Replaces any existing namespace assignments. The 'default' namespace is always preserved.
Set which namespaces a file belongs to. This **replaces** any existing namespace assignments for the file. The `default` namespace is always preserved.
## Overview
Use this endpoint to move a file between namespaces or assign it to multiple namespaces at once. The response returns the updated `namespace_ids` and resolved `namespaces` reflecting the new assignments.
## Example Request
```bash cURL theme={null}
curl -X PUT "https://api.vidnavigator.com/v1/file/file_abc123/namespaces" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"namespace_ids": ["64a1b2c3d4e5f6789abc0002", "64a1b2c3d4e5f6789abc0003"]
}'
```
```python Python theme={null}
import requests
file_id = "file_abc123"
url = f"https://api.vidnavigator.com/v1/file/{file_id}/namespaces"
headers = {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json"
}
data = {
"namespace_ids": ["64a1b2c3d4e5f6789abc0002", "64a1b2c3d4e5f6789abc0003"]
}
response = requests.put(url, headers=headers, json=data)
result = response.json()
```
```javascript JavaScript theme={null}
const fileId = 'file_abc123';
const response = await fetch(`https://api.vidnavigator.com/v1/file/${fileId}/namespaces`, {
method: 'PUT',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
namespace_ids: ['64a1b2c3d4e5f6789abc0002', '64a1b2c3d4e5f6789abc0003']
})
});
const result = await response.json();
```
## Success Response
```json theme={null}
{
"status": "success",
"message": "File namespaces updated successfully",
"data": {
"namespace_ids": ["64a1b2c3d4e5f6789abc0001", "64a1b2c3d4e5f6789abc0002", "64a1b2c3d4e5f6789abc0003"],
"namespaces": [
{ "id": "64a1b2c3d4e5f6789abc0001", "name": "default" },
{ "id": "64a1b2c3d4e5f6789abc0002", "name": "Client Calls" },
{ "id": "64a1b2c3d4e5f6789abc0003", "name": "Product Demos" }
]
}
}
```
The `default` namespace is always preserved in the response, even if not explicitly included in `namespace_ids`.
# Upload File
Source: https://docs.vidnavigator.com/api-reference/endpoint/upload-file
POST /upload/file
Upload a new audio or video file for analysis and start processing.
**Supported formats:**
- Video: mp4, webm, mov, avi, wmv, flv, mkv
- Audio: m4a, mp3, mpeg, mpga, wav
**Processing options:**
- `wait_for_completion=false` (default): Returns immediately, processing happens in background
- `wait_for_completion=true`: Waits for complete processing before returning response
Upload audio or video files for transcription and analysis.
## Overview
Upload your own audio or video files to VidNavigator for processing. The API supports a wide range of formats and provides flexible processing options.
### Supported Formats
mp4, webm, mov, avi, wmv, flv, mkv
m4a, mp3, mpeg, mpga, wav
### Processing Options
* **Asynchronous** (default): Returns immediately, processing happens in background
* **Synchronous**: Waits for complete processing before returning response
## Billing
File processing consumes `transcription_hour` usage for speech-to-text. 1 credit covers 3 hours of video/audio transcription. Uploaded files also count toward your storage quota.
### Namespace Assignment
You can assign the uploaded file to one or more namespaces by including `namespace_ids` in the form data. This accepts a comma-separated string or a JSON array (e.g., `'["ns1","ns2"]'`). See [Namespaces](/api-reference/endpoint/list-namespaces) for managing namespaces.
## Example Usage
```bash cURL (Async) theme={null}
curl -X POST "https://api.vidnavigator.com/v1/upload/file" \
-H "X-API-Key: YOUR_API_KEY" \
-F "file=@/path/to/your/video.mp4" \
-F "wait_for_completion=false"
```
```bash cURL (Sync) theme={null}
curl -X POST "https://api.vidnavigator.com/v1/upload/file" \
-H "X-API-Key: YOUR_API_KEY" \
-F "file=@/path/to/your/video.mp4" \
-F "wait_for_completion=true"
```
```bash cURL (with namespace) theme={null}
curl -X POST "https://api.vidnavigator.com/v1/upload/file" \
-H "X-API-Key: YOUR_API_KEY" \
-F "file=@/path/to/your/video.mp4" \
-F "namespace_ids=64a1b2c3d4e5f6789abc0002"
```
```python Python theme={null}
import requests
url = "https://api.vidnavigator.com/v1/upload/file"
headers = {"X-API-Key": "YOUR_API_KEY"}
with open("video.mp4", "rb") as f:
files = {"file": f}
data = {"wait_for_completion": "false"}
response = requests.post(url, headers=headers, files=files, data=data)
result = response.json()
print(f"File ID: {result['file_id']}")
```
```javascript JavaScript theme={null}
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('wait_for_completion', 'false');
const response = await fetch('https://api.vidnavigator.com/v1/upload/file', {
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY'
},
body: formData
});
const result = await response.json();
```
## Response Examples
All response variants include `data.file_info` with `namespace_ids` and `namespaces` reflecting the file's namespace assignments.
When `wait_for_completion` is `false` (the default), the API responds immediately with a file ID and a `processing` status.
```json theme={null}
{
"status": "success",
"file_id": "file_abc123",
"file_name": "video.mp4",
"file_status": "processing",
"message": "File uploaded successfully and is being processed",
"data": {
"file_info": {
"id": "file_abc123",
"name": "video.mp4",
"size": 15728640,
"type": "video/mp4",
"status": "processing",
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:00:00Z",
"has_transcript": false,
"namespace_ids": ["64a1b2c3d4e5f6789abc0001"],
"namespaces": [
{ "id": "64a1b2c3d4e5f6789abc0001", "name": "default" }
]
}
}
}
```
When `wait_for_completion` is `true`, the API waits until processing is finished before responding.
```json theme={null}
{
"status": "success",
"file_id": "file_abc123",
"file_name": "video.mp4",
"file_status": "completed",
"message": "File processed successfully",
"data": {
"file_info": {
"id": "file_abc123",
"name": "video.mp4",
"size": 15728640,
"type": "video/mp4",
"duration": 330,
"status": "completed",
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:00:00Z",
"has_transcript": true,
"namespace_ids": ["64a1b2c3d4e5f6789abc0001", "64a1b2c3d4e5f6789abc0002"],
"namespaces": [
{ "id": "64a1b2c3d4e5f6789abc0001", "name": "default" },
{ "id": "64a1b2c3d4e5f6789abc0002", "name": "Client Calls" }
]
},
"transcript": [
{
"text": "Welcome to this tutorial...",
"start": 0.0,
"end": 3.5
}
]
}
}
```
When `wait_for_completion` is `true` and processing exceeds 15 minutes, the API returns a `202 Accepted` response. Processing continues in the background — poll `GET /file/{file_id}` to check completion.
```json theme={null}
{
"status": "accepted",
"file_id": "file_abc123",
"file_name": "video.mp4",
"file_status": "processing",
"message": "File uploaded but processing is still in progress",
"note": "Processing is taking longer than expected. Use GET /file/{file_id} to check status.",
"data": {
"file_info": {
"id": "file_abc123",
"name": "video.mp4",
"size": 15728640,
"type": "video/mp4",
"status": "processing",
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:00:00Z",
"has_transcript": false,
"namespace_ids": ["64a1b2c3d4e5f6789abc0001"],
"namespaces": [
{ "id": "64a1b2c3d4e5f6789abc0001", "name": "default" }
]
}
}
}
```
# Get Usage
Source: https://docs.vidnavigator.com/api-reference/endpoint/usage
GET /usage
Retrieve credit balances, per-service activity counts, storage, and channels indexed for the authenticated user. All API operations consume credits from a shared pool — there are no per-service limits.
Retrieve your credit balances, resource usage counters, storage usage, and channel indexing metrics.
## Overview
All API operations consume credits from a **shared credit pool** — there are no per-service limits. This endpoint gives you a complete picture of your account's consumption and remaining capacity.
### What's Included
* **Credits**: Monthly allocation, remaining balance, and purchased credits
* **Usage counters**: Activity counts per resource type (informational, not individual limits)
* **Storage**: Used vs. allocated storage
* **Channels indexed**: Non-public YouTube channels indexed
* **Subscription details**: Plan, billing period, and status
## Example Request
```bash cURL theme={null}
curl "https://api.vidnavigator.com/v1/usage" \
-H "X-API-Key: YOUR_API_KEY"
```
```python Python theme={null}
import requests
url = "https://api.vidnavigator.com/v1/usage"
headers = {"X-API-Key": "YOUR_API_KEY"}
response = requests.get(url, headers=headers)
usage = response.json()
credits = usage["data"]["credits"]
print(f"Monthly remaining: {credits['monthly_remaining']}")
print(f"Purchased credits: {credits['purchased']}")
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.vidnavigator.com/v1/usage', {
headers: { 'X-API-Key': 'YOUR_API_KEY' }
});
const usage = await response.json();
const { credits } = usage.data;
console.log(`Monthly remaining: ${credits.monthly_remaining}`);
console.log(`Purchased credits: ${credits.purchased}`);
```
## Success Response (200 OK)
```json theme={null}
{
"status": "success",
"data": {
"usage_period": {
"start": "2026-03-01T00:00:00Z",
"end": "2026-04-01T00:00:00Z"
},
"billing_period": {
"start": "2026-03-15T00:00:00Z",
"end": "2026-04-15T00:00:00Z",
"interval": "month"
},
"subscription": {
"plan_id": "pro",
"plan_name": "Voyager",
"interval": "month",
"status": "active",
"cancel_at_period_end": false
},
"credits": {
"monthly_total": 100,
"monthly_remaining": 82.5,
"purchased": 50
},
"usage": {
"standard_request": { "used": 150 },
"residential_request": { "used": 45 },
"search_request": { "used": 30 },
"analysis_request": { "used": 12 },
"scene_analysis_hour": { "used": 3, "unit": "hours" },
"transcription_hour": { "used": 2.5, "unit": "hours" }
},
"channels_indexed": {
"used": 5,
"limit": 15,
"remaining": 10,
"percentage": 33.3
},
"storage": {
"used_bytes": 2684354560,
"used_formatted": "2.5 GB",
"limit_bytes": 536870912000,
"limit_formatted": "500 GB",
"remaining_bytes": 534186557440,
"remaining_formatted": "497.5 GB",
"percentage": 0.5
},
"generated_at": "2026-03-28T10:00:00Z"
}
}
```
### Credits Object
| Field | Type | Description |
| - | - | - |
| `monthly_total` | number \| `"unlimited"` | Total monthly credits included in your plan |
| `monthly_remaining` | number \| `"unlimited"` | Monthly credits remaining for the current period |
| `purchased` | number | Additional purchased credits (persist across billing periods) |
Enterprise plans return `"unlimited"` for `monthly_total` and `monthly_remaining`. The shared credit pool is what limits usage — individual service counters under `usage` are informational only and do not impose per-service caps.
### Usage Counters
The `usage` object shows how many resource units were used during the current period. These are **activity counters** — the shared credit pool is what actually limits usage.
| Counter | Description |
| - | - |
| `standard_request` | Non-YouTube transcripts and non-proxy scraper page fetches |
| `residential_request` | YouTube transcript retrievals and scraping requests that require residential proxies |
| `search_request` | Semantic (vector) searches over already indexed content, including indexed YouTube channels and uploaded files (`/search/file`). Global YouTube and TikTok searches do **not** use this meter — they bill `residential_request` (and `analysis_request` for YouTube search). |
| `analysis_request` | Video/file analysis and AI extraction units |
| `scene_analysis_hour` | Scene analysis hours processed |
| `transcription_hour` | Video/audio transcription hours processed by speech-to-text |
### Channels Indexed
A hard plan limit (not credit-gated). Shows how many non-public YouTube channels you have indexed vs. your plan's capacity.
This endpoint reports **cumulative** usage for the billing period. To see what a **single** API call charged, pass `include_usage=true` on that call and read the returned `usage` block — see [Per-Call Usage Disclosure](/api-reference/usage-costs#per-call-usage-disclosure).
### Error: No Subscription
If you don't have an active subscription, the endpoint returns `404`:
```json theme={null}
{
"status": "error",
"error": "no_subscription",
"message": "You are not subscribed to any plan."
}
```
# Search YouTube
Source: https://docs.vidnavigator.com/api-reference/endpoint/youtube-search
POST /youtube/search
Search YouTube for videos using AI analysis and ranking with optional filters.
**Process:**
1. Enhanced search with prompt engineering to find relevant videos
2. AI analysis of video content and transcripts (per-video residential proxy fetch + per-video LLM analysis)
3. Intelligent ranking based on query relevance
4. Structured results with rich metadata
**Billing:** for each candidate video the API bills one `residential_request` when fetching the transcript — set `max_results` to cap how many candidates are processed and therefore how many residential charges fire. After all per-video AI analyses AND the ranking step complete, a single consolidated `analysis_request` charge is recorded with `quantity = ceil(total_tokens / 15000)`, where `total_tokens` is the sum of LLM input + output tokens across all videos analyzed plus the ranking call. No `search_request` is billed — the residential fetches + analysis fully cover the workflow. If the search returns zero results, nothing is billed. Set `include_usage=true` to receive the full per-charge breakdown — the consolidated `analysis_request` entry also carries a nested `tokens` object reporting `prompt_tokens` / `completion_tokens` / `total_tokens`.
**Partial results:** if the user runs out of `residential_request` credits mid-search, the endpoint returns HTTP `402` with `status: "partial"`, the videos analyzed so far in `data.results`, and `error_code: "insufficient_credits_video_search"`. The `usage` block (when `include_usage=true`) reflects only the charges that succeeded.
Search YouTube for videos using AI-powered analysis and ranking with optional filters.
This endpoint was previously `/search/video`. It is now `/youtube/search` and is dedicated to searching YouTube. To search TikTok, use the [TikTok Search](/api-reference/endpoint/tiktok-search) endpoint. To search your own uploaded files, use [Search Files](/api-reference/endpoint/search-file).
## Overview
Find the most relevant YouTube videos using natural language queries. Our AI analyzes video content, transcripts, and metadata to deliver precisely ranked results.
### Search Process
1. **Enhanced query processing**: AI expands and optimizes your search query
2. **Content analysis**: per-video transcript fetch (residential proxy) + per-video AI analysis
3. **Intelligent ranking**: results ranked by relevance, not just keyword matching
4. **Rich results**: detailed video information with explanations
## Billing
Unlike a flat `search_request`, YouTube search bills for the work it actually does:
* **One `residential_request` per candidate video** whose transcript is fetched. Use `max_results` to cap how many candidates are processed — that directly caps how many residential charges fire.
* **One consolidated `analysis_request` charge** after all per-video analyses **and** the ranking step complete, with `quantity = ceil(total_tokens / 15000)` where `total_tokens` is the sum of LLM input + output tokens across every video analyzed plus the ranking call.
* **No `search_request`** is billed — the residential fetches + analysis fully cover the workflow.
* If the search returns zero results, **nothing is billed**.
Set `include_usage: true` to receive the full per-charge breakdown. The consolidated `analysis_request` entry also carries a nested `tokens` object reporting `prompt_tokens` / `completion_tokens` / `total_tokens`.
**Partial results:** if you run out of `residential_request` credits mid-search, the endpoint returns HTTP `402` with `status: "partial"`, the videos analyzed so far in `data.results`, and `error_code: "insufficient_credits_video_search"`. The `usage` block (when `include_usage=true`) reflects only the charges that succeeded.
### Search Options
AI-powered query expansion and optimization (recommended)
Filter results by publication date range
Limit results by maximum video duration
Prioritize relevance, popularity, or brevity
## Request Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `query` | string | Yes | Search query |
| `use_enhanced_search` | boolean | No | Whether to use enhanced search (default `true`) |
| `start_year` | integer | No | Filter by start year |
| `end_year` | integer | No | Filter by end year |
| `focus` | string | No | `relevance` (default), `popularity`, or `brevity` |
| `duration` | integer | No | Maximum duration in seconds |
| `max_results` | integer | No | Maximum number of videos to analyse and return. Each candidate triggers one `residential_request` and contributes tokens to the consolidated `analysis_request` charge, so lowering this caps the per-call cost. Defaults to your plan ceiling; values above it are silently clamped. |
| `include_usage` | boolean | No | When `true`, the response includes a `usage` block listing every meter charged, the total credits deducted, and the remaining balance. |
## Example Usage
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/youtube/search" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "What are the best practices for React development?",
"use_enhanced_search": true,
"start_year": 2020,
"end_year": 2024,
"focus": "relevance",
"duration": 1800,
"max_results": 5
}'
```
```python Python theme={null}
import requests
url = "https://api.vidnavigator.com/v1/youtube/search"
headers = {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json"
}
data = {
"query": "What are the best practices for React development?",
"use_enhanced_search": True,
"start_year": 2020,
"end_year": 2024,
"focus": "relevance",
"duration": 1800,
"max_results": 5
}
response = requests.post(url, headers=headers, json=data)
results = response.json()
for video in results['data']['results']:
print(f"Title: {video['title']}")
print(f"Relevance Score: {video['relevance_score']}")
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.vidnavigator.com/v1/youtube/search', {
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: 'What are the best practices for React development?',
use_enhanced_search: true,
start_year: 2020,
end_year: 2024,
focus: 'relevance',
duration: 1800,
max_results: 5
})
});
const results = await response.json();
```
## Response Example
```json theme={null}
{
"status": "success",
"data": {
"results": [
{
"title": "React Best Practices 2024: Clean Code & Performance",
"description": "Complete guide to writing maintainable React code...",
"url": "https://youtube.com/watch?v=example123",
"thumbnail": "https://img.youtube.com/vi/example123/maxresdefault.jpg",
"channel": "React Mastery",
"duration": "15:30",
"views": 125000,
"published_date": "2024-01-15",
"relevance_score": 0.95,
"explanation": "This video directly addresses React best practices with focus on clean code and performance optimization.",
"key_topics": ["React hooks", "Performance optimization", "Code organization"]
}
],
"query": "What are the best practices for React development?",
"total_found": 1,
"explanation": "Found videos discussing React development best practices, focusing on recent content and high relevance."
},
"usage": {
"charges": [
{ "service_type": "residential_request", "quantity": 5, "credits": 0.025 },
{
"service_type": "analysis_request",
"quantity": 2,
"credits": 0.02,
"tokens": { "prompt_tokens": 24000, "completion_tokens": 4200, "total_tokens": 28200 }
}
],
"total_credits": 0.045
}
}
```
The `usage` block is only included when `include_usage=true` in the request.
## Partial Results (402)
```json theme={null}
{
"status": "partial",
"error_code": "insufficient_credits_video_search",
"message": "Ran out of credits after analyzing some videos.",
"data": {
"results": [ ... ],
"query": "...",
"total_found": 3,
"explanation": "..."
},
"usage": { "charges": [ ... ], "total_credits": 0.02 }
}
```
## Use Cases
Find educational content on specific topics or technologies
Build curated lists of videos for courses or documentation
Analyze what content exists in your industry or niche
Identify trending topics and popular content creators
# Errors
Source: https://docs.vidnavigator.com/api-reference/errors
Error format and the error codes returned by the VidNavigator API
## Error format
The API uses standard HTTP status codes and returns errors as JSON:
```json theme={null}
{
"status": "error",
"error": "video_too_long",
"message": "This video is longer than 10 minutes. Use POST /v1/transcribe/async instead.",
"docs_url": "https://docs.vidnavigator.com/guides/async-jobs"
}
```
| Field | Description |
| - | - |
| `status` | Always `error`. |
| `error` | Machine-readable code. Branch on this, not on `message`. |
| `message` | Human-readable explanation. |
| `docs_url` | Present on actionable errors: a link to the page that explains how to fix it. |
For [async jobs](/guides/async-jobs), a job that fails in the background is reported by the poll endpoint (`200`, `task_status: "failed"`) with a `data.error` object carrying the same `error` code plus the `http_status` the synchronous endpoint would have returned.
## Error codes
### 400 — Bad request
| `error` | Meaning |
| - | - |
| `request_body_required` | The request body is missing. |
| `missing_parameter` | A required parameter is missing. |
| `invalid_parameter` | A parameter has an invalid value. |
| `invalid_url` | The URL is malformed. |
| `unsupported_url` / `unsupported_platform` | The URL or platform is not supported by this endpoint. |
| `no_videos_found` | The post carries no video (e.g. an image-only Instagram carousel). |
| `video_too_long` | The media is longer than **10 minutes**, the limit for synchronous endpoints. Use the [async endpoint](/guides/async-jobs). The metadata fetch that read the duration is billed and not refunded; no `transcription_hour` is charged. |
| `invalid_schema` | The extraction schema is invalid. |
| `input_too_large` | The input exceeds the size the endpoint accepts. |
| `invalid_cursor` | The pagination cursor is invalid. |
### 401 — Unauthorized
Missing, invalid or deactivated API key.
### 402 — Payment required
| `error` | Meaning |
| - | - |
| `limit_exceeded` | Not enough credits, or no active subscription. Async submit endpoints return it when less than 60 seconds of transcription credit is left. |
### 403 — Forbidden
| `error` | Meaning |
| - | - |
| `access_denied` | The API key lacks permission for this resource or endpoint. |
| `content_restricted` | The content requires login or age verification. |
### 404 — Not found
| `error` | Meaning |
| - | - |
| `video_not_found` | The video does not exist. |
| `video_unavailable` | The video exists but cannot be accessed (private, removed, …). |
| `transcript_not_available` | No transcript could be retrieved or generated. |
| `file_not_found` | The uploaded file does not exist. |
| `task_not_found` | The async task does not exist, has expired (results are kept 1 hour after the job finishes), or belongs to another account. |
### 413 — Payload too large
| `error` | Meaning |
| - | - |
| `storage_quota_exceeded` | Your storage quota is full. |
### 429 — Too many requests
| `error` | Meaning |
| - | - |
| `rate_limit_exceeded` | Per-endpoint rate limit exceeded. See [Rate Limiting](/api-reference/rate-limiting). |
| `too_many_active_jobs` | Too many async jobs already running for this account. Wait for some to finish, then retry. |
### 451 — Unavailable for legal reasons
| `error` | Meaning |
| - | - |
| `geo_restricted` | The content is not available in the region it is fetched from. |
### 500 — Server error
| `error` | Meaning |
| - | - |
| `metadata_fetch_failed` | The video's metadata could not be fetched from the platform. |
| `audio_extraction_failed` | The audio could not be extracted from the video. |
| `transcription_failed` | Speech-to-text failed. |
| `extraction_failed` | Structured data extraction failed. |
| `internal_server_error` | Unexpected server error. |
Charges are reverted when a request fails with a server error.
### 502 / 503
| Status | `error` | Meaning |
| - | - | - |
| `502` | — | Upstream fetch failed (e.g. the tweet could not be retrieved from X). |
| `503` | `system_overload` | The system is temporarily overloaded. Retry later. |
| `503` | `storage_not_configured` | File storage is not available. |
# API Reference
Source: https://docs.vidnavigator.com/api-reference/introduction
Complete reference for the VidNavigator Developer API
## Welcome to VidNavigator API
The VidNavigator Developer API provides transcription, analysis, and structured data extraction across YouTube, TikTok, Instagram, X/Twitter, Facebook, Vimeo, Loom, Dailymotion, and uploaded files. It also supports global YouTube and TikTok discovery plus semantic search over indexed YouTube channel content and uploaded files.
View the complete OpenAPI 3.0 specification
Quick start guide for making your first API call
## Authentication
All API endpoints require authentication using an API key passed in the `X-API-Key` header:
```bash theme={null}
X-API-Key: YOUR_API_KEY
```
Get your API key from the [VidNavigator Developer Dashboard](https://vidnavigator.com/studio/api).
## Base URL
The API is available at:
```
https://api.vidnavigator.com/v1
```
## Long Videos and Async Jobs
The speech-to-text endpoints run for as long as the media is long, so each one has an **async twin**. Synchronous calls are limited to **10 minutes** of media and reject longer media with `400 video_too_long`; the async endpoints have no duration cap and work for short videos too.
| Synchronous (≤ 10 min) | Async (any length) |
| - | - |
| [`POST /transcribe`](/api-reference/endpoint/transcribe) | `POST /transcribe/async` → `GET /transcribe/{task_id}` |
| [`POST /extract/video`](/api-reference/endpoint/extract-video) | `POST /extract/video/async` → `GET /extract/video/{task_id}` |
| [`POST /tweet/statement`](/api-reference/endpoint/tweet-claim-analysis) | `POST /tweet/statement/async` → `GET /tweet/statement/{task_id}` |
The async endpoint returns `202` with a `task_id` immediately. Poll the `check_status_url` (free), or get called back through a [webhook](/webhooks) — set a default endpoint in [Studio → API](https://vidnavigator.com/studio/api) or pass `webhook_url` per request.
Submit, poll and migrate from synchronous calls
Payload, signature verification and retries
## Error Handling
The API uses standard HTTP status codes and returns structured error responses:
```json theme={null}
{
"status": "error",
"error": "error_type",
"message": "Human readable error message",
"docs_url": "https://docs.vidnavigator.com/..."
}
```
Actionable errors also carry a `docs_url` pointing to the page that explains how to fix them. See [Errors](/api-reference/errors) for every error code.
### Common Error Codes
| Status Code | Error Type | Description |
| - | - | - |
| 400 | `missing_parameter`, `invalid_parameter`, … | Invalid request parameters |
| 400 | `video_too_long` | Media longer than 10 minutes sent to a synchronous endpoint — use the async endpoint |
| 401 | — | Missing or invalid API key |
| 402 | `limit_exceeded` | Credit limit exceeded or no active subscription |
| 403 | `access_denied` | Insufficient permissions |
| 404 | `video_not_found`, `task_not_found`, … | Resource not found |
| 429 | `rate_limit_exceeded`, `too_many_active_jobs` | Rate limit exceeded, or too many async jobs running |
| 500 | `internal_server_error` | Server error |
## API Categories
Extract transcripts from YouTube, TikTok, Instagram, X/Twitter, Facebook, Vimeo, Loom, and more
Upload, manage, and organize audio/video files with namespaces
Analyze video content with AI for insights and summaries
Extract structured data from transcripts using custom schemas
Discover videos with global YouTube and TikTok search
Search indexed YouTube channel content and uploaded files using vector similarity
# Rate Limiting
Source: https://docs.vidnavigator.com/api-reference/rate-limiting
Per-endpoint request limits and throttling behaviour
VidNavigator applies per-endpoint rate limits to ensure fair usage and platform stability.
## Current Limits
| Endpoint | Requests / minute |
| - | -: |
| `GET /health` | **300** |
| `GET /usage` | **300** |
| `POST /transcript` | **200** |
| `GET /files` | **200** |
| `GET /file/{file_id}` | **200** |
| `POST /analyze/video` | **30** |
| `POST /analyze/file` | **30** |
| `POST /transcribe` | **20** |
| `POST /youtube/search` | **10** |
| `POST /search/file` | **10** |
| `POST /upload/file` | **3** |
> **Temporary Ban**\
> Exceeding a limit results in a **429 Too Many Requests** response and a temporary ban of **1 minute** on the offending API key.
### Async Jobs
The [async submit endpoints](/guides/async-jobs) (`POST /transcribe/async`, `POST /extract/video/async`, `POST /tweet/statement/async`) also limit how many jobs can run at the same time for one account. Above that limit, the submit returns **429** with `error: "too_many_active_jobs"` — wait for running jobs to finish, then retry. Polling the result endpoints is free.
### Enterprise Plans
Enterprise customers can request higher or custom rate limits. Contact us at [contact@vidnavigator.com](mailto:contact@vidnavigator.com) for details.
# Usage Costs
Source: https://docs.vidnavigator.com/api-reference/usage-costs
Understand how API operations consume credits from your shared credit pool.
## Overview
Your VidNavigator plan includes a monthly **credit pool** shared across all API operations. Different services consume credits at different rates. You can monitor your current balance at any time by calling the [`/usage`](/api-reference/endpoint/usage) endpoint.
There are **no per-service limits** — all operations draw from the same credit pool. The activity counters shown in `/usage` are informational only.
## Credit Costs per Service
| Resource Unit | Credit Conversion | Used By |
| - | - | - |
| Standard Requests (`standard_request`) | 1 credit = 10,000 non-YouTube transcripts or non-proxy scraper pages | `/transcript` (non-YouTube), non-proxy TikTok profile pages |
| Residential Proxy Requests (`residential_request`) | 1 credit = 200 residential fetches | `/transcript` (YouTube), `/youtube/search`, `/tiktok/search`, proxy-backed TikTok profile pages |
| Analysis Requests (`analysis_request`) | 1 credit = 100 analysis requests (token-based, see below) | `/analyze/video`, `/analyze/file`, `/extract/video`, `/extract/file`, `/youtube/search`, `/search/file`, `/tweet/statement` |
| Search Requests (`search_request`) | 1 credit = 1,000 semantic searches | Indexed YouTube channel search, `/search/file` |
| Transcription Hours (`transcription_hour`) | 1 credit = 3 hours of video/audio transcription | `/upload/file`, `/transcribe` (and `/transcribe/async`), speech-to-text fallback |
| Scene Analysis Hours (`scene_analysis_hour`) | 1 credit = 1 hour of scene analysis | Scene analysis endpoints |
### Deduction Order
Credits are deducted in this order:
1. **Monthly credits** are consumed first
2. **Purchased credits** cover any overflow
Monthly credits reset each billing period. Purchased credits persist across periods.
## Plan Credit Allowances
| Plan | Monthly Credits | Storage | Indexed Channels |
| - | - | - | - |
| Trial | 5 | 5 GB | 2 |
| Navigator (\$19.99/mo) | 20 | 50 GB | 3 |
| Voyager (\$79.99/mo) | 100 | 500 GB | 15 |
| Odyssey (Custom) | Unlimited | Unlimited | Unlimited |
Voyager and above can purchase additional credit packs: 50 credits ($20), 300 credits ($100), or 1,200 credits (\$300).
## Service Details
Each non-YouTube transcript request to `/transcript` consumes `standard_request` units. 1 credit covers 10,000 standard requests.
Fetches that route through residential proxies consume `residential_request` units. 1 credit covers 200 residential requests.
For YouTube, even `metadata_only: true` routes through the residential proxy and is billed as a `residential_request`. YouTube search and TikTok search also bill per residential page fetched.
`/transcribe`, `/upload/file`, and speech-to-text fallback consume `transcription_hour` units. 1 credit covers 3 hours of audio/video processed.
Files uploaded via `/upload/file` also count towards your storage quota.
`/analyze/*`, `/extract/*`, `/youtube/search`, `/search/file`, and `/tweet/statement` consume `analysis_request` units. 1 credit covers 100 analysis requests.
LLM analysis is **token-based**: 1 `analysis_request` unit covers up to 15,000 total tokens. Larger contexts use `ceil(total_tokens / 15000)` units. Multiple internal LLM calls in one request are consolidated into a single charge.
Semantic (vector) searches over already indexed content consume a `search_request` unit. This includes indexed YouTube channel search and each call to `/search/file`. 1 credit covers 1,000 semantic searches.
Global YouTube search (`/youtube/search`) and TikTok keyword search (`/tiktok/search`) do **not** bill `search_request` — they bill per `residential_request` (and `analysis_request` for YouTube search) instead.
Each call to `/extract/video` or `/extract/file` uses AI to extract structured data from transcripts. Analysis Request units scale with token usage.
## Per-Call Usage Disclosure
Most billable endpoints accept an `include_usage` flag. When set to `true`, the response includes a `usage` block that lists **exactly what was charged for that specific call** — every meter that fired, the net credits deducted, and (for LLM endpoints) the token tally.
```json theme={null}
{
"usage": {
"charges": [
{ "service_type": "residential_request", "quantity": 5, "credits": 0.025 },
{
"service_type": "analysis_request",
"quantity": 2,
"credits": 0.02,
"tokens": { "prompt_tokens": 24000, "completion_tokens": 4200, "total_tokens": 28200 }
}
],
"total_credits": 0.045
}
}
```
| Field | Description |
| - | - |
| `charges[]` | One entry per meter (`service_type`). Multiple billings of the same meter in one request are consolidated. |
| `charges[].quantity` | Net units consumed. For `analysis_request` this is `ceil(total_tokens / 15000)`; for `transcription_hour` / `scene_analysis_hour` it's in hours. |
| `charges[].credits` | Net credits deducted for this meter (`0` when waived). |
| `charges[].waived` / `credits_saved` | Present when a charge was waived via a cache-hit sponsorship. |
| `charges[].tokens` | Only on the `analysis_request` entry for LLM endpoints — cumulative `prompt_tokens` / `completion_tokens` / `total_tokens`. |
| `total_credits` | Net credits deducted by the whole request. |
For **async** endpoints (`/transcribe/async`, `/extract/video/async`, `/tweet/statement/async`, `/tiktok/profile`, `/tiktok/search`), billing happens in the background worker, so `include_usage` is passed on the polling `GET` (once `task_status=completed`), not on submit. See [Async Jobs](/guides/async-jobs).
## Async Jobs and Long Videos
* Async endpoints are billed **exactly like** their synchronous counterparts — the async mode costs nothing extra.
* Polling result endpoints is **free**.
* A failed async job has all of its charges reverted.
* Async submit endpoints return `402 limit_exceeded` when less than **60 seconds** of transcription credit is left. The real, duration-based charge is computed when the job runs.
* A synchronous request rejected with `video_too_long` (media longer than 10 minutes) still bills the metadata fetch that read the duration (`standard_request` or `residential_request`, **not refunded**). No `transcription_hour` is charged, since no audio is processed.
This per-call `usage` block is distinct from the aggregate [`GET /usage`](/api-reference/endpoint/usage) endpoint, which reports cumulative counters and balances for the whole billing period.
## Hard Limits (Not Credit-Gated)
Some resources have fixed plan limits that are **not** deducted from your credit pool:
| Resource | How It's Enforced |
| - | - |
| **Storage** | Total file storage in bytes checked against your plan's storage cap |
| **Indexed Channels** | Non-public YouTube channels counted live from your channel list against your plan's cap |
## YouTube vs Other Platforms
**Why do Residential Proxy Requests cost more?**
YouTube and some scraper flows require residential proxy infrastructure for reliable access. This is reflected in the credit rate (1 credit per 200 Residential Proxy Requests vs. 1 per 10,000 Standard Requests).
## Free Endpoints
The following endpoints do **not** consume credits:
* `GET /files` — List your uploaded files
* `GET /file/{file_id}` — Get file info and transcript
* `GET /file/{file_id}/url` — Get temporary file URL
* `GET /namespaces` — List namespaces
* `POST /namespaces` — Create a namespace
* `PUT /namespaces/{namespace_id}` — Rename a namespace
* `DELETE /namespaces/{namespace_id}` — Delete a namespace
* `PUT /file/{file_id}/namespaces` — Update file namespace assignments
* `GET /tiktok/profile/{task_id}` — Poll a TikTok profile scrape (billing happens in the background worker)
* `GET /tiktok/search/{task_id}` — Poll a TikTok keyword search (billing happens in the background worker)
* `GET /health` — Health check
* `GET /usage` — Usage statistics
# Chrome Extension
Source: https://docs.vidnavigator.com/chrome-extension
Find exact moments in YouTube videos with the VidNavigator Chrome Extension
## VidNavigator Chrome Extension
The VidNavigator Chrome Extension brings powerful video search and analysis capabilities directly to your browser, allowing you to find exact moments in YouTube videos without leaving the page.
Get VidNavigator from the Chrome Web Store
## Features
Search for specific moments within any YouTube video using natural language queries.
Get precise timestamps for the moments you're looking for with AI-powered accuracy.
Find relevant content based on meaning, not just keywords.
Jump directly to the exact moment in the video with a single click.
## How to Use
Install the VidNavigator Chrome Extension from the Chrome Web Store.
Open any YouTube video you want to search through.
Click the VidNavigator extension icon in your browser toolbar.
Enter your search query in natural language:
* "Where does he talk about machine learning?"
* "Show me the part about pricing"
* "Find the demo section"
Click on any search result to jump directly to that moment in the video.
## Example Searches
Here are some example queries you can try with the extension:
* "Explain the concept of neural networks"
* "Show me the example calculation"
* "Where is the homework assignment mentioned?"
* "Find the part about the final exam"
* "How to install the software"
* "Show me the troubleshooting section"
* "Where does the configuration start?"
* "Find the advanced settings"
* "When do they discuss career advice?"
* "Find the part about startup funding"
* "Show me where they talk about the new product"
* "Where is the Q\&A session?"
* "Show me the performance benchmarks"
* "Find the pricing information"
* "Where do they compare it to competitors?"
* "Show me the unboxing part"
## Key Benefits
### 🎯 **Save Time**
No more scrubbing through long videos. Find exactly what you're looking for in seconds.
### 🧠 **Smart Understanding**
Our AI understands context and meaning, not just exact word matches.
### 📚 **Perfect for Learning**
Great for students, researchers, and professionals who need to find specific information in educational content.
### 🔍 **Works on Any Video**
Compatible with all YouTube videos that have captions or transcripts available.
## Technical Requirements
* **Browser**: Chrome (version 88+)
* **Platform**: Works on any website with YouTube videos
* **Internet**: Requires internet connection for AI processing
* **Account**: Free VidNavigator account (sign up at [vidnavigator.com](https://vidnavigator.com))
## Privacy & Security
* Your search queries are processed securely through our API
* No personal browsing data is stored
* Video content is not downloaded or stored locally
* All processing happens in the cloud with enterprise-grade security
The extension only requests minimal permissions:
* Access to YouTube pages (to integrate with video player)
* Network access (to communicate with VidNavigator API)
* No access to other websites or personal data
## Troubleshooting
1. Make sure you're on a YouTube video page
2. Check that the video has captions/transcripts available
3. Verify your internet connection
4. Try refreshing the page and reopening the extension
* Try rephrasing your search query
* Make sure the topic you're searching for is actually discussed in the video
* Check if the video has accurate captions (auto-generated captions may be less reliable)
* Be more specific in your search terms
* Try using different keywords or phrases
* Consider the context and how the speaker might phrase the topic
## Usage Limits
The Chrome extension uses your VidNavigator account's API quota:
* **Free Account**: Limited searches per month
* **Paid Plans**: Higher limits based on your subscription tier
***
Enhance your YouTube viewing experience with intelligent video search. Install the VidNavigator Chrome Extension today!
# Contact Us
Source: https://docs.vidnavigator.com/contact
Reach out to the VidNavigator team for support, feature requests, or general questions.
Have a feature request, need help with your integration, or just have a general question? We would love to hear from you!
Reach out to us at **[support@vidnavigator.com](mailto:support@vidnavigator.com)** or **[contact@vidnavigator.com](mailto:contact@vidnavigator.com)**.
We read all our emails and promise to reply to you quickly.
# Analyzing Videos
Source: https://docs.vidnavigator.com/guides/analyzing-videos
Learn how to analyze videos to extract summaries, topics, and other insights.
## Overview
VidNavigator's analysis capabilities allow you to go beyond simple transcription and understand the content of a video at a deeper level. You can get an AI-generated summary, identify key topics and speakers, and even ask specific questions about the video's content.
Every analysis returns a `transcript_analysis` object with:
* `summary`: an overview of the content
* `people`: people mentioned, with the context they appear in
* `places`: locations referenced
* `key_subjects`: important topics, with a description and importance
* `query_answer`: when you pass a `query`, the direct `answer` plus the `relevant_segments` of the transcript that support it
## Prerequisites
* A valid VidNavigator API key.
* The [Python](/sdks/python) or [JavaScript](/sdks/javascript) SDK, version 2.1.0 (`pip install vidnavigator==2.1.0` or `npm install vidnavigator@2.1.0`).
## Billing
AI analysis consumes `analysis_request` units based on context size. One unit covers up to **15,000 total tokens**, and longer contexts are billed with `ceil(total_tokens / 15000)`. For example, a video that takes 17,000 total tokens costs 2 `analysis_request` units.
Pass `include_usage=True` (Python) or `include_usage: true` (JavaScript) to receive a `usage` block with the exact charges and LLM token counts.
## Analyzing a Video
```python Python theme={null}
from vidnavigator import VidNavigatorClient, VidNavigatorError
client = VidNavigatorClient() # reads VIDNAVIGATOR_API_KEY
try:
resp = client.analyze_video(
video_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ",
query="What is the main theme of this song?",
)
analysis = resp.data.transcript_analysis
print("Summary:", analysis.summary)
print("Answer to your query:", analysis.query_answer.answer)
# people, places and key_subjects are lists of plain dicts
for person in analysis.people or []:
print("Person:", person) # {"name": ..., "context": ...}
for subject in analysis.key_subjects or []:
print("Subject:", subject) # {"name": ..., "description": ..., "importance": ...}
except VidNavigatorError as e:
print(f"An error occurred: {e}")
```
```javascript JavaScript theme={null}
import { VidNavigatorClient, VidNavigatorError } from 'vidnavigator';
const vn = new VidNavigatorClient({
apiKey: process.env.VIDNAVIGATOR_API_KEY,
});
try {
const { transcript_analysis } = await vn.analyzeVideo({
video_url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
query: 'What is the main theme of this song?',
});
console.log('Summary:', transcript_analysis.summary);
console.log('Answer to your query:', transcript_analysis.query_answer?.answer);
console.log('Supporting excerpts:', transcript_analysis.query_answer?.relevant_segments);
for (const person of transcript_analysis.people ?? []) {
console.log(`Person: ${person.name} -- ${person.context}`);
}
for (const subject of transcript_analysis.key_subjects ?? []) {
console.log(`Subject: ${subject.name} -- ${subject.description}`);
}
} catch (error) {
if (error instanceof VidNavigatorError) {
console.error(`An error occurred: ${error.message}`);
} else {
throw error;
}
}
```
`query` is optional. Without it you still get the summary, people, places and key subjects.
## Analyzing an Uploaded File
Files you have [uploaded](/guides/uploading-videos) are analyzed the same way, by `file_id`:
```python Python theme={null}
resp = client.analyze_file(
file_id="your_file_id",
query="What are the key action items from this meeting?",
)
print(resp.data.transcript_analysis.summary)
print(resp.data.transcript_analysis.query_answer.answer)
```
```javascript JavaScript theme={null}
const { transcript_analysis } = await vn.analyzeFile({
file_id: 'your_file_id',
query: 'What are the key action items from this meeting?',
});
console.log(transcript_analysis.summary);
console.log(transcript_analysis.query_answer?.answer);
```
## Next Steps
* Need specific fields rather than a free-form summary? Use [structured data extraction](/guides/data-extraction).
* See the [Analyze Video](/api-reference/endpoint/analyze-video) and [Analyze File](/api-reference/endpoint/analyze-file) API references.
# Async Jobs & Long Videos
Source: https://docs.vidnavigator.com/guides/async-jobs
Transcribe, extract and analyze videos of any length with async jobs, polling and webhooks.
## Overview
The speech-to-text endpoints run for as long as the media is long. A synchronous request holds the HTTP connection open for the whole download and transcription — reliable for short clips, not for long ones: clients, reverse proxies and marketplace gateways all time out.
That is why every speech-to-text endpoint has an **async twin**. You submit the job, get a `task_id` back immediately, and collect the result by polling or through a [webhook](/webhooks).
**Synchronous calls are limited to 10 minutes of media.** Longer media is rejected with `400 video_too_long`. Use the async endpoints for anything that may run past 10 minutes — they have **no duration cap**, and they work just as well for short videos.
## Which endpoint to use
| Synchronous (≤ 10 min) | Async submit (any length) | Poll result |
| - | - | - |
| [`POST /transcribe`](/api-reference/endpoint/transcribe) | [`POST /transcribe/async`](/api-reference/endpoint/transcribe-async) | `GET /transcribe/{task_id}` |
| [`POST /extract/video`](/api-reference/endpoint/extract-video) (with `transcribe=true`) | [`POST /extract/video/async`](/api-reference/endpoint/extract-video-async) | `GET /extract/video/{task_id}` |
| [`POST /tweet/statement`](/api-reference/endpoint/tweet-claim-analysis) | [`POST /tweet/statement/async`](/api-reference/endpoint/tweet-claim-analysis-async) | `GET /tweet/statement/{task_id}` |
Each async endpoint accepts **exactly the same body** as its synchronous counterpart, plus an optional `webhook_url`. The result you get back is **identical** to the synchronous response's `data` block, so the same parsing code works for both.
If you don't know the video's duration in advance, just use the async endpoint every time. There is no extra cost.
[TikTok profile scrape](/api-reference/endpoint/tiktok-profile) and [TikTok keyword search](/api-reference/endpoint/tiktok-search) are async by design and follow the same polling and webhook model.
## Using the SDKs
The [Python](/sdks/python#background-jobs) and [JavaScript](/sdks/javascript#background-jobs) SDKs (v2.1.0) **always use the async endpoints**, so you don't need to write any polling code. A single call submits the job, polls until it finishes, and returns the result (or raises the same error the synchronous endpoint would have returned):
```python Python theme={null}
from vidnavigator import VidNavigatorClient
client = VidNavigatorClient() # reads VIDNAVIGATOR_API_KEY
# Wait for the result, however long the video is
result = client.transcribe_video(video_url="https://www.tiktok.com/@user/video/1234567890")
print(result.data.video_info.title)
# Or submit now and collect later
job = client.submit_transcribe_video(
video_url="https://www.tiktok.com/@user/video/1234567890",
webhook_url="https://example.com/hooks/vidnavigator", # optional
)
print(job.task_id, job.status()) # "processing"
result = job.result() # waits, then returns the same object as transcribe_video
# From another process, within 1 hour of the job finishing
result = client.resume_job("transcribe", job.task_id).result()
```
```python Python (asyncio) theme={null}
import asyncio
from vidnavigator import AsyncVidNavigatorClient # pip install "vidnavigator[async]"
async def main():
async with AsyncVidNavigatorClient() as client: # reads VIDNAVIGATOR_API_KEY
# Wait for the result without blocking the event loop
result = await client.transcribe_video(video_url="https://www.tiktok.com/@user/video/1234567890")
print(result.data.video_info.title)
# Or submit now and collect later
job = await client.submit_transcribe_video(
video_url="https://www.tiktok.com/@user/video/1234567890",
webhook_url="https://example.com/hooks/vidnavigator", # optional
)
print(job.task_id, await job.status()) # "processing"
result = await job.result()
# Later, within 1 hour of the job finishing (resume_job itself is not awaited)
result = await client.resume_job("transcribe", job.task_id).result()
asyncio.run(main())
```
```javascript JavaScript theme={null}
import { VidNavigatorClient } from 'vidnavigator';
const vn = new VidNavigatorClient({ apiKey: process.env.VIDNAVIGATOR_API_KEY });
// Wait for the result, however long the video is
const { video_info, transcript } = await vn.transcribe('https://www.tiktok.com/@user/video/1234567890');
console.log(video_info.title);
// Or submit now and collect later
const job = await vn.transcribe.submit({
video_url: 'https://www.tiktok.com/@user/video/1234567890',
webhook_url: 'https://example.com/hooks/vidnavigator', // optional
});
console.log(job.task_id, await job.status()); // 'processing'
const result = await job.result(); // waits, then returns the same shape as vn.transcribe()
// From another process, within 1 hour of the job finishing
const again = await vn.transcribe.resume(job.task_id).result();
```
The same pattern covers every async operation. The Python `AsyncVidNavigatorClient` (new in 2.1.0) has the same method names as the sync client, as coroutines:
| Operation | Python (blocking / non-blocking) | JavaScript (wait / start) |
| - | - | - |
| Transcription | `transcribe_video` / `submit_transcribe_video` | `vn.transcribe()` / `vn.transcribe.submit()` |
| Structured extraction | `extract_video_data` / `submit_extract_video_data` | `vn.extractVideo()` / `vn.extractVideo.submit()` |
| Tweet claim analysis | `get_tweet_statement` / `submit_tweet_statement` | `vn.tweetStatement()` / `vn.tweetStatement.submit()` |
| TikTok profile scrape | `scrape_tiktok_profile` / `submit_tiktok_profile_scrape` | `vn.tiktokProfile()` / `vn.tiktokProfile.submit()` |
| TikTok keyword search | `search_tiktok` / `submit_tiktok_search` | `vn.tiktokSearch()` / `vn.tiktokSearch.submit()` |
The blocking calls wait up to 1 hour (Python) or 30 minutes (JavaScript) by default. On timeout they raise `JobTimeoutError` / `TaskTimeoutError` carrying the `task_id`. The job keeps running, so you can resume it. See the SDK pages for batch recipes and the running-job limit.
The rest of this guide describes the underlying HTTP protocol, for when you call the API directly.
## How it works
`POST` to the async endpoint. The API validates the request and returns `202 Accepted` with a `task_id` and a `check_status_url`.
Either poll `check_status_url` every few seconds while `task_status` is `processing`, or pass a `webhook_url` (or configure a default one in [Studio → API](https://vidnavigator.com/studio/api)) and get called back when the job finishes.
When `task_status` is `completed`, `data.result` holds the result. When it is `failed`, `data.error` carries the error code and HTTP status the synchronous endpoint would have returned.
## 1. Submit the job
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/transcribe/async" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://www.tiktok.com/@user/video/1234567890",
"webhook_url": "https://example.com/hooks/vidnavigator"
}'
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.vidnavigator.com/v1/transcribe/async",
headers={"X-API-Key": "YOUR_API_KEY"},
json={"video_url": "https://www.tiktok.com/@user/video/1234567890"},
)
response.raise_for_status()
task = response.json()["data"]
print(task["task_id"], task["check_status_url"])
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.vidnavigator.com/v1/transcribe/async', {
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
video_url: 'https://www.tiktok.com/@user/video/1234567890'
})
});
const { data: task } = await response.json();
console.log(task.task_id, task.check_status_url);
```
### Accepted response (202)
```json theme={null}
{
"status": "success",
"data": {
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"task_status": "processing",
"job_type": "transcribe",
"expires_at": "2026-09-23T13:00:00Z",
"check_status_url": "/v1/transcribe/550e8400-e29b-41d4-a716-446655440000",
"webhook_url": "https://example.com/hooks/vidnavigator",
"message": "Transcription job accepted.",
"docs_url": "https://docs.vidnavigator.com/guides/async-jobs"
}
}
```
| Field | Description |
| - | - |
| `task_id` | Identifier of the job. |
| `task_status` | Always `processing` at submit time. |
| `job_type` | `transcribe`, `extract_video` or `tweet_statement`. |
| `expires_at` | When the result is deleted. Provisional until the job finishes, then pushed out to **finish time + 1 hour**. |
| `check_status_url` | Relative URL to poll (prefix it with `https://api.vidnavigator.com`). |
| `webhook_url` | The URL the result will be POSTed to, or `null` if no webhook applies. |
## 2. Poll for the result
Polling is **free** — the `GET` endpoints consume no credits. A few seconds between polls is plenty.
```python Python theme={null}
import time
import requests
BASE = "https://api.vidnavigator.com"
HEADERS = {"X-API-Key": "YOUR_API_KEY"}
def wait_for_job(check_status_url, interval=5):
while True:
job = requests.get(BASE + check_status_url, headers=HEADERS).json()["data"]
if job["task_status"] == "completed":
return job["result"]
if job["task_status"] == "failed":
err = job["error"]
raise RuntimeError(f'{err["error"]} ({err["http_status"]}): {err["message"]}')
time.sleep(interval)
result = wait_for_job(task["check_status_url"])
print(result["video_info"]["title"])
for segment in result["transcript"]:
print(f'[{segment["start"]:.2f}s] {segment["text"]}')
```
```javascript JavaScript theme={null}
const BASE = 'https://api.vidnavigator.com';
const HEADERS = { 'X-API-Key': 'YOUR_API_KEY' };
async function waitForJob(checkStatusUrl, intervalMs = 5000) {
while (true) {
const { data: job } = await (await fetch(BASE + checkStatusUrl, { headers: HEADERS })).json();
if (job.task_status === 'completed') return job.result;
if (job.task_status === 'failed') {
throw new Error(`${job.error.error} (${job.error.http_status}): ${job.error.message}`);
}
await new Promise(r => setTimeout(r, intervalMs));
}
}
const result = await waitForJob(task.check_status_url);
console.log(result.video_info.title);
```
### Job states
| `task_status` | Meaning |
| - | - |
| `processing` | Accepted and being worked on. |
| `completed` | Terminal. `data.result` holds the result. |
| `failed` | Terminal. `data.error` holds `error`, `message` and `http_status`. All charges are reverted. |
### Completed response
```json theme={null}
{
"status": "success",
"data": {
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"task_status": "completed",
"job_type": "transcribe",
"request": {
"video_url": "https://www.tiktok.com/@user/video/1234567890",
"transcript_text": false,
"all_videos": false
},
"created_at": "2026-09-23T12:00:00Z",
"started_at": "2026-09-23T12:00:01Z",
"completed_at": "2026-09-23T12:03:40Z",
"expires_at": "2026-09-23T13:03:40Z",
"check_status_url": "/v1/transcribe/550e8400-e29b-41d4-a716-446655440000",
"webhook": {
"status": "delivered",
"attempts": 1,
"response_status": 200,
"last_error": null,
"delivered_at": "2026-09-23T12:03:41Z"
},
"result": {
"video_info": { "title": "A long podcast episode", "duration": 3540.0 },
"transcript": [
{ "text": "Welcome to the show.", "start": 0.5, "end": 2.1 }
]
},
"error": null
}
}
```
### Failed response
```json theme={null}
{
"status": "success",
"data": {
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"task_status": "failed",
"job_type": "transcribe",
"result": null,
"error": {
"error": "transcript_not_available",
"message": "Transcript could not be generated for this video.",
"http_status": 404
}
}
}
```
The poll request itself returns `200` even when the job failed — check `task_status`. `data.error` uses the same codes as the synchronous endpoint, so existing error handling can be reused unchanged. See [Errors](/api-reference/errors).
### Retention
* Results are kept for **1 hour after the job finishes** (not from submission), so even a long transcription leaves a full hour to collect it.
* Reading a task does **not** consume or delete it — re-read the same `task_id` as often as you like until it expires.
* After expiry, the poll endpoint returns `404 task_not_found`.
## Webhooks instead of polling
Pass `webhook_url` on the submit request, or configure an account-level default endpoint in [Studio → API](https://vidnavigator.com/studio/api). The per-request value wins; pass `"webhook_url": ""` to opt a single job out of the default.
See the full contract — payload, signature verification, retries — in the [Webhooks guide](/webhooks).
Webhooks are best effort. Treat polling as the source of truth: if a callback never arrives, the result is still available on `check_status_url`.
## Billing
Billing is **identical** to the synchronous endpoint — the async mode costs nothing extra — and happens in the background worker.
* `include_usage` is **not** accepted on the submit endpoint (the `202` fires before any charge is made). Pass `include_usage=true` on the poll endpoint instead: it replays the final charges once `task_status=completed`.
* A failed job has all of its charges reverted.
* Polling is free.
See [Usage & Costs](/api-reference/usage-costs) for unit prices.
## Submit-time errors
These are returned by the `POST` itself; no task is created.
| Status | `error` | When |
| - | - | - |
| `400` | `missing_parameter`, `invalid_parameter`, `invalid_schema`, … | Invalid body. For `/extract/video/async` the schema is validated at submit time, so an invalid schema is rejected before any transcription is billed. |
| `401` | — | Invalid or deactivated API key. |
| `402` | `limit_exceeded` | Less than **60 seconds** of transcription credit left. This is a spam gate, not a quote: the real charge is computed when the job runs, and a job whose video exceeds your remaining balance still fails with the same credit error a synchronous call would get. |
| `403` | — | The API key lacks permission for this endpoint. |
| `429` | `too_many_active_jobs` | Too many jobs already running for this account. Wait for some to finish, then retry. |
## Migrating from synchronous calls
1. Switch the URL: `/transcribe` → `/transcribe/async`, `/extract/video` → `/extract/video/async`, `/tweet/statement` → `/tweet/statement/async`. Keep the same body.
2. Remove `include_usage` from the submit body and pass it as a query parameter on the poll endpoint if you need it.
3. Poll `check_status_url` (or receive the webhook) and read `data.result` — it has the same shape as the synchronous `data` block.
4. Map `data.error.error` to your existing error handling.
# Extracting Structured Data
Source: https://docs.vidnavigator.com/guides/data-extraction
Turn any online video or uploaded file into clean, validated JSON using a schema you define.
If transcripts are for humans, extraction is for software. The Extract API lets you define the exact fields you want, then returns structured JSON that matches your schema so you can send the result straight into a database, CRM, workflow, or AI pipeline.
## What This Guide Helps You Do
By the end of this guide, you will know how to:
* choose the right extraction endpoint
* design a schema the model can follow reliably
* use `what_to_extract` to improve focus
* understand auto-transcription, caching, and billing
* move from one test video to a repeatable production workflow
## Why This Is More Than Just an LLM Prompt
The real challenge is not only extracting structured data. It is getting the video content into a usable text layer in the first place.
With VidNavigator, the workflow is end to end:
* fetch the transcript when the platform already exposes one
* auto-transcribe supported non-YouTube videos with speech-to-text when no transcript exists
* run AI extraction on that text using your schema
* return validated JSON you can use immediately in software
That is the main difference. You are not expected to manually retrieve captions, run a separate speech-to-text step, clean the text, and then prompt a model yourself.
Compared with building that flow manually, VidNavigator gives you:
* transcript retrieval and speech-to-text built into the same extraction workflow
* one API call instead of stitching together multiple tools
* a fixed response shape defined by your schema
* validated output instead of free-form text
* prompt compilation that is cached for 2 hours for repeated schemas
This is what makes video data extraction practical at scale, especially for platforms where transcripts are inconsistent, missing, or not easily accessible.
## Choose the Right Endpoint
Use `/v1/extract/video` when you have a public video URL from YouTube, Instagram, TikTok, Facebook, X, Vimeo, Dailymotion, Loom, and similar platforms.
Use `/v1/extract/file` when the content is already in your VidNavigator library and you want to extract from a `file_id`.
`extract/video` can auto-transcribe non-YouTube videos when `transcribe=true` (the default). `extract/file` does not transcribe for you, so the file must already have a transcript.
**Long videos:** when speech-to-text is needed, the synchronous `/v1/extract/video` accepts media up to **10 minutes** and rejects longer media with `400 video_too_long`. Use [`/v1/extract/video/async`](/api-reference/endpoint/extract-video-async) — same body, no duration cap, result by polling or [webhook](/webhooks). See [Async Jobs](/guides/async-jobs).
## How Extraction Works
The Extract API runs a simple 2-step pipeline:
1. You send a schema and optional `what_to_extract` instruction.
2. VidNavigator compiles an optimized extraction plan and caches it for 2 hours.
3. The plan is applied to the transcript for the current video or file.
4. The response is validated so the output matches your schema.
That cache matters. The first call for a new schema usually has a small compilation overhead. Reusing the same schema on later calls is faster because the compiled prompt is reused automatically.
## Start with a Small Schema
The fastest way to succeed is to start with 2-4 fields, validate the output, then expand.
Here is a good starter schema for product review videos:
```json JSON theme={null}
{
"products_mentioned": {
"type": "Array",
"description": "Products mentioned in the video",
"items": {
"type": "String",
"description": "Product name"
}
},
"main_claim": {
"type": "String",
"description": "The single most important claim or conclusion made in the video"
},
"sentiment": {
"type": "Enum",
"description": "Overall sentiment toward the main product discussed",
"enum": ["positive", "neutral", "negative"]
}
}
```
```yaml YAML theme={null}
products_mentioned:
type: Array
description: Products mentioned in the video
items:
type: String
description: Product name
main_claim:
type: String
description: The single most important claim or conclusion made in the video
sentiment:
type: Enum
description: Overall sentiment toward the main product discussed
enum:
- positive
- neutral
- negative
```
## Quickstart
### Extract from an Online Video
Use this when you have a public video URL:
```python Python theme={null}
from vidnavigator import VidNavigatorClient
client = VidNavigatorClient() # reads VIDNAVIGATOR_API_KEY
resp = client.extract_video_data(
video_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ",
what_to_extract="Focus on product names, pricing, and the speaker's overall verdict",
schema={
"products_mentioned": {
"type": "Array",
"description": "Products mentioned in the video",
"items": {"type": "String", "description": "Product name"},
},
"pricing_signals": {
"type": "Array",
"description": "Any price points, pricing plans, or cost references",
"items": {"type": "String", "description": "Pricing detail"},
},
"verdict": {
"type": "String",
"description": "Final recommendation or overall verdict from the speaker",
},
},
)
print(resp.data) # a dict shaped like your schema
print(resp.video_info.title) # the video's metadata
```
```javascript JavaScript theme={null}
import { VidNavigatorClient } from 'vidnavigator';
const vn = new VidNavigatorClient({ apiKey: process.env.VIDNAVIGATOR_API_KEY });
const { data, video_info } = await vn.extractVideoData({
video_url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
what_to_extract: "Focus on product names, pricing, and the speaker's overall verdict",
schema: {
products_mentioned: {
type: 'Array',
description: 'Products mentioned in the video',
items: { type: 'String', description: 'Product name' },
},
pricing_signals: {
type: 'Array',
description: 'Any price points, pricing plans, or cost references',
items: { type: 'String', description: 'Pricing detail' },
},
verdict: {
type: 'String',
description: 'Final recommendation or overall verdict from the speaker',
},
},
});
console.log(data); // an object shaped like your schema
console.log(video_info?.title); // the video's metadata
```
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/extract/video" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"what_to_extract": "Focus on product names, pricing, and the speaker''s overall verdict",
"schema": {
"products_mentioned": {
"type": "Array",
"description": "Products mentioned in the video",
"items": { "type": "String", "description": "Product name" }
},
"pricing_signals": {
"type": "Array",
"description": "Any price points, pricing plans, or cost references",
"items": { "type": "String", "description": "Pricing detail" }
},
"verdict": {
"type": "String",
"description": "Final recommendation or overall verdict from the speaker"
}
}
}'
```
The 2.1.0 SDKs run video extraction through [`/extract/video/async`](/api-reference/endpoint/extract-video-async), so videos of any length work with the same call. To run many extractions in parallel, use `client.submit_extract_video_data(...)` (Python) or `vn.extractVideo.submit(...)` (JavaScript). The SDK result contains the extracted `data` and the video's metadata in `video_info` (`resp.video_info` in Python, `video_info` in JavaScript).
### Extract from an Uploaded File
Use this when the transcript already lives in your VidNavigator workspace:
```python Python theme={null}
resp = client.extract_file_data(
file_id="file_abc123",
what_to_extract="Extract action items and owners from this meeting recording",
schema={
"action_items": {
"type": "Array",
"description": "Tasks agreed on during the meeting",
"items": {"type": "String", "description": "Action item"},
},
"owners": {
"type": "Array",
"description": "People assigned to specific tasks",
"items": {"type": "String", "description": "Person name and task if available"},
},
"deadline_mentions": {
"type": "Array",
"description": "Any due dates or timing commitments mentioned",
"items": {"type": "String", "description": "Deadline detail"},
},
},
)
print(resp.data)
```
```javascript JavaScript theme={null}
const { data } = await vn.extractFileData({
file_id: 'file_abc123',
what_to_extract: 'Extract action items and owners from this meeting recording',
schema: {
action_items: {
type: 'Array',
description: 'Tasks agreed on during the meeting',
items: { type: 'String', description: 'Action item' },
},
owners: {
type: 'Array',
description: 'People assigned to specific tasks',
items: { type: 'String', description: 'Person name and task if available' },
},
deadline_mentions: {
type: 'Array',
description: 'Any due dates or timing commitments mentioned',
items: { type: 'String', description: 'Deadline detail' },
},
},
});
console.log(data);
```
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/extract/file" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"file_id": "file_abc123",
"what_to_extract": "Extract action items and owners from this meeting recording",
"schema": {
"action_items": {
"type": "Array",
"description": "Tasks agreed on during the meeting",
"items": { "type": "String", "description": "Action item" }
},
"owners": {
"type": "Array",
"description": "People assigned to specific tasks",
"items": { "type": "String", "description": "Person name and task if available" }
},
"deadline_mentions": {
"type": "Array",
"description": "Any due dates or timing commitments mentioned",
"items": { "type": "String", "description": "Deadline detail" }
}
}
}'
```
## When to Use `what_to_extract`
Your schema defines the output shape. `what_to_extract` tells the model where to focus.
Use it when:
* the transcript covers multiple topics and you care about one of them
* you want the model to prioritize a specific lens such as pricing, claims, objections, or action items
* the field descriptions are correct but still too broad
Good examples:
* `Focus on feature claims, competitor mentions, and pricing strategy.`
* `Extract only factual claims and cited statistics. Ignore jokes and off-topic banter.`
* `Prioritize buyer intent, objections, and next steps from the sales call.`
## Schema Rules
Your schema must follow these limits:
* maximum `10` root fields
* maximum `3` nesting levels
* maximum `10` subfields per `Object`
* supported field types: `String`, `Number`, `Boolean`, `Integer`, `Array`, `Object`, `Enum`
* every field must include both `type` and `description`
Write descriptions like instructions, not labels. `Primary pricing strategy discussed, in one sentence` is much better than `pricing`.
## Best Practices That Improve Results
### 1. Be specific in field descriptions
Weak:
```json theme={null}
{
"topic": {
"type": "String",
"description": "Topic"
}
}
```
Better:
```json theme={null}
{
"primary_topic": {
"type": "String",
"description": "Primary topic discussed in the video, in 5 to 10 words"
}
}
```
### 2. Use enums when the answer should come from a fixed list
If a field is really a classification, use `Enum` instead of `String`. That makes downstream automation much easier.
### 3. Start simple, then expand
Do not begin with a 10-field nested schema unless you already know it works. Start with the 2-3 fields that matter most, test them on a few videos, then add more.
### 4. Write descriptions in the output language you want
The response is returned in the same language as your schema descriptions. If you write field descriptions in French, the output will also come back in French.
## Ready-to-Use Ideas
Common extraction patterns include:
* lead generation: companies, buyer intent, pain points, next steps
* market research: competitor mentions, feature claims, pricing strategy, objections
* creator analysis: hooks, quotes, sponsored mentions, content format
* RAG ingestion: dense summary, entities, claims, topics, language code
* fact-checking: claims, statistics, cited sources, controversy markers
## Online Videos vs. Uploaded Files
### `POST /extract/video`
Choose this endpoint when:
* you already have a public video URL
* you want VidNavigator to fetch the transcript for you
* you want automatic speech-to-text fallback for supported non-YouTube videos
Important behavior:
* `transcribe=true` by default
* automatic transcription applies to non-YouTube platforms only
* YouTube relies on platform captions and cannot use speech-to-text fallback through this endpoint
* when speech-to-text runs, media longer than 10 minutes must go through `POST /extract/video/async`
### `POST /extract/file`
Choose this endpoint when:
* the media is already uploaded to your VidNavigator account
* you want to extract from private or internal content
* you already completed transcription earlier in your workflow
Typical file workflow:
1. Upload the file.
2. Generate a transcript if needed.
3. Call `/v1/extract/file` with the resulting `file_id`.
## Billing and Performance
Each extraction consumes `analysis_request` units. One unit covers up to **15,000 total tokens**; longer transcripts scale with:
`ceil(total_tokens / 15000)`
A few practical notes:
* the first call for a new schema includes prompt compilation overhead
* repeated calls with the same schema reuse the cached plan for 2 hours
* `extract/video` may also trigger `transcription_hour` billing when auto-transcription is used
* failed requests are reverted and are not billed
## Troubleshooting
### `transcript_not_available`
This usually means the source does not have a usable transcript and auto-transcription is either disabled or unavailable.
* For non-YouTube URLs, keep `transcribe=true` on `/extract/video`.
* For uploaded files, transcribe the file before calling `/extract/file`.
### `video_too_long`
The video needs speech-to-text and is longer than 10 minutes. Send the same request to [`/v1/extract/video/async`](/api-reference/endpoint/extract-video-async) and collect the result by polling or webhook.
### `invalid_schema`
This usually means a field is missing `type` or `description`, nesting is too deep, or the schema exceeds the limits above.
### Results are too broad
Tighten the schema descriptions and add a more targeted `what_to_extract` instruction.
### First request is slower than later requests
That is expected when a schema is new. The compiled extraction plan is cached for 2 hours, so repeated schemas are faster.
## Next Steps
* [Extract from an online video](/api-reference/endpoint/extract-video)
* [Extract from an uploaded file](/api-reference/endpoint/extract-file)
* [Upload a file first](/api-reference/endpoint/upload-file)
* Schema files, token usage and batch extraction with the [Python](/sdks/python#structured-extraction) and [JavaScript](/sdks/javascript#structured-extraction) SDKs
# Getting Transcripts
Source: https://docs.vidnavigator.com/guides/getting-transcripts
Learn how to retrieve transcripts from online videos using the VidNavigator API and SDKs.
## Overview
VidNavigator is the ultimate video to text solution for extracting subtitles and captions from online videos, giving you all the essential information at your fingertips! Need full transcripts, timestamps, or video metadata? We've got it all. The JSON output is ready for instant integration into AI-powered applications.
## Transcript Endpoint
VidNavigator provides a **single transcript endpoint** for every supported platform, including YouTube. The [`/transcript`](/api-reference/endpoint/transcript) endpoint auto-detects the platform from the URL and routes internally.
This replaces the old, separate `/youtube/transcript` endpoint — YouTube URLs now go through the same `/transcript` endpoint as everything else. For **Instagram**, use [`/transcribe`](/api-reference/endpoint/transcribe) (speech-to-text) instead.
## Billing
Billing depends on the URL:
* **YouTube** transcript retrieval (or `metadata_only: true`) consumes `residential_request` usage — the fetch routes through the residential proxy.
* **Non-YouTube** transcript retrieval consumes `standard_request` usage (some platforms internally require residential and are billed accordingly).
Pass `include_usage=true` to receive a `usage` block disclosing exactly what was charged.
## Prerequisites
* A valid VidNavigator API key.
* The [Python](/sdks/python) or [JavaScript](/sdks/javascript) SDK, version 2.1.0:
```bash Python theme={null}
pip install vidnavigator==2.1.0
```
```bash JavaScript theme={null}
npm install vidnavigator@2.1.0
```
## Retrieving a Transcript
Use `get_transcript` (Python) or `getTranscript` (JavaScript) for **any** platform: YouTube, TikTok, X/Twitter, Facebook, Vimeo, and more.
### Retrieving a YouTube Transcript
```python Python theme={null}
from vidnavigator import VidNavigatorClient, VidNavigatorError
client = VidNavigatorClient() # reads VIDNAVIGATOR_API_KEY
try:
result = client.get_transcript(
video_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
)
print("Video Title:", result.data.video_info.title)
print("Available Languages:", result.data.video_info.available_languages)
# Each item in the transcript is a segment with text, start, and end times
for segment in result.data.transcript:
print(f"[{segment.start:.2f}s - {segment.end:.2f}s] {segment.text}")
except VidNavigatorError as e:
print(f"An error occurred: {e}")
```
```javascript JavaScript theme={null}
import { VidNavigatorClient, VidNavigatorError } from 'vidnavigator';
const vn = new VidNavigatorClient({
apiKey: process.env.VIDNAVIGATOR_API_KEY,
});
try {
const { video_info, transcript } = await vn.getTranscript({
video_url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
});
console.log('Video Title:', video_info.title);
console.log('Available Languages:', video_info.available_languages);
// Each item in the transcript is an object with text, start, and end properties
for (const segment of transcript) {
console.log(`[${segment.start.toFixed(2)}s - ${segment.end.toFixed(2)}s] ${segment.text}`);
}
} catch (error) {
if (error instanceof VidNavigatorError) {
console.error(`An error occurred: ${error.message}`);
} else {
throw error;
}
}
```
Pass `transcript_text=True` (Python) or `transcript_text: true` (JavaScript) to get the whole transcript as a single string instead of timed segments, and `language` to request a specific caption language (ISO 639-1, e.g. `"fr"`).
### Retrieving Transcripts from Other Platforms
The exact same method works for non-YouTube platforms (TikTok, X/Twitter, Facebook, Vimeo, etc.):
```python Python theme={null}
result = client.get_transcript(
video_url="https://twitter.com/user/status/123456789"
)
print("Video Title:", result.data.video_info.title)
for segment in result.data.transcript:
print(f"[{segment.start:.2f}s - {segment.end:.2f}s] {segment.text}")
```
```javascript JavaScript theme={null}
const { video_info, transcript } = await vn.getTranscript({
video_url: 'https://twitter.com/user/status/123456789',
});
console.log('Video Title:', video_info.title);
for (const segment of transcript) {
console.log(`[${segment.start.toFixed(2)}s - ${segment.end.toFixed(2)}s] ${segment.text}`);
}
```
## Getting Metadata Only
If you only need video information without the transcript, use the `metadata_only` parameter. This is faster and still counts as usage:
```python Python theme={null}
# YouTube (metadata_only still routes through the residential proxy → residential_request)
result = client.get_transcript(
video_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ",
metadata_only=True,
)
print(result.data.video_info.title, result.data.video_info.duration)
# Other platforms → standard_request
result = client.get_transcript(
video_url="https://twitter.com/user/status/123456789",
metadata_only=True,
)
```
```javascript JavaScript theme={null}
// YouTube (metadata_only still routes through the residential proxy → residential_request)
const { video_info } = await vn.getTranscript({
video_url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
metadata_only: true,
});
console.log(video_info.title, video_info.duration);
// Other platforms → standard_request
await vn.getTranscript({
video_url: 'https://twitter.com/user/status/123456789',
metadata_only: true,
});
```
## Graceful Fallback to Metadata
If you want to get metadata even when a transcript isn't available (instead of receiving a 404 error), use the `fallback_to_metadata` parameter:
```python Python theme={null}
result = client.get_transcript(
video_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ",
fallback_to_metadata=True,
)
# If the transcript is unavailable, you still get video_info,
# but the transcript is empty
```
```javascript JavaScript theme={null}
const { video_info, transcript } = await vn.getTranscript({
video_url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
fallback_to_metadata: true,
});
// If the transcript is unavailable, you still get video_info,
// but the transcript is empty
```
The `metadata_only` parameter takes precedence over `fallback_to_metadata`. If both are set, only metadata will be returned.
## When a Transcript Isn't Available
Sometimes a platform doesn't expose captions we can fetch directly (e.g., many Instagram Reels). In that case, the transcript endpoint will return an error such as `404 transcript_not_available`.
When this happens you can **fall back to speech-to-text**, which generates a fresh transcript from the audio.
Speech-to-text supports **Instagram, X (Twitter), Facebook, TikTok, Dailymotion, Loom, and Vimeo** URLs. YouTube videos should use the transcript endpoint instead.
```python Python theme={null}
result = client.transcribe_video(
video_url="https://www.instagram.com/reel/C86ZvEaqRmo/"
)
print("Transcript length:", len(result.data.transcript))
print("First line:", result.data.transcript[0].text)
```
```javascript JavaScript theme={null}
const { transcript } = await vn.transcribe('https://www.instagram.com/reel/C86ZvEaqRmo/');
console.log(`Transcript has ${transcript.length} segments`);
console.log('First line:', transcript[0].text);
```
A common pattern is to try captions first, since they are faster and cheaper, and fall back to speech-to-text:
```python Python theme={null}
from vidnavigator import BadRequestError, NotFoundError
def best_transcript(url):
try:
return client.get_transcript(video_url=url, transcript_text=True).data.transcript
except (NotFoundError, BadRequestError): # no captions, or a platform without captions
return client.transcribe_video(video_url=url, transcript_text=True).data.transcript
```
```javascript JavaScript theme={null}
import { BadRequestError, NotFoundError } from 'vidnavigator';
async function bestTranscript(url) {
try {
return (await vn.getTranscript({ video_url: url, transcript_text: true })).transcript;
} catch (error) {
if (error instanceof NotFoundError || error instanceof BadRequestError) {
return (await vn.transcribe({ video_url: url, transcript_text: true })).transcript;
}
throw error;
}
}
```
## Long Videos (over 10 minutes)
The synchronous [`/transcribe`](/api-reference/endpoint/transcribe) endpoint accepts media up to **10 minutes**; longer media is rejected with `400 video_too_long`. The async endpoint [`POST /transcribe/async`](/api-reference/endpoint/transcribe-async) has no duration cap.
**The 2.1.0 SDKs always use the async endpoint**, so `transcribe_video` / `transcribe` work on videos of any length with no extra code: they submit the job, poll until it finishes, and return the result. To run many transcriptions in parallel, submit them all first and collect the results afterwards:
```python Python theme={null}
urls = [
"https://www.facebook.com/watch/?v=1234567890",
"https://vimeo.com/123456789",
]
jobs = [client.submit_transcribe_video(video_url=url) for url in urls]
print([job.task_id for job in jobs]) # save these to resume later with client.resume_job("transcribe", task_id)
for url, job in zip(urls, jobs):
result = job.result() # waits until this job is done
print(url, result.data.transcript[0].text)
```
```python Python (asyncio) theme={null}
import asyncio
from vidnavigator import AsyncVidNavigatorClient # pip install "vidnavigator[async]"
urls = [
"https://www.facebook.com/watch/?v=1234567890",
"https://vimeo.com/123456789",
]
async def main():
async with AsyncVidNavigatorClient() as client:
results = await asyncio.gather(
*(client.transcribe_video(video_url=url) for url in urls),
return_exceptions=True, # one failed job doesn't cancel the others
)
for url, result in zip(urls, results):
print(url, result if isinstance(result, Exception) else result.data.transcript[0].text)
asyncio.run(main())
```
```javascript JavaScript theme={null}
const urls = [
'https://www.facebook.com/watch/?v=1234567890',
'https://vimeo.com/123456789',
];
const jobs = await Promise.all(urls.map((url) => vn.transcribe.submit(url)));
console.log(jobs.map((job) => job.task_id)); // save these to resume later with vn.transcribe.resume(taskId)
const results = await Promise.all(jobs.map((job) => job.result()));
results.forEach((r, i) => console.log(urls[i], r.transcript[0].text));
```
See [Async Jobs](/guides/async-jobs) for the raw HTTP workflow, timeouts and webhooks.
## Next Steps
Now that you have the transcript, you can:
* [Analyze the video](/guides/analyzing-videos) for deeper insights.
* [Search the video's content](/guides/searching-videos) with natural language queries.
* Browse every SDK method in the [Python](/sdks/python) and [JavaScript](/sdks/javascript) references.
# Searching Through Videos
Source: https://docs.vidnavigator.com/guides/searching-videos
Learn how to use natural language to search for specific moments within a single video or across a library of videos.
## Overview
VidNavigator supports two distinct search workflows: global platform search discovers videos on YouTube or TikTok, while semantic (vector) search finds relevant information in already indexed YouTube channel content and uploaded files.
## Prerequisites
* A valid VidNavigator API key.
* The [Python](/sdks/python) or [JavaScript](/sdks/javascript) SDK, version 2.1.0 (`pip install vidnavigator==2.1.0` or `npm install vidnavigator@2.1.0`).
## Billing
Billing depends on which search you run:
* **Semantic search over indexed content** consumes one `search_request` per call. This includes indexed YouTube channel search and file search (`/search/file`); file search can also bill `analysis_request` if the AI analysis pass runs. 1 credit covers 1,000 semantic searches.
* **Global YouTube search** (`/youtube/search`) does **not** bill `search_request`. It bills one `residential_request` per candidate video analyzed, plus a single consolidated `analysis_request` (`ceil(total_tokens / 15000)`). See [Search YouTube](/api-reference/endpoint/youtube-search).
* **TikTok keyword search** (`/tiktok/search`) does **not** bill `search_request`; it bills per residential page fetched. See [TikTok Search](/api-reference/endpoint/tiktok-search).
## Searching Videos
### Searching Across YouTube
This example searches YouTube for videos that match a query, via the `/youtube/search` endpoint. The results are ranked with an AI analysis of each video's transcript.
```python Python theme={null}
from vidnavigator import VidNavigatorClient, VidNavigatorError
client = VidNavigatorClient() # reads VIDNAVIGATOR_API_KEY
try:
results = client.search_youtube(
query="What are the best practices for React development?",
focus="relevance",
start_year=2022,
max_results=5, # caps how many candidates are analyzed, and the cost
)
print(f"Found {results.data.total_found} relevant videos:")
for video in results.data.results:
print(f"- {video.title} ({video.url}), score: {video.relevance_score}")
print(f" {video.transcript_summary}")
except VidNavigatorError as e:
print(f"An error occurred: {e}")
```
```javascript JavaScript theme={null}
import { VidNavigatorClient, VidNavigatorError } from 'vidnavigator';
const vn = new VidNavigatorClient({
apiKey: process.env.VIDNAVIGATOR_API_KEY,
});
try {
const results = await vn.searchYouTube({
query: 'What are the best practices for React development?',
focus: 'relevance',
start_year: 2022,
max_results: 5, // caps how many candidates are analyzed, and the cost
});
console.log(`Found ${results.total_found} relevant videos:`);
for (const video of results.results) {
console.log(`- ${video.title} (${video.url}), score: ${video.relevance_score}`);
console.log(` ${video.transcript_summary}`);
}
// If credits run out mid-search, the SDK returns the videos analyzed so far
if (results.status === 'partial') {
console.log('Partial results:', results.error_code);
}
} catch (error) {
if (error instanceof VidNavigatorError) {
console.error(`An error occurred: ${error.message}`);
} else {
throw error;
}
}
```
`focus` accepts `relevance` (default), `popularity` or `brevity`. You can also filter with `end_year` and `duration` (in seconds).
`search_videos` / `searchVideos` still work in the 2.1.0 SDKs as deprecated aliases. Use `search_youtube` / `searchYouTube` in new code.
### Searching Your Uploaded Files
This example searches across the files you have uploaded to VidNavigator. Each result includes `namespace_ids` and `namespaces` showing which namespaces the file belongs to, plus the timestamps where the matching content appears.
```python Python theme={null}
try:
results = client.search_files(query="Customer feedback on pricing")
print(f"Found {results.data.total_found} relevant files:")
for file_result in results.data.results:
ns_names = [ns.name for ns in file_result.namespaces or []]
print(f"- {file_result.name} (score: {file_result.relevance_score}, namespaces: {ns_names})")
print(f" Timestamps: {file_result.timestamps}")
except VidNavigatorError as e:
print(f"An error occurred: {e}")
```
```javascript JavaScript theme={null}
try {
const results = await vn.searchFiles({ query: 'Customer feedback on pricing' });
console.log(`Found ${results.total_found} relevant files:`);
for (const fileResult of results.results) {
const nsNames = (fileResult.namespaces ?? []).map((ns) => ns.name);
console.log(`- ${fileResult.name} (score: ${fileResult.relevance_score}, namespaces: ${nsNames.join(', ')})`);
console.log(` Answer: ${fileResult.query_answer}`);
}
} catch (error) {
if (error instanceof VidNavigatorError) {
console.error(`An error occurred: ${error.message}`);
} else {
throw error;
}
}
```
### Filtering File Search by Namespace
You can restrict search to specific namespaces by passing `namespace_ids`. If omitted, all namespaces are searched.
```python Python theme={null}
results = client.search_files(
query="Customer feedback on pricing",
namespace_ids=["64a1b2c3d4e5f6789abc0002"],
)
```
```javascript JavaScript theme={null}
const results = await vn.searchFiles({
query: 'Customer feedback on pricing',
namespace_ids: ['64a1b2c3d4e5f6789abc0002'],
});
```
## Next Steps
* Search TikTok by keyword or collect a creator's videos: [Searching TikTok](/guides/tiktok-search).
* Upload your own recordings to make them searchable: [Uploading and Processing Files](/guides/uploading-videos).
# Searching TikTok
Source: https://docs.vidnavigator.com/guides/tiktok-search
Collect TikTok videos from a profile or by keyword using the asynchronous TikTok endpoints, then poll for paginated results.
## Overview
VidNavigator offers two ways to collect TikTok videos:
Walk a single creator's account and return their videos, filtered by date and likes.
Search across TikTok by keyword and return matching videos from many creators.
Both are **asynchronous**: you submit a task, get a `task_id` back immediately, then poll a `GET` endpoint until the task is `completed`. Results are retained for about 1 hour, and large result sets can be downloaded as a single JSON file via a temporary `download_url`.
The [Python](/sdks/python#tiktok) and [JavaScript](/sdks/javascript#tiktok) SDKs (v2.1.0) wrap the submit-and-poll flow in a single call. Each section below starts with the SDK version, followed by the raw HTTP workflow (cURL, Python `requests`, and `fetch`) if you'd rather call the API directly.
## Prerequisites
* A valid VidNavigator API key.
* Optional: the SDK, version 2.1.0 (`pip install vidnavigator==2.1.0` or `npm install vidnavigator@2.1.0`).
## Billing
Both endpoints bill in the background worker, so **usage is disclosed on the polling `GET`** (pass `include_usage=true` there), not at submit time.
| Endpoint | What you're billed |
| - | - |
| Profile scrape (`/tiktok/profile`) | `standard_request` per TikTok page fetched (`residential_request` when a residential proxy is used). |
| Keyword search (`/tiktok/search`) | `residential_request` per page fetched (`pages_fetched × residential_request`). An N-slice search bills up to **N×** the pages. **No `search_request`.** |
See [Usage & Costs](/api-reference/usage-costs) for credit conversions.
## Searching a TikTok Profile
Send a public `profile_url` and optional filters. `max_posts` and `after_datetime` let the scraper stop paginating early.
### With the SDK
One call submits the scrape, waits for it to finish, and returns the videos. In JavaScript you get **every** matching video (the SDK walks all pages); in Python you get the first page of `limit` videos plus the pagination cursor.
```python Python theme={null}
from vidnavigator import VidNavigatorClient
client = VidNavigatorClient() # reads VIDNAVIGATOR_API_KEY
result = client.scrape_tiktok_profile(
profile_url="https://www.tiktok.com/@tiktok",
max_posts=50,
after_datetime="2024-01-01",
min_likes=10000,
limit=50, # size of the returned page
)
for video in result.data.videos or []:
print(video.published_at, video.likes, video.url) # published_at is a datetime
# Read the remaining pages, if any
task_id, pagination = result.data.task_id, result.data.pagination
while pagination and pagination.has_next:
page = client.get_tiktok_profile_scrape(task_id, cursor=pagination.next_cursor, limit=50)
for video in page.data.videos or []:
print(video.published_at, video.likes, video.url)
pagination = page.data.pagination
```
```javascript JavaScript theme={null}
import { VidNavigatorClient } from 'vidnavigator';
const vn = new VidNavigatorClient({ apiKey: process.env.VIDNAVIGATOR_API_KEY });
const task = await vn.tiktokProfile({
profile_url: 'https://www.tiktok.com/@tiktok',
max_posts: 50,
after_datetime: '2024-01-01',
min_likes: 10000,
});
console.log(`Collected ${task.videos.length} videos`, task.stats);
for (const video of task.videos.slice(0, 5)) {
console.log(video.published_at?.toISOString(), video.likes, video.url); // published_at is a Date
}
```
To start the scrape without waiting, use `client.submit_tiktok_profile_scrape(...)` (Python) or `vn.tiktokProfile.submit(...)` (JavaScript) and collect it later with `job.result()`. Both accept a `webhook_url` to be [notified](/webhooks) when the scrape finishes.
### With raw HTTP
#### 1. Submit the scrape
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/tiktok/profile" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"profile_url": "https://www.tiktok.com/@tiktok",
"max_posts": 50,
"after_datetime": "2024-01-01T00:00:00Z",
"min_likes": 10000
}'
```
```python Python theme={null}
import requests
BASE = "https://api.vidnavigator.com/v1"
HEADERS = {"X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json"}
resp = requests.post(
f"{BASE}/tiktok/profile",
headers=HEADERS,
json={
"profile_url": "https://www.tiktok.com/@tiktok",
"max_posts": 50,
"after_datetime": "2024-01-01T00:00:00Z",
"min_likes": 10000,
},
)
task_id = resp.json()["data"]["task_id"]
print("Task ID:", task_id)
```
```javascript JavaScript theme={null}
const BASE = "https://api.vidnavigator.com/v1";
const HEADERS = {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
};
const submit = await fetch(`${BASE}/tiktok/profile`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
profile_url: "https://www.tiktok.com/@tiktok",
max_posts: 50,
after_datetime: "2024-01-01T00:00:00Z",
min_likes: 10000,
}),
});
const { data } = await submit.json();
const taskId = data.task_id;
console.log("Task ID:", taskId);
```
#### 2. Poll until completed and page through results
Poll `GET /tiktok/profile/{task_id}` while `task_status` is `processing`. Once `completed`, walk the pages using the `next_cursor` from `data.pagination`.
```python Python theme={null}
import time
def get_profile_page(task_id, cursor=None, limit=50):
params = {"limit": limit}
if cursor:
params["cursor"] = cursor
r = requests.get(f"{BASE}/tiktok/profile/{task_id}", headers=HEADERS, params=params)
return r.json()["data"]
# Wait for completion
while True:
data = get_profile_page(task_id, limit=1)
if data["task_status"] != "processing":
break
time.sleep(3)
if data["task_status"] == "failed":
raise RuntimeError((data.get("error") or {}).get("message", "scrape failed"))
# Page through all videos
videos, cursor = [], None
while True:
page = get_profile_page(task_id, cursor=cursor, limit=100)
videos.extend(page["videos"])
pagination = page["pagination"]
if not pagination["has_next"]:
break
cursor = pagination["next_cursor"]
print(f"Collected {len(videos)} videos")
for v in videos[:5]:
print(v["published_at"], v["likes"], v["url"])
```
```javascript JavaScript theme={null}
async function getProfilePage(taskId, { cursor, limit = 50, includeUsage = false } = {}) {
const params = new URLSearchParams({ limit: String(limit) });
if (cursor) params.set("cursor", cursor);
if (includeUsage) params.set("include_usage", "true");
const r = await fetch(`${BASE}/tiktok/profile/${taskId}?${params}`, { headers: HEADERS });
return (await r.json()).data;
}
// Wait for completion
let status;
do {
status = await getProfilePage(taskId, { limit: 1 });
if (status.task_status === "processing") await new Promise((res) => setTimeout(res, 3000));
} while (status.task_status === "processing");
if (status.task_status === "failed") throw new Error(status.error?.message || "scrape failed");
// Page through all videos
const videos = [];
let cursor = null;
let hasNext = true;
while (hasNext) {
const page = await getProfilePage(taskId, { cursor, limit: 100 });
videos.push(...page.videos);
hasNext = page.pagination.has_next;
cursor = page.pagination.next_cursor;
}
console.log(`Collected ${videos.length} videos`);
```
For very large profiles, skip pagination and download the whole result at once: once the task is `completed`, `data.download_url` holds a short-lived signed URL to the full JSON. Call the `GET` endpoint again to mint a fresh URL if it expires.
## Searching TikTok by Keyword
Keyword search returns videos across many creators, sorted by `published_at` (newest first) unless you set `sort_by` (`relevance`, `most_liked`, `newest`). Use `published_within` (`past_24_hours`, `this_week`, `this_month`, `last_3_months`, `last_6_months`) or `after_datetime` / `before_datetime` for time windows, and `parallel_search_slices` (1–4) to lift the result ceiling (\~115–140 items per slice) — most useful on searches without a date window.
### With the SDK
```python Python theme={null}
result = client.search_tiktok(
query="ai tools",
sort_by="most_liked",
published_within="this_month",
min_likes=1000,
limit=50,
include_usage=True,
)
stats = result.data.stats
print(f"{stats.results_count} results, sorted by {stats.sort_by} within {stats.published_within}")
for item in result.data.results or []:
print(item.published_at, item.stats.likes if item.stats else None, item.url)
if result.usage:
print("Credits charged:", result.usage.total_credits)
```
```javascript JavaScript theme={null}
const task = await vn.tiktokSearch(
{
query: 'ai tools',
sort_by: 'most_liked',
published_within: 'this_month',
min_likes: 1000,
},
{ include_usage: true }
);
console.log(`${task.results.length} results, sorted by ${task.stats?.sort_by} within ${task.stats?.published_within}`);
for (const item of task.results.slice(0, 5)) {
console.log(item.published_at?.toISOString(), item.stats?.likes, item.url);
}
if (task.usage) console.log('Credits charged:', task.usage.total_credits);
```
Python pages through further results with `client.get_tiktok_search(task_id, cursor=...)`, exactly like the profile example. The JavaScript call already returns every result.
### With raw HTTP
#### 1. Submit the search
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/tiktok/search" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "ai tools",
"parallel_search_slices": 2,
"after_datetime": "2024-01-01T00:00:00Z",
"min_likes": 1000
}'
```
```python Python theme={null}
resp = requests.post(
f"{BASE}/tiktok/search",
headers=HEADERS,
json={
"query": "ai tools",
"parallel_search_slices": 2,
"after_datetime": "2024-01-01T00:00:00Z",
"min_likes": 1000,
},
)
task_id = resp.json()["data"]["task_id"]
print("Task ID:", task_id)
```
```javascript JavaScript theme={null}
const submit = await fetch(`${BASE}/tiktok/search`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
query: "ai tools",
parallel_search_slices: 2,
after_datetime: "2024-01-01T00:00:00Z",
min_likes: 1000,
}),
});
const taskId = (await submit.json()).data.task_id;
console.log("Task ID:", taskId);
```
#### 2. Poll, paginate, and read usage
The polling flow is identical to the profile scrape — just swap the path to `/tiktok/search/{task_id}` and read `data.results` instead of `data.videos`. Pass `include_usage=true` once the task is `completed` to see what was billed.
```python Python theme={null}
import time
def get_search_page(task_id, cursor=None, limit=50, include_usage=False):
params = {"limit": limit}
if cursor:
params["cursor"] = cursor
if include_usage:
params["include_usage"] = "true"
r = requests.get(f"{BASE}/tiktok/search/{task_id}", headers=HEADERS, params=params)
return r.json()
# Wait for completion
while True:
body = get_search_page(task_id, limit=1)
if body["data"]["task_status"] != "processing":
break
time.sleep(3)
# First completed page, with usage disclosure
body = get_search_page(task_id, limit=50, include_usage=True)
data = body["data"]
print(f"{data['stats']['results_count']} results across {data['stats']['pages_fetched']} pages")
for item in data["results"][:5]:
print(item["published_at"], item["stats"]["likes"], item["url"])
# Usage is at the top level of the response
if "usage" in body:
print("Credits charged:", body["usage"]["total_credits"])
```
```javascript JavaScript theme={null}
async function getSearchPage(taskId, { cursor, limit = 50, includeUsage = false } = {}) {
const params = new URLSearchParams({ limit: String(limit) });
if (cursor) params.set("cursor", cursor);
if (includeUsage) params.set("include_usage", "true");
const r = await fetch(`${BASE}/tiktok/search/${taskId}?${params}`, { headers: HEADERS });
return await r.json();
}
// Wait for completion
let body;
do {
body = await getSearchPage(taskId, { limit: 1 });
if (body.data.task_status === "processing") await new Promise((res) => setTimeout(res, 3000));
} while (body.data.task_status === "processing");
// First completed page with usage disclosure
body = await getSearchPage(taskId, { limit: 50, includeUsage: true });
const { results, stats } = body.data;
console.log(`${stats.results_count} results across ${stats.pages_fetched} pages`);
results.slice(0, 5).forEach((item) =>
console.log(item.published_at, item.stats.likes, item.url)
);
if (body.usage) console.log("Credits charged:", body.usage.total_credits);
```
Don't pass `include_usage` on the submit (`POST`) call — it's ignored because billing happens after the `202` response. Pass it on the polling `GET`, and only expect a `usage` block once `task_status=completed` (failed tasks have all charges refunded).
## Choosing Between Profile and Keyword Search
| Use case | Endpoint |
| - | - |
| Collect one creator's catalog | Profile scrape |
| Track a hashtag, topic, or trend across creators | Keyword search |
| Filter by date range and likes | Both (add `after_datetime` / `before_datetime`, `min_likes` / `max_likes`) |
| Filter by views | Keyword search (`min_views` / `max_views`) |
| Maximize coverage of a keyword | Keyword search with `parallel_search_slices` up to `4` (no date window) |
| Most popular recent videos for a keyword | Keyword search with `sort_by: "most_liked"` and `published_within` |
## Next Steps
* [TikTok Profile Scrape reference](/api-reference/endpoint/tiktok-profile)
* [TikTok Keyword Search reference](/api-reference/endpoint/tiktok-search)
* [Usage & Costs](/api-reference/usage-costs)
# Tweet Claim Analysis
Source: https://docs.vidnavigator.com/guides/tweet-claim-analysis
Understand what an X post actually means by combining the original tweet, quoted tweet, and attached media.
Most X APIs give you tweet text and metadata. That is useful for retrieval, but it often misses the actual message. On X, meaning is frequently split across the main tweet, the quoted tweet, and the media attached to one or both posts.
VidNavigator's Tweet Claim Analysis endpoint is built for that exact problem. It reconstructs intent across all of those layers and returns one structured intelligence object you can use in verification, monitoring, analytics, or AI workflows.
## What This Guide Helps You Do
By the end of this guide, you will know how to:
* decide when tweet claim analysis is the right tool
* send a valid `tweet_id`
* understand what the API reads before it writes a result
* use `final_statement`, `detailed_analysis`, and the classification axes correctly
* plug the result into fact-checking, social monitoring, or clustering workflows
## When to Use This Endpoint
Use tweet claim analysis when you care about what the author meant, not just what they typed.
Good fits:
* fact-checking and verification
* narrative tracking across many posts
* reputation and risk monitoring
* clustering similar claims into dashboards
* turning noisy social posts into structured AI-ready data
If you only need raw tweet text, engagement metrics, or basic metadata, this endpoint is probably more powerful than you need.
## Why This Is Different from Basic Tweet Retrieval
A normal tweet API can tell you:
* the tweet text
* the author
* the timestamp
* public metrics
This endpoint goes further. It:
* reads the original tweet
* checks whether a quoted tweet changes or completes the meaning
* analyzes media attached to the original tweet (videos are transcribed; images are described by a vision model)
* analyzes media attached to the quoted tweet
* returns one structured interpretation of the overall claim
That is the difference between collecting posts and extracting usable intelligence.
## What the API Actually Returns
`final_statement` is the normalized claim the author appears to be making after the full context is analyzed.
`detailed_analysis` explains the claim, context, evidence, and stance in plain language.
`topics` and `entities` surface the key subjects and named people/organizations/places for tagging and clustering.
`claim_type`, `intent`, `tone`, `emotion`, and `authority` classify the original tweet text on fixed taxonomies.
The classification axes are based on the original tweet text only. The broader fields such as `final_statement` and media summaries can reflect quoted content and attached media.
## How It Works
1. You send a numeric `tweet_id`.
2. VidNavigator fetches the original tweet.
3. If the tweet quotes another post, VidNavigator fetches that too.
4. If either layer includes video or audio, VidNavigator retrieves transcripts or falls back to speech-to-text when needed. Attached images are described by a vision model.
5. The full context is analyzed and returned as a structured claim object.
This is especially useful when the visible text is vague, sarcastic, reactive, or incomplete without the quoted post or media evidence.
## Quickstart
The endpoint accepts a tweet ID, not a full URL.
For example, in:
`https://x.com/user/status/1912345678901234567`
the `tweet_id` is:
`1912345678901234567`
### Basic Request
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/tweet/statement" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tweet_id": "1912345678901234567"
}'
```
```python Python theme={null}
from vidnavigator import VidNavigatorClient
client = VidNavigatorClient() # reads VIDNAVIGATOR_API_KEY
resp = client.get_tweet_statement(tweet_id="1912345678901234567")
print(resp.data.final_statement)
print(resp.data.detailed_analysis)
print(resp.data.claim_type, resp.data.intent, resp.data.tone)
print(resp.data.topics, resp.data.entities)
```
```javascript JavaScript theme={null}
import { VidNavigatorClient } from 'vidnavigator';
const vn = new VidNavigatorClient({ apiKey: process.env.VIDNAVIGATOR_API_KEY });
const statement = await vn.tweetStatement('1912345678901234567');
console.log(statement.final_statement);
console.log(statement.detailed_analysis);
console.log(statement.claim_type, statement.intent, statement.tone);
console.log(statement.topics, statement.entities);
```
The SDKs (v2.1.0) call the async variant `POST /v1/tweet/statement/async` for you, so tweets with long videos work with the same call. With raw HTTP, use the async endpoint if the tweet may carry a video longer than 10 minutes: same body, result by polling or webhook. See [Async Jobs](/guides/async-jobs).
To analyze many tweets in parallel, submit them all first and collect the results afterwards:
```python Python theme={null}
tweet_ids = ["1912345678901234567", "1912345678901234568"]
jobs = [client.submit_tweet_statement(tweet_id=tid) for tid in tweet_ids]
for tid, job in zip(tweet_ids, jobs):
print(tid, job.result().data.final_statement)
```
```javascript JavaScript theme={null}
const tweetIds = ['1912345678901234567', '1912345678901234568'];
const jobs = await Promise.all(tweetIds.map((id) => vn.tweetStatement.submit(id)));
const statements = await Promise.all(jobs.map((job) => job.result()));
statements.forEach((s, i) => console.log(tweetIds[i], s.final_statement));
```
## How to Read the Response
### `final_statement`
This is the field most teams care about first. It answers:
`What is this post really claiming once all context is taken into account?`
Use it for:
* human review
* case summaries
* analyst handoff
* verification-ready claim normalization (use it directly as a search/verification query)
### `detailed_analysis`
This is the explanation layer. It helps reviewers understand why the system interpreted the post the way it did.
### `topics` and `entities`
Use these for filtering, tagging, grouping, alerting, or analytics.
### Media summaries
`tweet_media_summary` and `quoted_media_summary` explain what the attached media contributed to the meaning, if media exists in either layer. For videos this is a transcript summary; for images it is a vision-model description (transcribed text, chart/graph data, recognizable people and logos, and any claims they make).
## Classification Axes
The endpoint returns five classification axes for the original tweet text:
* `claim_type`: what kind of post it is, such as `factual_claim`, `opinion`, or `news_sharing`
* `intent`: what the author seems to be trying to do, such as `inform`, `persuade`, or `challenge`
* `tone`: how the message is delivered, such as `serious`, `skeptical`, or `provocative`
* `emotion`: the dominant emotional signal, such as `urgency`, `outrage`, or `hope_inspiration`
* `authority`: what kind of backing the tweet appears to rely on, such as `data_driven`, `expert_led`, or `speculative`
These labels are useful for routing, analytics, prioritization, and moderation workflows.
## Common Workflows
### Fact-checking
Use `final_statement` as the normalized claim to verify — it doubles as a search query for reporting, source documents, or related video evidence.
### Reputation and crisis monitoring
Track emotionally charged posts, accusations, or fast-moving narratives. The combination of `claim_type`, `tone`, `emotion`, and media summaries gives you more signal than raw keyword alerts.
### Narrative clustering
Store `final_statement`, `topics`, and `entities` for every analyzed tweet. That makes it much easier to group similar claims even when the original wording differs.
### AI product integration
Feed the output into dashboards, RAG pipelines, search systems, or internal tools that need clean structured social intelligence instead of raw tweet text.
## Billing and Media Notes
Billing is **consolidated** — a single call may run several internal LLM calls (media extraction, image vision, and the main statement extraction) but produces just **one** `analysis_request` charge per request:
* a text-only tweet usually consumes `analysis_request` units only
* a tweet with attached media can also trigger `standard_request` or `residential_request` (per media URL fetched) and `transcription_hour` (if speech-to-text runs)
* a quoted tweet with its own media can add another layer of processing
* **image analysis is billed through the same `analysis_request` meter** (no separate meter)
Analysis Request billing is dynamic: 1 `analysis_request` unit covers up to 15,000 total tokens, and larger contexts use `ceil(total_tokens / 15000)` units.
In practice, this means some tweets are lightweight and some are multimodal analysis jobs. The endpoint handles that automatically, but it is useful to know why two tweets may not cost the same.
## Troubleshooting
### Invalid tweet ID
Send only the numeric ID. Do not send the full X URL.
### Missing media summaries
If no media exists, or the media could not be processed, `tweet_media_summary` and `quoted_media_summary` may be `null`.
### Unexpected classification
Remember that the five classification axes are based on the original tweet text only, while `final_statement` reflects the broader contextual interpretation.
### `video_too_long`
The tweet (or the tweet it quotes) carries a video longer than 10 minutes, the limit of the synchronous endpoint. Send the same request to [`/v1/tweet/statement/async`](/api-reference/endpoint/tweet-claim-analysis-async) and collect the result by polling or [webhook](/webhooks).
### Upstream fetch issues
If X content cannot be fetched or parsed upstream, the endpoint may return an error such as `502`.
## Next Steps
* [Tweet Claim Analysis API reference](/api-reference/endpoint/tweet-claim-analysis)
* [Extract structured data from videos](/guides/data-extraction)
# Uploading and Processing Files
Source: https://docs.vidnavigator.com/guides/uploading-videos
Learn how to upload your own video and audio files for transcription and analysis.
## Overview
In addition to online videos, VidNavigator allows you to upload and process your own local audio and video files. This is useful for analyzing private content or files that are not publicly available on the web.
**Supported formats:** mp4, webm, mov, avi, wmv, flv, mkv, m4a, mp3, mpeg, mpga, wav.
## Prerequisites
* A valid VidNavigator API key.
* The [Python](/sdks/python) or [JavaScript](/sdks/javascript) SDK, version 2.1.0 (`pip install vidnavigator==2.1.0` or `npm install vidnavigator@2.1.0`).
## Organizing Files with Namespaces
You can organize your uploaded files into **namespaces** (like folders). When uploading, pass `namespace_ids` to assign the file to one or more namespaces. You can also manage namespaces later via the [namespace endpoints](/api-reference/endpoint/list-namespaces).
All file responses (list, info, search) include `namespace_ids` and `namespaces` fields showing which namespaces each file belongs to.
## Billing
Uploading and processing audio/video files consumes `transcription_hour` usage for speech-to-text. 1 credit covers 3 hours of video/audio transcription. Uploaded files also count toward your plan's storage quota.
## Uploading a File
The upload is asynchronous by default: the API returns a file ID immediately and processes the file in the background. Pass `wait_for_completion` to block until processing has finished instead.
```python Python theme={null}
from vidnavigator import VidNavigatorClient, VidNavigatorError
client = VidNavigatorClient() # reads VIDNAVIGATOR_API_KEY
try:
response = client.upload_file(
"path/to/your/video.mp4",
wait_for_completion=True, # omit to return immediately
namespace_ids=["ns_meetings"], # optional
)
print(f"File ID: {response['file_id']}, status: {response['file_status']}")
except FileNotFoundError:
print("Error: The file was not found at the specified path.")
except VidNavigatorError as e:
print(f"An error occurred: {e}")
```
```javascript JavaScript theme={null}
import { VidNavigatorClient, VidNavigatorError } from 'vidnavigator';
const vn = new VidNavigatorClient({
apiKey: process.env.VIDNAVIGATOR_API_KEY,
});
try {
const upload = await vn.uploadFile({
filePath: 'path/to/your/video.mp4',
wait_for_completion: true, // omit to return immediately
namespace_ids: ['ns_meetings'], // optional
});
console.log(`File ID: ${upload.file_id}, status: ${upload.file_status}`);
} catch (error) {
if (error instanceof VidNavigatorError) {
console.error(`An error occurred: ${error.message}`);
} else {
throw error; // e.g. ENOENT when the file does not exist
}
}
```
## Checking Processing Status
Without `wait_for_completion`, poll the file until its status is no longer `processing`, then read its transcript:
```python Python theme={null}
import time
file_id = client.upload_file("path/to/your/interview.mp3")["file_id"]
info = client.get_file(file_id).data.file_info
while info.status == "processing":
time.sleep(5)
info = client.get_file(file_id).data.file_info
if info.status == "failed":
client.retry_file_processing(file_id)
else:
resp = client.get_file(file_id, transcript_text=True)
print(resp.data.transcript)
```
```javascript JavaScript theme={null}
const { file_id } = await vn.uploadFile({ filePath: 'path/to/your/interview.mp3' });
let { file_info } = await vn.getFile(file_id);
while (file_info.status === 'processing') {
await new Promise((r) => setTimeout(r, 5000));
({ file_info } = await vn.getFile(file_id));
}
if (file_info.status === 'failed') {
await vn.retryFileProcessing(file_id);
} else {
const { transcript } = await vn.getFile(file_id, { transcript_text: true });
console.log(transcript);
}
```
## Working with the File
Once processed, a file works just like an online video:
```python Python theme={null}
# Ask questions about it
analysis = client.analyze_file(file_id=file_id, query="What action items were discussed?")
print(analysis.data.transcript_analysis.query_answer.answer)
# Extract structured data
extraction = client.extract_file_data(
file_id=file_id,
schema={"action_items": {"type": "Array", "description": "Action items", "items": {"type": "String", "description": "One action item"}}},
)
print(extraction.data)
# Manage it
client.get_file_url(file_id) # signed URL to view or download the file
client.get_files(limit=20, status="completed")
client.delete_file(file_id)
```
```javascript JavaScript theme={null}
// Ask questions about it
const { transcript_analysis } = await vn.analyzeFile({ file_id, query: 'What action items were discussed?' });
console.log(transcript_analysis.query_answer?.answer);
// Extract structured data
const { data } = await vn.extractFileData({
file_id,
schema: { action_items: { type: 'Array', description: 'Action items', items: { type: 'String', description: 'One action item' } } },
});
console.log(data);
// Manage it
const { file_url } = await vn.getFileUrl(file_id); // signed URL to view or download the file
const { files } = await vn.getFiles({ limit: 20, status: 'completed' });
await vn.deleteFile(file_id);
```
## Next Steps
* [Search the content of your uploaded files](/guides/searching-videos#searching-your-uploaded-files).
* [Organize files into namespaces](/sdks/python#namespaces) with the SDK, or via the [namespace endpoints](/api-reference/endpoint/list-namespaces).
* [Extract structured data](/guides/data-extraction) from the transcript using a custom schema.
* See the [Upload File](/api-reference/endpoint/upload-file) and [File Info](/api-reference/endpoint/file-info) API references.
# VidNavigator AI
Source: https://docs.vidnavigator.com/index
VidNavigator is an AI-powered video intelligence platform. Transcribe, analyze, extract data, and search across videos from YouTube, TikTok, Instagram, X/Twitter, Facebook, Vimeo, Loom, Dailymotion, and your own uploaded files — all through a single API.
## Overview
VidNavigator gives you a single API to transcribe, analyze, extract structured data, and search across video content from all major platforms and your own uploaded files. Build applications that understand video content using state-of-the-art AI.
### Supported Platforms
| Capability | YouTube | TikTok, Instagram, X/Twitter, Facebook, Vimeo, Loom, Dailymotion | Your Uploaded Files |
| - | - | - | - |
| **Transcription** | Yes | Yes | Yes (speech-to-text) |
| **AI Analysis** | Yes | Yes | Yes |
| **Data Extraction** | Yes | Yes | Yes |
| **Platform Search** | Yes | TikTok | — |
| **Semantic Search** | Indexed channels | — | Yes |
Here are the core capabilities you can unlock with the VidNavigator API:
Retrieve metadata and transcripts from YouTube, TikTok, Instagram, X/Twitter, Facebook, Vimeo, Loom, Dailymotion, and more. Upload your own audio/video files for speech-to-text transcription. Supports multiple languages with structured, timestamped output.
Analyze video content to extract summaries, key topics, people, places, and insights. Ask specific questions about any video or uploaded file.
Define a custom JSON schema and extract typed, structured data from any video transcript — ideal for automated pipelines and content cataloging.
Discover videos with global YouTube and TikTok search, then analyze and rank the results.
Search vector-indexed YouTube channel content and your uploaded file library to find precise moments and answers.
## Explore Our Resources
Dive deeper into our platform with our SDKs, API reference, and detailed guides.
Get started with our Python library for seamless integration.
Use our JavaScript library for web and Node.js applications.
Explore detailed documentation for every endpoint.
Follow step-by-step guides for common use cases.
# MCP Server
Source: https://docs.vidnavigator.com/mcp-server
Give Cursor, Claude Desktop, n8n or any MCP client the full VidNavigator API: video search, transcription, analysis, data extraction, TikTok and your files
## VidNavigator MCP Server
The VidNavigator MCP (Model Context Protocol) server lets an AI assistant search, transcribe, analyze and extract data from online videos, and work with the files you uploaded, all from natural language. It is hosted at `https://api.vidnavigator.com/mcp/` and works with any MCP client that supports HTTP servers: Cursor, Claude Desktop, n8n, and others.
AI-ranked YouTube search, with filters for year, duration and purpose (find videos that support or oppose a claim).
Transcripts from YouTube, TikTok, X, Facebook, Vimeo and more, and speech-to-text for videos without captions, Instagram included, of any length.
Summaries and Q\&A on any video, and structured data extracted into the fields you define.
Keyword search with sort, date and popularity filters, and full profile scrapes.
Extract the core claim of a tweet, including its attached video or images, ready to fact-check.
List, search, analyze and extract data from the files you uploaded to VidNavigator.
## Quick Setup
Get your API key from [VidNavigator Studio → API](https://vidnavigator.com/studio/api) first. The server accepts it as `Authorization: Bearer ` or as an `X-API-Key: ` header.
### Cursor
Open Cursor → Settings → Tools and integration → Add MCP server, then paste:
```jsonc theme={null}
{
"mcpServers": {
"vidnavigator": {
"type": "http",
"url": "https://api.vidnavigator.com/mcp/",
"headers": {
"Authorization": "Bearer YOUR_API_KEY_HERE"
}
}
}
}
```
Replace `YOUR_API_KEY_HERE` with your API key.
You can now ask things like:
* "Find a video where Neil deGrasse Tyson explains gravity"
* "Transcribe this Instagram reel and summarize it: [https://www.instagram.com/reel/](https://www.instagram.com/reel/)..."
* "Find TikToks about AI tools posted in the last 24 hours"
### Claude Desktop
The VidNavigator extension connects Claude Desktop to the hosted server, so you get every tool, and new ones appear without reinstalling it.
Download `vidnavigator.dxt` from the [latest GitHub release](https://github.com/vidnavigator/vidnavigator-mcp-starter/releases/latest). Version 2.0.0 or later is required for the full tool set.
Open Claude Desktop → Settings → Extensions → Install Extension, then select the downloaded `.dxt` file and follow the prompts.
Enter your API key in the extension settings and enable the extension.
VidNavigator appears in the tools menu of a new conversation.
If you prefer not to install an extension, point Claude Desktop at the hosted server with [`mcp-remote`](https://www.npmjs.com/package/mcp-remote), which requires [Node.js](https://nodejs.org) 18 or later. Open Settings → Developer → Edit Config, add the following to `claude_desktop_config.json`, and restart Claude Desktop:
```json theme={null}
{
"mcpServers": {
"vidnavigator": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://api.vidnavigator.com/mcp/",
"--header",
"X-API-Key:YOUR_API_KEY_HERE"
]
}
}
}
```
Keep `X-API-Key:` and the key together with no space: on Windows, a space in a header argument splits it in two and the key is lost.
### n8n
You can integrate VidNavigator into your own workflows with [n8n](https://n8n.io/), an open-source automation tool.
Watch this video to learn how to set up an n8n workflow that connects to the VidNavigator MCP server, and follow the steps below.
Start by creating a new, empty workflow in your n8n instance.
Add an **AI Agent** node. Inside the node, connect it to your preferred Large Language Model (LLM), such as OpenAI, and configure a memory option.
Add the native **MCP Client** tool from n8n. This client will connect to the VidNavigator MCP server, giving your workflow access to all the available video tools.
In the MCP Client node, set the **Server URL** to `https://api.vidnavigator.com/mcp/` and ensure the transport is set to `HTTP Streamable Server`.
In the MCP Client node, set up a header with the name `X-API-Key` and paste your API key as the value.
Connect the MCP Client to the AI Agent. You can now send natural language prompts to the AI Agent to search, analyze, or transcribe any online video.
## How long-running tools work
Transcription, data extraction, tweet analysis and the two TikTok tools can take from a few seconds to several minutes, depending on the video. They run as [background jobs](/guides/async-jobs), so no video is too long:
1. The tool returns a `task_id` right away.
2. The assistant calls `get_task_status` every 3 seconds until `task_status` is `completed`, which includes the result, or `failed`, which includes an error with a code and a message.
The tool descriptions tell the assistant to do this, so you just ask your question and wait for the answer. Polling is free, and results are kept for one hour after the job finishes. If you set a default webhook in [Studio → API](https://vidnavigator.com/studio/api), it is also called when each job finishes.
## Available Tools
### Online videos
AI-ranked YouTube search. Filters: year range, maximum duration, focus (relevance, popularity, brevity) and purpose (`support`, `oppose`, `illustrate`, `example`, `explain`). `max_results` caps how many videos are analyzed, and so the cost.
**Example**: "Find videos that explain how mRNA vaccines work, under 10 minutes"
The transcript of a YouTube, TikTok, X, Facebook, Vimeo, Dailymotion or Loom video, as timestamped segments or plain text. Can also return the video's metadata only.
**Example**: "Get the transcript of [https://www.youtube.com/watch?v=](https://www.youtube.com/watch?v=)..."
Speech-to-text for videos without captions: Instagram, TikTok, X, Facebook, Vimeo, Rumble, Loom and more, of any length. Can transcribe every video of an Instagram carousel at once. Runs as a background job.
**Example**: "Transcribe this Instagram reel: [https://www.instagram.com/reel/](https://www.instagram.com/reel/)..."
Summarize a video, or answer a question about it, with the people, places and key subjects it covers and the moment that best answers the question.
**Example**: "Summarize this video and tell me what it says about pricing"
Ask another question about a video you already analyzed.
**Example**: "What did the speaker say about the main challenges?"
Extract structured data from a video into fields you define: products and prices, people mentioned, sentiment, claims, and so on. The video is transcribed first when it has no captions. Runs as a background job, and returns the video's metadata with the extracted data.
**Example**: "From this product review, extract the product name, the price and the pros and cons"
Extract the core claim of an X/Twitter post, with its topics, entities, tone and intent. Attached videos are transcribed and images are described. Chain it with `search_videos` to find videos that support or contradict the claim. Runs as a background job.
**Example**: "Fact-check this tweet: [https://x.com/.../status/](https://x.com/.../status/)..."
### TikTok
TikTok keyword search, with each video's author, stats (views, likes, comments, shares) and publish date. The assistant picks the filters from your request: newest videos for "latest" or "since" requests, most liked for "viral" or "popular" requests, and a publication window such as this week or this month. Runs as a background job.
**Example**: "Find the most liked TikToks about skincare routines this week"
Scrape the videos of a TikTok profile, optionally limited by date, likes or number of posts. Runs as a background job.
**Example**: "Get the last 30 videos posted by [https://www.tiktok.com/@nasa](https://www.tiktok.com/@nasa)"
### Your files
Browse the files you uploaded to VidNavigator, read their transcripts, and list the namespaces they are organized into.
**Example**: "List my uploaded files that finished processing"
Semantic search across your uploaded files, with a direct answer, the relevant quote and its timestamps.
**Example**: "In my customer calls, what did people say about the onboarding?"
Summarize an uploaded file, answer a question about it, or extract structured data from it.
**Example**: "Extract the action items and owners from my last meeting recording"
### Tasks and account
Get the status, and then the result, of a background job. The assistant calls it on its own after the tools that run as background jobs.
Your credit balance, activity for the current billing period, storage and channels indexed.
**Example**: "How many VidNavigator credits do I have left?"
## Example Queries
```text Video Search theme={null}
"Find a video where Neil deGrasse Tyson explains gravity"
```
```text Social Media Transcription theme={null}
"Transcribe this Instagram reel and give me the three key points: [link]"
```
```text Data Extraction theme={null}
"From this YouTube review, extract the product, the price and the verdict: [link]"
```
```text Fact-checking theme={null}
"What is this tweet claiming, and find me videos that contradict it: [link]"
```
```text TikTok Research theme={null}
"Find TikToks about AI tools posted in the last 24 hours, with their authors and view counts"
```
```text Your Files theme={null}
"Search my uploaded interviews for mentions of remote work"
```
## Credits & Usage
Every tool draws from your plan's shared [credit pool](https://vidnavigator.com/pricing), exactly like the equivalent API endpoint: YouTube search and TikTok search are billed per video or page fetched, transcription per audio hour, and analysis and extraction per AI analysis. Polling a background job is free. Ask the assistant to check your balance at any time with `get_usage`.
## Support
Need help with the MCP server setup or have questions?
View source code and documentation
Report bugs or request features
Test the MCP server API endpoints
# Quickstart
Source: https://docs.vidnavigator.com/quickstart
Get up and running with the VidNavigator API in less than 5 minutes.
This guide will walk you through the essential steps to start using the VidNavigator API with our official SDKs.
## Step 1: Get Your API Key
First, you'll need an API key. Sign up for a free account on the [VidNavigator Developer Dashboard](https://vidnavigator.com/studio/api) to get your key.
Keep this key safe, as you'll need it to authenticate all of your API requests.
## Step 2: Install an SDK
We provide official SDKs for Python and JavaScript/TypeScript. These docs cover **version 2.1.0**.
```bash Python theme={null}
pip install vidnavigator==2.1.0
```
```bash JavaScript theme={null}
npm install vidnavigator@2.1.0
```
Available on [PyPI](https://pypi.org/project/vidnavigator/).
Available on [npm](https://www.npmjs.com/package/vidnavigator).
Then expose your key as an environment variable. Both SDKs read it from there:
```bash theme={null}
export VIDNAVIGATOR_API_KEY="your_api_key_here"
```
## Step 3: Make Your First API Call
Now you're ready to make your first API call. Here's a simple example that retrieves the transcript of a YouTube video. You can also use the Postman collection to make your first API call.
[
](https://documenter.getpostman.com/view/47580454/2sBXinJWxN)
VidNavigator uses a **single transcript method** for every platform. Use `get_transcript` for YouTube, TikTok, X/Twitter, Facebook, Vimeo, and more — the endpoint auto-detects the platform from the URL.
```python Python theme={null}
from vidnavigator import VidNavigatorClient, VidNavigatorError
client = VidNavigatorClient() # reads VIDNAVIGATOR_API_KEY
try:
# Works for YouTube and every other supported platform
result = client.get_transcript(
video_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
)
print("Successfully retrieved transcript for:", result.data.video_info.title)
print("Transcript snippet:", result.data.transcript[0].text)
except VidNavigatorError as e:
print(f"An error occurred: {e}")
```
```javascript JavaScript theme={null}
import { VidNavigatorClient, VidNavigatorError } from 'vidnavigator';
const vn = new VidNavigatorClient({
apiKey: process.env.VIDNAVIGATOR_API_KEY,
});
try {
// Works for YouTube and every other supported platform
const { video_info, transcript } = await vn.getTranscript({
video_url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
});
console.log('Successfully retrieved transcript for:', video_info.title);
console.log('Transcript snippet:', transcript[0].text);
} catch (error) {
if (error instanceof VidNavigatorError) {
console.error(`An error occurred: ${error.message}`);
} else {
throw error;
}
}
```
Need speech-to-text for a video without captions, like an Instagram Reel? Call `client.transcribe_video(video_url=...)` in Python or `await vn.transcribe(url)` in JavaScript. It works on videos of any length.
## Next Steps
You've successfully made your first API call! Here's where you can go next:
Dive deep into all the available endpoints and their parameters.
Follow our step-by-step guides for common use cases.
# JavaScript SDK
Source: https://docs.vidnavigator.com/sdks/javascript
Install and use the official VidNavigator JavaScript/TypeScript SDK (v2.1.0) to transcribe, analyze, extract, and search video content.
The official JavaScript and TypeScript SDK for the VidNavigator API. Responses are parsed into typed model classes, and long-running work such as speech-to-text, extraction, tweet analysis and TikTok scraping runs as background jobs, so **media of any length works with a single `await`**.
[](https://www.npmjs.com/package/vidnavigator)
[](https://github.com/vidnavigator/vidnavigator-js/blob/main/LICENSE)
`vidnavigator` 2.1.0 on the npm registry.
Source code, issues and changelog.
This page documents **version 2.1.0**. Coming from 1.x? See [Upgrading from 1.x](#upgrading-from-1x).
## Installation
```bash npm theme={null}
npm install vidnavigator@2.1.0
```
```bash yarn theme={null}
yarn add vidnavigator@2.1.0
```
```bash pnpm theme={null}
pnpm add vidnavigator@2.1.0
```
**Requirements:** Node.js 16+ (18+ for the examples that use the global `fetch`). The only runtime dependencies are `axios` and `form-data`. Full TypeScript types ship with the package.
The SDK is for **server-side** use. It reads local files for uploads and schema files, and your API key must never be shipped to a browser.
## Initialization
Get your API key in the [Developer Dashboard](https://vidnavigator.com/studio/api).
```typescript ES modules / TypeScript theme={null}
import { VidNavigatorClient } from 'vidnavigator';
const vn = new VidNavigatorClient({
apiKey: process.env.VIDNAVIGATOR_API_KEY!,
});
```
```javascript CommonJS theme={null}
const { VidNavigatorClient } = require('vidnavigator');
const vn = new VidNavigatorClient({
apiKey: process.env.VIDNAVIGATOR_API_KEY,
});
```
### Configuration
| Option | Default | Description |
| - | - | - |
| `apiKey` | (required) | Your VidNavigator API key |
| `baseURL` | `https://api.vidnavigator.com/v1` | API base URL |
| `axiosConfig` | | Passed to the underlying [axios](https://axios-http.com/docs/req_config) instance: HTTP timeout, proxy, extra headers, ... |
```typescript theme={null}
const vn = new VidNavigatorClient({
apiKey: process.env.VIDNAVIGATOR_API_KEY!,
axiosConfig: {
timeout: 120_000, // per HTTP request, in ms
proxy: { protocol: 'http', host: 'proxy.internal', port: 3128 },
},
});
```
`axiosConfig.timeout` limits each HTTP request. How long the SDK waits for a background job is set per call with `timeoutMs` (see [Background jobs](#background-jobs)).
### Supported platforms
| Platform | Transcript (captions) | Transcribe (speech-to-text) | Carousel |
| - | :-: | :-: | :-: |
| YouTube | Yes | - | - |
| Instagram Reels | - | Yes | Yes |
| Instagram Posts | - | Yes | Yes (`all_videos`) |
| TikTok | Yes | Yes | - |
| X / Twitter | Yes | Yes | - |
| Vimeo | Yes | Yes | - |
| Facebook | Yes | Yes | - |
| Dailymotion | Yes | Yes | - |
| Loom | Yes | Yes | - |
| Uploaded files | Yes | Yes | - |
**Transcript** = fast extraction of the platform's own captions. **Transcribe** = speech-to-text by AI models, for when captions are unavailable.
***
## Transcripts
### Captions from any platform
`getTranscript` auto-detects the platform from the URL. Instagram has no captions, so use [`transcribe`](#speech-to-text) for it.
```typescript theme={null}
const { video_info, transcript } = await vn.getTranscript({
video_url: 'https://youtube.com/watch?v=dQw4w9WgXcQ',
language: 'en',
});
console.log(video_info.title); // "Rick Astley - Never Gonna Give You Up"
console.log(video_info.channel); // "Rick Astley"
console.log(video_info.duration); // 212
// Timed segments by default (a single string when transcript_text: true)
if (Array.isArray(transcript)) {
for (const seg of transcript.slice(0, 3)) {
console.log(`[${seg.start.toFixed(1)}s] ${seg.text}`);
}
}
// [0.0s] We're no strangers to love
// [3.4s] You know the rules and so do I
```
**Common options:** `video_url`, `language`, `metadata_only`, `fallback_to_metadata`, `transcript_text` (full transcript as one string), `include_usage`.
`getYouTubeTranscript()` still works but is deprecated: it forwards to `getTranscript()` and logs a warning.
### Speech-to-text
`vn.transcribe()` runs speech-to-text as a [background job](#background-jobs) and resolves with the result, whatever the video length.
```typescript theme={null}
// Instagram Reel (speech-to-text only)
const { video_info, transcript } = await vn.transcribe('https://www.instagram.com/reel/C86ZvEaqRmo/');
console.log(video_info.title);
console.log(transcript);
// More options: pass an object instead of a URL
const result = await vn.transcribe(
{ video_url: 'https://www.tiktok.com/@user/video/1234567890', transcript_text: true },
{ include_usage: true }
);
console.log(result.usage?.total_credits);
```
For TikTok, X, Vimeo, Facebook, Dailymotion and Loom you can use either `getTranscript` (captions, fast) or `transcribe` (speech-to-text).
### Instagram carousels
Select one video by index, or transcribe every video in the post:
```typescript theme={null}
// Transcribe the 2nd video in a carousel
const single = await vn.transcribe('https://www.instagram.com/p/ABC123/?img_index=2');
console.log(single.video_info.carousel_info);
// { total_items: 5, video_count: 3, image_count: 2, selected_index: 2 }
// Transcribe ALL videos in a carousel
const all = await vn.transcribe({
video_url: 'https://www.instagram.com/p/ABC123/',
all_videos: true,
});
if ('carousel_info' in all) {
console.log(`${all.carousel_info.transcribed_count} of ${all.carousel_info.video_count} videos transcribed`);
for (const video of all.videos) {
if (video.status === 'success') {
console.log(`Video #${video.index}: ${video.video_info?.title}`, video.transcript);
} else {
console.log(`Video #${video.index}: failed — ${video.message}`);
}
}
}
```
***
## AI analysis
Get a summary, the people, places and key subjects, and a direct answer to a natural-language question. `query` is optional.
```typescript Online video theme={null}
const { video_info, transcript_analysis } = await vn.analyzeVideo({
video_url: 'https://www.youtube.com/watch?v=4czjS9h4Fpg',
query: 'Where did the rover land?',
});
console.log(transcript_analysis.summary);
console.log(transcript_analysis.query_answer?.answer); // direct answer to your question
console.log(transcript_analysis.query_answer?.relevant_segments); // supporting transcript excerpts
console.log(transcript_analysis.people); // [{ name, context }, ...]
console.log(transcript_analysis.places);
console.log(transcript_analysis.key_subjects); // [{ name, description, importance }, ...]
```
```typescript Uploaded file theme={null}
const { transcript_analysis } = await vn.analyzeFile({
file_id: 'your_file_id',
query: 'What action items were discussed?',
});
console.log(transcript_analysis.summary);
console.log(transcript_analysis.query_answer?.answer);
```
See the [Analyzing Videos guide](/guides/analyzing-videos) for billing details.
***
## Structured extraction
Define a schema and get back structured data extracted from any video or file transcript. See the [Data Extraction guide](/guides/data-extraction) for schema design tips.
### From an online video
```typescript theme={null}
const { data, video_info, usage } = await vn.extractVideoData({
video_url: 'https://youtube.com/watch?v=dQw4w9WgXcQ',
schema: {
topic: { type: 'String', description: 'Main topic of the video' },
language: { type: 'String', description: 'Primary spoken language (ISO 639-1)' },
tone: { type: 'Enum', description: 'Overall tone', enum: ['positive', 'negative', 'neutral', 'mixed'] },
key_quotes: { type: 'Array', description: 'Top 3 memorable quotes', items: { type: 'String', description: 'A quote' } },
},
what_to_extract: 'Determine the topic, language, tone, and notable quotes.',
include_usage: true,
});
console.log(data);
// { topic: "A classic pop love song", language: "en", tone: "positive", key_quotes: [...] }
console.log(video_info?.title, video_info?.duration); // "Rick Astley - Never Gonna Give You Up" 212
console.log(usage?.analysis_tokens?.total_tokens); // 847
console.log(usage?.total_credits); // 2
```
`extractVideoData()` runs as a [background job](#background-jobs), so videos of any length work. With `transcribe: true` (the default), a video without captions is transcribed first. The result has the extracted `data`, the video's `video_info` (a `VideoInfo`), and `usage` when requested. `vn.extractVideo()` and `job.result()` on `vn.extractVideo.submit()` return the same shape.
`video_info` on extraction results requires **2.1.0**. In 2.0.0 it is always `undefined`.
### From an uploaded file
```typescript theme={null}
const { data } = await vn.extractFileData({
file_id: 'your-file-id',
schema: {
action_items: { type: 'Array', description: 'Action items from the meeting', items: { type: 'String', description: 'An action item' } },
next_meeting: { type: 'String', description: 'When is the next meeting scheduled?' },
sentiment: { type: 'Enum', description: 'Overall meeting mood', enum: ['productive', 'tense', 'casual', 'urgent'] },
},
});
```
### From a schema file
For larger schemas, pass a JSON or YAML file. It is sent as multipart form-data.
```typescript theme={null}
const { data } = await vn.extractVideoData({
video_url: 'https://youtube.com/watch?v=dQw4w9WgXcQ',
schemaFilePath: './schemas/video-extraction.yaml',
});
```
**Schema rules:** every field needs `type` and `description`. Supported types are `String`, `Number`, `Boolean`, `Integer`, `Object`, `Array` and `Enum`. `Array` fields need `items`, `Object` fields list sub-fields in `properties` (up to 10), and `Enum` fields list allowed values in `enum`. At most 10 top-level fields and 3 levels of nesting. An invalid schema is rejected with a `BadRequestError` before anything runs.
***
## Tweet claim analysis
Pass a tweet ID (not the full URL). Attached media of any length is transcribed, so this also runs as a [background job](#background-jobs).
```typescript theme={null}
const statement = await vn.tweetStatement('1585841080431321088');
console.log(statement.final_statement); // the core claim, in the author's voice
console.log(statement.detailed_analysis); // claim, context, evidence and stance
console.log(statement.claim_type, statement.intent, statement.tone, statement.emotion, statement.authority);
console.log(statement.topics, statement.entities);
console.log(statement.tweet_media_summary); // summary of attached video/images, if any
```
See the [Tweet Claim Analysis guide](/guides/tweet-claim-analysis).
***
## Background jobs
Transcription, structured extraction, tweet analysis and TikTok scraping run as background jobs on VidNavigator. The SDK always uses the [async endpoints](/guides/async-jobs), so **video length is never a problem**.
Each operation comes in two shapes:
| Operation | Wait for the result | Start it and come back later |
| - | - | - |
| Transcription | `await vn.transcribe(url)` | `await vn.transcribe.submit(url)` |
| Structured extraction | `await vn.extractVideo({ video_url, schema })` | `await vn.extractVideo.submit({ ... })` |
| Tweet claim analysis | `await vn.tweetStatement(tweetId)` | `await vn.tweetStatement.submit(tweetId)` |
| TikTok profile scrape | `await vn.tiktokProfile(profileUrl)` | `await vn.tiktokProfile.submit(profileUrl)` |
| TikTok keyword search | `await vn.tiktokSearch(query)` | `await vn.tiktokSearch.submit(query)` |
The 1.x methods `transcribeVideo()`, `extractVideoData()` and `getTweetStatement()` still work with the same arguments. They are now wrappers around the operations above.
### Start many at once
`.submit()` returns a `Job` right away. Each job has a `task_id`, `status()` (checks once), `result()` (waits, then returns the result) and `wait()` (waits, then returns the final task without throwing on failure):
```typescript theme={null}
const urls = ['https://www.instagram.com/reel/A/', 'https://www.instagram.com/reel/B/', 'https://vimeo.com/123'];
const jobs = await Promise.all(urls.map((url) => vn.transcribe.submit(url)));
console.log(jobs.map((job) => job.task_id)); // store these if you may need to come back later
const results = await Promise.allSettled(jobs.map((job) => job.result()));
results.forEach((r, i) => console.log(urls[i], r.status === 'fulfilled' ? r.value : r.reason.message));
```
```typescript theme={null}
const job = await vn.extractVideo.submit({
video_url: 'https://www.facebook.com/watch/?v=1234567890',
schema: {
speakers: { type: 'Array', description: 'Names of everyone who speaks', items: { type: 'String', description: 'A speaker name' } },
verdict: { type: 'String', description: 'The final conclusion of the video' },
},
});
console.log(await job.status()); // 'processing' | 'completed' | 'failed'
const { data } = await job.result();
```
### Come back later
A job keeps running even if your process stops waiting, and its result stays available for **1 hour after it finishes**. Keep the `task_id` and reattach with `.resume()`:
```typescript theme={null}
const job = vn.transcribe.resume(savedTaskId);
const result = await job.result();
```
### Timeouts
The one-liners and `result()` wait up to 30 minutes by default, then throw a `TaskTimeoutError` that **carries the `task_id`**. The job is not lost:
```typescript theme={null}
import { TaskTimeoutError } from 'vidnavigator';
try {
const result = await vn.transcribe(url, { timeoutMs: 5 * 60 * 1000 });
} catch (err) {
if (err instanceof TaskTimeoutError) {
saveForLater(err.task_id); // later: await vn.transcribe.resume(err.task_id).result()
} else {
throw err;
}
}
```
Pass these to the one-liner or to `result()` / `wait()`:
| Option | Default | Description |
| - | - | - |
| `timeoutMs` | 30 minutes | Stop waiting and throw `TaskTimeoutError` (with `task_id`). `0` waits forever |
| `intervalMs` | `3000` | Time between checks. Checking costs no credits and is not rate-limited |
| `fastStart` | `true` | Check every second for the first 10 seconds, so short clips return quickly |
| `signal` | | An `AbortSignal` to stop waiting early (the job keeps running) |
| `onPoll` | | Called with each task snapshot, e.g. for progress logs |
| `include_usage` | `false` | Attach a `usage` block to the result (one-liner, `.submit()` and `.resume()` options) |
| Member | Description |
| - | - |
| `.submit(input, { include_usage? })` | Start the job and return a `Job` immediately |
| `.resume(task_id, { include_usage? })` | Get a `Job` for a job you already started (no request is made) |
| `job.task_id`, `job.job_type`, `job.check_status_url`, `job.webhook_url` | Job details from the submit response |
| `job.status()` | Check once; returns `'processing'`, `'completed'` or `'failed'` |
| `job.refresh()` | Check once; returns the full task (`error`, `webhook`, timestamps, ...) |
| `job.result(options?)` | Wait until finished and return the result. Throws on failure or timeout; every error carries `task_id` |
| `job.wait(options?)` | Wait until finished and return the final task, without throwing on failure |
`transcribe` → same shape as `transcribeVideo()`; `extractVideo` → `{ data, video_info, usage? }`; `tweetStatement` → `TweetStatement`; `tiktokProfile` / `tiktokSearch` → the completed task with every page of `videos` / `results`.
### Failures
**When a job fails**, `result()` and the one-liners throw the same error class as any other API error (for example `NotFoundError` for a missing video), built from the job's `error` details and carrying the `task_id`. `wait()` returns the failed task instead, with `task.error` set.
**When a job can't start**, submitting throws right away and no job is created:
* `InsufficientCreditsError` (402): not enough credits. A job can also run out of credits while it runs; `result()` then throws the same error, so handle it in both places.
* `TooManyActiveJobsError` (429): too many of your jobs are already running. Wait for some to finish and retry.
### Large batches
Your account can only run a limited number of jobs at once, so `Promise.all` over hundreds of URLs will hit `TooManyActiveJobsError`. Keep a fixed number in flight:
```typescript theme={null}
import { TooManyActiveJobsError } from 'vidnavigator';
async function transcribeAll(urls: string[], concurrency = 10) {
const results = new Map();
const queue = [...urls];
async function worker() {
for (let url = queue.shift(); url; url = queue.shift()) {
for (;;) {
try {
results.set(url, await vn.transcribe(url));
break;
} catch (err) {
if (err instanceof TooManyActiveJobsError) {
await new Promise((r) => setTimeout(r, 10_000)); // wait for running jobs to finish
continue;
}
results.set(url, err); // record the failure and move on
break;
}
}
}
}
await Promise.all(Array.from({ length: concurrency }, worker));
return results;
}
```
### Progress and restarts
```typescript theme={null}
// Log every task snapshot while waiting
const started = Date.now();
const result = await vn.transcribe(url, {
onPoll: (task) => console.log(`${task.task_id}: ${task.task_status} after ${Math.round((Date.now() - started) / 1000)}s`),
});
```
Save each `task_id` as soon as you submit, so a crash or deploy doesn't lose the work:
```typescript theme={null}
import { writeFileSync, readFileSync, existsSync } from 'fs';
const job = await vn.transcribe.submit(url);
writeFileSync('pending-jobs.json', JSON.stringify([...pending(), job.task_id]));
// After a restart: pick every job back up
for (const taskId of pending()) {
const result = await vn.transcribe.resume(taskId).result();
// ... store the result, then drop taskId from pending-jobs.json
}
function pending(): string[] {
return existsSync('pending-jobs.json') ? JSON.parse(readFileSync('pending-jobs.json', 'utf8')) : [];
}
```
***
## Webhooks
Pass `webhook_url` to any job operation (transcription, extraction, tweet analysis, TikTok profile and search), or set an account-wide default in [Studio → API](https://vidnavigator.com/studio/api). A per-job `webhook_url` overrides the default, and `webhook_url: ''` turns it off for one job. Webhooks are optional: waiting with `result()` works the same either way and remains the source of truth.
```typescript theme={null}
const job = await vn.transcribe.submit({
video_url: 'https://www.instagram.com/reel/C86ZvEaqRmo/',
webhook_url: 'https://example.com/hooks/vidnavigator',
});
console.log(job.webhook_url); // where the notification will be sent, or null
```
Verify each delivery with `constructWebhookEvent()`, passing the **raw** request body:
```typescript theme={null}
import express from 'express';
import { VidNavigatorClient, constructWebhookEvent, WebhookSignatureError, WEBHOOK_HEADERS } from 'vidnavigator';
const vn = new VidNavigatorClient({ apiKey: process.env.VIDNAVIGATOR_API_KEY! });
const app = express();
app.post('/hooks/vidnavigator', express.raw({ type: 'application/json' }), async (req, res) => {
let event;
try {
event = constructWebhookEvent(
req.body, // raw Buffer, not parsed JSON
req.headers[WEBHOOK_HEADERS.signature], // "t=,v1="
process.env.VIDNAVIGATOR_WEBHOOK_SECRET!
);
} catch (err) {
if (err instanceof WebhookSignatureError) return res.status(400).send('Invalid signature');
throw err;
}
res.sendStatus(200); // acknowledge quickly; any 2xx counts
// Deliveries can repeat: dedupe on req.headers[WEBHOOK_HEADERS.delivery].
if (event.type === 'transcribe.completed') {
const result = await vn.transcribe.resume(event.data.task_id).result();
console.log(result);
} else if (event.type === 'tiktok_profile.completed') {
const task = await vn.tiktokProfile.resume(event.data.task_id).result(); // every video, all pages
console.log(task.videos.length);
} else if (event.type.endsWith('.failed')) {
console.error(event.data.task_id, event.data.error); // { error, message, http_status }
}
});
```
* `event.data.result` holds the raw result for transcription, extraction and tweet events, unless it was over 256 KB (`result_truncated: true`). For TikTok events it is only a summary. `resume(task_id).result()` always gets the full parsed result.
* For `extract_video.completed` events, `event.data.video_info` holds the raw video metadata. It is sent even when the result was truncated.
* `verifyWebhookSignature(rawBody, signatureHeader, secret)` returns `true` or `false` instead of throwing.
* Deliveries older than 5 minutes are rejected by default. Change this with `{ toleranceSeconds }`; `0` turns the check off.
* `(await job.refresh()).webhook` shows the delivery status: `{ status, attempts, response_status, last_error, delivered_at }`.
See [Webhooks](/webhooks) for the full delivery contract.
***
## TikTok
### Profile scraping
`vn.tiktokProfile()` scrapes a public profile in the background and returns the finished task with **every** matching video (the SDK fetches all result pages for you):
```typescript theme={null}
const task = await vn.tiktokProfile({
profile_url: 'https://www.tiktok.com/@tiktok',
max_posts: 250,
after_datetime: '2024-01-01',
});
console.log(`Matched ${task.videos.length} videos`, task.stats);
const videos = task.videos; // TikTokVideo[], published_at parsed as Date
```
Options: `max_posts`, `after_datetime`, `before_datetime`, `min_likes`, `max_likes`, `webhook_url`. Datetimes accept `YYYY-MM-DD` or full ISO strings with a timezone.
To read the results one page at a time, or grab them as a single JSON file, submit the job and use the page-level method:
```typescript theme={null}
const job = await vn.tiktokProfile.submit('https://www.tiktok.com/@tiktok');
await job.wait();
// Page by page
let cursor: string | undefined;
do {
const page = await vn.getTikTokProfileScrape(job.task_id, { limit: 100, cursor });
handle(page.videos);
cursor = page.pagination?.next_cursor ?? undefined;
} while (cursor);
// Or the whole result as one JSON file (short-lived signed URL)
const { download_url } = await vn.getTikTokProfileScrape(job.task_id, { limit: 1 });
if (download_url) {
const fullProfile = await (await fetch(download_url)).json();
console.log(fullProfile.videos.length);
}
```
Then feed each video into the transcript or extraction APIs:
```typescript theme={null}
for (const video of videos) {
if (!video.url) continue;
const { transcript } = await vn.getTranscript({
video_url: video.url,
transcript_text: true,
fallback_to_metadata: true,
});
console.log(video.published_at?.toISOString(), video.title, transcript);
}
```
For large profiles, process videos sequentially or with a small concurrency limit so you do not exhaust credits or hit rate limits.
### Keyword search
`vn.tiktokSearch()` works the same way and returns the finished task with every result:
```typescript theme={null}
const task = await vn.tiktokSearch({
query: 'ai tools',
max_results: 100,
sort_by: 'most_liked', // 'relevance' | 'most_liked' | 'newest'
published_within: 'this_month', // 'all' | 'past_24_hours' | 'this_week' | 'this_month' | 'last_3_months' | 'last_6_months'
min_views: 10000,
});
console.log(task.stats?.sort_by, task.stats?.published_within); // what the search actually ran with
for (const result of task.results) {
console.log(result.published_at?.toISOString(), result.url, result.stats?.views);
}
```
* Without `sort_by`, results come back newest first. Without `published_within`, an `after_datetime` more than 24 hours ago picks the smallest window that covers it.
* `parallel_search_slices` (`2` to `4`) runs several search chains in parallel to go past TikTok's single-chain limit (about 115 to 140 items). Billing grows with the number of slices.
* Other filters: `after_datetime`, `before_datetime`, `min_likes`, `max_likes`, `min_views`, `max_views`, `webhook_url`. Page through results with `getTikTokSearch(task_id, { cursor, limit })`.
See the [Searching TikTok guide](/guides/tiktok-search).
***
## Search
### Search YouTube
```typescript theme={null}
const videoResults = await vn.searchYouTube({
query: 'how to deploy a Node.js app',
focus: 'relevance', // 'relevance' (default) | 'popularity' | 'brevity'
max_results: 5, // caps how many candidates are analysed (and the cost)
include_usage: true,
});
console.log(`Found ${videoResults.total_found} results`);
for (const r of videoResults.results) {
console.log(`${r.title} — score: ${r.relevance_score}`);
console.log(` ${r.transcript_summary}`);
}
// If credits run out mid-search, a partial result is returned instead of throwing
if (videoResults.status === 'partial') {
console.log('Partial results:', videoResults.error_code); // "insufficient_credits_video_search"
}
```
Other options: `use_enhanced_search` (default `true`), `start_year`, `end_year`, `duration`.
`searchVideos()` still works but is deprecated: it forwards to `searchYouTube()` and logs a warning.
### Search your uploaded files
```typescript theme={null}
const fileResults = await vn.searchFiles({
query: 'quarterly revenue discussion',
namespace_ids: ['ns_finance'], // optional: scope to namespaces
});
for (const r of fileResults.results) {
console.log(`${r.name} (score: ${r.relevance_score})`);
console.log(` Answer: ${r.query_answer}`);
console.log(` Namespaces: ${r.namespaces?.map((n) => n.name).join(', ')}`);
}
```
***
## Files
Supported formats: mp4, webm, mov, avi, wmv, flv, mkv, m4a, mp3, mpeg, mpga, wav.
### Upload and analyze
```typescript theme={null}
const upload = await vn.uploadFile({
filePath: './meeting-recording.mp4',
wait_for_completion: true,
namespace_ids: ['ns_meetings'], // optional
});
console.log(upload.file_id); // "64a1b2c3d4e5f6789..."
console.log(upload.file_info?.namespace_ids); // ["ns_meetings"]
const { transcript_analysis } = await vn.analyzeFile({
file_id: upload.file_id,
query: 'What action items were discussed?',
});
console.log(transcript_analysis.query_answer?.answer);
```
### Upload without waiting
Without `wait_for_completion`, the upload returns right away (`status: 'accepted'`) while the file is processed. Check on it with `getFile()`:
```typescript theme={null}
const { file_id } = await vn.uploadFile({ filePath: './interview.mp3' });
let { file_info } = await vn.getFile(file_id);
while (file_info.status === 'processing') {
await new Promise((r) => setTimeout(r, 5000));
({ file_info } = await vn.getFile(file_id));
}
if (file_info.status === 'failed') {
await vn.retryFileProcessing(file_id); // or cancelFileUpload(file_id) while it is still processing
}
```
### Manage files
```typescript theme={null}
const { transcript } = await vn.getFile(file_id, { transcript_text: true }); // full text
const { file_url } = await vn.getFileUrl(file_id); // signed URL to view or download
const { files, has_more } = await vn.getFiles({ status: 'completed', limit: 20, offset: 0 });
await vn.deleteFile(file_id);
```
### Namespaces
```typescript theme={null}
const ns = await vn.createNamespace({ name: 'Client Calls' });
const updated = await vn.updateFileNamespaces(fileId, { namespace_ids: [ns.id!] });
console.log(updated.namespaces); // [{ id: "...", name: "Client Calls" }]
const files = await vn.getFiles({ namespace_id: ns.id! });
const all = await vn.getNamespaces();
await vn.updateNamespace(ns.id!, { name: 'Sales Calls' });
await vn.deleteNamespace(ns.id!);
```
***
## Usage and billing
### Per-call usage
Pass `include_usage: true` to `getTranscript`, `analyzeVideo`, `analyzeFile`, `extractVideoData`, `extractFileData`, `searchYouTube` or `searchFiles` to receive a `usage` block describing what the request cost. For background jobs, pass it in the options: `vn.transcribe(url, { include_usage: true })`. A failed job has all its charges reverted.
```typescript theme={null}
const { usage } = await vn.getTranscript({
video_url: 'https://youtube.com/watch?v=dQw4w9WgXcQ',
include_usage: true,
});
if (usage) {
console.log(usage.total_credits); // net credits deducted by this request
console.log(usage.credits_remaining_after); // balance after the request (when provided)
for (const charge of usage.charges) {
console.log(charge.service_type, charge.quantity, charge.credits, charge.waived);
}
const residential = usage.charge_for('residential_request'); // matching charge or undefined
console.log(usage.analysis_tokens?.total_tokens); // LLM tokens (analyze / extract / search)
}
```
Meters: `standard_request`, `residential_request`, `transcription_hour`, `analysis_request`, `search_request`, `scene_analysis_hour`. See [Usage & Costs](/api-reference/usage-costs).
### Account usage
```typescript theme={null}
const usage = await vn.getUsage();
console.log(`Credits remaining: ${usage.credits.monthly_remaining}`);
console.log(`Transcription hours used: ${usage.usage.transcriptionHour?.used}`);
console.log(`Storage: ${usage.storage.usedFormatted} / ${usage.storage.limitFormatted}`);
```
`vn.healthCheck()` returns the API status and needs no authentication.
***
## Error handling
Every API error is mapped to a specific class, all extending `VidNavigatorError`:
```typescript theme={null}
import { AuthenticationError, NotFoundError, RateLimitExceededError, VidNavigatorError } from 'vidnavigator';
try {
await vn.getFile('nonexistent-id');
} catch (error) {
if (error instanceof NotFoundError) {
console.log(error.status_code); // 404
console.log(error.error_message); // "File not found"
} else if (error instanceof RateLimitExceededError) {
console.log('Slow down! Rate limited.');
} else if (error instanceof VidNavigatorError) {
console.log(error.error_code, error.message);
} else {
throw error;
}
}
```
| Error class | HTTP | When |
| - | :-: | - |
| `BadRequestError` | 400 | Invalid parameters |
| `AuthenticationError` | 401 | Invalid or missing API key |
| `PaymentRequiredError` | 402 | Credit limit reached |
| `InsufficientCreditsError` | 402 | A job could not start: not enough credits. Extends `PaymentRequiredError` |
| `AccessDeniedError` | 403 | Insufficient permissions |
| `NotFoundError` | 404 | Resource does not exist, or a job's `task_id` is unknown or expired |
| `StorageQuotaExceededError` | 413 | Storage quota exceeded |
| `RateLimitExceededError` | 429 | Too many requests |
| `TooManyActiveJobsError` | 429 | A job could not start: too many jobs already running. Extends `RateLimitExceededError` |
| `GeoRestrictedError` | 451 | Content unavailable in your region |
| `ServerError` | 5xx | Unexpected server error |
| `SystemOverloadError` | 503 | Temporary overload (has `retry_after_seconds`) |
| `TaskTimeoutError` | - | Waiting for a job took longer than `timeoutMs`. Has `task_id`; the job keeps running |
| `WebhookSignatureError` | - | `constructWebhookEvent()` got a missing, invalid or expired signature |
Every error exposes `status_code`, `error_code`, `error_message` and `details`, and errors raised while waiting for a job also have `task_id`. See [Errors](/api-reference/errors) for every API error code.
***
## TypeScript models
All API responses are parsed into typed classes with static `fromJSON()` constructors.
| Class | Description |
| - | - |
| `VideoInfo` | Video metadata: title, channel, duration, views, likes, published date, keywords, carousel info |
| `FileInfo` | Uploaded file: name, size, type, duration, status, `namespace_ids`, `namespaces` |
| `TranscriptSegment` | Timed segment: `text`, `start`, `end` (seconds) |
| `AnalysisResult` | AI analysis: summary, people, places, key subjects, query answer |
| `CarouselInfo` | Carousel summary: total items, video/image count, transcribed count, total duration |
| `CarouselVideoResult` | Per-video result in a carousel: index, status, video info, transcript |
| `TweetStatement` | Structured X/Twitter claim analysis, topics, entities, tone, intent and source text |
| Class | Description |
| - | - |
| `VideoSearchResult` | Extends `VideoInfo` with `relevance_score`, `transcript_summary` and search metadata |
| `FileSearchResult` | Extends `FileInfo` with `relevance_score`, `query_answer`, timestamps and a signed URL |
| `Namespace` | Full namespace: id, name, created/updated timestamps |
| `NamespaceRef` | Lightweight `{ id, name }` reference embedded in file and search responses |
| Class | Description |
| - | - |
| `Job` | Handle returned by `.submit()` / `.resume()`: `task_id`, `status()`, `refresh()`, `result()`, `wait()` |
| `AsyncJob` | Task snapshot of a transcription, extraction or tweet job: `task_status`, `result`, `video_info` (extraction jobs), `error`, `webhook`, `request`, timestamps |
| `TikTokProfileTask` | Profile scrape task: status, profile metadata, videos, pagination, `download_url`, `error`, `webhook` |
| `TikTokVideo` | Public TikTok video metadata, with `published_at` as a `Date` and integer counters |
| `TikTokSearchTask` | Keyword search task: status, results, pagination, `download_url`; `stats` shows the `sort_by` / `published_within` used |
| `TikTokSearchResult` | Normalized keyword result, with `published_at` as a `Date` and integer counters |
| Class | Description |
| - | - |
| `UsageData` | Account usage from `getUsage()`: credits, per-service activity, storage, subscription |
| `UsageBlock` | Per-call usage: `charges[]`, `total_credits`, `credits_remaining_after`, `waived`, plus `charge_for()` and `analysis_tokens` |
| `UsageCharge` | One meter charge: `service_type`, `quantity`, `credits`, `waived`, `credits_saved`, and `tokens` on `analysis_request` |
***
## What's new in 2.1.0
* **`video_info` on extraction:** `extractVideoData()`, `vn.extractVideo()` and `job.result()` on `vn.extractVideo.submit()` now return the video's metadata as `video_info`. The task snapshot from `job.refresh()` / `job.wait()` has it too, and `extract_video.completed` webhook events carry it in `event.data.video_info`.
No other changes are needed to upgrade from 2.0.0.
## Upgrading from 1.x
Transcription, structured extraction and tweet analysis now always run as background jobs, so they work on videos of any length. Existing calls keep working with the same arguments, with these differences:
* `getTweetStatement()` no longer returns `statement_query`, which was never part of the documented API.
* `searchVideos()` and `getYouTubeTranscript()` are deprecated in favor of `searchYouTube()` and `getTranscript()`.
**New in 2.0:** `vn.transcribe`, `vn.extractVideo`, `vn.tweetStatement`, `vn.tiktokProfile` and `vn.tiktokSearch`, each with `.submit()` and `.resume()`, plus the webhook signature helpers and the `InsufficientCreditsError`, `TooManyActiveJobsError` and `TaskTimeoutError` classes.
***
## Resources
Package page and release history.
Source code and issue tracker.
The same API for Python.
Every endpoint, parameter and response field.
# Python SDK
Source: https://docs.vidnavigator.com/sdks/python
Install and use the official VidNavigator Python SDK (v2.1.0) to transcribe, analyze, extract, and search video content.
The official Python client for the VidNavigator API. Every response is a typed Pydantic model (v1 and v2 both work), and long-running work such as speech-to-text, extraction, tweet analysis and TikTok scraping runs as background jobs, so **media of any length works with a single call**. It ships a synchronous client and an [asyncio client](#async-client) with the same methods.
[](https://pypi.org/project/vidnavigator/)
[](https://pypi.org/project/vidnavigator/)
[](https://github.com/vidnavigator/vidnavigator-python/blob/main/LICENSE)
`vidnavigator` 2.1.0 on the Python Package Index.
Source code, issues and changelog.
This page documents **version 2.1.0**. Coming from 1.x? See [Upgrading from 1.x](#upgrading-from-1x).
## Installation
```bash pip theme={null}
pip install vidnavigator==2.1.0
```
```bash poetry theme={null}
poetry add vidnavigator@2.1.0
```
```bash uv theme={null}
uv add vidnavigator==2.1.0
```
For the [asyncio client](#async-client), install the `async` extra, which adds [`httpx`](https://pypi.org/project/httpx/) >= 0.23:
```bash pip theme={null}
pip install "vidnavigator[async]==2.1.0"
```
```bash poetry theme={null}
poetry add "vidnavigator[async]==2.1.0"
```
```bash uv theme={null}
uv add "vidnavigator[async]==2.1.0"
```
**Requirements:** Python 3.8+, [`requests`](https://pypi.org/project/requests/) >= 2.31 and [`pydantic`](https://pypi.org/project/pydantic/) >= 1.10 (installed automatically).
## Authentication
Get your API key in the [Developer Dashboard](https://vidnavigator.com/studio/api).
```python theme={null}
from vidnavigator import VidNavigatorClient
# Reads the VIDNAVIGATOR_API_KEY environment variable
client = VidNavigatorClient()
# Or pass the key explicitly
client = VidNavigatorClient(api_key="YOUR_API_KEY")
```
### Configuration
```python theme={null}
client = VidNavigatorClient(
api_key="YOUR_API_KEY", # or set VIDNAVIGATOR_API_KEY
base_url="https://...", # override the API base URL
timeout=60, # per HTTP request, in seconds (default: 30)
session=my_requests_session, # bring your own requests.Session
)
```
The client's `timeout` applies to each HTTP request. It is separate from the `timeout` argument of job methods such as `transcribe_video(timeout=...)`, which limits how long to wait for a whole [background job](#background-jobs).
Use the client as a context manager to close its HTTP session automatically. One client can be reused for any number of calls; if you use threads, create one client per thread. For asyncio code, use [`AsyncVidNavigatorClient`](#async-client).
```python theme={null}
with VidNavigatorClient() as client:
resp = client.get_transcript(video_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
print(resp.data.transcript)
```
## Quick start
```python theme={null}
from vidnavigator import VidNavigatorClient
client = VidNavigatorClient()
# 1. Captions of a YouTube video
resp = client.get_transcript(video_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ", transcript_text=True)
print(resp.data.transcript)
# 2. Speech-to-text for a video without captions (any length)
resp = client.transcribe_video(video_url="https://www.instagram.com/reel/C86ZvEaqRmo/", transcript_text=True)
print(resp.data.transcript)
# 3. Structured data from a video
resp = client.extract_video_data(
video_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ",
schema={"mood": {"type": "String", "description": "Overall mood in one word"}},
)
print(resp.data) # {"mood": "upbeat"}
```
## Which method should I use?
| I want to... | Use | Returns |
| - | - | - |
| Get the captions of a video (fast, cheap) | `get_transcript` | immediately |
| Transcribe audio when there are no captions (Instagram, TikTok, ...) | `transcribe_video` | when the job finishes |
| Summarize a video or ask a question about it | `analyze_video` | immediately |
| Pull specific fields out of a video into a `dict` | `extract_video_data` | when the job finishes |
| Analyze the claim made in a tweet | `get_tweet_statement` | when the job finishes |
| List a TikTok profile's videos | `scrape_tiktok_profile` | when the job finishes |
| Search TikTok by keyword | `search_tiktok` | when the job finishes |
| Find YouTube videos about a topic | `search_youtube` | immediately |
| Work with your own audio/video files | `upload_file`, then `get_file`, `analyze_file`, `extract_file_data`, `search_files` | immediately |
| Process **many** videos at once | the `submit_*` version of a job method | a job handle, right away |
| Call the API from asyncio code (FastAPI, aiohttp, bots, agents) | [`AsyncVidNavigatorClient`](#async-client) | the same results, awaited |
"When the job finishes" methods run as [background jobs](#background-jobs): they block until the result is ready, however long the media is. Each one also has a `submit_*` version that returns immediately.
### Supported platforms
| Platform | Transcript (captions) | Transcribe (speech-to-text) | Carousel |
| - | :-: | :-: | :-: |
| YouTube | Yes | - | - |
| Instagram Reels | - | Yes | Yes |
| Instagram Posts | - | Yes | Yes (`all_videos`) |
| TikTok | Yes | Yes | - |
| X / Twitter | Yes | Yes | - |
| Vimeo | Yes | Yes | - |
| Facebook | Yes | Yes | - |
| Dailymotion | Yes | Yes | - |
| Loom | Yes | Yes | - |
| Uploaded files | Yes | Yes | - |
***
## Transcripts
### Captions from any platform
`get_transcript` handles every supported platform; the API detects the platform from the URL. Instagram has no captions, so use [`transcribe_video`](#speech-to-text) for it.
```python theme={null}
resp = client.get_transcript(video_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
print(resp.data.video_info.title)
# "Rick Astley - Never Gonna Give You Up (Official Music Video)"
for segment in resp.data.transcript:
print(f"[{segment.start:.1f}s] {segment.text}")
# [0.0s] We're no strangers to love
# [3.2s] You know the rules and so do I
```
Pass `transcript_text=True` to get one plain string instead of segments:
```python theme={null}
resp = client.get_transcript(video_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ", transcript_text=True)
print(resp.data.transcript)
# "We're no strangers to love. You know the rules and so do I..."
```
| Parameter | Type | Description |
| - | - | - |
| `video_url` | `str` | URL of the video |
| `language` | `str` | ISO 639-1 language code (e.g. `"en"`, `"fr"`) |
| `transcript_text` | `bool` | Return the transcript as a single string instead of segments |
| `metadata_only` | `bool` | Return only video metadata, no transcript |
| `fallback_to_metadata` | `bool` | Return metadata with an empty transcript instead of raising `NotFoundError` when no captions exist |
| `include_usage` | `bool` | Attach a per-call [`usage`](#per-call-usage) block |
`get_youtube_transcript` still works as a deprecated alias for `get_transcript`.
### Speech-to-text
For videos without captions, `transcribe_video` runs speech-to-text as a [background job](#background-jobs): it submits the video, waits for the transcript, and returns it. Long videos work exactly like short ones.
```python theme={null}
resp = client.transcribe_video(video_url="https://www.instagram.com/reel/C86ZvEaqRmo/")
print(resp.data.video_info.title)
for segment in resp.data.transcript:
print(f"[{segment.start:.1f}s] {segment.text}")
```
| Parameter | Type | Description |
| - | - | - |
| `video_url` | `str` | URL of the video (`?img_index=N` selects one video of an Instagram carousel) |
| `transcript_text` | `bool` | Return the transcript as a single string instead of segments |
| `all_videos` | `bool` | Carousel posts only: transcribe every video. Returns `carousel_info` and `videos` instead of `video_info` and `transcript` |
| `include_usage` | `bool` | Attach a `usage` block to the result |
| `webhook_url` | `str` | Optional; see [Webhooks](#webhooks) |
| `timeout` | `float` | Seconds to wait for the job (default `3600`; `None` waits indefinitely) |
### Instagram carousels
Pick one video with `?img_index=N` in the URL, or transcribe all of them:
```python theme={null}
resp = client.transcribe_video(
video_url="https://www.instagram.com/p/ABC123/",
all_videos=True,
)
print(resp.data.carousel_info.video_count)
for video in resp.data.videos:
print(f"Video {video.index} ({video.status}): {video.transcript}")
```
### Captions first, speech-to-text as a fallback
Captions are faster and cheaper than speech-to-text, so a common pattern is to try them first:
```python theme={null}
from vidnavigator import BadRequestError, NotFoundError
def best_transcript(url):
try:
return client.get_transcript(video_url=url, transcript_text=True).data.transcript
except (NotFoundError, BadRequestError): # no captions, or a platform without captions
return client.transcribe_video(video_url=url, transcript_text=True).data.transcript
```
***
## AI analysis
Get a summary, the people, places and key subjects, and a direct answer to a natural-language question. `query` is optional.
```python Online video theme={null}
resp = client.analyze_video(
video_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ",
query="What is the main message of this song?",
)
analysis = resp.data.transcript_analysis
print(analysis.summary)
# "The song is a declaration of unwavering commitment and loyalty..."
print(analysis.query_answer.answer)
# "The main message is a promise to never give up on someone..."
# people, places and key_subjects are lists of plain dicts
for person in analysis.people or []:
print(person) # {"name": "Rick Astley", "context": "Singer and performer of the song"}
for subject in analysis.key_subjects or []:
print(subject) # {"name": "commitment", "description": "...", "importance": "high"}
```
```python Uploaded file theme={null}
resp = client.analyze_file(
file_id="your_file_id",
query="What are the key action items from this meeting?",
)
print(resp.data.transcript_analysis.summary)
```
Pass `transcript_text=True` to also receive the transcript as one string. See the [Analyzing Videos guide](/guides/analyzing-videos) for billing details.
***
## Structured extraction
Describe the fields you want and the API fills them in from the transcript. See the [Data Extraction guide](/guides/data-extraction) for schema design tips.
### From an online video
```python theme={null}
resp = client.extract_video_data(
video_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ",
schema={
"mood": {"type": "String", "description": "Overall mood of the video in one word"},
"main_topics": {
"type": "Array",
"description": "List of main topics discussed",
"items": {"type": "String", "description": "A topic"},
},
"has_spoken_lyrics": {
"type": "Boolean",
"description": "Whether the video contains spoken or sung lyrics",
},
},
what_to_extract="Focus on the tone and content of the lyrics.",
)
print(resp.data)
# {"mood": "upbeat", "main_topics": ["love", "commitment"], "has_spoken_lyrics": True}
print(resp.video_info.title, resp.video_info.duration)
# "Rick Astley - Never Gonna Give You Up (Official Music Video)" 212.0
```
`extract_video_data` runs as a [background job](#background-jobs). With `transcribe=True` (the default), a video without captions is transcribed first, however long it is. Set `transcribe=False` to never run speech-to-text.
The extracted fields are in `resp.data`, and the video's metadata (a `VideoInfo`) in `resp.video_info`, so you don't need a separate `get_transcript(metadata_only=True)` call.
`video_info` on extraction results requires **2.1.0**. In 2.0.0 it is always `None`.
### Schema format
Each field needs a `type` and a `description`. A schema can have at most 10 top-level fields and 3 levels of nesting.
| `type` | Extra keys | Example value |
| - | - | - |
| `String`, `Number`, `Integer`, `Boolean` | -- | `"upbeat"`, `4.5`, `3`, `True` |
| `Enum` | `enum`: the allowed values | `"positive"` |
| `Array` | `items`: the schema of one element | `["love", "commitment"]` |
| `Object` | `properties`: the sub-fields | `{"name": "Rick", "role": "singer"}` |
```python theme={null}
schema = {
"sentiment": {
"type": "Enum",
"description": "Overall sentiment of the video",
"enum": ["positive", "negative", "neutral"],
},
"speaker": {
"type": "Object",
"description": "The main speaker or singer",
"properties": {
"name": {"type": "String", "description": "Their name"},
"role": {"type": "String", "description": "Their role in the video"},
},
},
"key_takeaway": {"type": "String", "description": "The single most important takeaway"},
}
```
An invalid schema raises `BadRequestError` with `error_code == "invalid_schema"` **before** anything is transcribed or billed.
### From a schema file
Keep schemas in JSON or YAML files and pass `schema_file` instead of `schema`:
```yaml meeting-schema.yaml theme={null}
action_items:
type: Array
description: Action items mentioned in the meeting
items:
type: String
description: One action item
decisions_made:
type: Array
description: Key decisions that were agreed upon
items:
type: String
description: One decision
```
```python Python theme={null}
resp = client.extract_video_data(video_url="...", schema_file="./meeting-schema.yaml")
```
### From an uploaded file
```python theme={null}
resp = client.extract_file_data(
file_id="your_file_id",
schema_file="./meeting-schema.yaml", # or schema={...}
what_to_extract="Extract action items and deadlines.",
)
print(resp.data)
```
### Token usage
```python theme={null}
resp = client.extract_video_data(
video_url="...",
schema={"summary": {"type": "String", "description": "One-line summary"}},
include_usage=True,
)
tokens = resp.usage.analysis_tokens
print(f"LLM tokens: {tokens.total_tokens} (prompt {tokens.prompt_tokens}, completion {tokens.completion_tokens})")
```
***
## Tweet claim analysis
```python theme={null}
resp = client.get_tweet_statement(tweet_id="1585841080431321088")
print(resp.data.final_statement) # the core claim, in the author's voice
print(resp.data.detailed_analysis) # claim, context, evidence and stance
print(resp.data.claim_type, resp.data.intent, resp.data.tone)
print(resp.data.topics, resp.data.entities)
print(resp.data.tweet_media_summary) # summary of attached video/images, if any
```
Videos attached to the tweet (or to the tweet it quotes) are transcribed in full, so this runs as a [background job](#background-jobs) too. `claim_type`, `intent`, `tone`, `emotion` and `authority` are based on the original tweet text only. See the [Tweet Claim Analysis guide](/guides/tweet-claim-analysis).
***
## Background jobs
Speech-to-text, extraction, tweet analysis and TikTok operations run as **background jobs** on VidNavigator's servers. The SDK always uses the [async endpoints](/guides/async-jobs), so media of any length works without special handling.
Each job operation comes in two shapes, which take the same parameters:
| Operation | Blocking: wait and return the result | Non-blocking: return a `Job` handle |
| - | - | - |
| Speech-to-text | `transcribe_video(...)` | `submit_transcribe_video(...)` |
| Structured extraction | `extract_video_data(...)` | `submit_extract_video_data(...)` |
| Tweet claim analysis | `get_tweet_statement(...)` | `submit_tweet_statement(...)` |
| TikTok profile scrape | `scrape_tiktok_profile(...)` | `submit_tiktok_profile_scrape(...)` |
| TikTok keyword search | `search_tiktok(...)` | `submit_tiktok_search(...)` |
With a handle, `include_usage`, `timeout` and (for TikTok) `limit` are passed to `job.result()` instead.
### Submit now, collect later
```python theme={null}
job = client.submit_transcribe_video(video_url="https://www.instagram.com/reel/C86ZvEaqRmo/")
print(job.task_id) # "550e8400-e29b-41d4-a716-446655440000" -- save it
print(job.status()) # "processing"
# ... do other work ...
resp = job.result() # waits if needed, then returns the same TranscriptResponse as transcribe_video
```
| Member | Description |
| - | - |
| `task_id` | The job's id. Keep it so you can [resume](#timeouts-never-lose-the-task_id) later |
| `job_type` | `"transcribe"`, `"extract_video"`, `"tweet_statement"`, `"tiktok_profile"` or `"tiktok_search"` |
| `status()` | Polls once and returns `"processing"`, `"completed"` or `"failed"` |
| `done()` | Polls once; `True` when the job is `completed` or `failed` |
| `result(...)` | Waits, then returns the same object as the blocking method; raises if the job failed |
| `wait(...)` | Waits, then returns the raw poll response (job metadata, the `request` you sent, `webhook` delivery status) |
| `refresh()` | Polls once and returns the raw poll response |
| `webhook_url`, `check_status_url`, `data` | What the API returned when the job was submitted |
| Argument | Default | Description |
| - | - | - |
| `timeout` | `3600` | Seconds to wait; `None` waits indefinitely |
| `include_usage` | `False` | Attach the job's `usage` block (available once completed) |
| `poll_interval` | `3` | Seconds between polls |
| `fast_start` | `True` | Poll every second for the first 10 seconds, so short clips return quickly |
| `limit`, `cursor` | -- | TikTok jobs only: which page of results to return |
| `raise_on_failure` | `True` | `wait()` only: pass `False` to get a failed job back instead of an exception |
| Operation | Returned by the blocking method and by `result()` |
| - | - |
| Transcribe | `TranscriptResponse` (`data.video_info`, `data.transcript`), or `TranscribeAllVideosResponse` with `all_videos=True` |
| Extract | `ExtractionApiResponse` (`data` is a `dict` matching your schema, `video_info` the video's metadata) |
| Tweet | `TweetStatementResponse` (`data.final_statement`, `data.detailed_analysis`, ...) |
| TikTok profile / search | `TikTokProfileResponse` / `TikTokSearchResponse`: the first page of results |
Polling is free and not rate-limited. A job's result stays readable for **1 hour after it finishes**, and reading it does not consume it.
### Timeouts: never lose the `task_id`
Blocking calls and `result()` wait up to `timeout` seconds (default one hour), then raise `JobTimeoutError`. The job **keeps running on the server**, and the error carries the `task_id` and a ready-to-use handle:
```python theme={null}
from vidnavigator import JobTimeoutError
try:
resp = client.transcribe_video(video_url=url, timeout=600)
except JobTimeoutError as exc:
print("Still running:", exc.task_id)
resp = exc.job.result() # keep waiting on the same job
```
Later, or from another process, rebuild the handle from a saved id with `resume_job`:
```python theme={null}
job = client.resume_job("transcribe", task_id)
if job.done():
resp = job.result()
```
### Failures
A failed job raises the exception that matches its `error.http_status`, so the same `except` clauses work for direct calls and jobs. A failed job has all its charges reverted.
```python theme={null}
from vidnavigator import BadRequestError, PaymentRequiredError, VidNavigatorError
try:
resp = client.transcribe_video(video_url=url)
except PaymentRequiredError:
print("Out of transcription credit")
except BadRequestError as exc:
print(exc.error_code, exc) # e.g. "unsupported_platform"
except VidNavigatorError as exc:
print("Job failed:", exc)
```
To inspect a failure without an exception, use `job.wait(raise_on_failure=False)` and read `resp.data.error`.
Some errors happen **at submit time**, before a job exists:
| Exception | Status | When |
| - | - | - |
| `PaymentRequiredError` | 402 | Not enough credit to start the job. Can also be raised by `result()` if the job runs out of credit while running |
| `TooManyActiveJobsError` | 429 | Too many jobs already running for your account; retry once some finish ([recipe](#stay-under-the-running-job-limit)) |
| `BadRequestError` | 400 | Invalid parameters, an invalid extraction schema, or a non-public `webhook_url` |
***
## Recipes
### Process many videos at once
Submit every job first, then collect the results. All the jobs run **at the same time on the server**:
```python theme={null}
urls = [
"https://www.tiktok.com/@a/video/1",
"https://www.tiktok.com/@b/video/2",
"https://www.tiktok.com/@c/video/3",
]
jobs = [client.submit_transcribe_video(video_url=url, transcript_text=True) for url in urls]
results = [job.result() for job in jobs]
for url, resp in zip(urls, results):
print(url, resp.data.transcript[:80])
```
With jobs that take 60 s, 40 s and 90 s, the batch takes about **90 s** (the slowest job). Calling the blocking `transcribe_video` in a loop would take 190 s.
To let one job fail without stopping the batch, catch errors per job:
```python theme={null}
from vidnavigator import VidNavigatorError
for url, job in zip(urls, jobs):
try:
print(url, job.result().data.transcript[:80])
except VidNavigatorError as exc:
print(url, "failed:", exc.error_code, exc)
```
### Handle results as soon as they finish
```python theme={null}
import time
pending = list(jobs)
while pending:
for job in list(pending):
if job.done(): # one free poll
handle(job.result()) # already finished: returns immediately
pending.remove(job)
time.sleep(3)
```
### Stay under the running-job limit
Past your account's limit, a submit raises `TooManyActiveJobsError` and no task is created. For large batches, keep a bounded number of jobs running:
```python theme={null}
import time
from vidnavigator import TooManyActiveJobsError, VidNavigatorError
MAX_RUNNING = 5
queue = list(urls)
running = [] # (url, job) pairs
results, errors = {}, {}
while queue or running:
# Fill free slots
while queue and len(running) < MAX_RUNNING:
try:
running.append((queue[0], client.submit_transcribe_video(video_url=queue[0])))
queue.pop(0)
except TooManyActiveJobsError:
break # the account is at its limit; retry after some jobs finish
# Collect finished jobs
for url, job in list(running):
if job.done():
try:
results[url] = job.result()
except VidNavigatorError as exc:
errors[url] = exc
running.remove((url, job))
time.sleep(3)
```
### Survive a restart
Save the `task_id` when you submit, and resume with `resume_job`:
```python theme={null}
import json
job = client.submit_extract_video_data(video_url=url, schema=schema)
with open("jobs.json", "w") as f:
json.dump({"job_type": job.job_type, "task_id": job.task_id}, f)
# ... after a restart ...
with open("jobs.json") as f:
saved = json.load(f)
resp = client.resume_job(saved["job_type"], saved["task_id"]).result()
```
### Transcribe every video of a TikTok profile
```python theme={null}
profile = client.scrape_tiktok_profile(profile_url="https://www.tiktok.com/@tiktok", max_posts=20, limit=20)
jobs = [
client.submit_transcribe_video(video_url=video.url, transcript_text=True)
for video in profile.data.videos or []
if video.url
]
for job in jobs:
print(job.result().data.transcript[:80])
```
***
## Async client
`AsyncVidNavigatorClient` has the same methods, arguments and return types as `VidNavigatorClient`, as coroutines. Use it from asyncio code such as FastAPI, aiohttp, Discord bots or agent frameworks: while a job runs, waiting uses `asyncio.sleep`, so the event loop keeps serving other work.
New in **2.1.0**. Install the `async` extra: `pip install "vidnavigator[async]==2.1.0"`.
```python theme={null}
import asyncio
from vidnavigator import AsyncVidNavigatorClient
async def main():
async with AsyncVidNavigatorClient() as client: # reads VIDNAVIGATOR_API_KEY
resp = await client.transcribe_video(video_url="https://www.instagram.com/reel/C86ZvEaqRmo/")
print(resp.data.transcript)
asyncio.run(main())
```
### Many jobs at once
`asyncio.gather` runs every call concurrently. Each job is submitted immediately and polled in the background of the same event loop:
```python theme={null}
async with AsyncVidNavigatorClient() as client:
results = await asyncio.gather(
*(client.transcribe_video(video_url=url, transcript_text=True) for url in urls),
return_exceptions=True, # one failed job doesn't cancel the others
)
for url, resp in zip(urls, results):
if isinstance(resp, Exception):
print(url, "failed:", resp)
else:
print(url, resp.data.transcript[:80])
```
To cap how many jobs run at once (see [the running-job limit](#stay-under-the-running-job-limit)), wrap each call in an `asyncio.Semaphore`:
```python theme={null}
limit = asyncio.Semaphore(5)
async def transcribe(url):
async with limit:
return await client.transcribe_video(video_url=url)
results = await asyncio.gather(*(transcribe(url) for url in urls))
```
### Async job handles
`submit_*` methods return an `AsyncJob`. It has the same members as [`Job`](#submit-now-collect-later), with `status()`, `done()`, `refresh()`, `wait()` and `result()` as coroutines.
```python theme={null}
job = await client.submit_extract_video_data(video_url=url, schema=schema)
print(job.task_id)
if not await job.done():
... # do other work
resp = await job.result()
print(resp.data, resp.video_info.title)
```
Timeouts work as in the sync client: `JobTimeoutError` carries the `task_id` and an `AsyncJob` handle, so `await exc.job.result()` resumes waiting. `client.resume_job(job_type, task_id)` rebuilds a handle later; it is a plain method, not a coroutine.
### In a FastAPI app
Create one client for the app's lifetime and share it across requests:
```python theme={null}
from contextlib import asynccontextmanager
from fastapi import FastAPI
from vidnavigator import AsyncVidNavigatorClient
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.vidnavigator = AsyncVidNavigatorClient()
yield
await app.state.vidnavigator.aclose()
app = FastAPI(lifespan=lifespan)
@app.post("/jobs")
async def start_transcription(video_url: str):
job = await app.state.vidnavigator.submit_transcribe_video(video_url=video_url)
return {"task_id": job.task_id}
@app.get("/jobs/{task_id}")
async def job_status(task_id: str):
job = app.state.vidnavigator.resume_job("transcribe", task_id)
resp = await job.refresh()
return {"status": resp.data.task_status}
```
### Async configuration
```python theme={null}
import httpx
client = AsyncVidNavigatorClient(
api_key="YOUR_API_KEY", # or set VIDNAVIGATOR_API_KEY
base_url="https://...", # override the API base URL
timeout=60, # per HTTP request, in seconds (default: 30)
http_client=httpx.AsyncClient(limits=httpx.Limits(max_connections=20)), # optional; you close it yourself
)
```
* Close the client with `await client.aclose()`, or use `async with`. A client you pass as `http_client` is not closed for you.
* The [webhook helpers](#webhooks) are plain functions and work unchanged in async code.
* The deprecated aliases `get_youtube_transcript` and `search_videos` are not available on the async client. Use `get_transcript` and `search_youtube`.
***
## Webhooks
Every job method, blocking or `submit_*`, accepts a `webhook_url`. **Webhooks are optional:** polling always works, whether or not a webhook is configured.
```python theme={null}
job = client.submit_extract_video_data(
video_url="https://www.tiktok.com/@user/video/1234567890",
schema={"summary": {"type": "String", "description": "One-line summary"}},
webhook_url="https://example.com/hooks/vidnavigator",
)
print(job.webhook_url) # which webhook the job will call, or None
```
* The URL must be a publicly reachable `https` address; private, loopback and link-local hosts raise `BadRequestError`.
* A default webhook can be set in [Studio → API](https://vidnavigator.com/studio/api). A per-request `webhook_url` overrides it, and `webhook_url=""` opts a single job out.
### Receive and verify deliveries
`construct_webhook_event` checks the HMAC-SHA256 signature and the timestamp, then returns a typed `WebhookEvent`. Always pass the **raw** request body:
```python theme={null}
from flask import Flask, request
from vidnavigator import VidNavigatorClient, WebhookSignatureError, construct_webhook_event
app = Flask(__name__)
client = VidNavigatorClient()
WEBHOOK_SECRET = "your_signing_secret"
@app.post("/hooks/vidnavigator")
def vidnavigator_webhook():
try:
event = construct_webhook_event(
request.get_data(),
request.headers.get("X-VidNavigator-Signature"),
WEBHOOK_SECRET,
)
except WebhookSignatureError:
return "invalid signature", 400
delivery_id = request.headers.get("X-VidNavigator-Delivery") # stable across retries
if already_processed(delivery_id):
return "", 200
if event.type == "transcribe.completed":
# The job handle gives the typed result, even when the event omits it
resp = client.resume_job("transcribe", event.data.task_id).result()
save_transcript(event.data.task_id, resp.data.transcript)
elif event.type.endswith(".failed"):
print(event.data.task_id, event.data.error.error, event.data.error.message)
return "", 200
```
* `event.data.result` also holds the result as a plain `dict`, unless it was over 256 KB (`data.result_truncated` is `True`).
* For `extract_video.completed` events, `event.data.video_info` holds the video's metadata as a plain `dict`. It is sent even when the result was truncated.
* For TikTok events, `event.data.result` is only a summary (`stats`, `download_url_available`); read the videos with `client.resume_job("tiktok_search", event.data.task_id).result()`.
* Deliveries more than 5 minutes old are rejected; change the window with `tolerance_seconds=` (or `None` to skip the check).
* `verify_webhook_signature(body, header, secret)` verifies without parsing and raises the same `WebhookSignatureError`.
Event types are `transcribe.*`, `extract_video.*`, `tweet_statement.*`, `tiktok_profile.*` and `tiktok_search.*`, each `.completed` or `.failed`. See [Webhooks](/webhooks) for the full delivery contract.
***
## TikTok
### Profile scraping
`scrape_tiktok_profile` runs a [background job](#background-jobs) and returns the first page of videos:
```python theme={null}
result = client.scrape_tiktok_profile(
profile_url="https://www.tiktok.com/@tiktok",
max_posts=100,
after_datetime="2024-01-01",
limit=50,
)
task_id = result.data.task_id
for video in result.data.videos or []:
print(video.title, video.published_at, video.likes, video.url)
```
| Parameter | Type | Description |
| - | - | - |
| `profile_url` | `str` | Public TikTok profile URL |
| `max_posts` | `int` | Stop once this many matching videos are collected |
| `after_datetime` / `before_datetime` | `str` / `date` / `datetime` | Only include videos published in this window |
| `min_likes` / `max_likes` | `int` | Like-count filters |
| `webhook_url` | `str` | Optional; see [Webhooks](#webhooks) |
| `limit` | `int` | Page size of the returned first page (default 50) |
| `include_usage`, `timeout` | | As for other [job methods](#background-jobs) |
`video.published_at` is a Python `datetime`, and `views`, `likes`, `reposts` and `comments` are integers. To start the scrape without waiting, use `submit_tiktok_profile_scrape(...)`, then `job.result(limit=50)`.
**Read the remaining pages:**
```python theme={null}
pagination = result.data.pagination
while pagination and pagination.has_next:
page = client.get_tiktok_profile_scrape(task_id, cursor=pagination.next_cursor, limit=50)
for video in page.data.videos or []:
print(video.title, video.url)
pagination = page.data.pagination
```
**Or download the full result** from the short-lived (about 1 hour) `download_url`. Calling `get_tiktok_profile_scrape` again mints a fresh link.
```python theme={null}
import json
from urllib.request import urlopen
if result.data.download_url:
with urlopen(result.data.download_url) as response:
full_profile = json.load(response)
for video in full_profile.get("videos", []):
print(video.get("title"), video.get("url")) # plain dicts, not typed models
```
### Keyword search
`search_tiktok` works the same way and returns the first page of results:
```python theme={null}
result = client.search_tiktok(
query="ai tools",
max_results=100,
sort_by="most_liked",
published_within="this_month",
min_views=1000,
limit=50,
)
for item in result.data.results or []:
print(item.description, item.published_at, item.stats.views if item.stats else None, item.url)
print(result.data.stats.sort_by, result.data.stats.published_within) # what the search actually ran with
```
| Parameter | Type | Description |
| - | - | - |
| `query` | `str` | Keyword phrase (at least 2 characters) |
| `max_results` | `int` | Cap on merged results. `0` or omitted means no cap |
| `parallel_search_slices` | `int` | `1`-`4` concurrent search chains. More slices return more unique videos but bill up to N times the pages |
| `sort_by` | `str` | `"relevance"`, `"most_liked"` or `"newest"` |
| `published_within` | `str` | `"all"`, `"past_24_hours"`, `"this_week"`, `"this_month"`, `"last_3_months"` or `"last_6_months"` |
| `after_datetime` / `before_datetime` | `str` / `date` / `datetime` | Exact publish-date bounds |
| `min_likes` / `max_likes` / `min_views` / `max_views` | `int` | Engagement filters |
| `webhook_url` | `str` | Optional; see [Webhooks](#webhooks) |
| `limit` | `int` | Page size of the returned first page (default 50) |
Read further pages with `get_tiktok_search(task_id, cursor=...)`, exactly as for profiles. See the [Searching TikTok guide](/guides/tiktok-search).
***
## Search
### Search YouTube
Runs a YouTube search, then ranks the results with AI analysis of their transcripts:
```python theme={null}
results = client.search_youtube(
query="how to train a neural network from scratch",
start_year=2023,
max_results=5,
)
print(f"Found {results.data.total_found} results")
print(results.data.explanation)
for r in results.data.results:
print(f"{r.title} (score: {r.relevance_score})")
print(f" {r.transcript_summary}")
```
| Parameter | Type | Description |
| - | - | - |
| `query` | `str` | Natural-language search query |
| `use_enhanced_search` | `bool` | AI-enhanced query (default: `True`) |
| `start_year` / `end_year` | `int` | Filter by publish date |
| `focus` | `str` | `"relevance"` (default), `"popularity"`, or `"brevity"` |
| `duration` | `int` | Filter by video duration in seconds |
| `max_results` | `int` | Max candidate videos to analyze; each one is billed, so this caps cost |
| `include_usage` | `bool` | Attach a per-call `usage` block |
`search_videos` still works as a deprecated alias for `search_youtube`.
### Search your uploaded files
```python theme={null}
results = client.search_files(
query="customer feedback about pricing",
namespace_ids=["ns_client_calls"], # optional: scope to specific namespaces
)
for r in results.data.results:
print(f"{r.name} (score: {r.relevance_score})")
print(f" Timestamps: {r.timestamps}")
print(f" {r.relevant_text}")
```
***
## Files
Supported formats: mp4, webm, mov, avi, wmv, flv, mkv, m4a, mp3, mpeg, mpga, wav.
### Upload and process
```python theme={null}
result = client.upload_file("./meeting.mp4", wait_for_completion=True)
file_id = result["file_id"]
print(result["file_status"]) # "completed"
```
With `wait_for_completion=False` (the default), the upload returns immediately and processing continues in the background. Check `get_file(file_id).data.file_info.status` until it is `"completed"`.
### Retrieve a file and its transcript
```python theme={null}
resp = client.get_file(file_id) # pass transcript_text=True for a single string
info = resp.data.file_info
print(f"{info.name} ({info.duration}s, {info.status})")
print(f"Namespaces: {[ns.name for ns in info.namespaces or []]}")
for segment in resp.data.transcript or []:
print(f"[{segment.start:.1f}s] {segment.text}")
```
### List and filter files
```python theme={null}
resp = client.get_files(limit=20, status="completed")
for f in resp.data.files:
print(f"{f.name} -- {f.status}")
resp = client.get_files(namespace_id="ns_client_calls") # filter by namespace
resp = client.get_files(limit=20, offset=20) # paginate
print(resp.data.has_more)
```
### Manage files
```python theme={null}
client.get_file_url(file_id) # signed URL to view or download the file
client.retry_file_processing(file_id) # retry after a failure
client.cancel_file_upload(file_id) # cancel in-progress processing
client.delete_file(file_id) # permanently delete
```
### Namespaces
Group uploaded files by project, client or topic, then scope listings and searches to them.
```python theme={null}
ns = client.create_namespace(name="Client Calls")
print(ns.data.id) # "ns_abc123"
for ns in client.get_namespaces().data:
print(f"{ns.id}: {ns.name}")
client.update_namespace("ns_abc123", name="Sales Calls")
# Upload a file straight into a namespace
client.upload_file("./call.mp3", namespace_ids=["ns_abc123"])
# Reassign a file's namespaces
resp = client.update_file_namespaces(file_id, namespace_ids=["ns_abc123", "ns_def456"])
print(resp.data.namespaces)
# Delete (files are unlinked, not deleted)
client.delete_namespace("ns_abc123")
```
***
## Usage and billing
### Per-call usage
Pass `include_usage=True` to see which meters a call charged, how many credits were deducted, and how many LLM tokens were used. It is supported by `get_transcript`, `transcribe_video`, `analyze_video`, `analyze_file`, `extract_video_data`, `extract_file_data`, `search_youtube`, `search_files`, `get_tweet_statement`, the TikTok methods, and `job.result(include_usage=True)`.
```python theme={null}
resp = client.search_youtube(query="react best practices", include_usage=True)
usage = resp.usage
if usage:
print(f"Credits this call: {usage.total_credits}")
for charge in usage.charges or []:
print(f" {charge.service_type}: {charge.quantity} unit(s), {charge.credits} credits")
tokens = usage.analysis_tokens # analyze / extract / YouTube search / tweet analysis
if tokens:
print(f" LLM tokens: {tokens.total_tokens}")
```
| Field | Description |
| - | - |
| `charges` | One `UsageCharge` per meter: `service_type`, `quantity`, `credits`, `waived`, `credits_saved`, and (analysis only) nested `tokens` |
| `total_credits` | Net credits deducted by the call |
| `waived` | Present when a cache hit waived charges (`waived.credits_saved`) |
| `analysis_tokens` | LLM token counts (`prompt_tokens`, `completion_tokens`, `total_tokens`) |
| `charge_for(service_type)` | The charge for one meter, e.g. `usage.charge_for("transcription_hour")` |
Meters: `standard_request`, `residential_request`, `transcription_hour`, `analysis_request`, `search_request` and `scene_analysis_hour`. See [Usage & Costs](/api-reference/usage-costs).
### Account usage
```python theme={null}
usage = client.get_usage().data
print(f"Credits: {usage.credits.monthly_remaining} / {usage.credits.monthly_total}")
print(f"Storage: {usage.storage.used_formatted} / {usage.storage.limit_formatted}")
if usage.usage:
print(f"Standard requests: {usage.usage.standard_request.used}")
print(f"Transcription hours: {usage.usage.transcription_hour.used}")
```
`client.health_check()` returns the API status and needs no authentication.
***
## Error handling
Every API error maps to a specific exception, all inheriting from `VidNavigatorError`:
```python theme={null}
from vidnavigator import BadRequestError, NotFoundError, RateLimitExceeded, VidNavigatorError
try:
client.get_file("nonexistent-id")
except NotFoundError:
print("File not found")
except RateLimitExceeded:
print("Rate limit hit -- slow down")
except VidNavigatorError as exc:
print("Something else went wrong:", exc)
```
API exceptions carry the details of the error response:
```python theme={null}
try:
client.extract_video_data(video_url=url, schema={"x": {"type": "Nope", "description": "?"}})
except BadRequestError as exc:
print(exc.status_code) # 400
print(exc.error_code) # "invalid_schema" -- machine-readable, safe to branch on
print(exc.docs_url) # link to the relevant docs, when provided
print(exc.payload) # the full JSON error body
```
| Exception | Status | When |
| - | - | - |
| `BadRequestError` | 400 | Invalid parameters or schema |
| `AuthenticationError` | 401 | Missing or invalid API key |
| `PaymentRequiredError` | 402 | Not enough credits |
| `AccessDeniedError` | 403 | Insufficient permissions, or content that requires login or age verification |
| `NotFoundError` | 404 | Resource not found, including an unknown or expired job `task_id` |
| `StorageQuotaExceededError` | 413 | Storage quota exceeded |
| `RateLimitExceeded` | 429 | Too many requests |
| `TooManyActiveJobsError` | 429 | Too many background jobs already running (subclass of `RateLimitExceeded`) |
| `GeoRestrictedError` | 451 | Content unavailable in your region |
| `SystemOverloadError` | 503 | Temporary overload (check `.retry_after_seconds`) |
| `ServerError` | 5xx | Unexpected server error |
| `JobTimeoutError` | -- | A blocking call or `job.result()` hit its `timeout`. The job keeps running; resume with `.job` or `.task_id` |
| `WebhookSignatureError` | -- | A webhook delivery failed signature or timestamp verification |
| `VidNavigatorError` | -- | Base class for all errors, also raised for network failures |
See [Errors](/api-reference/errors) for every API error code.
***
## What's new in 2.1.0
* **[Async client](#async-client):** `AsyncVidNavigatorClient` and `AsyncJob`, with the same methods as the sync client, as coroutines. Install with `pip install "vidnavigator[async]"`.
* **`video_info` on extraction:** `extract_video_data(...)` and `submit_extract_video_data(...).result()` now return the video's metadata in `resp.video_info`, and `extract_video.completed` webhook events carry it in `event.data.video_info`.
* **Python 3.8+:** support for Python 3.7 was dropped.
No other changes are needed to upgrade from 2.0.0.
## Upgrading from 1.x
Version 2.0 runs speech-to-text, extraction, tweet analysis and TikTok operations as background jobs. Most code keeps working unchanged. Check these points:
| In 1.x | In 2.0 |
| - | - |
| `transcribe_video`, `extract_video_data` and `get_tweet_statement` made one long HTTP request | They submit a job and poll it. Same arguments and return types, no media duration limit. They can raise `JobTimeoutError` (default wait: one hour) |
| `extract_video_data(..., include_usage=True).usage.total_tokens` | Use `usage.analysis_tokens.total_tokens` |
| `submit_tiktok_profile_scrape` / `submit_tiktok_search` returned the submit response | They return a `Job`. `job.data.task_id` still works; **`job.status` is now a method** |
| Manual polling loops on `get_tiktok_profile_scrape` | `scrape_tiktok_profile(...)`, or `job.result()` on a submitted job |
| `task.data.error_message` on failed TikTok tasks | Deprecated; read `task.data.error` (`error`, `message`, `http_status`), or let `job.result()` raise |
| `search_videos(...)` | `search_youtube(...)` (the old name is a deprecated alias) |
| A 401 raised the generic `VidNavigatorError` | It raises `AuthenticationError` |
**New in 2.0:** `submit_transcribe_video`, `submit_extract_video_data`, `submit_tweet_statement`, `scrape_tiktok_profile`, `search_tiktok`, `resume_job`, `webhook_url` on every job method, `sort_by` and `published_within` for TikTok search, the webhook helpers, and `status_code` / `error_code` / `docs_url` / `payload` on every API exception.
***
## Resources
Package page and release history.
Source code and issue tracker.
The same API for Node.js and TypeScript.
Every endpoint, parameter and response field.
# Webhooks
Source: https://docs.vidnavigator.com/webhooks
Get called back when an async job finishes instead of polling.
## Overview
Rather than polling, an [async job](/guides/async-jobs) can call you back when it reaches a terminal state (`completed` or `failed`). Webhooks are available for every async endpoint:
* `POST /transcribe/async`
* `POST /extract/video/async`
* `POST /tweet/statement/async`
* `POST /tiktok/profile`
* `POST /tiktok/search`
## Configure a webhook
There are two ways to set the endpoint, and they combine:
In **Studio → API**, set a default webhook URL and get your **signing secret**. Every async job uses it unless the request says otherwise.
Pass `webhook_url` in the body of the submit request. It **overrides** the account default for that job only.
| `webhook_url` in the request | Result |
| - | - |
| omitted | The account default from Studio is used (if one is configured). |
| `"https://…"` | This URL is used for this job, instead of the default. |
| `""` (empty string) | No webhook for this job, even if a default is configured. |
```bash cURL theme={null}
curl -X POST "https://api.vidnavigator.com/v1/transcribe/async" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://www.instagram.com/reel/C86ZvEaqRmo/",
"webhook_url": "https://example.com/hooks/vidnavigator"
}'
```
The URL must be a **publicly reachable `https` URL**. Private, loopback and link-local hosts are rejected. The submit response echoes the URL that will be used in `data.webhook_url` (or `null`).
## The event
When the job finishes, VidNavigator sends a `POST` with a JSON body to your URL.
```json theme={null}
{
"id": "evt_9f2c1b4e8a7d4c1e9b3a5f6d7c8e9a0b",
"type": "transcribe.completed",
"created_at": "2026-09-23T12:03:41Z",
"api_version": "1.0.0",
"data": {
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"task_status": "completed",
"job_type": "transcribe",
"check_status_url": "/v1/transcribe/550e8400-e29b-41d4-a716-446655440000",
"result": {
"video_info": { "title": "A long podcast episode", "duration": 3540.0 },
"transcript": [{ "text": "Welcome to the show.", "start": 0.5, "end": 2.1 }]
},
"result_truncated": false,
"error": null
}
}
```
| Field | Description |
| - | - |
| `id` | Unique event id. |
| `type` | Event type (see below). |
| `created_at` | When the event was created. |
| `api_version` | API version that produced the event. |
| `data.task_id` | The job's `task_id`. |
| `data.task_status` | `completed` or `failed`. |
| `data.job_type` | The job type. |
| `data.check_status_url` | Where to fetch the full result. |
| `data.result` | The job's result — same shape as the synchronous endpoint's `data` block. Omitted when `result_truncated` is `true`. |
| `data.video_info` | `extract_video` events only: the video's metadata, the same `video_info` the synchronous `POST /extract/video` returns beside `data`. Sent even when `result_truncated` is `true`. |
| `data.result_truncated` | `true` when the result exceeded **256 KB** and was left out. Fetch it from `check_status_url`. |
| `data.error` | On failure: `error`, `message`, `http_status` — the same codes as the synchronous endpoint. |
### Event types
| Type | Sent by |
| - | - |
| `transcribe.completed` / `transcribe.failed` | `POST /transcribe/async` |
| `extract_video.completed` / `extract_video.failed` | `POST /extract/video/async` |
| `tweet_statement.completed` / `tweet_statement.failed` | `POST /tweet/statement/async` |
| `tiktok_profile.completed` / `tiktok_profile.failed` | `POST /tiktok/profile` |
| `tiktok_search.completed` / `tiktok_search.failed` | `POST /tiktok/search` |
**TikTok events are notifications, not payloads.** A scrape can hold thousands of videos, so `tiktok_profile.*` and `tiktok_search.*` events carry the task `stats` and point you to `check_status_url` to read the results (paginated or through `download_url`).
### Headers
| Header | Description |
| - | - |
| `X-VidNavigator-Event` | Same as `type`. |
| `X-VidNavigator-Delivery` | Delivery id, **stable across retries**. Deduplicate on it. |
| `X-VidNavigator-Task-Id` | The job's `task_id`. |
| `X-VidNavigator-Signature` | `t=,v1=` |
## Verify the signature
Every delivery is signed with HMAC-SHA256 using your **signing secret** from [Studio → API](https://vidnavigator.com/studio/api).
1. Parse `t` and `v1` from `X-VidNavigator-Signature`.
2. Compute `HMAC-SHA256(secret, "{t}.{raw_request_body}")` as hex. Use the **raw** body bytes, before any JSON parsing.
3. Compare with `v1` in constant time.
4. Reject deliveries whose `t` is more than \~5 minutes old (replay protection).
```python Python (Flask) theme={null}
import hashlib
import hmac
import time
from flask import Flask, abort, request
app = Flask(__name__)
SIGNING_SECRET = b"YOUR_SIGNING_SECRET"
seen_deliveries = set() # use a persistent store in production
@app.post("/hooks/vidnavigator")
def vidnavigator_webhook():
header = request.headers.get("X-VidNavigator-Signature", "")
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
t, v1 = parts.get("t"), parts.get("v1")
if not t or not v1 or abs(time.time() - int(t)) > 300:
abort(400)
raw_body = request.get_data()
expected = hmac.new(SIGNING_SECRET, f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, v1):
abort(401)
delivery_id = request.headers["X-VidNavigator-Delivery"]
if delivery_id in seen_deliveries:
return "", 200 # already processed
seen_deliveries.add(delivery_id)
event = request.get_json()
if event["data"]["task_status"] == "completed":
... # use event["data"]["result"], or fetch check_status_url if result_truncated
else:
... # event["data"]["error"]
return "", 200
```
```javascript Node.js (Express) theme={null}
const crypto = require('crypto');
const express = require('express');
const app = express();
const SIGNING_SECRET = 'YOUR_SIGNING_SECRET';
const seenDeliveries = new Set(); // use a persistent store in production
app.post('/hooks/vidnavigator', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-VidNavigator-Signature') || '';
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
const { t, v1 } = parts;
if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(400);
const expected = crypto
.createHmac('sha256', SIGNING_SECRET)
.update(`${t}.`)
.update(req.body) // raw Buffer
.digest('hex');
if (expected.length !== v1.length ||
!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) {
return res.sendStatus(401);
}
const deliveryId = req.get('X-VidNavigator-Delivery');
if (seenDeliveries.has(deliveryId)) return res.sendStatus(200);
seenDeliveries.add(deliveryId);
const event = JSON.parse(req.body);
// event.data.result, or fetch event.data.check_status_url if result_truncated
res.sendStatus(200);
});
```
### Verify with the SDK
The [Python](/sdks/python#webhooks) and [JavaScript](/sdks/javascript#webhooks) SDKs (v2.1.0) do steps 1–4 for you. `construct_webhook_event` / `constructWebhookEvent` checks the signature and the 5-minute window, then returns the parsed event. Reattaching to the job with its `task_id` gives you the full, typed result even when the event's `result` was truncated:
```python Python (Flask) theme={null}
from flask import Flask, request
from vidnavigator import VidNavigatorClient, WebhookSignatureError, construct_webhook_event
app = Flask(__name__)
client = VidNavigatorClient()
SIGNING_SECRET = "YOUR_SIGNING_SECRET"
@app.post("/hooks/vidnavigator")
def vidnavigator_webhook():
try:
event = construct_webhook_event(
request.get_data(), # raw body
request.headers.get("X-VidNavigator-Signature"),
SIGNING_SECRET,
)
except WebhookSignatureError:
return "invalid signature", 400
# Deduplicate on request.headers["X-VidNavigator-Delivery"] here
if event.type == "transcribe.completed":
resp = client.resume_job("transcribe", event.data.task_id).result()
print(resp.data.transcript)
elif event.type.endswith(".failed"):
print(event.data.task_id, event.data.error.error, event.data.error.message)
return "", 200
```
```javascript Node.js (Express) theme={null}
import express from 'express';
import { VidNavigatorClient, constructWebhookEvent, WebhookSignatureError, WEBHOOK_HEADERS } from 'vidnavigator';
const vn = new VidNavigatorClient({ apiKey: process.env.VIDNAVIGATOR_API_KEY });
const app = express();
app.post('/hooks/vidnavigator', express.raw({ type: 'application/json' }), async (req, res) => {
let event;
try {
event = constructWebhookEvent(
req.body, // raw Buffer
req.headers[WEBHOOK_HEADERS.signature],
process.env.VIDNAVIGATOR_WEBHOOK_SECRET
);
} catch (err) {
if (err instanceof WebhookSignatureError) return res.status(400).send('Invalid signature');
throw err;
}
res.sendStatus(200);
// Deduplicate on req.headers[WEBHOOK_HEADERS.delivery] here
if (event.type === 'transcribe.completed') {
const result = await vn.transcribe.resume(event.data.task_id).result();
console.log(result.transcript);
} else if (event.type.endsWith('.failed')) {
console.error(event.data.task_id, event.data.error);
}
});
```
## Delivery and retries
* **Acknowledge with any `2xx`**, quickly. Do heavy processing asynchronously on your side.
* Delivery is **at-least-once** and **best effort**: up to **5 attempts**, backing off over \~13 minutes.
* **Retried:** `5xx`, `429`, timeouts and network errors.
* **Not retried:** any other `4xx` — it is treated as a permanent rejection.
* Because the same event can arrive more than once, make your receiver **idempotent** on `X-VidNavigator-Delivery`.
* Treat **polling as the source of truth**: if a delivery never arrives, the result is still available on `check_status_url` for 1 hour after the job finishes.
### Delivery status on the job
The poll endpoint exposes the delivery state in `data.webhook` (it is `null` when no webhook applies). The URL itself is never echoed back, since it frequently embeds a token.
```json theme={null}
"webhook": {
"status": "failed",
"attempts": 5,
"response_status": 503,
"last_error": "HTTP 503",
"delivered_at": null
}
```
| Field | Description |
| - | - |
| `status` | `pending`, `delivered` or `failed`. |
| `attempts` | Delivery attempts made (max 5). |
| `response_status` | HTTP status your endpoint returned on the last attempt. |
| `last_error` | Last delivery error, if any. |
| `delivered_at` | When the delivery succeeded. |
## Auto-disable
An **account-level default** endpoint is switched off automatically when it fails **20 deliveries in a row over more than 72 hours**. The owner is notified in Studio.
* Both conditions must hold: a busy afternoon of failures won't disable a healthy endpoint, and neither will two failures a week apart.
* A single successful delivery resets the count.
* While disabled, jobs still run and results are still available by polling — only the callback stops.
* Re-enable it in [Studio → API](https://vidnavigator.com/studio/api) once the receiver is fixed.
* A per-request `webhook_url` is never auto-disabled, because it is not stored.