curl --request POST \
--url https://api.vidnavigator.com/v1/transcribe/async \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"video_url": "https://www.youtube.com/watch?v=long-podcast",
"transcript_text": false,
"all_videos": false,
"webhook_url": "https://example.com/hooks/vidnavigator"
}
'import requests
url = "https://api.vidnavigator.com/v1/transcribe/async"
payload = {
"video_url": "https://www.youtube.com/watch?v=long-podcast",
"transcript_text": False,
"all_videos": False,
"webhook_url": "https://example.com/hooks/vidnavigator"
}
headers = {
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
video_url: 'https://www.youtube.com/watch?v=long-podcast',
transcript_text: false,
all_videos: false,
webhook_url: 'https://example.com/hooks/vidnavigator'
})
};
fetch('https://api.vidnavigator.com/v1/transcribe/async', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.vidnavigator.com/v1/transcribe/async",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'video_url' => 'https://www.youtube.com/watch?v=long-podcast',
'transcript_text' => false,
'all_videos' => false,
'webhook_url' => 'https://example.com/hooks/vidnavigator'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.vidnavigator.com/v1/transcribe/async"
payload := strings.NewReader("{\n \"video_url\": \"https://www.youtube.com/watch?v=long-podcast\",\n \"transcript_text\": false,\n \"all_videos\": false,\n \"webhook_url\": \"https://example.com/hooks/vidnavigator\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-API-Key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.vidnavigator.com/v1/transcribe/async")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"video_url\": \"https://www.youtube.com/watch?v=long-podcast\",\n \"transcript_text\": false,\n \"all_videos\": false,\n \"webhook_url\": \"https://example.com/hooks/vidnavigator\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.vidnavigator.com/v1/transcribe/async")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"video_url\": \"https://www.youtube.com/watch?v=long-podcast\",\n \"transcript_text\": false,\n \"all_videos\": false,\n \"webhook_url\": \"https://example.com/hooks/vidnavigator\"\n}"
response = http.request(request)
puts response.read_body{
"status": "success",
"data": {
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"task_status": "processing",
"job_type": "transcribe",
"expires_at": "2023-11-07T05:31:56Z",
"check_status_url": "/v1/transcribe/550e8400-e29b-41d4-a716-446655440000",
"webhook_url": "<string>",
"message": "<string>",
"docs_url": "<string>"
}
}{
"status": "error",
"error": "<string>",
"message": "<string>"
}{
"status": "error",
"error": "limit_exceeded",
"error_code": "limit_exceeded",
"message": "<string>"
}{
"status": "error",
"error": "too_many_active_jobs",
"message": "<string>"
}{
"status": "error",
"error": "metadata_fetch_failed",
"message": "<string>"
}Transcribe Online Video (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.
curl --request POST \
--url https://api.vidnavigator.com/v1/transcribe/async \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"video_url": "https://www.youtube.com/watch?v=long-podcast",
"transcript_text": false,
"all_videos": false,
"webhook_url": "https://example.com/hooks/vidnavigator"
}
'import requests
url = "https://api.vidnavigator.com/v1/transcribe/async"
payload = {
"video_url": "https://www.youtube.com/watch?v=long-podcast",
"transcript_text": False,
"all_videos": False,
"webhook_url": "https://example.com/hooks/vidnavigator"
}
headers = {
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
video_url: 'https://www.youtube.com/watch?v=long-podcast',
transcript_text: false,
all_videos: false,
webhook_url: 'https://example.com/hooks/vidnavigator'
})
};
fetch('https://api.vidnavigator.com/v1/transcribe/async', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.vidnavigator.com/v1/transcribe/async",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'video_url' => 'https://www.youtube.com/watch?v=long-podcast',
'transcript_text' => false,
'all_videos' => false,
'webhook_url' => 'https://example.com/hooks/vidnavigator'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.vidnavigator.com/v1/transcribe/async"
payload := strings.NewReader("{\n \"video_url\": \"https://www.youtube.com/watch?v=long-podcast\",\n \"transcript_text\": false,\n \"all_videos\": false,\n \"webhook_url\": \"https://example.com/hooks/vidnavigator\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-API-Key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.vidnavigator.com/v1/transcribe/async")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"video_url\": \"https://www.youtube.com/watch?v=long-podcast\",\n \"transcript_text\": false,\n \"all_videos\": false,\n \"webhook_url\": \"https://example.com/hooks/vidnavigator\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.vidnavigator.com/v1/transcribe/async")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"video_url\": \"https://www.youtube.com/watch?v=long-podcast\",\n \"transcript_text\": false,\n \"all_videos\": false,\n \"webhook_url\": \"https://example.com/hooks/vidnavigator\"\n}"
response = http.request(request)
puts response.read_body{
"status": "success",
"data": {
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"task_status": "processing",
"job_type": "transcribe",
"expires_at": "2023-11-07T05:31:56Z",
"check_status_url": "/v1/transcribe/550e8400-e29b-41d4-a716-446655440000",
"webhook_url": "<string>",
"message": "<string>",
"docs_url": "<string>"
}
}{
"status": "error",
"error": "<string>",
"message": "<string>"
}{
"status": "error",
"error": "limit_exceeded",
"error_code": "limit_exceeded",
"message": "<string>"
}{
"status": "error",
"error": "too_many_active_jobs",
"message": "<string>"
}{
"status": "error",
"error": "metadata_fetch_failed",
"message": "<string>"
}task_id immediately, then collect the transcript by polling or through a webhook.
POST /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
Submit the job
POST /transcribe/async with the same body as POST /transcribe, plus an optional webhook_url. You get 202 Accepted with a task_id and a check_status_url.Wait for the result
GET /transcribe/{task_id} every few seconds while task_status is processing — polling is free — or receive a webhook when the job finishes.Read the result
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 likePOST /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; pass "" to opt this job out of the default. Must be a public https URL. See Webhooks. |
include_usage is not accepted on the submit request — pass it on the poll request instead.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"
}'
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"])
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)
{
"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
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. |
task_status is processing. Results are kept for 1 hour after the job finishes, and reading a task doesn’t delete it.
curl "https://api.vidnavigator.com/v1/transcribe/550e8400-e29b-41d4-a716-446655440000?include_usage=true" \
-H "X-API-Key: YOUR_API_KEY"
completed, data.result is identical to the data block of the synchronous response (video_info + transcript, or carousel_info + videos when all_videos=true).
{
"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
}
}
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
Passwebhook_url on the submit request, or configure a default endpoint in Studio → API, to be called back when the job finishes. See Webhooks for the payload and signature verification, and Async Jobs for the full workflow.Authorizations
API key authentication. Include your VidNavigator API key in the X-API-Key header.
Body
URL of the video to transcribe. For Instagram carousel posts, append ?img_index=N to select a specific video.
"https://www.youtube.com/watch?v=long-podcast"
When true, returns the transcript as a single plain-text string instead of an array of segments.
For Instagram carousel posts only. When true, transcribes ALL videos in the post.
Where to POST the result when the job finishes. Overrides the account-level default configured in Studio → API. Pass an empty string to opt this job out of that default. Must be a publicly reachable https URL — private, loopback and link-local hosts are rejected. See https://docs.vidnavigator.com/guides/webhooks
"https://example.com/hooks/vidnavigator"

