# VidiPrompt Video API

Current API availability: enabled.

Base URL: `https://vidiprompt.com/api/v1`

OpenAPI 3.1: `https://vidiprompt.com/openapi.json`

Human documentation: `https://vidiprompt.com/developers/api`

The VidiPrompt API is a versioned REST API for creating asynchronous faceless videos from terminal scripts, servers, and AI agents. It is not a CLI. Creator and Pro accounts receive API access, and API work uses the same allowance and outstanding-job capacity as videos created in the dashboard.

## Plans and shared limits

| Plan | Price | Video allowance | Max outstanding | API access |
| --- | ---: | ---: | ---: | --- |
| Free | $0 | 1 per rolling 7 days | 1 | no API access |
| Basic | $4.99/month | 7 per Stripe billing period | 1 | no API access |
| Creator ($9.99/month) | $9.99/month | 15 per Stripe billing period | 2 | yes |
| Pro ($14.99/month) | $14.99/month | 35 per Stripe billing period | 5 | yes |

Completed dashboard videos, completed API videos, and currently reserved jobs all count toward the same account limit. Account downgrades and entitlement loss fail closed for new API calls.

| Limit | Value | Meaning |
| --- | --- | --- |
| Creator allowance | 15 videos / month | 2 outstanding jobs |
| Pro allowance | 35 videos / month | 5 outstanding jobs |
| Quota accounting | Shared | Dashboard and API generations use the same plan allowance. |
| Active keys | 1 per account | Regenerating immediately revokes the previous key. |
| Key lifetime | 365 days | Generate a replacement before the current key expires. |
| Video retention | 7 days | Download completed MP4 files before expires_at. |
| Idempotency retention | 24 hours | A repeated key and identical body return the original create result. |

## Authentication and API keys

Manage your one active API key in **Account Settings → API Access** at `https://vidiprompt.com/account#api-access`. A new key is shown once. Store it in a secret manager or the server-side environment:

`VIDIPROMPT_API_KEY=paste-the-one-time-key-here`

Send the key only in this header:

`Authorization: Bearer $VIDIPROMPT_API_KEY`

Never put a key in a query string, URL, JSON body, cookie, `x-api-key` header, client-side bundle, log, screenshot, or prompt. VidiPrompt stores the credential as a one-way hash and allows one active API key per account. Every key has fixed full access to Generate and manage your videos and expires after 365 days. Regenerating a key invalidates the old key immediately; revoking it stops API access immediately. API keys never create dashboard sessions.

## First-video workflow

1. Save your API key — Store the one-time key in the VIDIPROMPT_API_KEY environment variable.
2. Check capabilities and usage — Read the valid generation options and your shared monthly allowance before creating work.
3. Create a video — POST your settings with a new Idempotency-Key and keep the 202 response.
4. Save the job URL — Keep the response id and links.self value; video generation runs asynchronously.
5. Poll until terminal — While queued or processing, wait for the latest Retry-After value before requesting links.self again.
6. Download the MP4 — When status is completed, make an authenticated request to links.download.

The create operation returns `202 Accepted` because generation is asynchronous. Continue polling only while status is `queued` or `processing`. The other terminal statuses are `completed`, `failed`, `cancelled`, and `expired`. API v1 has no customer cancellation operation.

## Complete quickstarts

The cURL script needs Bash, cURL, and Python 3's standard library; it does not require jq. The JavaScript example needs Node.js 18 or later. The Python example needs Python 3 and the `requests` package (install with `python3 -m pip install requests`). Each script writes `<video_id>.mp4` in the current directory.

### cURL

