mirrorstagemirrorstagerender

← Home

API Reference

Endpoints

No signup. No API key. No avatar library. Send any face photo to render.mirrorstage.ai and get a lip-synced video stream back. Payment happens inline via x402 — your wallet is your only credential.

Getting started

1.POST /v1/sessions with a face photo. Omit output_url to use LiveKit, or provide RTMP if you need it.
2.Get back a 402 with the price, wallet address, and network in the header.
3.Sign a USDC authorization with your wallet and retry the same request.
4.Session starts warming. Poll for status — when it's "active", connect the WebSocket.
5.Stream audio in, and lip-synced video appears on the session’s LiveKit or RTMP output.

That's it. No dashboard, no API key, no avatar training. USDC on Base or Solana.

Payment

Sessions are paid with USDC via x402. First 5 minutes are free, then $0.10/min. Minimum session is 15 minutes ($1.00). Both Base (EVM) and Solana are accepted.

1.POST /v1/sessions without a payment header — returns 402 with a PAYMENT-REQUIRED header.
2.Decode the header (base64 JSON) to get the price, wallet, and network for each chain.
3.Use the x402 SDK to sign a USDC payment with your wallet.
4.Retry the exact same request with the PAYMENT-SIGNATURE header.

EVM (Base) — Python

pip install 'x402[evm]' httpx eth-account

import asyncio, httpx, os
from eth_account import Account
from x402 import x402Client
from x402.http import x402HTTPClient
from x402.mechanisms.evm import EthAccountSigner
from x402.mechanisms.evm.exact.register import register_exact_evm_client

GATEWAY = "https://render.mirrorstage.ai"

async def create_session(face_path, output_url=""):
    # Set up x402 client with your EVM wallet
    account = Account.from_key(os.environ["EVM_PRIVATE_KEY"])
    client = x402Client()
    register_exact_evm_client(client, EthAccountSigner(account))
    http_client = x402HTTPClient(client)

    async with httpx.AsyncClient() as http:
        # Step 1: POST without payment — returns 402
        files = {"reference_image": (face_path, open(face_path, "rb"), "image/png")}
        data = {"output_url": output_url, "duration_minutes": "15", "accept_tos": "true"}
        resp = await http.post(f"{GATEWAY}/v1/sessions", files=files, data=data)

        if resp.status_code == 402:
            # Step 2: Sign the USDC payment
            payment_headers, _ = await http_client.handle_402_response(
                headers=dict(resp.headers), body=resp.content)

            # Step 3: Retry with payment header
            files = {"reference_image": (face_path, open(face_path, "rb"), "image/png")}
            resp = await http.post(f"{GATEWAY}/v1/sessions",
                files=files, data=data, headers=payment_headers)

        return resp.json()  # session_id, token, websocket_url, etc.

Solana

# For Solana instead of EVM:
pip install 'x402[svm]'

# Then replace the wallet setup:
from solders.keypair import Keypair
from x402.mechanisms.svm.signers import KeypairSigner
from x402.mechanisms.svm.exact.register import register_exact_svm_client

keypair = Keypair.from_base58_string(os.environ["SOLANA_PRIVATE_KEY"])
client = x402Client()
register_exact_svm_client(client, KeypairSigner(keypair))
# Everything else is the same — the SDK picks the right chain

Always confirm with the user before paying. Show the price, duration, and wallet — never sign a transaction without explicit approval.

Payment is settled on-chain when the session is created. USDC moves from your wallet.

Each signed payment can only be used once (replay protection).

If the retry fails for a non-payment reason, you have not been charged.

The 402 response lists all accepted chains — the SDK picks the first one your wallet supports.

Examples

Your agent already generates text. Pipe it through any TTS provider, then stream the audio to Mirrorstage. The avatar speaks what your agent writes.

Any TTS provider works — here are a few

ElevenLabspcm_16000Raw PCM, lowest latency
Fish Audiomp3 / pcmCheap alternative, good quality
OpenAI TTSmp3Auto-decoded on server
Google CloudLINEAR16Raw PCM
Deepgramlinear16Raw PCM

Send PCM for the lowest latency, or any encoded format (MP3, WAV, OGG, FLAC) — auto-detected on the server.

Agent + ElevenLabs

Agent generates text → ElevenLabs converts to PCM → Mirrorstage renders the avatar.

import asyncio, httpx, json, websockets

API = "https://render.mirrorstage.ai"
FACE = "face.jpg"
OUTPUT_URL = ""  # omit to use the gateway's configured LiveKit URL
XI_KEY = "your-elevenlabs-key"
XI_VOICE = "your-voice-id"

async def speak(ws, text):
    """Convert agent text to speech and stream to the avatar."""
    import requests
    audio = requests.post(
        f"https://api.elevenlabs.io/v1/text-to-speech/{XI_VOICE}?output_format=pcm_16000",
        headers={"xi-api-key": XI_KEY, "Content-Type": "application/json"},
        json={"text": text, "model_id": "eleven_turbo_v2_5"},
    ).content
    # Stream PCM in 100ms chunks — GPU renders each frame as it arrives
    for i in range(0, len(audio), 3200):
        await ws.send(audio[i:i+3200])
        await asyncio.sleep(0.1)
    await ws.send(json.dumps({"type": "speak_done"}))

