Atlas API v8.0
API DOCS
Generate photorealistic lip-sync avatar videos and speech audio through an async job queue. Submit a job, poll for status, then download the result. All generation endpoints return 202 Accepted with a job ID — no more waiting for a synchronous response.
Offline API
Pre-rendered Videos
Submit audio + image, get an MP4 back. Best for content creation, batch processing, and async workflows.
Realtime API
Live Interactive Avatars
WebRTC stream with sub-second latency. Best for customer support, virtual assistants, and live demos.
Realtime Modes
| Interactive (default) | Rendering (you provide audio) |
|---|
| You provide | Face image | Face image + audio stream |
| You get back | AI-powered video call | Lip-synced video stream |
| Speech recognition / AI / Voice | Managed by Atlas | You bring your own |
| Built-in AI (LLM) | ✓ | — |
| Built-in voice generation | ✓ | — |
| Your own audio source | — | ✓ |
| GPU rendering | ✓ | ✓ |
| One-shot — any face image | ✓ | ✓ |
| Infinite session length | ✓ | ✓ |
| No quality collapse | ✓ | ✓ |
| React SDK | ✓ | ✓ |
| Price | $10/hr | $6/hr |
How It Works — 3-Step Flow
Step 1Submit
POST to a generation endpoint. Returns 202 with job_id
Step 2Poll
GET /v1/jobs/{id} until status is completed
Step 3Download
GET /v1/jobs/{id}/result for a presigned download URL
Base URLhttps://api.atlasv1.com
Interactive
Live Examples
See the API in action — try avatar generation and more with working demos.
→Authentication
All endpoints (except GET / and /v1/health) require an API key via the Authorization header:
Authorization: Bearer <your_api_key>
Generate API keys from your dashboard after adding a payment method. For enterprise plans, contact eric@northmodellabs.com.
GET
/
Returns API info and available endpoints. No authentication required.
{
"name": "Atlas API",
"version": "8.0",
"endpoints": {
"POST /v1/generate": "Audio + image → lip-sync avatar video",
"GET /v1/jobs/{id}": "Get job status and details",
"GET /v1/jobs/{id}/result": "Get presigned download URL for completed job output",
"GET /v1/jobs": "List your recent jobs",
"POST /v1/realtime/session": "Create realtime avatar session (conversation or passthrough mode)",
"PATCH /v1/realtime/session/{id}": "Hot-swap face image mid-session",
"GET /v1/realtime/session/{id}": "Get session status",
"DELETE /v1/realtime/session/{id}": "End session and release GPU",
"GET /v1/health": "Health check",
"GET /v1/status": "System status",
"GET /v1/me": "Your API key info and usage"
},
"authentication": "Authorization: Bearer <api_key>",
"flow": {
"offline": "POST → 202 {job_id} → poll GET /v1/jobs/{id} → GET /v1/jobs/{id}/result",
"realtime": "POST /v1/realtime/session → connect to LiveKit with token → DELETE to end"
}
}POST
/v1/generate
Submit a lip-sync avatar video generation job. Returns immediately with a job ID — poll GET /v1/jobs/{id} for status.
Content-Type: multipart/form-data
Request Fields
| Field | Type | Required | Description |
|---|
| audio | file | yes | Audio file for lip-sync |
| image | file | yes | Reference face image |
Supported audio: wav, mp3, mpeg, ogg, webm
Supported images: png, jpeg, webp
Max upload: 50 MB combined
Works with any TTS provider. Generate speech audio with ElevenLabs, OpenAI TTS, Deepgram, or any other service — then pass the audio file to this endpoint.
Offline generation is billed at $6/hour ($0.10/min) of generated video. See pricing.
Response 202 Accepted
{
"job_id": "a1b2c3d4e5f6",
"status": "pending",
"message": "Job accepted. Poll GET /v1/jobs/a1b2c3d4e5f6 for status."
}# Step 1 — Submit job
curl -X POST "https://api.atlasv1.com/v1/generate" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "audio=@speech.mp3" \
-F "image=@face.jpg"
# Returns: {"job_id": "a1b2c3d4e5f6", "status": "pending", ...}
# Step 2 — Poll
curl "https://api.atlasv1.com/v1/jobs/a1b2c3d4e5f6" \
-H "Authorization: Bearer YOUR_API_KEY"
# Returns: {"status": "completed", ...} when done
# Step 3 — Get presigned URL
curl "https://api.atlasv1.com/v1/jobs/a1b2c3d4e5f6/result" \
-H "Authorization: Bearer YOUR_API_KEY"
# Returns: {"url": "https://...", "content_type": "video/mp4", "expires_in": 86400}
# Download from presigned URL (no auth needed)
curl -o output.mp4 "PRESIGNED_URL_FROM_ABOVE"import requests, time
API_KEY = "YOUR_API_KEY"
headers = {"Authorization": f"Bearer {API_KEY}"}
# Step 1 — Submit
job = requests.post(
"https://api.atlasv1.com/v1/generate",
headers=headers,
files={
"audio": ("speech.mp3", open("speech.mp3", "rb"), "audio/mp3"),
"image": ("face.jpg", open("face.jpg", "rb"), "image/jpeg"),
},
).json()
job_id = job["job_id"]
print(f"Job submitted: {job_id}")
# Step 2 — Poll until complete
while True:
status = requests.get(
f"https://api.atlasv1.com/v1/jobs/{job_id}",
headers=headers,
).json()
if status["status"] == "completed":
break
elif status["status"] == "failed":
raise Exception(f"Job failed: {status['error']}")
time.sleep(2)
# Step 3 — Get presigned URL + download
result = requests.get(
f"https://api.atlasv1.com/v1/jobs/{job_id}/result",
headers=headers,
).json()
# Download from presigned URL (no auth needed)
video = requests.get(result["url"])
with open("output.mp4", "wb") as f:
f.write(video.content)
print(f"Saved — {status['timing']['latency_ms'] / 1000:.1f}s") Try it liveSee working demos of avatar generation→Job Queue
Jobs
Every generation endpoint returns a job_id. Use these endpoints to track progress, list history, and download results. Jobs run concurrently on the server — you can submit multiple jobs and poll them all in parallel.
GET
/v1/jobs
List your recent jobs, newest first. Paginated.
Query Parameters
| Param | Type | Default | Description |
|---|
| limit | int | 20 | Number of results to return (max 100) |
| offset | int | 0 | Number of results to skip |
{
"jobs": [
{
"job_id": "a1b2c3d4e5f6",
"type": "video",
"status": "completed",
"created_at": "2026-03-25T16:47:07Z",
"completed_at": "2026-03-25T16:47:52Z",
"latency_ms": 43000,
"output_duration": null,
"input_chars": null,
"error_code": null
},
],
"count": 42,
"limit": 20,
"offset": 0
}GET
/v1/jobs/{id}
Poll the status of a specific job. This is the core endpoint you call in a loop until the job completes or fails.
Job Statuses
| Status | Description |
|---|
| pending | Job is queued, waiting to be processed |
| processing | Job is actively being processed |
| completed | Job finished — output ready to download |
| failed | Job failed — check error field for details |
{
"job_id": "a1b2c3d4e5f6",
"type": "video",
"status": "processing",
"queue_position": 0,
"input": {
"text_preview": null,
"language": null,
"chars": null,
"audio_size": 105000,
"image_size": 52000
},
"output": {
"duration": null,
"size_bytes": null,
"sample_rate": null
},
"error": null,
"error_code": null,
"timing": {
"created_at": "2026-03-25T16:47:07Z",
"started_at": "2026-03-25T16:47:09Z",
"completed_at": null,
"latency_ms": null
}
}{
"job_id": "a1b2c3d4e5f6",
"type": "video",
"status": "completed",
"queue_position": 0,
"input": {
"text_preview": null,
"language": null,
"chars": null,
"audio_size": 105000,
"image_size": 52000
},
"output": {
"duration": null,
"size_bytes": 4634000,
"sample_rate": null,
"has_result": true
},
"error": null,
"error_code": null,
"timing": {
"created_at": "2026-03-25T16:47:07Z",
"started_at": "2026-03-25T16:47:09Z",
"completed_at": "2026-03-25T16:47:52Z",
"latency_ms": 43000
},
"url": "https://storage.example.com/jobs/.../output.mp4?X-Amz-Algorithm=...",
"expires_in": 86400,
"result_url": "/v1/jobs/a1b2c3d4e5f6/result"
}{
"job_id": "a1b2c3d4e5f6",
"type": "video",
"status": "failed",
"queue_position": 0,
"input": {
"text_preview": null,
"language": null,
"chars": null,
"audio_size": 105000,
"image_size": 52000
},
"output": {
"duration": null,
"size_bytes": null,
"sample_rate": null
},
"error": "Processing failed. Please try again.",
"error_code": "generation_failed",
"timing": {
"created_at": "2026-03-25T16:47:07Z",
"started_at": "2026-03-25T16:47:09Z",
"completed_at": "2026-03-25T16:47:40Z",
"latency_ms": 31000
}
}GET
/v1/jobs/{id}/result
Get a time-limited presigned download URL for a completed job's output. The URL is valid for 24 hours and can be used directly in browsers, video players, or download links without authentication.
Important: Only call this after the job status is completed. Calling it on a pending or processing job returns 409 not_ready. The presigned URL is also included in the poll response (GET /v1/jobs/{id}) when the job is completed.
Response 200 OK
{
"url": "https://storage.example.com/jobs/.../output.mp4?X-Amz-Algorithm=...",
"content_type": "video/mp4",
"expires_in": 86400
}| Field | Description |
|---|
| url | Presigned download URL — no auth needed, expires after expires_in seconds |
| content_type | MIME type of the output (video/mp4 or audio/mpeg) |
| expires_in | URL validity in seconds (default 24 hours) |
GET
/v1/me
Check your API key status, current rate limit usage, and plan details.
{
"authenticated": true,
"key_prefix": "sk_live_A7xQ",
"name": "My API Key",
"tier": "starter",
"requests_used": 14,
"rate_limit": {
"requests_per_minute": 30,
"remaining": 28,
"resets_in": "42s"
},
"billing": "pay_as_you_go"
}GET
/v1/status
Check system status. Requires authentication.
{
"status": "operational",
"services": {
"avatar_generation": "available",
"voice_synthesis": "available"
}
}Each service reports available (capacity is free) or busy (all capacity is occupied).
GET
/v1/health
Health check endpoint. No authentication required.
{
"status": "all systems go",
"services": {
"avatar_generation": "online"
}
}Realtime
Live interactive avatars over WebRTC with sub-second latency. Unlike the Offline API (send files → get video back), the Realtime API streams audio and video like a video call with an AI face.
Integration uses the LiveKit client SDK. Request a session token from our API, then connect with the LiveKit SDK on the client side. Rendering mode is billed at $6/hour and Interactive mode at $10/hour, prorated to the second.
Each session runs for up to 1 hour (configurable). An automatic reaper ends sessions that exceed the max duration. Get your API key from the dashboard.
Two modes available:
conversation (default — Interactive) — We handle everything: speech recognition, AI responses, voice generation, and avatar rendering. Use the React SDK for a turnkey integration.
passthrough (Rendering) — You provide the audio, we render the face. Stream audio in via LiveKit, get animated avatar video back. Use your own AI stack — just GPU rendering from your audio.
Session Lifecycle
POST
/v1/realtime/session
Create a realtime avatar session. Returns a LiveKit room token and connection URL for client-side WebRTC connection.
Content-Type: application/json or multipart/form-data
Request Body
| Field | Type | Required | Description |
|---|
| face_url | string | no | HTTPS URL of a reference face image. Must be HTTPS, max 2048 chars. |
| face | file | no | Face image file upload (PNG/JPEG/WebP, max 10 MB). Use with multipart/form-data. |
mode | string | no | "conversation" (default, Interactive — $10/hr) or "passthrough" (Rendering — $6/hr). Interactive includes speech recognition, AI, and voice generation. Rendering is GPU only — you provide the audio. |
Rendering mode: Set mode: "passthrough" to use your own audio. Publish an audio track to the LiveKit room and the avatar will lip-sync to it in realtime. You handle speech recognition, AI, and voice generation — we handle the GPU rendering.
Response 200 OK
Interactive mode (default — $10/hr) {
"session_id": "ses_a1b2c3d4e5f6...",
"livekit_url": "wss://your-livekit-instance.livekit.cloud",
"token": "<livekit_jwt_token>",
"room": "atlas-rt-ses_a1b2c3d4e5f6...",
"mode": "conversation",
"max_duration_seconds": 3600,
"pricing": "$10/hour, prorated per second"
}{
"session_id": "ses_x9y8z7w6v5u4...",
"livekit_url": "wss://your-livekit-instance.livekit.cloud",
"token": "<livekit_jwt_token>",
"room": "atlas-rt-ses_x9y8z7w6v5u4...",
"mode": "passthrough",
"max_duration_seconds": 3600,
"pricing": "$6/hour, prorated per second"
}Interactive: $10/hour · Rendering: $6/hour — both prorated to the second.
Rendering Mode — Quick Start
import requests
API_KEY = "YOUR_API_KEY"
headers = {"Authorization": f"Bearer {API_KEY}"}
# Create a rendering session
session = requests.post(
"https://api.atlasv1.com/v1/realtime/session",
headers={**headers, "Content-Type": "application/json"},
json={
"face_url": "https://example.com/face.jpg",
"mode": "passthrough",
},
).json()
print(f"Session: {session['session_id']}")
print(f"Mode: {session['mode']}") # "passthrough" (rendering)
print(f"LiveKit URL: {session['livekit_url']}")
print(f"Token: {session['token'][:20]}...")
# Connect to LiveKit with the token, publish your TTS audio track,
# and subscribe to the avatar video track.
# Use livekit-client (JS) or livekit SDK (Python) to connect.Error Responses
| Status | Error | Description |
|---|
| 400 | invalid_face_url | face_url must use HTTPS or exceeds 2048 chars |
| 400 | invalid_mode | mode must be 'conversation' (Interactive) or 'passthrough' (Rendering) |
| 401 | unauthorized | Missing or invalid Authorization header |
| 403 | forbidden | Invalid or revoked API key |
| 429 | rate_limited | Too many requests — wait and retry |
| 503 | no_capacity | All GPU pods are busy. Retry after 30 seconds. |
GET
/v1/realtime/session/{session_id}
Retrieve the status and details of a specific realtime session. Only accessible by the API key that created it.
Path Parameters
| Parameter | Type | Description |
|---|
| session_id | string | The session ID returned from POST /v1/realtime/session |
Response 200 OK (active session) {
"session_id": "ses_a1b2c3d4e5f6...",
"status": "active",
"room": "atlas-rt-ses_a1b2c3d4e5f6...",
"started_at": "2026-04-01T01:30:00Z",
"ended_at": null,
"duration_seconds": 142.5
}Response 200 OK (ended session) {
"session_id": "ses_a1b2c3d4e5f6...",
"status": "ended",
"room": "atlas-rt-ses_a1b2c3d4e5f6...",
"started_at": "2026-04-01T01:30:00Z",
"ended_at": "2026-04-01T01:35:22Z",
"duration_seconds": 322.0
}POST
/v1/realtime/session/{session_id}
PATCHHot-swap face image mid-session
Change the avatar face image during an active session without disconnecting. The avatar seamlessly transitions to the new face. Rate-limited to prevent abuse.
Path Parameters
| Parameter | Type | Description |
|---|
| session_id | string | The active session ID to update |
Request Body
Content-Type: application/json or multipart/form-data
| Field | Type | Required | Description |
|---|
| face_url | string | no | HTTPS URL of the new face image |
| face | file | no | New face image file (PNG/JPEG/WebP, max 10 MB) |
Note: Provide either face_url or face, not both. The session must be active — patching an ended session returns 404.
{
"session_id": "ses_a1b2c3d4e5f6...",
"status": "active",
"message": "Face updated successfully."
}Error Responses
| Status | Error | Description |
|---|
| 400 | invalid_face_url | face_url must use HTTPS |
| 404 | not_found | Session not found or already ended |
| 429 | rate_limited | Too many face swaps — wait before retrying |
DELETE
/v1/realtime/session/{session_id}
End a realtime session. The LiveKit room is destroyed and billing duration is recorded. Returns the final cost.
Path Parameters
| Parameter | Type | Description |
|---|
| session_id | string | The session ID to end |
{
"session_id": "ses_a1b2c3d4e5f6...",
"status": "ended",
"duration_seconds": 322.0,
"estimated_cost": "$0.89"
}Response 409 Conflict (already ended) {
"error": "already_ended",
"message": "Session already ended."
}SDK Integration
Connect to a realtime session from your frontend using the LiveKit client SDK. Your backend creates the session and passes the token to the client.
Installnpm install livekit-client
import { Room, RoomEvent } from "livekit-client";
async function startRealtimeAvatar(faceUrl?: string) {
// Step 1 — Create session via your backend (which calls Atlas API)
const res = await fetch("/api/realtime-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ face_url: faceUrl }),
});
const { session_id, livekit_url, token, room: roomName } = await res.json();
// Step 2 — Connect to LiveKit room
const room = new Room();
room.on(RoomEvent.TrackSubscribed, (track) => {
if (track.kind === "video") {
const element = track.attach();
document.getElementById("avatar-container")?.appendChild(element);
}
});
await room.connect(livekit_url, token);
console.log("Connected to room:", roomName);
return { room, sessionId: session_id };
}
// Usage
const { room, sessionId } = await startRealtimeAvatar(
"https://example.com/face.jpg"
);
// Send text to make the avatar speak
room.localParticipant.publishData(
new TextEncoder().encode(JSON.stringify({
type: "speak",
text: "Hello! How can I help you today?",
})),
{ reliable: true }
);
// When done — disconnect client and end session
room.disconnect();
await fetch(`/api/realtime-session/${sessionId}`, { method: "DELETE" });Backend route (Next.js API route) // app/api/realtime-session/route.ts
export async function POST(req: Request) {
const { face_url, mode } = await req.json();
const res = await fetch("https://api.atlasv1.com/v1/realtime/session", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ATLAS_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ face_url, mode }), // "conversation" (Interactive) | "passthrough" (Rendering)
});
return Response.json(await res.json(), { status: res.status });
}Session limits: Each session runs for up to 1 hour. When all GPU pods are busy, new session requests return 503 with a retry_after_seconds field. Always call DELETE when done to stop billing.
Part of the full hosted API — all infrastructure managed by North Model Labs. The @northmodellabs/atlas-react package provides a single useAtlasSession() hook that handles all LiveKit wiring — room lifecycle, video/audio track subscription, microphone, transcriptions, latency measurement, and cleanup. Replace ~60 lines of manual LiveKit setup with one hook call.
Installnpm install @northmodellabs/atlas-react livekit-client
Quick Start
import { useAtlasSession } from "@northmodellabs/atlas-react";
function AvatarPage() {
const session = useAtlasSession({
createSession: async (face) => {
const form = new FormData();
if (face) form.append("face", face);
const res = await fetch("/api/session", { method: "POST", body: form });
return res.json(); // { sessionId, livekitUrl, token }
},
deleteSession: async (id) => {
await fetch(`/api/session/${id}`, { method: "DELETE" });
},
});
return (
<div>
<div ref={session.videoRef} style={{ width: 512, height: 512 }} />
{session.status === "idle" && (
<button onClick={() => session.connect(myFaceFile)}>Start</button>
)}
{session.status === "connected" && (
<>
<button onClick={() => session.setMicEnabled(session.muted)}>
{session.muted ? "Unmute" : "Mute"}
</button>
<button onClick={session.disconnect}>End</button>
</>
)}
{session.messages.filter(m => m.final).map((msg) => (
<p key={msg.id}><b>{msg.role}:</b> {msg.text}</p>
))}
</div>
);
}Hook Options
| Option | Type | Default | Description |
|---|
createSession | (face, faceUrl) => Promise | required | Creates a session on your backend — API key stays server-side |
deleteSession | (sessionId) => Promise | — | Tears down the session on your backend |
autoEnableMic | boolean | true | Auto-enable microphone after connecting |
autoCleanup | boolean | true | Auto-disconnect on unmount / tab close |
Returned Session Object
| Field | Type | Description |
|---|
status | "idle" | "connecting" | "connected" | "disconnected" | "error" | Connection state |
error | string | null | Error message if status is error |
sessionId | string | null | Active session ID |
messages | TranscriptMessage[] | Transcripts — check .final for completed segments |
muted | boolean | Whether mic is muted |
volume | number | Playback volume (0–100) |
latency | number | Round-trip time in ms |
videoRef | RefObject<HTMLDivElement> | Attach to a <div> — video renders inside |
connect(face?, faceUrl?) | () => Promise | Start a session |
disconnect() | () => Promise | End the session |
setMicEnabled(enabled) | (boolean) => void | Mute / unmute |
setVolume(v) | (number) => void | Set playback volume 0–100 |
sendChat(text) | (string) => void | Send a text message to the agent |
room | Room | null | Underlying LiveKit Room — for advanced scenarios |
publishAudio(audio) | (string | Blob | ArrayBuffer) => Promise<AudioPlaybackHandle> | Publish audio to the room (passthrough mode) — handles mic muting and track lifecycle |
What the Hook Handles
- — Creates a LiveKit Room with
adaptiveStream and dynacast - — Subscribes to video and audio tracks, attaches them to the DOM
- — Enables microphone after connecting
- — Listens for
TranscriptionReceived events and surfaces them as messages - — Measures round-trip latency
- — Disconnects and cleans up on unmount and
beforeunload - — Calls your
deleteSession callback to tear down the server-side session - — Provides
publishAudio() for passthrough mode — publishes TTS audio to the room, auto-mutes mic, and cleans up tracks
Passthrough Mode
In passthrough mode, you bring your own LLM, TTS, and audio pipeline — Atlas provides the GPU compute and WebRTC video. Create your session with mode: "passthrough", then use publishAudio() to send TTS audio to the avatar for lip-sync.
Passthrough — Publish TTS Audio const session = useAtlasSession({
createSession: async (face) => {
const form = new FormData();
if (face) form.append("face", face);
form.append("mode", "passthrough");
const res = await fetch("/api/session", { method: "POST", body: form });
return res.json();
},
deleteSession: async (id) => {
await fetch(`/api/session/${id}`, { method: "DELETE" });
},
});
// After connecting, publish audio from your own TTS:
async function handleUserMessage(text: string) {
// 1. Call your LLM
const llmResponse = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text }),
});
const { audio } = await llmResponse.json(); // base64 audio from your TTS
// 2. Publish to the room — avatar lip-syncs, mic auto-mutes
const handle = await session.publishAudio(audio);
// Optional: stop early
// handle.stop();
}publishAudio Details
| Feature | Detail |
|---|
| Input formats | Base64 string, Blob, or ArrayBuffer |
| Mic management | Automatically mutes mic during playback, restores when done |
| Track lifecycle | Creates a temporary LiveKit audio track, publishes it, unpublishes on completion |
| Cleanup | Cleaned up automatically on disconnect or when new audio is published |
| Return value | AudioPlaybackHandle with a stop() method for early termination |
Backend proxy (required) — Next.js API route // app/api/session/route.ts
const API_KEY = process.env.ATLAS_API_KEY;
const API_URL = process.env.ATLAS_API_URL || "https://api.atlasv1.com";
export async function POST(req: Request) {
const contentType = req.headers.get("content-type") || "";
// Supports both form uploads and JSON (for rendering mode)
let body: BodyInit;
let headers: Record<string, string> = { Authorization: `Bearer ${API_KEY}` };
if (contentType.includes("multipart/form-data")) {
body = await req.formData();
} else {
const json = await req.json();
body = JSON.stringify(json); // { face_url, mode: "passthrough" } for rendering
headers["Content-Type"] = "application/json";
}
const res = await fetch(`${API_URL}/v1/realtime/session`, {
method: "POST",
headers,
body,
});
return Response.json(await res.json(), { status: res.status });
}
// app/api/session/[id]/route.ts
export async function DELETE(_req: Request, { params }: { params: { id: string } }) {
const res = await fetch(`${API_URL}/v1/realtime/session/${params.id}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${API_KEY}` },
});
return Response.json(await res.json(), { status: res.status });
}Security: Your API key never touches the client. The createSession callback calls your own backend, which proxies to the Atlas API. See the full documentation on npm.
Event Notifications
Webhooks
Get notified when a job completes or fails instead of polling. Pass a callback_urlwhen submitting a job and we'll POST the result to your endpoint.
How to Use
Add callback_url to your request. For /v1/generate (multipart), send it as the X-Callback-URL header.
| Rule | Detail |
|---|
| Protocol | HTTPS only — HTTP and localhost are rejected |
| Retries | 3 attempts with backoff (5s, 30s, 120s) |
| Timeout | 10 seconds per attempt |
| Success | Any 2xx response counts as delivered |
Payload — Completed
POST to your callback_url {
"event": "job.completed",
"job_id": "a1b2c3d4e5f6",
"type": "video",
"status": "completed",
"url": "https://storage.example.com/jobs/.../output.mp4?X-Amz-Algorithm=...",
"expires_in": 86400,
"result_url": "https://api.atlasv1.com/v1/jobs/a1b2c3d4e5f6/result",
"created_at": "2026-03-31T16:47:07+00:00",
"completed_at": "2026-03-31T16:47:52+00:00"
}Payload — Failed
POST to your callback_url {
"event": "job.failed",
"job_id": "a1b2c3d4e5f6",
"type": "video",
"status": "failed",
"error_code": "generation_failed",
"created_at": "2026-03-31T16:47:07+00:00",
"completed_at": "2026-03-31T16:47:40+00:00"
}import requests
headers = {"Authorization": "Bearer YOUR_API_KEY"}
# For /v1/generate (multipart) — use X-Callback-URL header
job = requests.post(
"https://api.atlasv1.com/v1/generate",
headers={**headers, "X-Callback-URL": "https://yourapp.com/webhook/atlas"},
files={
"audio": ("speech.mp3", open("speech.mp3", "rb"), "audio/mp3"),
"image": ("face.jpg", open("face.jpg", "rb"), "image/jpeg"),
},
).json()
# No polling needed — your endpoint receives the resultVerifying Signatures
Every webhook includes X-Atlas-Signature and X-Atlas-Timestamp headers. Verify them to confirm the request came from Atlas.
import hmac, hashlib
def verify_atlas_webhook(body: bytes, signature: str, timestamp: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
f"{timestamp}.{body.decode()}".encode(),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
# In your Flask/FastAPI handler:
# signature = request.headers["X-Atlas-Signature"]
# timestamp = request.headers["X-Atlas-Timestamp"]
# verify_atlas_webhook(request.body, signature, timestamp, YOUR_SECRET)Error Responses
All errors return a consistent JSON format:
{
"error": "error_code",
"message": "Human-readable description"
}Error Codes
| Code | Error | Description |
|---|
| 400 | invalid_input | Empty or invalid audio/image file |
| 400 | invalid_mode | mode must be 'conversation' (Interactive) or 'passthrough' (Rendering) |
| 401 | unauthorized | Missing or malformed Authorization header |
| 403 | forbidden | Invalid API key |
| 404 | not_found | Endpoint or job does not exist |
| 404 | no_output | No stored output available for this job |
| 405 | method_not_allowed | Wrong HTTP method |
| 409 | not_ready | Downloading result before job completes |
| 413 | payload_too_large | Upload exceeds 50 MB limit |
| 415 | unsupported_media_type | Format not supported |
| 422 | validation_error | Missing required fields |
| 429 | rate_limit_exceeded | Rate limit hit |
| 429 | monthly_cap_exceeded | Monthly request cap reached |
| 502 | generation_failed | Generation failed after retries |
| 503 | queue_unavailable | Job queue is temporarily unavailable |
| 503 | storage_unavailable | Output storage is temporarily unavailable |
| 503 | storage_error | Failed to store or retrieve job output |
| 503 | temporarily_unavailable | Service is temporarily unavailable |
| 500 | url_generation_failed | Failed to generate download URL |
| 500 | internal_error | Unexpected server error |
| 503 | auth_unavailable | Authentication service is temporarily unavailable |
Example Errors
{
"error": "unauthorized",
"message": "Missing Authorization header. Use: Authorization: Bearer <api_key>"
}{
"error": "unauthorized",
"message": "Invalid Authorization format. Use: Authorization: Bearer <api_key>"
}{
"error": "not_ready",
"message": "Job is still processing. Poll GET /v1/jobs/{id} until status is completed."
}{
"error": "rate_limit_exceeded",
"message": "Slow down! You've hit the limit of 30 requests per minute.",
"retry_after_seconds": 12
}{
"error": "queue_unavailable",
"message": "Job queue is temporarily unavailable. Please try again in a moment."
}Bring Your Own TTS
TTS Integration
Atlas focuses on video generation — bring your own TTS provider for speech audio. Generate audio with ElevenLabs, OpenAI TTS, Deepgram, or any other service, then pass the audio file to POST /v1/generate.
The same async flow applies — submit audio + face image, poll for status, download the video.
Provider Examples
from elevenlabs import ElevenLabs
import requests, time
client = ElevenLabs(api_key="YOUR_ELEVENLABS_KEY")
audio = client.text_to_speech.convert(
text="Hello, welcome to our demo.",
voice_id="JBFqnCBsd6RMkjVDRZzb",
output_format="mp3_44100_128",
)
audio_bytes = b"".join(audio)
headers = {"Authorization": "Bearer YOUR_API_KEY"}
job = requests.post(
"https://api.atlasv1.com/v1/generate",
headers=headers,
files={
"audio": ("speech.mp3", audio_bytes, "audio/mp3"),
"image": ("face.jpg", open("face.jpg", "rb"), "image/jpeg"),
},
).json()
while True:
s = requests.get(f"https://api.atlasv1.com/v1/jobs/{job['job_id']}", headers=headers).json()
if s["status"] in ("completed", "failed"):
break
time.sleep(2)
r = requests.get(f"https://api.atlasv1.com/v1/jobs/{job['job_id']}/result", headers=headers).json()
video = requests.get(r["url"])
with open("output.mp4", "wb") as f:
f.write(video.content)from openai import OpenAI
import requests, time
speech = OpenAI().audio.speech.create(
model="tts-1-hd", voice="nova",
input="Hello, welcome to our demo.",
)
headers = {"Authorization": "Bearer YOUR_API_KEY"}
job = requests.post(
"https://api.atlasv1.com/v1/generate",
headers=headers,
files={
"audio": ("speech.mp3", speech.content, "audio/mp3"),
"image": ("face.jpg", open("face.jpg", "rb"), "image/jpeg"),
},
).json()
while True:
s = requests.get(f"https://api.atlasv1.com/v1/jobs/{job['job_id']}", headers=headers).json()
if s["status"] in ("completed", "failed"):
break
time.sleep(2)
r = requests.get(f"https://api.atlasv1.com/v1/jobs/{job['job_id']}/result", headers=headers).json()
video = requests.get(r["url"])
with open("output.mp4", "wb") as f:
f.write(video.content)from deepgram import DeepgramClient
import requests, time
deepgram = DeepgramClient("YOUR_DEEPGRAM_KEY")
deepgram.speak.v("1").save(
"speech.mp3",
{"text": "Hello, welcome to our demo."},
{"model": "aura-asteria-en"},
)
headers = {"Authorization": "Bearer YOUR_API_KEY"}
job = requests.post(
"https://api.atlasv1.com/v1/generate",
headers=headers,
files={
"audio": ("speech.mp3", open("speech.mp3", "rb"), "audio/mp3"),
"image": ("face.jpg", open("face.jpg", "rb"), "image/jpeg"),
},
).json()
while True:
s = requests.get(f"https://api.atlasv1.com/v1/jobs/{job['job_id']}", headers=headers).json()
if s["status"] in ("completed", "failed"):
break
time.sleep(2)
r = requests.get(f"https://api.atlasv1.com/v1/jobs/{job['job_id']}/result", headers=headers).json()
video = requests.get(r["url"])
with open("output.mp4", "wb") as f:
f.write(video.content)Rate Limits
- — Default: 30 requests per minute per API key
- — Rate limits use a sliding window
- — Check your current usage via
GET /v1/me - — When exceeded, the response includes
retry_after_seconds - — Check your plan and billing via
GET /v1/me - — Contact us for higher limits on enterprise plans
Limits & Constraints
| Constraint | Value |
|---|
| Max upload size | 50 MB |
| Server processing timeout (video) | 300s (5 min) |
| Max retries on failure | 3 |
| Rate limit (default) | 30 RPM |
| Job result availability | 24 hours after completion |
| Video output format | MP4 |
| Typical video generation | 40–50s |
Examples
See the API in Action
Working demos of avatar generation and the full pipeline — all interactive, no setup required.
View Examples→