Skip to content
Lumean API

Quickstart: from zero to mp3

Five steps from an empty terminal to a downloaded file — voice, template, order, waiting, link.

Speech synthesis in Lumean is a pipeline of five links, and each link takes an ID from the previous one's response: voice → template → order → waiting → file. No link can be skipped: the voice is set only through a template (there is no orders.voice_id field), and the result is delivered only as a temporary link from POST /storage/url — the order holds paths, not URLs.

The public API pipelineEight steps from key to file link: key, scopes, voice, template, order, chunks, files, link.1. KeyX-API-KEY2. Scopesorders.write3. VoiceGET /voices4. TemplatePOST /templates5. OrderPOST /orders6. ChunksGET /orders/{order}7. Filesresult.files[]8. LinkPOST /storage/url

What you need first

An API key with the voices.read, templates.write, orders.write, and orders.download scopes. You create it in My API keys; every request uses the same header, X-API-KEY.

Gotcha

The built-in automation preset does not include templates.write (only templates.read), so you cannot complete this walkthrough with it: step 2 will return 403. Use the full preset, or add templates.write manually when you create the key.

Careful

Step 3 creates a real order: it consumes your subscription quota and is billed like any other. You can cancel an order after it is created (POST /orders/{order}/cancel), but what was spent on completed chunks is not refunded — start with one short phrase, not a book chapter.

End-to-end scenario

Variables

Steps run with your key. Without a key only the code is available — you can still copy it.

  1. 1

    Pick a voice

    Find a voice in the catalogue and keep its voice_id — the template needs it.

    curl -X GET 'https://api.lumean.app/api/public/voices/elevenlabs/library?page=0&page_size=20' \
      -H 'Accept: application/json' \
      -H 'X-API-KEY: YOUR_API_KEY'
    Open in the reference
  2. 2

    Create a template

    The voice is set by the template only: voice_id goes into config.tts_settings.

    curl -X POST 'https://api.lumean.app/api/public/templates' \
      -H 'Accept: application/json' \
      -H 'X-API-KEY: YOUR_API_KEY' \
      -H 'Content-Type: application/json' \
      -d '{
      "service_key": "elevenlabs",
      "name": "API quickstart",
      "config": {
        "tts_settings": {
          "mode": "mode_v1",
          "model_id": "eleven_multilingual_v2",
          "voice_id": "{{voice_id}}",
          "advanced_voice_settings": true,
          "voice_settings": {
            "stability": 0.5,
            "similarity_boost": 0.75,
            "use_speaker_boost": true,
            "speed": 1.0
          }
        }
      }
    }'
    Open in the reference
  3. 3

    Create an order

    The text goes into top-level input_text, the template into template_id. The answer is 201 with status created.

    This step creates a real order and spends your subscription tokens.

    curl -X POST 'https://api.lumean.app/api/public/orders' \
      -H 'Accept: application/json' \
      -H 'X-API-KEY: YOUR_API_KEY' \
      -H 'Content-Type: application/json' \
      -d '{
      "template_id": "{{template_id}}",
      "input_text": "Hello from the Lumean public API."
    }'
    Open in the reference
  4. 4

    Wait for the result

    Orders are asynchronous: poll the status or listen over WebSocket. partially_completed is NOT terminal.

    curl -X GET 'https://api.lumean.app/api/public/orders/{{order_id}}' \
      -H 'Accept: application/json' \
      -H 'X-API-KEY: YOUR_API_KEY'
    Open in the reference
  5. 5

    Get a file link

    result.files[] are paths. The download link comes from POST /storage/url.

    curl -X POST 'https://api.lumean.app/api/public/storage/url' \
      -H 'Accept: application/json' \
      -H 'X-API-KEY: YOUR_API_KEY' \
      -H 'Content-Type: application/json' \
      -d '{
      "path": "{{path}}",
      "download": true
    }'
    Open in the reference

The full script

A ready-made run from voice to file. You need curl and jq; substitute your own KEY.

#!/usr/bin/env bash
set -euo pipefail

BASE=https://api.lumean.app/api/public
KEY=<your_api_key>
AUTH=(-H "X-API-KEY: $KEY" -H "Content-Type: application/json")