```bash
#!/usr/bin/env bash
set -euo pipefail

: "${VIDIPROMPT_API_KEY:?Set VIDIPROMPT_API_KEY before running this script}"
BASE_URL="https://vidiprompt.com/api/v1"
IDEMPOTENCY_KEY="$(python3 -c 'import uuid; print(uuid.uuid4())')"

curl --fail-with-body --silent --show-error \
  --request POST "$BASE_URL/videos" \
  --header "Authorization: Bearer $VIDIPROMPT_API_KEY" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: $IDEMPOTENCY_KEY" \
  --data @- \
  --dump-header response.headers \
  --output video.json <<'JSON'
{
  "topic": "The emperor who erased his own brother from history",
  "video_mode": "darkHistory",
  "style_pack": "documentary",
  "voice": "warm-storyteller",
  "language": "en",
  "captions": {
    "style": "tiktok",
    "position": "center"
  }
}
JSON

VIDEO_ID="$(python3 -c 'import json; print(json.load(open("video.json"))["id"])')"
VIDEO_URL="$(python3 -c 'import json; print(json.load(open("video.json"))["links"]["self"])')"
printf 'Created video %s\n' "$VIDEO_ID"

while true; do
  curl --fail-with-body --silent --show-error \
    --header "Authorization: Bearer $VIDIPROMPT_API_KEY" \
    --dump-header response.headers \
    --output video.json \
    "$VIDEO_URL"

  STATUS="$(python3 -c 'import json; print(json.load(open("video.json"))["status"])')"
  printf 'Status: %s\n' "$STATUS"

  case "$STATUS" in
    completed) break ;;
    queued|processing)
      RETRY_AFTER="$(python3 -c 'import re; data=open("response.headers").read(); match=re.search(r"(?im)^Retry-After:\s*(\d+)", data); print(match.group(1) if match else 5)')"
      sleep "$RETRY_AFTER"
      ;;
    *)
      printf 'Video ended with status %s. See video.json for details.\n' "$STATUS" >&2
      exit 1
      ;;
  esac
done

DOWNLOAD_URL="$(python3 -c 'import json; print(json.load(open("video.json"))["links"]["download"])')"
curl --fail-with-body --location --continue-at - \
  --header "Authorization: Bearer $VIDIPROMPT_API_KEY" \
  --output "$VIDEO_ID.mp4" \
  "$DOWNLOAD_URL"

printf 'Saved %s.mp4\n' "$VIDEO_ID"
```

### JavaScript

```javascript
import { randomUUID } from 'node:crypto';
import { writeFile } from 'node:fs/promises';

const apiKey = process.env.VIDIPROMPT_API_KEY;
if (!apiKey) throw new Error('Set VIDIPROMPT_API_KEY before running this script.');

const baseUrl = 'https://vidiprompt.com/api/v1';
const authHeaders = { Authorization: 'Bearer ' + apiKey };
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));

async function readJson(response) {
  const data = await response.json();
  if (!response.ok) {
    throw new Error(data.detail || data.title || 'VidiPrompt API request failed');
  }
  return data;
}

let response = await fetch(baseUrl + '/videos', {
  method: 'POST',
  headers: {
    ...authHeaders,
    'Content-Type': 'application/json',
    'Idempotency-Key': randomUUID(),
  },
  body: JSON.stringify({
  "topic": "The emperor who erased his own brother from history",
  "video_mode": "darkHistory",
  "style_pack": "documentary",
  "voice": "warm-storyteller",
  "language": "en",
  "captions": {
    "style": "tiktok",
    "position": "center"
  }
}),
});

let video = await readJson(response);
let retryAfter = Number(response.headers.get('Retry-After') || 5);
console.log('Created video', video.id);

while (video.status === 'queued' || video.status === 'processing') {
  await sleep(retryAfter);
  response = await fetch(video.links.self, { headers: authHeaders });
  video = await readJson(response);
  retryAfter = Number(response.headers.get('Retry-After') || 5);
  console.log('Status:', video.status);
}

if (video.status !== 'completed' || !video.links.download) {
  throw new Error('Video ended with status ' + video.status + '.');
}

const download = await fetch(video.links.download, { headers: authHeaders });
if (!download.ok) throw new Error('Download failed with HTTP ' + download.status + '.');
await writeFile(video.id + '.mp4', Buffer.from(await download.arrayBuffer()));
console.log('Saved', video.id + '.mp4');
```

### Python

