North Model LabsNorth
Model Labs
ShowcaseResearchAPIPricingEnterpriseSolutionsFAQTeam
Dashboard

Models

  • Atlas Realtime Avatar
  • Showcase
  • Benchmark
  • Compare

Developers

  • Documentation
  • Examples
  • Pricing
  • Dashboard ↗

Solutions

  • Customer support
  • Sales / SDR
  • AI tutors
  • All solutions

Enterprise

  • Enterprise overview
  • Safety & identity
  • Compare
  • Book deployment review ↗
  • eric@northmodellabs.com

Company

  • Team
  • Partnerships
  • FAQ
  • eric@northmodellabs.com

Connect

  • GitHub ↗
  • Discord ↗
  • eric@northmodellabs.com
North Model Labs© 2026 North Model Labs
PrivacyTermseric@northmodellabs.comBook 30 min ↗

Atlas

Realtime Avatar

api · v8.0

Getting Started
  • Overview
  • Authentication
Examples
  • Live Examples→
Avatar Endpoints
  • GET /GET
  • POST /v1/generatePOST
Jobs
  • GET /v1/jobsGET
  • GET /v1/jobs/{id}GET
  • GET /v1/jobs/{id}/resultGET
Account & Status
  • GET /v1/meGET
  • GET /v1/statusGET
  • GET /v1/healthGET
TTS Integration (BYOB)
  • How It Works
Organizations
  • POST /v1/organizationsPOST
  • GET /v1/organizationsGET
  • GET /v1/organizations/{id}GET
  • Members
SSO & SCIM
  • SSO Setup
  • SCIM Tokens
  • SCIM Discovery
  • SCIM Users
Realtime Avatar
  • How It Works
  • POST /v1/realtime/sessionPOST
  • POST /…/{id}/viewer ✦ NEWPOST
  • GET /v1/realtime/session/{id}GET
  • PATCH /v1/realtime/session/{id}PATCH
  • DELETE /v1/realtime/session/{id}DEL
  • SDK Integration
  • React SDK
Webhooks
  • How Webhooks Work
  • Verifying Signatures
Reference
  • Error Codes
  • TTS Examples
  • Rate Limits
  • Limits & Constraints
  • Response Headers
  • Plugin: external LiveKit

Talk to us

eric@northmodellabs.comBook 30 min ↗

Atlas · Realtime Avatar API · v8.0

API documentation

Lip-sync avatar videos and live WebRTC streams through one async API. Submit, poll, download, or open a passthrough session and stream audio in for realtime face rendering.

Atlas Realtime Avatar
NEW

Multi-Viewer Sessions

Issue view-only tokens for an active realtime session so multiple viewers can watch the same rendered stream without starting another render session. See the endpoint →

UPDATE

OpenClaw (ClawHub)

The Atlas Avatar skill lets OpenClaw agents create passthrough realtime sessions (LiveKit) and offline generate jobs via the same HTTP API you use here, see the listing for install steps, env vars (NORTH_API_KEY), and security notes. Atlas Avatar on ClawHub →

Generate photorealistic lip-sync avatar videos through an async job queue. Bring your own speech audio, submit a job, poll for status, then download the result. Generation endpoints return 202 Accepted with a job ID, no more waiting for a synchronous response.

Skip to a working app

Offline Example

Next.js · Text/Audio → Video

→

Realtime Example

Next.js · Live Avatar + LLM

→

React SDK

npm · useAtlasSession()

→
Code Examples In
Offline API

Pre-rendered Videos

Submit audio + image, get an MP4 back. Best for content creation, batch processing, and async workflows.

Realtime API

Live Avatars (WebRTC)

Realtime WebRTC stream. You send audio; we return lip-synced video. Best for agents, support, and live demos.

Realtime (passthrough)

Rendering, you provide audio
You provideFace image + audio stream (your STT / LLM / TTS)
You get backLip-synced WebRTC video
GPU rendering✓
One-shot reference image✓
No hard max duration✓
Role in your stackVisual rendering layer
React SDK✓
Price$7/hr (prorated per second)

How It Works, 3-Step Flow

Step 1

Submit

POST to a generation endpoint. Returns 202 with job_id

Step 2

Poll

GET /v1/jobs/{id} until status is completed

Step 3

Download

GET /v1/jobs/{id}/result for a presigned download URL

Base URLhttps://api.atlasv1.com
Examples