async def main():
    # 1. Create session (x402 payment — see Getting Started)
    async with httpx.AsyncClient(timeout=30) as c:
        r = await c.post(f"{API}/v1/sessions",
            files={"reference_image": open(FACE, "rb")},
            data={"output_url": OUTPUT_URL, "accept_tos": "true"})
        s = r.json()

    # 2. Wait for GPU to be ready
    async with httpx.AsyncClient(timeout=15) as c:
        while True:
            r = await c.get(f"{API}/v1/sessions/{s['session_id']}",
                headers={"Authorization": f"Bearer {s['token']}"})
            if r.json()["status"] == "active": break
            await asyncio.sleep(5)

    # 3. Agent loop — each response becomes a spoken utterance
    async with websockets.connect(f"{s['websocket_url']}?token={s['token']}", ping_interval=None) as ws:
        agent_responses = [
            "Welcome to the stream. I'm your AI host.",
            "Let me tell you about what we're building today.",
            "If you have questions, drop them in the chat.",
        ]
        for text in agent_responses:
            await speak(ws, text)

asyncio.run(main())

Agent + Fish Audio

Budget-friendly alternative. Send MP3 as one frame — the server handles decoding.

import asyncio, httpx, json, websockets

API = "https://render.mirrorstage.ai"
FACE = "face.jpg"
OUTPUT_URL = ""  # omit to use the gateway's configured LiveKit URL
FISH_KEY = "your-fish-audio-key"
FISH_VOICE = "your-voice-model-id"

async def speak(ws, text):
    """Convert agent text to speech via Fish Audio and stream to the avatar."""
    import requests
    audio = requests.post(
        "https://api.fish.audio/v1/tts",
        headers={"Authorization": f"Bearer {FISH_KEY}", "Content-Type": "application/json"},
        json={"text": text, "reference_id": FISH_VOICE, "format": "mp3"},
    ).content
    # Send MP3 as one frame — server auto-detects and decodes
    await ws.send(audio)
    await ws.send(json.dumps({"type": "speak_done"}))

async def main():
    # 1. Create session (x402 payment — see Getting Started)
    async with httpx.AsyncClient(timeout=30) as c:
        r = await c.post(f"{API}/v1/sessions",
            files={"reference_image": open(FACE, "rb")},
            data={"output_url": OUTPUT_URL, "accept_tos": "true"})
        s = r.json()

    # 2. Wait for GPU to be ready
    async with httpx.AsyncClient(timeout=15) as c:
        while True:
            r = await c.get(f"{API}/v1/sessions/{s['session_id']}",
                headers={"Authorization": f"Bearer {s['token']}"})
            if r.json()["status"] == "active": break
            await asyncio.sleep(5)

    # 3. Agent loop
    async with websockets.connect(f"{s['websocket_url']}?token={s['token']}", ping_interval=None) as ws:
        agent_responses = [
            "Hey everyone, welcome to the stream.",
            "Today we're going to talk about autonomous agents.",
        ]
        for text in agent_responses:
            await speak(ws, text)

asyncio.run(main())
GET/health

Check if the service is up and get current pricing. No auth required.

AuthNone
bash
curl https://render.mirrorstage.ai/health
response
{
  "status": "available",
  "available": true,
  "livekit_enabled": true,
  "livekit_url": "wss://your-project.livekit.cloud",
  "price_per_minute_usd": 0.10,
  "free_trial_minutes": 5,
  "min_duration_minutes": 10
}
GET/tos

Terms of Service. Must accept before creating a session.

AuthNone
bash
curl https://render.mirrorstage.ai/tos
POST/v1/sessions

Send any face photo. If you omit output_url, the gateway uses its configured LiveKit URL. If you provide output_url, it can be LiveKit or RTMP. The server returns 402 with payment details — sign and retry. No pre-trained avatars, no face IDs. The photo you send is the avatar. If you already have a running session, use POST /extend to add time or send a change_reference message over the WebSocket to swap the face — don't create a new session.