```python
import os
import time
import uuid
from pathlib import Path

import requests

api_key = os.environ.get("VIDIPROMPT_API_KEY")
if not api_key:
    raise RuntimeError("Set VIDIPROMPT_API_KEY before running this script.")

base_url = "https://vidiprompt.com/api/v1"
auth_headers = {"Authorization": f"Bearer {api_key}"}

response = requests.post(
    f"{base_url}/videos",
    headers={
        **auth_headers,
        "Content-Type": "application/json",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
  "topic": "The emperor who erased his own brother from history",
  "video_mode": "darkHistory",
  "style_pack": "documentary",
  "voice": "warm-storyteller",
  "language": "en",
  "captions": {
    "style": "tiktok",
    "position": "center"
  }
},
    timeout=30,
)
response.raise_for_status()
video = response.json()
retry_after = int(response.headers.get("Retry-After", "5"))
print("Created video", video["id"])

while video["status"] in {"queued", "processing"}:
    time.sleep(retry_after)
    response = requests.get(video["links"]["self"], headers=auth_headers, timeout=30)
    response.raise_for_status()
    video = response.json()
    retry_after = int(response.headers.get("Retry-After", "5"))
    print("Status:", video["status"])

if video["status"] != "completed" or not video["links"].get("download"):
    raise RuntimeError(f"Video ended with status {video['status']}.")

output = Path(f"{video['id']}.mp4")
with requests.get(video["links"]["download"], headers=auth_headers, stream=True, timeout=60) as download:
    download.raise_for_status()
    with output.open("wb") as file:
        for chunk in download.iter_content(chunk_size=1024 * 1024):
            if chunk:
                file.write(chunk)

print("Saved", output)
```

## Canonical request and responses

Create request:

```json
{
  "topic": "The emperor who erased his own brother from history",
  "video_mode": "darkHistory",
  "style_pack": "documentary",
  "voice": "warm-storyteller",
  "language": "en",
  "captions": {
    "style": "tiktok",
    "position": "center"
  }
}
```

Accepted job (`202 Accepted`, with `Location` and `Retry-After` response headers):

```json
{
  "id": "123e4567-e89b-42d3-a456-426614174000",
  "object": "video",
  "status": "queued",
  "created_at": "2026-08-13T18:00:00.000Z",
  "queue": {
    "position": 2
  },
  "settings": {
    "video_mode": "darkHistory",
    "style_pack": "documentary",
    "voice": "warm-storyteller",
    "language": "en",
    "captions": {
      "style": "tiktok",
      "position": "center"
    }
  },
  "usage": {
    "used": 3,
    "reserved": 1,
    "remaining": 11,
    "limit": 15
  },
  "links": {
    "self": "https://vidiprompt.com/api/v1/videos/123e4567-e89b-42d3-a456-426614174000",
    "download": null
  }
}
```

Completed job:

```json
{
  "id": "123e4567-e89b-42d3-a456-426614174000",
  "object": "video",
  "status": "completed",
  "source": "api",
  "created_at": "2026-08-13T18:00:00.000Z",
  "updated_at": "2026-08-13T18:04:00.000Z",
  "expires_at": "2026-08-20T18:00:00.000Z",
  "settings": {
    "video_mode": "darkHistory",
    "style_pack": "documentary",
    "voice": "warm-storyteller",
    "language": "en",
    "captions": {
      "style": "tiktok",
      "position": "center"
    }
  },
  "progress": {
    "percent": 100,
    "stage": "completed",
    "detail": "Complete!"
  },
  "output": {
    "duration_seconds": 42.5
  },
  "links": {
    "self": "https://vidiprompt.com/api/v1/videos/123e4567-e89b-42d3-a456-426614174000",
    "download": "https://vidiprompt.com/api/v1/videos/123e4567-e89b-42d3-a456-426614174000/download"
  }
}
```

Keep `id` and `links.self` from the accepted response. The `usage` object reflects the shared reservation. After completion, download `links.download` with Bearer authentication before `expires_at`.

## Operations

### `POST /videos`

**Create a video.** Reserve quota and enqueue an asynchronous video generation job.

