API reference
Each call below has a request and a response example, followed by the job states, error codes, limits and retention times. New here? Start with the developer guide.
Overview
- Base URL:
https://api.video-to-notes.com. Every path starts with/api/v1. - Requests and responses are JSON over HTTPS. Send
Content-Type: application/jsonwith every request body. - Times are Unix timestamps in seconds (UTC). Positions in a recording are seconds from the start of the file.
- IDs and cursors are opaque strings. Store them exactly as you receive them.
- Every error response contains a
request_id. Include it when you contact support. - Responses are sent with
Cache-Control: no-store; do not cache them in shared caches. - The machine-readable contract is in openapi.json (OpenAPI 3.1) and paid-api.schema.json (JSON Schema).
Authentication
Send your API key as a bearer token with every API request. Keys start with vtn_prod_. Create and revoke keys while signed in on the website; an API key cannot manage keys, billing or the account.
Authorization: Bearer vtn_prod_...A missing, malformed, expired or revoked key returns 401 UNAUTHORIZED. Never send the key to the signed storage URLs: they carry their own authorization. The curl examples on this page use this small helper, which passes the key to curl through standard input:
# Sends the key through stdin, so it never appears in the process list.
vtn() {
printf 'header = "Authorization: Bearer %s"\n' "$VTN_API_KEY" |
curl -s --config - "$@"
}Account
GET /api/v1/account
Returns the plan, the remaining plan seconds, the end of the current allowance and the capabilities and limits that apply to the account right now. Read it before you upload: when eligible or capabilities.api_admission_enabled is false, new jobs are refused.
vtn "$VTN_API_BASE/api/v1/account"{
"plan": "basic",
"eligible": true,
"remaining_seconds": 64800,
"allowance_expires_at": 1792886400,
"capabilities": {
"api_admission_enabled": true,
"key_creation_enabled": true,
"server_upload_enabled": true,
"long_jobs": true,
"formats": [
"mp4", "m4v", "mov", "webm", "mkv", "ogv", "avi", "wmv", "asf",
"flv", "mpg", "mpeg", "vob", "ts", "mts", "m2ts", "3gp", "3g2",
"mxf", "mp3", "m4a", "aac", "wav", "flac", "ogg", "oga", "opus",
"wma", "amr", "aiff", "aif", "caf", "ac3"
],
"audio_languages": [
"auto", "af", "am", "ar", "hy", "az", "be", "bn", "bs", "bg", "my",
"ca", "zh", "hr", "cs", "da", "nl", "en", "et", "tl", "fi", "fr",
"gl", "ka", "de", "el", "gu", "ha", "he", "hi", "hu", "is", "id",
"it", "ja", "kn", "kk", "km", "ko", "lv", "lt", "mk", "ms", "ml",
"mt", "mr", "mn", "ne", "no", "fa", "pl", "pt", "pa", "ro", "ru",
"sr", "sk", "sl", "es", "sw", "sv", "ta", "te", "th", "tr", "uk",
"uz", "vi"
],
"notes_languages": [
"auto", "af", "am", "ar", "hy", "az", "be", "bn", "bs", "bg", "my",
"ca", "zh", "hr", "cs", "da", "nl", "en", "et", "tl", "fi", "fr",
"gl", "ka", "de", "el", "gu", "ha", "he", "hi", "hu", "is", "id",
"it", "ja", "kn", "kk", "km", "ko", "lv", "lt", "mk", "ms", "ml",
"mt", "mr", "mn", "ne", "no", "fa", "pl", "pt", "pa", "ro", "ru",
"sr", "sk", "sl", "es", "sw", "sv", "ta", "te", "th", "tr", "uk",
"uz", "vi"
],
"download_formats": ["transcript_txt", "notes_md", "notes_html", "notes_pdf"],
"email_opt_in": false,
"limits": {
"max_upload_bytes": 8589934592,
"max_selection_seconds": null,
"active_jobs": 1,
"active_uploads": 1,
"daily_uploads": 10,
"daily_upload_bytes": 21474836480,
"new_requests_per_minute": 12,
"read_requests_per_minute": 60,
"poll_after_seconds": 5,
"result_page_bytes": 1000000,
"result_page_items": 32,
"upload_url_seconds": 900,
"download_url_seconds": 600,
"result_retention_seconds": 86400,
"recovery_retention_seconds": 172800,
"idempotency_retention_seconds": 34560000
}
}
}Uploads
Files are uploaded in parts straight to storage through signed URLs. An account can have one active upload at a time.
POST /api/v1/uploads
Starts an upload. Send the exact file size in bytes and the file extension. The response gives part_size and part_count: split the file into parts of part_size bytes; the last part may be smaller.
vtn -X POST "$VTN_API_BASE/api/v1/uploads" \
-H "Content-Type: application/json" \
-d '{
"byte_size": 1048576,
"extension": "mp4"
}'{
"upload_id": "upload-1",
"state": "created",
"expires_at": 1792807200,
"part_size": 16777216,
"part_count": 1,
"error": null
}POST /api/v1/uploads/{upload_id}/parts
Returns signed upload URLs for 1 to 100 part numbers. Each URL is valid for up to 15 minutes; request a new one when it expires or when a part has to be sent again.
vtn -X POST "$VTN_API_BASE/api/v1/uploads/upload-1/parts" \
-H "Content-Type: application/json" \
-d '{
"part_numbers": [1]
}'{
"upload_id": "upload-1",
"targets": [
{
"part_number": 1,
"url": "https://example.invalid/signed",
"expires_at": 1792800900
}
]
}PUT {signed part URL}
Send each part as the raw bytes of an HTTP PUT to its signed URL, without an Authorization header. Keep the ETag response header exactly as returned, including its quotes. Each URL accepts only its part's exact size: part_size for every part except the last, which carries the remaining bytes. A body of any other length is rejected with HTTP 403.
# PART_URL comes from the parts response; send no Authorization header.
curl -s --upload-file part-0001.bin --dump-header - --output /dev/null \
"$PART_URL" | grep -i '^etag:'POST /api/v1/uploads/{upload_id}/complete
Finishes the upload. Send every part number with its ETag. Completing an upload does not start processing; create a job next.
vtn -X POST "$VTN_API_BASE/api/v1/uploads/upload-1/complete" \
-H "Content-Type: application/json" \
-d '{
"parts": [
{
"part_number": 1,
"etag": "opaque-etag"
}
]
}'{
"upload_id": "upload-1",
"state": "uploaded",
"expires_at": 1792807200,
"part_size": 16777216,
"part_count": 1,
"error": null
}GET /api/v1/uploads/{upload_id}
Returns the state of an upload: created, uploading, uploaded, probing, probed, failed, aborted or expired.
vtn "$VTN_API_BASE/api/v1/uploads/upload-1"{
"upload_id": "upload-1",
"state": "uploaded",
"expires_at": 1792807200,
"part_size": 16777216,
"part_count": 1,
"error": null
}POST /api/v1/uploads/{upload_id}/abort
Cancels an upload you will not use and frees the upload slot of the account. Calling it again is safe.
vtn -X POST "$VTN_API_BASE/api/v1/uploads/upload-1/abort" \
-H "Content-Type: application/json" \
-d '{}'Jobs
A job processes one uploaded file into a transcript and notes. An account can run one job at a time.
POST /api/v1/jobs
Starts processing an uploaded file. The Idempotency-Key header is required: 1 to 128 printable ASCII characters without spaces, new for every job. Optional fields are range (start_seconds and end_seconds, both required when present), audio_language and notes_language. Returns 202 with the job in state preparing. The same key with the same body returns the same job; the same key with another body returns 409.
vtn -X POST "$VTN_API_BASE/api/v1/jobs" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"upload_id": "upload-1",
"range": {
"start_seconds": 5,
"end_seconds": 60
},
"audio_language": null,
"notes_language": "en"
}'{
"job_id": "job-1",
"state": "preparing",
"stage": "probe",
"created_at": 1792800000,
"expires_at": 1792807200,
"result_expires_at": null,
"idempotency_expires_at": 1827360000,
"poll_after_seconds": 5,
"status_url": "/api/v1/jobs/job-1",
"result_url": "/api/v1/jobs/job-1/result",
"completed_units": 0,
"total_units": null,
"charged_seconds": 0,
"result_available": false,
"can_resume": false,
"warnings": [],
"error": null
}GET /api/v1/jobs/{job_id}
Returns the state, the current stage, the progress (completed_units of total_units), charged_seconds, warnings and any error. A failed job still returns 200; read its error.
vtn "$VTN_API_BASE/api/v1/jobs/job-1"{
"job_id": "job-1",
"state": "completed",
"stage": "terminal",
"created_at": 1792800000,
"expires_at": 1792807200,
"result_expires_at": 1792886400,
"idempotency_expires_at": 1827360000,
"poll_after_seconds": 5,
"status_url": "/api/v1/jobs/job-1",
"result_url": "/api/v1/jobs/job-1/result",
"completed_units": 8,
"total_units": 8,
"charged_seconds": 1140,
"result_available": true,
"can_resume": false,
"warnings": [],
"error": null
}POST /api/v1/jobs/{job_id}/cancel
Stops a job. Finished parts stay available and reserved time that was not used is returned. Calling it again is safe.
vtn -X POST "$VTN_API_BASE/api/v1/jobs/job-1/cancel" \
-H "Content-Type: application/json" \
-d '{}'POST /api/v1/jobs/{job_id}/resume
Continues an interrupted job when its status reports can_resume: true. It needs an active plan and checks your remaining time again. Parts that were already processed are not charged again.
vtn -X POST "$VTN_API_BASE/api/v1/jobs/job-1/resume" \
-H "Content-Type: application/json" \
-d '{}'Results
GET /api/v1/jobs/{job_id}/result
Returns one page of results with up to 32 items and 1 MB. Items are ordered and have a kind: transcript (text with timed segments), notes (Markdown), gap (audio that could not be processed) or warning. Pass next_cursor as the cursor query parameter to get the next page, until it is null. Each page can list up to four download links in artifacts, valid for up to 10 minutes. If the job is resumed while you read, the API returns 409 JOB_CONFLICT; start again from the first page.
vtn --get "$VTN_API_BASE/api/v1/jobs/job-1/result" \
--data-urlencode "cursor=$NEXT_CURSOR" # omit for the first page{
"job_id": "job-1",
"snapshot": "generation-1",
"items": [
{
"index": 0,
"kind": "transcript",
"text": "Welcome to the weekly planning meeting.",
"segments": [
{
"text": "Welcome to the weekly planning meeting.",
"start_seconds": 0,
"end_seconds": 4.2,
"timing_quality": "approximate",
"timing_method": "provider_segment"
}
]
},
{
"index": 0,
"kind": "notes",
"text": "## Summary\n\nThe team agreed on the launch plan.\n\n## Key points\n\n- Launch moves to Monday."
}
],
"next_cursor": "opaque-account-bound-cursor",
"artifacts": [
{
"format": "notes_md",
"url": "https://example.invalid/signed-notes",
"expires_at": 1792800600
}
],
"charged_seconds": 1140,
"partial": false,
"expires_at": 1792886400
}Each transcript segment reports its timing_quality: aligned, approximate or unavailable. Approximate times are close to the speech but not accurate to the word.
Download formats are transcript_txt (TXT), notes_md (Markdown), notes_html (HTML) and notes_pdf (PDF). Long notes can be split across several files; download them in order. The account call lists the formats available right now.
Job states
| State | Meaning |
|---|---|
preparing | The server is checking the file and planning the work. |
queued | The job is waiting to start. |
processing | The audio is being extracted, transcribed and turned into notes. |
completed | Finished. The full transcript and the notes are available. |
completed_with_gaps | Finished, but some audio could not be processed. See the gap items. |
interrupted | Stopped before the end. Check can_resume. |
cancelled | Stopped at your request. Finished parts stay available. |
failed | The job could not be processed. See error. |
expired | The content is no longer kept. |
stage shows the step in progress: probe, planning, admission, extraction, transcription, notes, publication and finally terminal.
Errors
Every error has the same JSON body. retryable tells you whether the same request can succeed later, and retry_after_seconds how long to wait.
{
"error": {
"code": "RATE_LIMITED",
"message": "Request refused.",
"retryable": true,
"retry_after_seconds": 5
},
"request_id": "request-1"
}| Code | HTTP | Meaning | What to do |
|---|---|---|---|
UNAUTHORIZED | 401 | The key is missing, malformed, expired, revoked or for another environment. | Check the key, or create a new one. |
FORBIDDEN | 403 | The key cannot be used for this request. | Manage keys and billing on the website. |
NOT_FOUND | 404 | The upload or job does not exist or belongs to another account. | Check the ID. |
VALIDATION_ERROR | 400 | The body, a header or a query parameter is invalid. | Fix the request; do not retry it unchanged. |
PLAN_REQUIRED | 403 | The account has no active Basic or Plus plan. | Choose a plan, then try again. |
INSUFFICIENT_BALANCE | 403 | The remaining plan time does not cover the selected audio. | Select a shorter range or wait until your hours renew. |
CAPABILITY_DISABLED | 503 | New jobs are paused, or a feature is turned off. | Try again later; the account call shows the current state. |
UNSUPPORTED_MEDIA | 400 | The format is not supported, or the uploaded size differs from the declared size. | Upload a supported file with its exact size. |
MEDIA_TOO_LARGE | 413 | The file is larger than the limit. | Upload a file of at most 8 GiB. |
PROBE_FAILED | 400 | The file has no audio track that can be decoded, or the range is outside the file. | Check the file and the range. No time is charged. |
IDEMPOTENCY_CONFLICT | 409 | The Idempotency-Key was already used with other inputs. | Use a new key for a new job. |
IDEMPOTENCY_EXPIRED | 410 | The original job for this key has expired. | Do not retry it; start a new job with a new key. |
JOB_CONFLICT | 409 | Another upload or job is active, the state does not allow this action, or the result changed while you were paging. | Wait for the current job, or restart paging from the first page. |
RESULT_EXPIRED | 410 | The result is no longer kept. | Save results before they expire. |
RATE_LIMITED | 429 | Too many requests, or the daily upload limit is reached. | Wait for Retry-After, then retry. |
UNAVAILABLE | 503 | A service is temporarily unavailable. | Retry with increasing delays. |
Limits
These are the defaults. The account call returns the values that apply to your account; rely on those.
| Limit | Default | Field in capabilities.limits |
|---|---|---|
| File size | 8 GiB | max_upload_bytes |
| Selected audio per job | Your remaining plan time (null means no other cap) | max_selection_seconds |
| Active uploads and jobs | One of each per account | active_uploads, active_jobs |
| Uploads per UTC day | 10 uploads, 20 GiB in total | daily_uploads, daily_upload_bytes |
| New jobs and resumes | 12 per minute | new_requests_per_minute |
| Other requests | 60 per minute across all keys | read_requests_per_minute |
| Status polling | No more often than every 5 seconds | poll_after_seconds |
| Result page | 32 items and 1 MB | result_page_items, result_page_bytes |
| Signed upload URL | Valid for up to 15 minutes | upload_url_seconds |
| Download link | Valid for up to 10 minutes | download_url_seconds |
| Failed authentication | 30 per minute per IP address; after that, every request from that address gets 429, even with a valid key, until the minute has passed | — |
Retention
| Data | Kept for |
|---|---|
| Upload that has no job yet | About 2 hours |
| Result pages and download files | About 24 hours after the job finishes |
| Transcript recovery | About 48 hours |
| Idempotency record, without content | 400 days |
The expires_at values in each response are authoritative; use them rather than these defaults.
Changelog
The API version is part of the path. Changes to v1 are listed here with their dates.
- 2026-10-24: API v1 available for Basic and Plus plans.