Authx402 (PAYMENT-SIGNATURE header)
Parameters
reference_image*fileAny face photo (JPEG/PNG, max 10MB). No training, no library — just a clear, front-facing headshot. Face is auto-cropped.
output_urlstringOptional destination. Omit it to use the gateway’s configured LiveKit URL. If provided, it can be LiveKit (wss://...) or RTMP (rtmp://...).
duration_minutesintSession length in minutes (default: 15, minimum: 10)
accept_tos*boolMust be true
walletstringYour wallet address. Required when paying entirely with credits; optional otherwise (extracted from payment signature).
bash
# Step 1: Get payment requirements
curl -X POST https://render.mirrorstage.ai/v1/sessions \
  -F "reference_image=@face.jpg" \
  -F "accept_tos=true"
# → 402 with PAYMENT-REQUIRED header

# Step 2: Sign USDC authorization, retry with payment
curl -X POST https://render.mirrorstage.ai/v1/sessions \
  -H "PAYMENT-SIGNATURE: <signed_payment>" \
  -F "reference_image=@face.jpg" \
  -F "accept_tos=true"
response
{
  "session_id": "abc-123",
  "token": "sk_...",
  "status": "warming",
  "output_protocol": "livekit",
  "livekit_url": "wss://your-project.livekit.cloud",
  "view_url": "https://render.mirrorstage.ai/view/abc-123?share=...",
  "websocket_url": "wss://render.mirrorstage.ai/v1/sessions/abc-123/stream?token=sk_...",
  "status_url": "https://render.mirrorstage.ai/v1/sessions/abc-123",
  "estimated_ready_seconds": 300,
  "duration_minutes": 15,
  "price_usd": 1.00,
  "wallet": "0x..."
}
GET/v1/sessions/:id

Poll until status is "active", then start streaming audio. Cold starts take ~5 minutes. Warm starts are instant. The billable timer doesn't start until you're live.

AuthBearer token
bash
curl https://render.mirrorstage.ai/v1/sessions/abc-123 \
  -H "Authorization: Bearer sk_..."
response
{
  "session_id": "abc-123",
  "status": "active",
  "ready": true,
  "output_protocol": "livekit",
  "livekit_url": "wss://your-project.livekit.cloud",
  "view_url": "https://render.mirrorstage.ai/view/abc-123?share=...",
  "remaining_seconds": 900,
  "duration_minutes": 15,
  "expires_at": "2026-02-24T12:15:00Z",
  "started_at": "2026-02-24T12:00:00Z",
  "warming_started_at": "2026-02-24T11:55:00Z",
  "ended_at": null,
  "wallet": "0x...",
  "failure_reason": null
}
POST/v1/sessions/:id/share

Create a shareable `/view/...` URL for browser playback or an OBS Browser Source.

AuthBearer token
bash
curl -X POST https://render.mirrorstage.ai/v1/sessions/abc-123/share \
  -H "Authorization: Bearer sk_..."
response
{
  "session_id": "abc-123",
  "share_url": "https://render.mirrorstage.ai/view/abc-123?share=...",
  "view_url": "https://render.mirrorstage.ai/view/abc-123?share=...",
  "share_kind": "view"
}
GET/v1/view/:id

Redeem a share token for a subscribe-only LiveKit viewer credential set.

AuthShare token query param
Parameters
share*stringOpaque share token from POST /v1/sessions/:id/share.
bash
curl "https://render.mirrorstage.ai/v1/view/abc-123?share=..."
response
{
  "session_id": "abc-123",
  "status": "active",
  "ready": true,
  "output_protocol": "livekit",
  "livekit_url": "wss://your-project.livekit.cloud",
  "livekit_access_token": "eyJ..."
}
WS/v1/sessions/:id/stream

Stream audio in while lip-synced video is delivered on the session’s configured LiveKit or RTMP output. Send any audio format — it's auto-detected and decoded on the server.

AuthToken as query param (?token=...)
Parameters
Binary frames*bytesAudio data — PCM (int16, 16kHz, mono) or encoded (MP3, WAV, OGG, FLAC). Format is auto-detected.
{"type": "speak_done"}JSONFlush the final partial utterance chunk.
{"type": "stop"}JSONEnd the session.
{"type": "change_reference", ...}JSONSwap the avatar face mid-session. Include reference_image_b64 with base64 PNG/JPG.
import websockets

async with websockets.connect(
    "wss://render.mirrorstage.ai/v1/sessions/abc-123/stream?token=sk_...",
    ping_interval=None,
) as ws:
    # Send audio — PCM, MP3, WAV, OGG, or FLAC (auto-detected)
    await ws.send(audio_bytes)

    # Receive control messages
    msg = await ws.recv()
    # → {"type": "chunk_done"}, {"type": "time_warning"}, {"type": "error"}, ...
POST/v1/sessions/:id/extend

Add more time without dropping the stream. Same avatar, no re-warm. Payment required for the additional minutes. Must pay from the same wallet that created the session.

AuthBearer token + x402
Parameters
duration_minutesintMinutes to add (default: 5)
# Since extend is JSON (not multipart), x402HttpxClient handles it automatically
async with x402HttpxClient(client) as http:
    resp = await http.post(
        f"{GATEWAY}/v1/sessions/{session_id}/extend",
        json={"duration_minutes": 10},
        headers={"Authorization": f"Bearer {token}"})
    await resp.aread()
    result = resp.json()
    print(f"Extended to {result['new_expires_at']}")
response
{
  "session_id": "abc-123",
  "added_minutes": 10,
  "new_expires_at": "2026-02-24T12:25:00Z",
  "total_remaining_seconds": 1500,
  "price_usd": 1.00
}
DELETE/v1/sessions/:id

End a session early. The GPU releases immediately and the LiveKit or RTMP output stops.

AuthBearer token
bash
curl -X DELETE https://render.mirrorstage.ai/v1/sessions/abc-123 \
  -H "Authorization: Bearer sk_..."
response
{
  "session_id": "abc-123",
  "status": "ended",
  "gpu_seconds_used": 420
}