Implementation Examples

Review complete integration patterns for avatar generation and realtime rendering.

→

Authentication

Most API endpoints require an API key via the Authorization header. Public endpoints are GET /, /v1/health, /healthz, SCIM discovery endpoints, and the SSO login/callback flow.

Header

Authorization: Bearer <your_api_key>

Generate API keys from your dashboard after adding a payment method. For enterprise plans, email eric@northmodellabs.com or book a 30-min call ↗.

GET

/

Returns API info and available endpoints. No authentication required.

Response 200 OK
{
  "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",
    "POST /v1/realtime/session/{id}/viewer": "Get a view-only token for an active session",
    "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",
    "POST /v1/organizations": "Create an organization",
    "GET /v1/organizations": "List your organizations",
    "GET /v1/organizations/{id}": "Organization details and members",
    "POST /v1/organizations/{id}/members": "Add a member",
    "DELETE /v1/organizations/{id}/members/{mid}": "Remove a member",
    "POST /v1/organizations/{id}/scim-tokens": "Generate SCIM bearer token",
    "GET /v1/organizations/{id}/sso": "Get SSO configuration",
    "POST /v1/organizations/{id}/sso/setup": "Generate WorkOS Admin Portal setup link",
    "POST /v1/organizations/{id}/sso/enable": "Enable or disable SSO",
    "GET /v1/auth/sso": "Start SSO login with org_slug",
    "GET /scim/v2/Users": "SCIM: List provisioned users",
    "POST /scim/v2/Users": "SCIM: Provision a user",
    "GET /scim/v2/Users/{id}": "SCIM: Get user",
    "PUT /scim/v2/Users/{id}": "SCIM: Replace user",
    "PATCH /scim/v2/Users/{id}": "SCIM: Update user attributes",
    "DELETE /scim/v2/Users/{id}": "SCIM: Deprovision a user",
    "GET /scim/v2/ServiceProviderConfig": "SCIM: Service provider capabilities",
    "GET /scim/v2/Schemas": "SCIM: Supported schemas",
    "GET /scim/v2/ResourceTypes": "SCIM: Resource types"
  },
  "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

FieldTypeRequiredDescription
audiofileyesAudio file for lip-sync
imagefileyesReference 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 $7/hour ($0.117/min · $0.0019/sec) of output video duration. See pricing.

Response 202 Accepted

Response
{
  "job_id": "a1b2c3d4e5f6",
  "status": "pending",
  "message": "Job accepted. Poll GET /v1/jobs/a1b2c3d4e5f6 for status."
}
cURL
# 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("Saved output.mp4")
View examplesReview working integration code for 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

ParamTypeDefaultDescription
limitint20Number of results to return (max 100)
offsetint0Number of results to skip
Response 200 OK
{
  "jobs": [
    {
      "job_id": "a1b2c3d4e5f6",
      "type": "video",
      "status": "completed",
      "created_at": "2026-03-25T16:47:07Z",
      "completed_at": "2026-03-25T16:47:52Z",
      "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

StatusDescription
pendingJob is queued, waiting to be processed
processingJob is actively being processed
completedJob finished, output ready to download
failedJob failed, check error field for details
Response, Processing
{
  "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
  }
}
Response, Completed
{
  "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"
  },
  "url": "https://t3.storage.dev/atlas-jobs/.../output.mp4?X-Amz-Algorithm=...",
  "expires_in": 86400,
  "result_url": "/v1/jobs/a1b2c3d4e5f6/result"
}
Response, Failed
{
  "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"
  }
}
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

Response JSON
{
  "url": "https://t3.storage.dev/atlas-jobs/.../output.mp4?X-Amz-Algorithm=...",
  "content_type": "video/mp4",
  "expires_in": 86400
}
FieldDescription
urlPresigned download URL, no auth needed, expires after expires_in seconds
content_typeMIME type of the output (video/mp4 for video jobs; audio/wav for WAV audio jobs)
expires_inURL validity in seconds (default 24 hours)
GET

/v1/me

Check your API key status, current rate limit usage, and plan details.

