Developer API

Lumavo exposes a REST API to integrate generation directly into your applications, automations or internal tools — without going through the interface.

Availability

The API follows the gradual rollout of Lumavo's capabilities:

Base

https://api.getlumavo.com/v1

All requests are over HTTPS and exchange JSON (Content-Type: application/json).

Authentication

Every request must carry your API key in the X-LUMAVO-API-KEY header:

X-LUMAVO-API-KEY: your_key

Create and manage your keys from your account, API Keys section. A key carries permissions (voice, video, avatar, script): it can only access the capabilities you grant it. Keep your keys secret; if one leaks, revoke it and create a new one.

The legacy X-LUMA-API-KEY header is still accepted (backward compatibility).

Quick start (client)

Dependency-free clients, ready to copy. They create a voice and wait for the result.

Node.js (18+)

const KEY = process.env.LUMAVO_API_KEY, BASE = 'https://api.getlumavo.com';
const call = (p, o = {}) => fetch(BASE + p, { ...o,
  headers: { 'X-LUMAVO-API-KEY': KEY, 'Content-Type': 'application/json', ...(o.headers || {}) },
}).then((r) => r.ok ? r.json() : Promise.reject(r.status));

const job = await call('/v1/tts', { method: 'POST', body: JSON.stringify({ text: 'Hello!', voice: 'narrator' }) });
let res;
do { await new Promise((r) => setTimeout(r, 1500)); res = await call(`/v1/jobs/${job.id}`); }
while (res.status !== 'done' && res.status !== 'failed');
console.log(res.resultUrl);   // → downloadable WAV

Python (3.8+)

import os, time, json, urllib.request
KEY, BASE = os.environ["LUMAVO_API_KEY"], "https://api.getlumavo.com"
def call(path, method="GET", body=None):
    req = urllib.request.Request(BASE + path, data=json.dumps(body).encode() if body else None,
        method=method, headers={"X-LUMAVO-API-KEY": KEY, "Content-Type": "application/json"})
    return json.load(urllib.request.urlopen(req))

job = call("/v1/tts", "POST", {"text": "Hello!", "voice": "narrator"})
res = job
while res["status"] not in ("done", "failed"):
    time.sleep(1.5); res = call(f"/v1/jobs/{job['id']}")
print(res["resultUrl"])   # → downloadable WAV

Create a text-to-speech

curl -X POST https://api.getlumavo.com/v1/tts \
  -H "X-LUMAVO-API-KEY: your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Hello, this is my voice generated by Lumavo.",
    "voice": "calm",
    "speed": 1.0
  }'

The response (202 Accepted) contains the job id:

{ "id": "b1f2…", "status": "pending" }
Field Type Required Detail
text string yes The text to read (up to 8000 chars)
voice string no Voice/style — see GET /v1/voices
emotion string no Tone (e.g. calm, dynamic)
speed number no Speed, between 0.5 and 2 (default 1)

Available voices

curl https://api.getlumavo.com/v1/voices -H "X-LUMAVO-API-KEY: your_key"

Returns the list of voices for the voice field: calm, narrator, cinematic, ad, tiktok (social).

Transcribe media (speech-to-text)

Converts an audio or video file (reachable by URL) into text, with timestamped segments — perfect to generate subtitles, index or analyze content. Direct (synchronous) response, no job to poll.

curl -X POST https://api.getlumavo.com/v1/transcribe \
  -H "X-LUMAVO-API-KEY: your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "mediaUrl": "https://example.com/audio.mp3",
    "language": "en"
  }'

Response (200 OK):

{
  "text": "Hello and welcome to Lumavo.",
  "language": "en",
  "languageSource": "detected",
  "duration": 2.4,
  "segments": [
    { "start": 0.0, "end": 2.4, "text": "Hello and welcome to Lumavo." }
  ]
}
Field Type Required Detail
mediaUrl string yes http(s) URL of the audio/video file to transcribe
language string no ISO language code (e.g. en, fr) — auto-detected if omitted

Languages: pass an ISO 639-1 code (en, fr, es, de, ar, zh…) to force the language, or omit language for auto-detection (around a hundred languages supported).

The languageSource field tells you where language comes from, so you know whether to trust it as detection:

The response also includes duration (audio length in seconds).

Required scope: transcribe.

Transcribe a local file (upload)

The API does not receive the file directly: you upload first to a signed URL, then pass the resulting URL to /v1/transcribe. Three steps:

1. Request an upload URL (POST /v1/uploads):

curl -X POST https://api.getlumavo.com/v1/uploads \
  -H "X-LUMAVO-API-KEY: your_key" -H "Content-Type: application/json" \
  -d '{ "filename": "meeting.mp3", "contentType": "audio/mpeg" }'
{
  "method": "PUT",
  "uploadUrl": "https://cdn.getlumavo.com/luma-uploads/…?X-Amz-…",
  "mediaUrl":  "https://cdn.getlumavo.com/luma-uploads/…?X-Amz-…",
  "objectKey": "…",
  "uploadExpiresIn": 3600,
  "mediaExpiresIn": 86400
}