# 1. Voice: take the first voice_id from the ElevenLabs public library.
#    External provider catalogs are wrapped in data.voices[]; pages start at ZERO.
VOICE_ID=$(curl -s "$BASE/voices/elevenlabs/library?page=0&page_size=20" \
  -H "X-API-KEY: $KEY" | jq -r '.data.voices[0].voice_id')

# 2. A TTS template with this voice. tts_settings.mode is required,
#    language_code (ISO-639-1) is optional.
TEMPLATE_UUID=$(curl -s -X POST "$BASE/templates" "${AUTH[@]}" -d '{
  "service_key": "elevenlabs",
  "name": "My TTS",
  "config": {
    "tts_settings": {
      "mode": "mode_v1",
      "model_id": "eleven_multilingual_v2",
      "voice_id": "'"$VOICE_ID"'",
      "advanced_voice_settings": true,
      "voice_settings": {
        "stability": 0.5, "similarity_boost": 0.75,
        "use_speaker_boost": true, "speed": 1.0
      }
    }
  }
}' | jq -r '.data.id')

# 3. An order from the template. The text goes in top-level input_text, NOT in task_data.
ORDER_UUID=$(curl -s -X POST "$BASE/orders" "${AUTH[@]}" -d '{
  "template_id": "'"$TEMPLATE_UUID"'",
  "input_text": "Привет! Это тестовая озвучка через Lumean API."
}' | jq -r '.data.id')
# → status: "created" — this response has no result, and it cannot have one

# 4. Wait until it is ready. Terminal statuses: completed, result_delivered,
#    failed, compensated, cancelled. partially_completed is NOT terminal.
while :; do
  STATUS=$(curl -s "$BASE/orders/$ORDER_UUID" -H "X-API-KEY: $KEY" | jq -r '.data.status')
  echo "status: $STATUS"
  case "$STATUS" in
    completed|partially_completed|result_delivered) break ;;
    failed|cancelled|compensated) echo "order failed"; exit 1 ;;
  esac
  sleep 5
done

# 5. File: result.files[] are PATHS. Exchange the path for a temporary link and download it.
PATHNAME=$(curl -s "$BASE/orders/$ORDER_UUID" -H "X-API-KEY: $KEY" \
  | jq -r '.data.result.files[0]')
URL=$(curl -s -X POST "$BASE/storage/url" "${AUTH[@]}" \
  -d '{ "path": "'"$PATHNAME"'", "download": true }' | jq -r '.data.url')
curl -sL "$URL" -o result.mp3

What is easy to get wrong here

PitfallHow it actually works
Looking for voice_id in the order bodyThe voice lives in the template's config.tts_settings.voice_id; POST /orders has no such field
Putting the text in task_data.textFor template-based TTS, the text goes in the top-level input_text
Treating data.result.files[] as linksThey are paths; a link is issued by POST /storage/url, and it is short-lived
Waiting for completed foreverpartially_completed is not a terminal status: some chunks are done, others are waiting for your decision
Pagination of provider catalogsFor ElevenLabs and HeyGen, page is counted from zero; for regular API lists, from one
Leaving the file “for later”Order storage is purged according to retention (by default, 7 days after a terminal status), while the order keeps showing the paths, now dead

If you get an error instead of 201

Three codes come up at step 3 more often than others, and each one is handled differently.

402402 — payment required
What happened
The order was not created and no money was charged. The code is returned by `POST /orders` and `POST /orders/{order}/retry` — they go through the same billing flow. The body always contains `reason`, and that is what determines whether this is an error or an offer to top up.
What to do right now
Branch on `reason`, not on `message`: message wording changes without notice and is not an error code. Do not retry blindly: a retry without confirmation returns a fresh 402 with a new `quote_token`, while a blind retry WITH confirmation charges money without the user knowing.
Open the code page

402 is not a rejection but a payment fork: payg_topup_required brings the top-up price and a quote_token, which you must send back together with confirm_payg_topup: true. A blind retry here is either useless or charges money without the user's knowledge — see LMC, quotas and PAYG.

429 is five different limits under one code; they are told apart by reason and by the presence of Retry-After (Limits and the 429 code). A 422 without errors but with reason is a domain rejection: the request shape is not the problem, and repeating the same body will get the same response.

Next steps

Feed this page to an AI

Open .mdllm.md

Updated 09/04/2026 · spec 0.0.1