Response 200 OK
{
  "authenticated": true,
  "key_prefix": "ak_...",
  "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.

Response 200 OK
{
  "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.

Response 200 OK
{
  "status": "operational"
}
Realtime Avatar API

Realtime

Live avatar rendering over WebRTC. Unlike the Offline API (send files → get video back), the Realtime API accepts your audio stream and returns lip-synced video in real time.

Integration uses the LiveKit client SDK. Request a session token from our API, then connect with the LiveKit SDK on the client side. Realtime sessions use passthrough (rendering): you publish audio, we stream lip-synced video. Billed at $7/hour, prorated to the second.

Realtime sessions have no fixed session-length cap while active. Billing is prorated to the second. Idle rooms close after the configured idle_timeout. Get your API key from the dashboard, or reach out to discuss your use case.

Passthrough (rendering)

Set mode: "passthrough" (or omit mode, it defaults to passthrough). You provide the audio stream; Atlas renders the face over WebRTC. Bring your own STT, LLM, and TTS. Use the React SDK for less boilerplate.

Session Lifecycle

1

Create

2

Connect

3

Active

4

Disconnect

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

FieldTypeRequiredDescription
face_urlstringnoHTTPS URL of a reference face image. Must be HTTPS, max 2048 chars.
facefilenoFace image file upload (PNG/JPEG/WebP, max 10 MB). Use with multipart/form-data.
modestringno"passthrough" is the rendering mode and the default.
idle_timeoutintegernoSeconds an empty/inactive room can remain open before cleanup. Default 300, clamped 30–3600.

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

Passthrough session ($7/hr)
{
  "session_id": "ses_x9y8z7w6v5u4...",
  "livekit_url": "wss://your-livekit-instance.livekit.cloud",
  "token": "<livekit_jwt_token>",
  "room": "atlas-rt-ses_x9y8z7w6v5u4...",
  "mode": "passthrough",
  "idle_timeout_seconds": 300,
  "pricing": "$7/hour, prorated per second"
}

Realtime passthrough: $7/hour, 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

StatusErrorDescription
400invalid_face_urlface_url must use HTTPS or exceeds 2048 chars
400invalid_modemode must be "passthrough"
401unauthorizedMissing or invalid Authorization header
403forbiddenInvalid or revoked API key
429rate_limitedToo many requests, wait and retry
503no_capacityAll GPU pods are busy. Retry after 30 seconds.
POST

/v1/realtime/session/{session_id}/viewer

NEW

Issue a view-only LiveKit token for an existing active session. Viewers can watch the avatar stream but cannot publish audio, video, or data. Perfect for multi-viewer scenarios, one user drives the avatar, others watch the same stream. No additional GPU is consumed.

Path Parameters

ParameterTypeDescription
session_idstringThe session ID returned from POST /v1/realtime/session

Headers

HeaderRequiredDescription
AuthorizationYesBearer <api_key>, must be the same key that created the session
Response 200 OK
{
  "session_id": "ses_a1b2c3d4e5f6...",
  "livekit_url": "wss://livekit.example.com",
  "token": "eyJ...",
  "room": "atlas-rt-ses_a1b2c3d4e5f6...",
  "viewer_id": "viewer-abc123def456",
  "role": "viewer",
  "permissions": {
    "can_publish": false,
    "can_subscribe": true,
    "can_publish_data": false
  }
}

Multi-Viewer Architecture

Each call returns a unique viewer token. Viewers subscribe to the same avatar video and audio tracks without starting another render session. Confirm participant limits for your LiveKit deployment. To let viewers interact by text or voice, relay messages through your application backend.

import requests

r = requests.post(
    "https://api.atlasv1.com/v1/realtime/session/ses_abc123/viewer",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)
viewer = r.json()
print(viewer["token"])    # give this to the viewer client
print(viewer["role"])     # "viewer"
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

ParameterTypeDescription
session_idstringThe 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
}
PATCH

/v1/realtime/session/{session_id}

Hot-swap the avatar face during an active session without disconnecting. Uses a secure file upload (arbitrary URLs are not accepted on this endpoint). Rate-limited per session.

Path Parameters

ParameterTypeDescription
session_idstringThe active session ID to update

Request Body

Content-Type: multipart/form-data, field face (image file, PNG/JPEG/WebP, max 10 MB).

FieldTypeRequiredDescription
facefileyesNew face image file (PNG/JPEG/WebP, max 10 MB)

HTTPS face URL: set face_url on POST /v1/realtime/session (JSON or multipart at create). To change the face mid-session, upload a new file with this PATCH.

Response 200 OK
{
  "session_id": "ses_a1b2c3d4e5f6...",
  "face_updated": true,
  "metadata_pushed": true,
  "message": "Face image updated. The avatar will switch within seconds."
}

Error Responses

StatusErrorDescription
404not_foundSession not found
409session_not_activeSession is ended or not active, cannot update face
429rate_limitedToo 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

ParameterTypeDescription
session_idstringThe session ID to end
Response 200 OK
{
  "session_id": "ses_a1b2c3d4e5f6...",
  "status": "ended",
  "duration_seconds": 322.0,
  "estimated_cost": "$0.8944",
  "credits_deducted_cents": 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.

Recommended: Use @northmodellabs/atlas-react to replace all the boilerplate below with a single useAtlasSession() hook call. See React SDK →

Installnpm install livekit-client
React / Next.js
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"
);

// Publish your TTS / pipeline audio (see Passthrough Mode, persistent audio track)
// const audioTrack = await createLocalAudioTrack({ ... });
// await room.localParticipant.publishTrack(audioTrack);

// 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 } = 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: "passthrough" }),
  });

  return Response.json(await res.json(), { status: res.status });
}

