Developers
Ozor API reference.
Programmatic access to Ozor — generate, edit, and export videos, and turn documents into narrated videos, straight from your own code. JSON in, JSON out, with simple polling for long-running jobs.
Base URL
https://ozor.aiAuth header
X-API-KeyVersion
/api/v1/01
Getting started
Create an API key
API keys are created from the dashboard at ozor.ai (Settings → API Keys) or programmatically through the key-management endpoints. Those management endpoints are authenticated with your Firebase ID token — the same session your browser uses after logging in — not with an API key.
Key format: sk_live_ followed by 32 hex characters (40 characters total). The raw key is shown once, at creation time. Ozor stores only a SHA-256 hash and can never show it again — save it to a secret manager immediately.
Limit: a maximum of 5 active keys per account. Revoke unused keys before creating new ones.
Make your first call
curl -X POST https://ozor.ai/api/v1/videos/generate \
-H "X-API-Key: sk_live_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6" \
-H "Content-Type: application/json" \
-d '{"prompt": "A 15-second ad for a minimalist desk lamp"}'02
Authentication
All public endpoints live under /api/v1/* and require the X-API-Key header:
X-API-Key: sk_live_<32-hex-chars>| Status | Meaning |
|---|---|
401 Unauthorized | Header missing, key not recognized, or key revoked. |
403 Forbidden | Key is valid but does not own the resource you are accessing. |
Keys are scoped to the account that created them. Every resource (video, plan, export, job) can only be read or modified by the owning account.
There is no rate limiting — usage is governed entirely by your credit balance.
03
API key management
These endpoints manage API keys themselves. They are authenticated with a Firebase ID token (Authorization: Bearer <firebase_id_token>), not an API key.
/api/api-keysCreate a new API key.
Request
{ "name": "Production" }Parameters
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | Yes | 1–64 chars. Shown in the dashboard to identify the key. |
Response (200)
{
"keyId": "abc123xyz",
"name": "Production",
"prefix": "sk_live_a1b2",
"rawKey": "sk_live_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"createdAt": 1705312200000
}rawKey is returned only in this response. All later list/read operations return only prefix.
Errors
400 if the 5-key limit is already reached; 401 if the Firebase token is missing or invalid.
/api/api-keysList all keys (active and revoked) for the current account.
Response (200)
[
{
"keyId": "abc123xyz",
"name": "Production",
"prefix": "sk_live_a1b2",
"status": "active",
"createdAt": 1705312200000,
"lastUsedAt": 1705400100000
}
]status is active or revoked. lastUsedAt is null until the key is first used.
/api/api-keys/{keyId}Revoke an API key. Takes effect immediately — the next request using this key fails with 401.
Response (200)
{ "message": "API key abc123xyz revoked" }Errors
404 if the key does not exist.
04
Credits
Every generation operation deducts from your account's credit balance.
| Endpoint | Cost | When deducted |
|---|---|---|
POST /api/v1/videos/generate | 1 credit | On successful job completion |
POST /api/v1/videos/{videoId}/message | 1 credit | On successful job completion |
POST /api/v1/videos/{videoId}/export | 0 credits | — |
POST /api/v1/documents/analyze | 1 credit | Immediately |
POST /api/v1/documents/plans/{planId}/generate | floor(voiceover_scenes / 2) | On successful completion |
If your balance is insufficient for an operation, the endpoint responds with 402 Payment Required and a JSON detail describing the shortfall. No partial work is performed, and no credit is deducted on failure.
New accounts receive 10 free credits. You can buy more — a subscription or a one-time credit top-up — from the dashboard.
05
Quickstart
“Generate, render, and give me a shareable link”:
# Step 1 — Kick off generation + auto-export as public
curl -X POST https://ozor.ai/api/v1/videos/generate \
-H "X-API-Key: $OZOR_API_KEY" -H "Content-Type: application/json" \
-d '{
"prompt": "A 20s product teaser for a wireless headphone",
"aspect": "16:9",
"export": true,
"exportQuality": "1080p",
"exportIsPublic": true
}'
# -> { "videoId": "abc123", "jobId": "job_...", "status": "pending" }
# Step 2 — Poll until the export is done
curl https://ozor.ai/api/v1/videos/abc123 \
-H "X-API-Key: $OZOR_API_KEY"
# -> when exportStatus == "complete":
# { "shareUrl": "...", "downloadUrl": "...", "shareCode": "a3kf92p", ... }See Polling Patterns for the full set of workflows.
06
Video endpoints
All video endpoints live under /api/v1/videos/* and use X-API-Key auth.
/api/v1/videos/generateCreate a new video from a natural-language prompt. Asynchronous — the agent runs in the background. The response returns immediately with a jobId to poll and a videoId referencing the newly created project.
Deducts 1 credit on successful completion. If you pass export: true, an MP4 render is automatically triggered once the agent finishes.
Request
{
"prompt": "Create a 30-second product showcase for a wireless headphone",
"aspect": "16:9",
"export": true,
"exportQuality": "1080p",
"exportIsPublic": true,
"images": [
{ "url": "https://example.com/product.jpg" }
],
"videos": [
{ "url": "https://example.com/clip.mp4", "mimeType": "video/mp4", "durationSec": 8.5 }
]
}Parameters
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
prompt | string | Yes | — | 1–2000 chars. |
aspect | "16:9" | "9:16" | No | "16:9" | Landscape or portrait. |
export | bool | No | false | Auto-trigger an MP4 render after the agent finishes. |
exportQuality | "720p" | "1080p" | "4k" | No | "720p" | Applied only when export=true. |
exportIsPublic | bool | No | false | true → the auto-export gets a permanent shareUrl + shareCode. |
images | ChatImage[] | No | — | See Media Attachments. |
videos | ChatVideo[] | No | — | See Media Attachments. |
Response (200)
{ "videoId": "abc123", "jobId": "job_s0m3R4nd0mId", "status": "pending" }Next steps
- Poll
GET /api/v1/videos/{videoId}/jobs/{jobId}untilstatusis"completed"to get the agent's reply. - If
export=true, also pollGET /api/v1/videos/{videoId}untilexportStatusis"complete"to get the rendered MP4.
/api/v1/videosList all videos created via the public API by the current account, newest first.
Query parameters
| Name | Type | Default | Range |
|---|---|---|---|
limit | int | 20 | 1–100 |
Response (200)
{
"videos": [
{
"videoId": "abc123",
"title": "Product showcase for a wirel...",
"status": "draft",
"editorUrl": "https://ozor.ai/editor?projectId=abc123",
"createdAt": "2026-04-20T10:30:00Z"
}
]
}Videos created inside the web app do not appear here — this list is scoped to API-originated videos.
/api/v1/videos/{videoId}Full status for a video, including its latest export. This is the endpoint you poll while an export renders.
Response (200)
{
"videoId": "abc123",
"title": "Product showcase...",
"status": "draft",
"editorUrl": "https://ozor.ai/editor?projectId=abc123",
"createdAt": "2026-04-20T10:30:00Z",
"exportId": "exp_xyz789",
"exportStatus": "complete",
"downloadUrl": "https://storage.googleapis.com/.../video.mp4",
"shareUrl": "https://storage.googleapis.com/.../video.mp4",
"shareCode": "a3kf92p",
"thumbnailUrl":"https://storage.googleapis.com/.../thumb.jpg",
"exportError": null
}Export-related fields appear only once an export has been triggered (manually via /export or implicitly via generate with export: true).
| exportStatus | Meaning |
|---|---|
queued | Render accepted, not yet picked up by the renderer. |
processing | Renderer actively working. |
complete | downloadUrl is set; if public, shareUrl + shareCode are too. |
failed | Render failed — exportError contains the reason. |
URL lifetimes
downloadUrl— signed URL, valid ~24 hours. Re-fetch this endpoint for a fresh one.shareUrl— permanent and public, never expires (only set when the export was public).
Errors
404 if the video does not exist; 403 if the key does not own it.
/api/v1/videos/{videoId}/exportManually trigger an MP4 render for an existing video. Use this when you generated without export: true, or when you want a second render at a different quality or visibility.
Request
{ "quality": "1080p", "isPublic": true }Parameters
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
quality | "720p" | "1080p" | "4k" | No | "1080p" | — |
isPublic | bool | No | false | true → returns a permanent shareUrl + shareCode. |
Response (200)
{
"videoId": "abc123",
"exportId": "exp_xyz789",
"exportStatus": "queued",
"shareCode": "a3kf92p"
}Caching: if an identical export (same scene content + quality + visibility) already exists, the cached result is returned immediately with exportStatus: "complete" and its URLs already populated.
Poll GET /api/v1/videos/{videoId} until exportStatus is "complete".
Errors
404, 403, 500 if the export could not be triggered.
/api/v1/videos/{videoId}/messageSend a natural-language edit instruction to the agent. Asynchronous — a jobId is returned immediately. Deducts 1 credit on successful completion.
Use this to iterate: change copy, tweak pacing, swap colors, add a logo, reorder scenes, etc.
Request
{
"message": "Add our logo in the top-right corner of every scene",
"images": [ { "base64": "<base64-encoded-png>", "mimeType": "image/png" } ]
}Parameters
| Field | Type | Required | Notes |
|---|---|---|---|
message | string | Yes | 1–2000 chars. |
images | ChatImage[] | No | See Media Attachments. |
videos | ChatVideo[] | No | See Media Attachments. |
Response (200)
{ "videoId": "abc123", "jobId": "job_s0m3R4nd0mId", "status": "queued" }Poll GET /api/v1/videos/{videoId}/jobs/{jobId} for the result, then re-fetch GET /api/v1/videos/{videoId} for the updated state (and trigger a fresh export if needed).
Errors
404, 403, 402 if out of credits.
/api/v1/videos/{videoId}/jobs/{jobId}Poll the status of an agent job (from generate or message).
Response — running
{
"videoId": "abc123",
"jobId": "job_s0m3R4nd0mId",
"status": "processing",
"createdAt": "2026-04-20T10:30:00Z"
}Response — completed
{
"videoId": "abc123",
"jobId": "job_s0m3R4nd0mId",
"status": "completed",
"response": "I've added the logo to the top-right corner of all 4 scenes.",
"createdAt": "2026-04-20T10:30:00Z"
}Response — failed
{
"videoId": "abc123",
"jobId": "job_s0m3R4nd0mId",
"status": "failed",
"error": "Agent encountered an error: ...",
"createdAt": "2026-04-20T10:30:00Z"
}| status | Meaning |
|---|---|
queued | Job accepted, agent not started yet. |
processing | Agent actively running. |
completed | Agent finished — response holds the reply. |
failed | Agent errored — error holds details. No credit charged. |
Errors
404 if the video or job does not exist; 403.
/api/v1/videos/{videoId}/conversations/{conversationId}/tool-traceDiagnostic endpoint. Returns a chronological trace of the tools the agent invoked in a conversation — useful for debugging and observability. Read-only.
Response (200)
{
"videoId": "abc123",
"conversationId": "conv_xyz",
"totalAssistantTurns": 3,
"totalToolCalls": 11,
"messages": [
{
"messageId": "msg_1",
"createdAt": "2026-04-20T10:30:05Z",
"textPreview": "I've created the opening scene...",
"toolCalls": [
{ "name": "create_scene", "args": { "order": 1, "title": "Intro" } }
]
}
]
}Errors
404, 403.
07
Document-to-video endpoints
Turn a PDF, PowerPoint, Word document, or web URL into a narrated video with an auto-selected voice, scenes, and images extracted from the source.
The flow
- 1Analyze — upload a file or URL. You receive a
planIdand a scene-by-scene plan (narration script + visual descriptions) to review. - 2Update (optional) — edit the scenes' voiceover text, prompts, or voice settings.
- 3Generate — render the plan. Progress streams over Server-Sent Events; the final event carries a
projectIdyou can then use with the/api/v1/videos/*endpoints (e.g. to export).
/api/v1/documents/voicesList the available text-to-speech voices.
Response (200)
{
"voices": [
{ "id": "aria", "label": "Aria", "gender": "female", "character": "Clear, precise" },
{ "id": "nova", "label": "Nova", "gender": "female", "character": "Warm, natural" },
{ "id": "felix", "label": "Felix", "gender": "male", "character": "Playful, upbeat" },
{ "id": "marcus", "label": "Marcus", "gender": "male", "character": "Neutral, professional" },
{ "id": "thor", "label": "Thor", "gender": "male", "character": "Deep, authoritative" },
{ "id": "luna", "label": "Luna", "gender": "female", "character": "Soft, calm" },
{ "id": "owen", "label": "Owen", "gender": "male", "character": "Steady, trustworthy" },
{ "id": "sky", "label": "Sky", "gender": "neutral", "character": "Light, friendly" }
]
}Use any id in voiceSettings.voiceId on PATCH /api/v1/documents/plans/{planId}. If omitted, a voice is auto-selected by document language.
/api/v1/documents/analyzeExtract scenes and narration from a document or web URL. Deducts 1 credit immediately.
Content type: multipart/form-data (not JSON).
Form fields
| Field | Type | Required | Notes |
|---|---|---|---|
file | file | One of file / url | PDF, PPTX, or DOCX. Max 50 MB. |
url | string | One of file / url | http(s):// URL — its readable text is analyzed. |
target_duration_sec | int | No | Target output duration. Default 30. Drives scene count. |
prompt | string | No | Optional guidance (tone, focus, audience, language). |
aspect_ratio | "16:9" | "9:16" | No | Default "16:9". |
Provide exactly one of file or url.
Supported file MIME types
application/pdfapplication/vnd.openxmlformats-officedocument.presentationml.presentation(.pptx)application/vnd.openxmlformats-officedocument.wordprocessingml.document(.docx)
Response (200)
{
"plan": {
"planId": "plan_a1b2c3d4e5f6",
"fileName": "pitch-deck.pdf",
"fileType": "pdf",
"sourceUrl": null,
"aspectRatio": "16:9",
"status": "draft",
"language": "en",
"style": {
"colors": ["#0F172A", "#3B82F6"],
"fonts": ["Inter"],
"tone": "professional",
"layout": "slide"
},
"scenes": [
{
"order": 1,
"sceneTitle": "Introduction",
"scenePrompt": "A blue-tinted title card with the company logo fading in...",
"voiceoverText": "Welcome to Acme — we're rethinking how teams collaborate.",
"imageRefs": [
{ "storagePath": "users/uid/plans/plan_.../page_0_img_0.png",
"url": "https://storage.googleapis.com/..." }
],
"keyPoints": ["intro", "brand"]
}
],
"projectId": null,
"error": null,
"createdAt": 1705312200,
"updatedAt": 1705312200,
"createdVia": "api",
"creditsCost": 3
}
}creditsCost is the future cost of calling /generate on this plan — it equals floor(count_of_scenes_with_voiceover / 2).
Errors
400— neither/bothfileandurl, unsupported file type, file > 50 MB, invalidaspect_ratio.402— out of credits.500— URL fetch failed or analysis failed.
/api/v1/documents/plans/{planId}Retrieve a plan. Image URLs are refreshed on every read (they are short-lived signed URLs), so always fetch immediately before displaying or editing.
Response (200): same shape as analyze.
Errors
404 if the plan does not exist; 403.
/api/v1/documents/plans/{planId}Edit the plan before generating — rewrite voiceover, reorder scenes, drop scenes, or lock in a voice.
Valid only while the plan's status is draft or failed.
Request
{
"scenes": [
{
"order": 1,
"sceneTitle": "Introduction",
"scenePrompt": "A blue-tinted title card...",
"voiceoverText": "Welcome to Acme — we're reimagining collaboration.",
"imageRefs": [
{ "storagePath": "users/uid/plans/plan_.../page_0_img_0.png" }
],
"keyPoints": ["intro"]
}
],
"voiceSettings": {
"voiceId": "nova",
"speakingStyle": "warm",
"speakingRate": 1.0
}
}Parameters
| Field | Type | Notes |
|---|---|---|
scenes | scene[] | Full replacement — send the complete list. Each scene needs order, sceneTitle, scenePrompt, voiceoverText. |
voiceSettings.voiceId | string | A voice id from /documents/voices. Omit to auto-select by language. |
voiceSettings.speakingStyle | string | Free-text style hint ("formal", "energetic", "calm"). |
voiceSettings.speakingRate | float | 0.5–2.0. Default 1.0. |
Response (200): the updated plan (same shape as analyze).
Errors
400 if the plan is already generating or generated; 404; 403.
/api/v1/documents/plans/{planId}/generateRender the plan into a real video project. Valid only while plan status is draft or failed.
Credit cost: floor(voiceover_scenes / 2) — e.g. a 6-scene plan costs 3 credits. The balance is checked before streaming begins; if you are short you receive 402 up front and no work is done.
Response: text/event-stream (SSE). Each event is a line:
data: {"step": "...", "detail": "...", "pct": 0-100}Typical sequence
data: {"step": "init", "detail": "Starting generation", "pct": 0}
data: {"step": "tts", "detail": "Generating voiceover 1/6", "pct": 15}
data: {"step": "scenes", "detail": "Rendering scene 3/6", "pct": 55}
data: {"step": "assembly", "detail": "Assembling project", "pct": 90}
data: {"step": "done", "detail": "Video project ready!", "pct": 100, "projectId": "abc123"}On error
data: {"step": "error", "detail": "TTS failed: ...", "pct": -1}Consume with any SSE-capable client. Once you receive the done event, use the returned projectId as videoId with the rest of the API — e.g. POST /api/v1/videos/{projectId}/export to render the MP4.
Errors
400 if the plan is not in draft/failed state; 402 out of credits; 404; 403.
08
Media attachments
Both POST /api/v1/videos/generate and POST /api/v1/videos/{videoId}/message accept optional images and videos arrays. Each item may be supplied as a URL, a base64 payload, or both.
Image object (ChatImage)
| Field | Type | Notes |
|---|---|---|
url | string | Publicly accessible HTTPS URL. |
base64 | string | Raw base64 (no data: prefix needed). |
mimeType | string | image/jpeg, image/png, or image/webp. Recommended with base64. |
Video object (ChatVideo)
| Field | Type | Notes |
|---|---|---|
url | string | Publicly accessible HTTPS URL. |
base64 | string | Raw base64. |
mimeType | string | video/mp4 or video/webm. Recommended with base64. |
durationSec | float | Clip duration — helps the agent reason about pacing. |
thumbnailUrl | string | Optional thumbnail URL. |
When base64 is provided it takes precedence — the data is uploaded to Ozor storage and url is ignored.
09
Polling patterns
Generate + auto-export
POST /api/v1/videos/generate { ..., "export": true, "exportIsPublic": true }
-> { videoId, jobId, status: "pending" }
Poll GET /api/v1/videos/{videoId}
- wait until "exportStatus" appears (agent finished, export queued)
- wait until "exportStatus" === "complete"
-> { shareUrl, downloadUrl, shareCode, thumbnailUrl, ... }Suggested cadence: 2–3 seconds between polls for the first minute, 5–10 seconds after that. Typical end-to-end time ranges from tens of seconds to a few minutes, depending on prompt complexity and export quality.
Generate, then export separately
POST /api/v1/videos/generate { prompt: "..." }
-> { videoId, jobId, status: "pending" }
Poll GET /api/v1/videos/{videoId}/jobs/{jobId} until status === "completed"
POST /api/v1/videos/{videoId}/export { quality: "1080p", isPublic: true }
-> { exportId, exportStatus: "queued", shareCode }
Poll GET /api/v1/videos/{videoId} until exportStatus === "complete"Iterative edit
POST /api/v1/videos/{videoId}/message { message: "Change background to dark blue" }
-> { jobId, status: "queued" }
Poll GET /api/v1/videos/{videoId}/jobs/{jobId} until status === "completed"
POST /api/v1/videos/{videoId}/export { quality: "1080p" }
-> poll GET /api/v1/videos/{videoId} until exportStatus === "complete"Document → Video
POST /api/v1/documents/analyze (multipart: file + target_duration_sec + ...)
-> { plan: { planId, scenes: [...], creditsCost } }
(optional) PATCH /api/v1/documents/plans/{planId} { scenes, voiceSettings }
POST /api/v1/documents/plans/{planId}/generate (consume SSE stream)
-> final event: { step: "done", projectId }
POST /api/v1/videos/{projectId}/export { quality: "1080p", isPublic: true }
-> poll GET /api/v1/videos/{projectId} until exportStatus === "complete"10
Error responses
All errors are JSON with a detail field.
| HTTP | When it happens |
|---|---|
400 Bad Request | Validation failed — missing field, value out of range, unsupported file type, file too large, invalid aspect_ratio, plan not in draft/failed state, etc. |
401 Unauthorized | X-API-Key missing, unrecognized, or revoked. For key-management endpoints: Firebase token missing/expired. |
402 Payment Required | Credit balance insufficient for this operation. detail includes the cost when relevant. |
403 Forbidden | The API key is valid but does not own the target video/plan/job. |
404 Not Found | The videoId, jobId, planId, or exportId does not exist. |
500 Internal Server Error | Upstream failure — document fetch, renderer, or analysis error. Safe to retry after a short delay. |
Example
{ "detail": "You are out of credits. Please add more to continue chatting." }11
Reference tables
Aspect ratios
| Value | Use case |
|---|---|
16:9 | Desktop, YouTube, landing pages (1920×1080 at 1080p). |
9:16 | Mobile, TikTok, Reels, Shorts (1080×1920 at 1080p). |
Export qualities
| Value | Resolution (16:9 / 9:16) |
|---|---|
720p | 1280×720 / 720×1280 |
1080p | 1920×1080 / 1080×1920 |
4k | 3840×2160 / 2160×3840 |
Output is always mp4 at 30 fps.
Share code
When an export is public, a 7-character alphanumeric shareCode (e.g. a3kf92p) is generated. shareUrl is the permanent public URL to the MP4; shareCode is the handle used internally and in the web UI to look up the shared video.
Video statuses
| status (project) | Meaning |
|---|---|
draft | Project exists and is editable. |
rendering | A render is in progress. |
completed | A render has completed at least once. |
Export statuses
| exportStatus | Meaning |
|---|---|
queued | Accepted, not yet picked up by the renderer. |
processing | Actively rendering. |
complete | Done. downloadUrl (and shareUrl if public) are set. |
failed | Render failed. exportError contains the reason. |
Agent job statuses
| status (job) | Meaning |
|---|---|
queued | Job accepted, agent not yet started. |
pending | Returned synchronously by /generate; equivalent to queued. |
processing | Agent running. |
completed | Done. response is set. |
failed | Errored. error is set. No credit deducted. |
TTS voices
| id | gender | character |
|---|---|---|
aria | female | Clear, precise |
nova | female | Warm, natural |
felix | male | Playful, upbeat |
marcus | male | Neutral, professional |
thor | male | Deep, authoritative |
luna | female | Soft, calm |
owen | male | Steady, trustworthy |
sky | neutral | Light, friendly |
Document formats
| Format | MIME |
|---|---|
PDF | application/pdf |
PPTX | application/vnd.openxmlformats-officedocument.presentationml.presentation |
DOCX | application/vnd.openxmlformats-officedocument.wordprocessingml.document |
Max file size: 50 MB. A http(s):// URL may be supplied instead of a file.
Ready to build with Ozor?
Create an API key from your dashboard and make your first call in minutes. New accounts start with 10 free credits.