Lumavo exposes a REST API to integrate generation directly into your applications, automations or internal tools — without going through the interface.
The API follows the gradual rollout of Lumavo's capabilities:
/v1/tts) — available now./v1/transcribe) — available now: audio/video → text./v1/jobs/:id) and key management — available./v1/render), talking avatar (/v1/lipsync) and script
generation (/v1/script) — coming soon, enabled progressively.https://api.getlumavo.com/v1
All requests are over HTTPS and exchange JSON (Content-Type: application/json).
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-KEYheader is still accepted (backward compatibility).
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
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) |
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).
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:
"detected" — you omitted language → the value is the language actually
detected in the audio;"requested" — you forced a language → the value is what you asked for (a
simple echo, not detection; the engine transcribes in that language even if the
audio is in another).The response also includes duration (audio length in seconds).
Required scope: transcribe.
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.
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" }'
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.
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).
| Method & route | Purpose |
|---|---|
POST /v1/keys |
Create a key |
GET /v1/keys |
List your keys |
POST /v1/keys/:id/revoke |
Revoke a key |
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 |
These endpoints exist and will be enabled progressively:
POST /v1/script — generates a script from a topic.POST /v1/render — produces a full video.POST /v1/lipsync — creates a talking avatar.They follow the same principle: the request returns a job, whose status you track
via /v1/jobs/:id.
| 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) |
429 response.X-RateLimit-Limit and X-RateLimit-Remaining
headers (requests left in the current window). A 429 returns Retry-After (seconds).GET /v1/usage: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 } }