Billing: Realtime sessions have no fixed session-length cap and are billed per second while active. 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.

React SDK

npm
Hosted API

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 LiveKit wiring, room lifecycle, avatar video subscription, optional mic helpers, realtime session state, and cleanup. For audio-driven lip-sync, use passthrough and the persistent audio track pattern.

Installnpm install @northmodellabs/atlas-react livekit-client

Quick Start

React Component
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>
        </>
      )}
    </div>
  );
}

Hook Options

OptionTypeDefaultDescription
createSession(face, faceUrl) => PromiserequiredCreates a session on your backend, API key stays server-side
deleteSession(sessionId) => Promise-Tears down the session on your backend
autoEnableMicbooleantrueAuto-enable microphone after connecting
autoCleanupbooleantrueAuto-disconnect on unmount / tab close

Returned Session Object

FieldTypeDescription
status"idle" | "connecting" | "connected" | "disconnected" | "error"Connection state
errorstring | nullError message if status is error
sessionIdstring | nullActive session ID
mutedbooleanWhether mic is muted
volumenumberPlayback volume (0–100)
videoRefRefObject<HTMLDivElement>Attach to a <div>, video renders inside
connect(face?, faceUrl?)() => PromiseStart a session
disconnect()() => PromiseEnd the session
setMicEnabled(enabled)(boolean) => voidMute / unmute
setVolume(v)(number) => voidSet playback volume 0–100
roomRoom | nullUnderlying LiveKit Room, for advanced scenarios
publishAudio(audio)(string | Blob | ArrayBuffer) => Promise<AudioPlaybackHandle>Publish audio to the room (passthrough mode). Recommended: use the persistent audio track pattern instead, see Passthrough Mode

What the Hook Handles

  • - Creates a LiveKit Room with adaptiveStream and dynacast
  • - Subscribes to video and audio tracks, attaches them to the DOM
  • - Optional mic helpers after connecting (your STT / pipeline may use this)
  • - Tracks realtime session state for your UI
  • - Disconnects and cleans up on unmount and beforeunload
  • - Calls your deleteSession callback to tear down the server-side session
  • - Exposes room for passthrough mode, use the persistent audio track pattern for freeze-free lip-sync

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 publish audio to the avatar for lip-sync.

Important: Persistent Audio Track Pattern

Use a persistent audio track that stays published for the entire session. The track feeds silence when idle (keeping the avatar animated) and TTS audio when speaking. Avoid calling publishAudio() directly, it tears down the track after each call, causing the avatar to freeze between messages.

Passthrough, Persistent Audio Track (Recommended)
import { useEffect, useRef } from "react";
import { useAtlasSession } from "@northmodellabs/atlas-react";
import { LocalAudioTrack, Track } from "livekit-client";

const session = useAtlasSession({
  autoEnableMic: false,
  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" });
  },
});

// Refs for the persistent audio pipeline
const audioCtxRef = useRef(null);
const destRef = useRef(null);
const ttsSourceRef = useRef(null);