- Input: Bearer auth, Content-Type: application/json, Idempotency-Key, and the create body.
- Success: `202 Accepted`. Location, Retry-After, request ID, and rate-limit headers.

The Idempotency-Key must contain 16 through 128 visible ASCII characters. Reuse it only for an exact retry of the same normalized request. Unknown JSON fields are rejected, the body limit is 32 KiB, and topic is limited to 500 Unicode characters. An exact saved replay adds `Idempotent-Replayed: true`; using the key with different input returns `409 idempotency_conflict`.

### `GET /videos/{video_id}`

**Retrieve a video.** Read the current status, progress, settings, usage, and links for one job.

- Input: Bearer auth and the video_id path value.
- Success: `200 OK`. Retry-After while queued or processing, plus request ID and rate-limit headers.

Public status is one of `queued`, `processing`, `completed`, `failed`, `cancelled`, `expired`. Poll `queued` and `processing` no faster than the latest `Retry-After`. Internal errors, storage paths, worker states, and direct media URLs are never returned.

### `GET /videos`

**List videos.** List API and dashboard videos with cursor-based pagination.

- Input: Bearer auth; optional limit (1–100), cursor, and exact status query values.
- Success: `200 OK`. A stable list with has_more and an opaque next_cursor.

Ordering is stable by creation time and ID descending. Cursors are signed, expire, and are bound to the account and exact status filter.

### `GET /usage`

**Retrieve usage.** Read the shared plan allowance, reservations, remaining videos, and outstanding jobs.

- Input: Bearer auth.
- Success: `200 OK`. The current allowance window, source split, and outstanding-job cap.

The response includes the exact rolling or Stripe billing window, completed `used`, in-progress `reserved`, `remaining`, dashboard/API source split, and shared outstanding-job cap. Create-time enforcement remains authoritative and atomic.

### `GET /capabilities`

**Retrieve capabilities.** Discover accepted modes, styles, voices, languages, captions, defaults, and limits.

- Input: Bearer auth.
- Success: `200 OK`. Current generation catalogs, defaults, limits, retention, and polling advice.

Use this response instead of hardcoding generation catalogs. It also returns plan limits, defaults, 7-day output retention, 24-hour idempotency retention, and the polling recommendation.

### `GET|HEAD /videos/{video_id}/download`

**Download a video.** Download a completed MP4 with Bearer authentication and optional range requests.

- Input: Bearer auth and video_id; optional single Range: bytes=... header.
- Success: `200 / 206`. Private video/mp4 with Content-Length, ETag, and range headers.

The video must be owned, completed, and unexpired. One `Range: bytes=...` request is supported along with `ETag`, `If-None-Match`, `HEAD`, `206 Content-Range`, and `416` for invalid ranges. Responses use `video/mp4` and `Cache-Control: private, no-store`. Missing, foreign, deleted, and expired resources all return `404 video_not_found`; owned unfinished videos return `409 video_not_ready`.

## Create body fields

Unknown fields are rejected. The complete request body must be no larger than 32 KiB.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `topic` | `string` | yes | The video subject. Must contain 1–500 Unicode characters after trimming. |
| `video_mode` | `string` | no | Narrative format ID. Use GET /capabilities for current values and the default. |
| `style_pack` | `string` | no | Visual style ID. Use GET /capabilities for current values and the default. |
| `voice` | `string` | no | Narration voice ID. Use GET /capabilities for current values and the default. |
| `language` | `string` | no | Narration language code. Use GET /capabilities for supported values. |
| `captions` | `object` | no | Caption settings with style and position IDs returned by GET /capabilities. |

## Supported values