2. Send your file with PUT to uploadUrl (no API key here — the URL is already signed):

curl -X PUT "<uploadUrl>" --data-binary @meeting.mp3 -H "Content-Type: audio/mpeg"

3. Transcribe by passing mediaUrl:

curl -X POST https://api.getlumavo.com/v1/transcribe \
  -H "X-LUMAVO-API-KEY: your_key" -H "Content-Type: application/json" \
  -d '{ "mediaUrl": "<mediaUrl>", "language": "en" }'

POST /v1/uploads is free (it only generates signed URLs); only the transcription is billed. The file never needs to be public: mediaUrl is a temporary signed URL.

Retention: uploaded files are automatically deleted after ~24 h (beyond that, the signed URL has expired anyway). Transcribe right after the upload; do not rely on durable storage on Lumavo's side.

Avoid duplicates (idempotency)

Add an Idempotency-Key header (a unique id you generate) to POST /v1/tts · /render · /lipsync. If you replay the same request (network timeout, retry), Lumavo returns the already-created job instead of creating a new one — and does not bill twice.

curl -X POST https://api.getlumavo.com/v1/tts \
  -H "X-LUMAVO-API-KEY: your_key" \
  -H "Idempotency-Key: 3f8c1e2a-…" \
  -H "Content-Type: application/json" \
  -d '{ "text": "Hello", "voice": "calm" }'

Retrieve the result

Generation is asynchronous: poll the job status until it's done, then fetch the file URL.

curl https://api.getlumavo.com/v1/jobs/b1f2… \
  -H "X-LUMAVO-API-KEY: your_key"
{ "id": "b1f2…", "type": "tts_only", "status": "done", "progress": 100, "resultUrl": "https://…/audio.wav" }

Every job has a stable shape: { id, type, status, progress, resultUrl, error, duration, createdAt }. Poll this endpoint every 1–2 seconds until status: "done" (or "failed"). The returned URL is directly downloadable.

List your jobs

curl "https://api.getlumavo.com/v1/jobs?limit=20" \
  -H "X-LUMAVO-API-KEY: your_key"

Returns your most recent jobs, newest first (optional limit parameter, max 100).

Manage your keys via the API

Method & route Purpose
POST /v1/keys Create a key
GET /v1/keys List your keys
POST /v1/keys/:id/revoke Revoke a key

Webhooks (recommended)

Instead of polling /v1/jobs/:id, register a callback URL: Lumavo notifies you as soon as a job completes (job.completed) or fails (job.failed).

curl -X POST https://api.getlumavo.com/v1/webhooks \
  -H "X-LUMAVO-API-KEY: your_key" -H "Content-Type: application/json" \
  -d '{"url":"https://your-app.com/lumavo/callback"}'
{ "id": "…", "url": "https://your-app.com/lumavo/callback", "secret": "whsec_…", "active": true }

Keep the secret (shown only once). Each delivery is signed: the X-LUMAVO-Signature header contains HMAC-SHA256(body, secret) — recompute it server-side to verify authenticity. Received body:

{ "event": "job.completed", "data": { "jobId": "…", "status": "done", "resultUrl": "https://…" }, "timestamp": 1720000000000 }

Verify the signature (essential) — recompute the HMAC over the raw body (not the re-serialized JSON) and compare in constant time:

// Express
import { createHmac, timingSafeEqual } from 'node:crypto';
app.post('/callback', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.header('X-LUMAVO-Signature') || '';
  const expected = createHmac('sha256', SECRET).update(req.body).digest('hex');
  const ok = sig.length === expected.length && timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
  if (!ok) return res.sendStatus(401);
  const { event, data } = JSON.parse(req.body);   // data = { jobId, status, resultUrl }
  res.sendStatus(200);
});
# Flask
import hmac, hashlib
@app.post("/callback")
def callback():
    sig = request.headers.get("X-LUMAVO-Signature", "")
    expected = hmac.new(SECRET.encode(), request.data, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, sig):
        abort(401)
    event = request.get_json()   # { event, data: { jobId, status, resultUrl }, timestamp }
    return "", 200
Method & route Purpose
POST /v1/webhooks Register a URL
GET /v1/webhooks List your webhooks
POST /v1/webhooks/:id/test Send a test event
DELETE /v1/webhooks/:id Delete

Coming soon

These endpoints exist and will be enabled progressively:

They follow the same principle: the request returns a job, whose status you track via /v1/jobs/:id.

Response codes

Code Meaning
202 Job accepted and queued
200 Successful request (status, lists)
401 API key missing or invalid
403 The key lacks the required permission
402 Insufficient credits
429 Too many requests — slow down (per-key rate limit)

Limits & good practice

curl https://api.getlumavo.com/v1/usage -H "X-LUMAVO-API-KEY: your_key"
{ "rateLimit": { "limit": 60, "remaining": 58, "windowSeconds": 60, "resetSeconds": 42 },
  "dailyQuota": { "limit": 2000, "used": 12, "remaining": 1988 } }