// Publish a persistent audio track when connected
useEffect(() => {
  if (session.status !== "connected" || !session.room) return;

  const audioCtx = new AudioContext();
  const dest = audioCtx.createMediaStreamDestination();
  const mediaTrack = dest.stream.getAudioTracks()[0];
  const lkTrack = new LocalAudioTrack(mediaTrack);

  audioCtxRef.current = audioCtx;
  destRef.current = dest;

  session.room.localParticipant.publishTrack(lkTrack, {
    name: "tts-audio",
    source: Track.Source.Unknown,
  });

  return () => {
    ttsSourceRef.current?.stop();
    session.room?.localParticipant.unpublishTrack(lkTrack);
    lkTrack.stop();
    audioCtx.close();
  };
}, [session.status, session.room]);

// Play TTS audio through the persistent track
function playTtsAudio(base64Audio) {
  const audioCtx = audioCtxRef.current;
  const dest = destRef.current;
  if (!audioCtx || !dest) return;

  ttsSourceRef.current?.stop(); // cancel previous if still playing

  const binary = atob(base64Audio);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);

  audioCtx.decodeAudioData(bytes.buffer.slice(0)).then((buf) => {
    const source = audioCtx.createBufferSource();
    source.buffer = buf;
    source.connect(dest);
    ttsSourceRef.current = source;
    source.onended = () => { source.disconnect(); ttsSourceRef.current = null; };
    source.start();
  });
}

// Use it: get LLM text first, then fetch TTS separately
async function handleUserMessage(text) {
  // Text appears instantly (no waiting for TTS)
  const chatRes = await fetch("/api/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ text }),
  });
  const { text: reply } = await chatRes.json();

  // TTS audio fetched in background (avatar speaks when ready)
  if (reply) {
    const ttsRes = await fetch("/api/tts", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ text: reply }),
    });
    const { audio } = await ttsRes.json();
    if (audio) playTtsAudio(audio);
  }
}

How the Persistent Track Works

StateWhat happens
Session connectsA single MediaStreamDestination is published as an audio track, outputs silence
Avatar idleSilence frames flow through the track → GPU renders idle animation (avatar stays alive)
TTS playsA BufferSource connects to the same destination → TTS audio flows through → avatar lip-syncs
TTS endsBufferSource disconnects → back to silence → avatar returns to idle animation
Session disconnectsTrack is unpublished and AudioContext is closed

The key insight: one persistent track, never torn down. Silence when idle, TTS audio when speaking. No track re-publishing, no mic toggling, no avatar freeze.

Voice Input & Echo Cancellation

Use ElevenLabs Scribe v2 for speech-to-text instead of the browser's Web Speech API. The Web Speech API picks up the avatar's TTS audio from the speakers and feeds it back as user input, causing the avatar to talk to itself. ElevenLabs Scribe connects to the mic via getUserMedia with echoCancellation: true, so the browser's built-in Acoustic Echo Cancellation strips the speaker output at the hardware level before it reaches the STT model.

Install npm install @elevenlabs/react, see the example app for the full implementation with useScribe hook and server-side token endpoint.

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 URL when submitting a job and Atlas can POST the result to your endpoint when webhook signing is configured. Polling remains the supported fallback for every job.

How to Use

For /v1/generate (multipart), send your callback as the X-Callback-URL header. Atlas validates the URL at submit time and again at delivery time.

RuleDetail
ProtocolHTTPS only, HTTP and localhost are rejected
SigningRequires Atlas webhook signing to be configured; otherwise poll job status
Retries3 attempts with backoff (5s, 30s, 120s)
Timeout10 seconds per attempt
SuccessAny 2xx response counts as delivered

Payload, Completed