- Video modes: `financeWarning`, `darkHistory`, `weirdFacts`, `cleanEducational`, `motivational`
- Style packs: `documentary`, `dramaticExplainer`, `darkFinance`, `cleanEducational`
- Voices: `warm-storyteller`, `deep-documentary`, `bright-explainer`, `playful-upbeat`, `friendly-casual`, `warm-calm`
- Languages: `en`, `es`, `zh`, `hi`, `ar`, `pt`, `fr`, `ja`, `de`, `ru`
- Caption styles: `tiktok`, `hormozi`, `mrbeast`, `minimal`, `elegant`, `neonGreen`, `neonPink`, `neonBlue`, `knockout`, `electric`, `vaporwave`, `retrowave`, `outlineWhite`, `outlineRainbow`, `gaming`, `streamer`, `fire`, `ice`
- Caption positions: `top`, `center`, `bottom`

Call `GET /capabilities` instead of hardcoding these values; the endpoint is the current account-aware source of truth.

## Errors, retries, and rate limits

Errors use `application/problem+json` with the RFC 9457 shape shown below. Branch on the stable `code`, show the safe `detail`, and retain `request_id` for support.

```json
{
  "type": "https://vidiprompt.com/problems/quota-exceeded",
  "title": "Video allowance exhausted",
  "status": 403,
  "code": "quota_exceeded",
  "detail": "Your current video allowance is exhausted.",
  "instance": "/api/v1/videos",
  "request_id": "00000000-0000-4000-8000-000000000001"
}
```

| Code | HTTP | Corrective action |
| --- | ---: | --- |
| `invalid_api_key` | 401 | Check that the Bearer token is the current key and contains no spaces or quotes. |
| `api_access_required` | 403 | Use a Creator or Pro account, then generate a key from Account settings. |
| `quota_exceeded` | 403 | Wait for the plan allowance to reset or upgrade; dashboard and API usage share the same quota. |
| `too_many_outstanding_jobs` | 409 | Wait for an existing queued or processing job to finish before creating another. |
| `idempotency_required` | 400 | Send a unique Idempotency-Key header with every create request. |
| `idempotency_conflict` | 409 | The key was already used with different input. Retry with a new key. |
| `rate_limited` | 429 | Wait for Retry-After, then retry the same safe request. |
| `video_not_ready` | 409 | Poll links.self until the video is completed before downloading. |
| `video_not_found` | 404 | Check the video ID and key owner; expired or inaccessible videos are not returned. |
| `service_unavailable` | 503 | Wait briefly and retry with backoff. Reuse the same Idempotency-Key for the same create input. |

The complete contract also defines validation, cursor, permission, media, and range failures. Every authenticated response includes `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset`. Respect `Retry-After` on `429`, temporary `503`, and polling responses. Retry network failures and retryable server failures with bounded exponential backoff and jitter. Do not blindly retry validation, permission, quota, conflict, not-found, or invalid-range failures.

## Copy-ready integration prompt

You are integrating the VidiPrompt Video API into an application. Follow these steps exactly:

1. Read the canonical Markdown documentation at https://vidiprompt.com/developers/api.md and the OpenAPI 3.1 contract at https://vidiprompt.com/openapi.json before writing code.
2. Read the API key only from the environment variable VIDIPROMPT_API_KEY; send it as Authorization: Bearer $VIDIPROMPT_API_KEY, never log it, never put it in a URL, and never expose it to browser code.
3. Call GET /api/v1/capabilities and GET /api/v1/usage before generation so the integration uses supported values and can display the account's shared dashboard/API allowance.
4. Create with POST /api/v1/videos, Content-Type: application/json, and a new cryptographically random Idempotency-Key of 16 through 128 visible ASCII characters for each logical video request; reuse that key only when retrying the same normalized request.
5. Save the returned video id and poll links.self no faster than the Retry-After header until status is completed, failed, cancelled, or expired.
6. Treat every non-2xx application/problem+json body by its stable code; obey Retry-After and RateLimit headers, and do not retry validation, permission, quota, idempotency-conflict, or not-found errors blindly.
7. After completed, download links.download with the same Bearer header and support one Range request so interrupted downloads can resume; verify the final bytes before marking the file delivered.
8. Handle key revocation, entitlement loss, expiration, cross-user 404s, and API unavailability safely; never call provider APIs directly and keep the VidiPrompt OpenAPI contract as the machine-readable authority.