POST to your callback endpoint
{
  "event": "job.completed",
  "job_id": "a1b2c3d4e5f6",
  "type": "video",
  "status": "completed",
  "url": "https://t3.storage.dev/atlas-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 endpoint
{
  "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 result

Verifying 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 Format
{
  "error": "error_code",
  "message": "Human-readable description"
}

Error Codes

CodeErrorDescription
400invalid_inputEmpty or invalid audio/image file
400invalid_modemode must be "passthrough"
401unauthorizedMissing or malformed Authorization header
403forbiddenInvalid API key
404not_foundEndpoint or job does not exist
404no_outputNo stored output available for this job
405method_not_allowedWrong HTTP method
409not_readyDownloading result before job completes
409already_endedRealtime session DELETE when session is already ended
413payload_too_largeUpload exceeds 50 MB limit
415unsupported_media_typeFormat not supported
422validation_errorMissing required fields
429rate_limit_exceededBasic self-serve rate limit reached
502generation_failedGeneration failed after retries
503queue_unavailableJob queue is temporarily unavailable
503storage_unavailableOutput storage is temporarily unavailable
503storage_errorFailed to store or retrieve job output
503temporarily_unavailableService is temporarily unavailable
500url_generation_failedFailed to generate download URL
500internal_errorUnexpected server error
503auth_unavailableAuthentication service is temporarily unavailable

Example Errors

401 Unauthorized
{
  "error": "unauthorized",
  "message": "Missing Authorization header. Use: Authorization: Bearer <api_key>"
}
401 Invalid Format
{
  "error": "unauthorized",
  "message": "Invalid Authorization format. Use: Authorization: Bearer <api_key>"
}
409 Not Ready
{
  "error": "not_ready",
  "message": "Job is still processing. Poll GET /v1/jobs/{id} until status is completed."
}
429 Too Many Requests
{
  "error": "rate_limit_exceeded",
  "message": "Basic self-serve rate limit reached. Retry after the returned delay.",
  "retry_after_seconds": 12
}
503 Queue Unavailable
{
  "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)
Enterprise Identity

Organizations, SSO, SCIM

Organization endpoints let teams manage members, configure WorkOS SSO, and provision users through SCIM. Organization management requires a user-linked dashboard API key; demo keys, fallback keys, and environment-only service keys cannot create organizations. SCIM user endpoints use an organization-scoped SCIM bearer token.

POST

/v1/organizations

Create an organization. The API key must belong to a dashboard user, and that user becomes the owner.

FieldTypeRequiredDescription
namestringyesOrganization name, 2–100 characters
Request / Response
POST /v1/organizations
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{ "name": "Acme AI" }

// 201 Created
{
  "id": "org_...",
  "name": "Acme AI",
  "slug": "acme-ai",
  "owner": "owner@example.com"
}
GET

/v1/organizations

List organizations the authenticated user belongs to.

Response 200 OK
[
  {
    "id": "org_...",
    "name": "Acme AI",
    "slug": "acme-ai",
    "plan": "enterprise",
    "role": "owner"
  }
]
GET

/v1/organizations/{org_id}

Get organization details and members. The caller must be an active member.

Response 200 OK
{
  "id": "org_...",
  "name": "Acme AI",
  "slug": "acme-ai",
  "plan": "enterprise",
  "your_role": "owner",
  "members": [
    {
      "id": "mem_...",
      "email": "owner@example.com",
      "role": "owner",
      "active": true,
      "display_name": "Owner",
      "created_at": "2026-05-21T18:00:00+00:00"
    }
  ]
}

Organization Members

EndpointRolePurpose
POST /v1/organizations/{org_id}/membersadmin or ownerAdd or reactivate a member
DELETE /v1/organizations/{org_id}/members/{member_id}admin or ownerDeactivate a member and revoke their org-scoped API keys
DELETE /v1/organizations/{org_id}ownerSoft-delete the org, deactivate members, revoke keys and SCIM tokens
Add Member
POST /v1/organizations/org_.../members
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{
  "email": "teammate@example.com",
  "role": "member"
}

// role: "admin", "member", or "viewer"

SAML SSO

SSO is configured through WorkOS. Owners create an Admin Portal setup link, enable SSO after a connection is configured, and users start login with an organization slug.

EndpointAuthPurpose
GET /v1/organizations/{org_id}/ssoAPI key, admin or ownerRead SSO configuration status
POST /v1/organizations/{org_id}/sso/setupAPI key, ownerGenerate a WorkOS Admin Portal setup link
POST /v1/organizations/{org_id}/sso/enableAPI key, ownerEnable or disable SSO enforcement
GET /v1/auth/sso?org_slug=...publicStart SSO login and receive a WorkOS authorization URL
GET /v1/auth/sso/callbackpublic callbackExchange WorkOS code for an Atlas SSO session token
POST /v1/auth/sso/webhookWorkOS signatureReceive WorkOS connection lifecycle events
SSO Setup
POST /v1/organizations/org_.../sso/setup
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{
  "org_slug": "acme-ai",
  "return_url": "https://yourapp.com/settings/sso"
}

// 200 OK
{
  "setup_url": "https://id.workos.com/portal/...",
  "workos_org_id": "org_..."
}
Start SSO Login
GET /v1/auth/sso?org_slug=acme-ai

// 200 OK
{
  "authorization_url": "https://api.workos.com/sso/authorize?...",
  "state": "..."
}

SCIM Tokens

Owners create organization-scoped SCIM bearer tokens for IdP provisioning. The raw token is returned only once; store it in your IdP immediately.

EndpointRolePurpose
POST /v1/organizations/{org_id}/scim-tokensownerCreate a SCIM token
DELETE /v1/organizations/{org_id}/scim-tokens/{token_id}ownerRevoke a SCIM token
Create SCIM Token
POST /v1/organizations/org_.../scim-tokens
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{ "label": "Okta production" }

// 201 Created
{
  "token_id": "sctk_...",
  "token": "scim_...",
  "label": "Okta production",
  "expires_at": "2027-05-21T18:00:00+00:00",
  "warning": "Store this token securely. It will not be shown again."
}

SCIM Discovery

SCIM discovery endpoints are public so IdPs can detect capabilities before sending a bearer token.

EndpointAuthPurpose
GET /scim/v2/ServiceProviderConfignoneSCIM capabilities, patch supported, bulk disabled
GET /scim/v2/SchemasnoneSupported User schema
GET /scim/v2/ResourceTypesnoneSupported resource types

SCIM Users

SCIM user endpoints require Authorization: Bearer scim_.... Deprovisioning a user revokes their organization-scoped API keys, ends active sessions, and revokes SSO sessions.

EndpointPurpose
GET /scim/v2/UsersList users, supports userName eq and externalId eq filters
POST /scim/v2/UsersProvision a user
GET /scim/v2/Users/{id}Get one user
PUT /scim/v2/Users/{id}Replace a user
PATCH /scim/v2/Users/{id}Patch active, email, name, or externalId
DELETE /scim/v2/Users/{id}Deprovision a user
Provision User
POST /scim/v2/Users
Authorization: Bearer scim_...
Content-Type: application/scim+json

{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
  "userName": "user@example.com",
  "active": true,
  "externalId": "00u123",
  "name": {
    "givenName": "Ada",
    "familyName": "Lovelace"
  },
  "emails": [
    { "value": "user@example.com", "primary": true }
  ]
}
Deactivate User
PATCH /scim/v2/Users/mem_...
Authorization: Bearer scim_...
Content-Type: application/scim+json

{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
  "Operations": [
    { "op": "replace", "path": "active", "value": false }
  ]
}

Rate Limits

Self-serve developer keys ship with a default limit so a single key can't saturate shared capacity. Production and enterprise contracts include custom concurrency, throughput, and dedicated GPU capacity, no fixed RPM ceiling.

  • - Self-serve default: 30 requests per minute per API key, sliding window
  • - Production: high-volume throughput, burst, and concurrency, set per workload
  • - Enterprise: reserved GPU pool, dedicated capacity, no shared-tier rate limit
  • - Check your current usage and plan via GET /v1/me
  • - When exceeded, the response includes retry_after_seconds
  • - Higher limits / dedicated capacity: /enterprise or email eric@northmodellabs.com

Limits & Constraints

ConstraintValue
Max upload size50 MB
Offline job processing timeoutConfigurable, default 300s; can be set higher or disabled for dedicated deployments
Realtime processing timeoutNone while the session is active; idle cleanup is controlled by idle_timeout
Max retries on failure3
Rate limit30 RPM for basic self-serve keys; production and enterprise use custom/high-volume limits with no shared fixed RPM ceiling
Job result availability24 hours after completion
Video output formatMP4
Realtime modeLive WebRTC avatar streaming once connected
Examples

See the API in Action

Implementation examples for offline generation, webhooks, and BYO-audio realtime rendering.

View Examples→

Plugin, external LiveKit room

POST /v1/avatar/session is omitted from the GET / endpoint list. Use it when you already have a LiveKit room and want Atlas to join with the livekit-plugins-atlas SDK flow.

POST

/v1/avatar/session

Multipart: livekit_url, livekit_token, room_name (required strings); optional avatar_image file.

Response 200 OK
{
  "session_id": "ses_...",
  "status": "ok"
}

Response Headers

Included in all responses for security and caching:

HeaderValue
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
Cache-Controlno-store
Strict-Transport-Securitymax-age=31536000; includeSubDomains
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policycamera=(), microphone=(), geolocation=()
X-Permitted-Cross-Domain-Policiesnone