Getting Started
From zero to your first synthesized audio in about 30 minutes. Lecturia is in invite-only beta — if you don't have an invitation yet, join the waitlist first.
Base URL for everything below:
https://api.lecturia.ai/v1/saasEvery endpoint except the two /auth/* calls authenticates with a bearer
API key (Authorization: Bearer lk_live_... or lk_test_...). Details in
Authentication.
1. Create your account
Register with the invite token from your invitation email. When
invite_token is present, the account is created for the address the
invitation is bound to (the email field is ignored):
curl -X POST https://api.lecturia.ai/v1/saas/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "dev@example.com",
"password": "correct horse battery staple",
"invite_token": "YOUR_INVITE_TOKEN"
}'201returns your account and a session.202means email confirmation is pending — click the link in your inbox, then log in withPOST /auth/login.403 invite_invalidmeans the token can't be honoured (expired, consumed, or unknown — the API deliberately doesn't say which).
2. Get your first API key
During the beta your first key (with admin scope) is provisioned by the
Lecturia team together with your invitation. If you don't have one, reply to
your invitation email.
Keys are shown exactly once at creation and are never recoverable — store the plaintext in your secret manager immediately.
Once you hold an admin-scoped key you mint further keys yourself. Start with a sandbox key — it exercises the whole API without spending credits:
curl -X POST https://api.lecturia.ai/v1/saas/keys \
-H "Authorization: Bearer lk_live_YOUR_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "local dev", "scopes": ["read", "write"], "environment": "test" }'The response's key field is your lk_test_... sandbox key. More in
Sandbox.
3. Verify the key works
curl https://api.lecturia.ai/v1/saas/me \
-H "Authorization: Bearer lk_test_YOUR_KEY"A 200 with your account profile means you're authenticated.
4. Pick a voice
Voices ("e-readers") are listed with per-character pricing:
curl https://api.lecturia.ai/v1/saas/ereaders \
-H "Authorization: Bearer lk_test_YOUR_KEY"Note an id from the response — the next call needs it.
5. Your first audio
POST /tts:synthesize turns up to 5,000 characters into an MP3 and returns
a 24-hour presigned URL. On a sandbox key this costs nothing:
curl -X POST "https://api.lecturia.ai/v1/saas/tts:synthesize" \
-H "Authorization: Bearer lk_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "text": "Hello from Lecturia.", "ereader_id": "YOUR_EREADER_ID", "format": "mp3" }'All examples follow the canonical language set every Lecturia API surface uses: Go, TypeScript, Rust, Java, Ruby, Python and cURL (cURL above).
// Go 1.22+, standard library only
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
payload, _ := json.Marshal(map[string]string{
"text": "Hello from Lecturia.",
"ereader_id": os.Getenv("LECTURIA_EREADER_ID"),
"format": "mp3",
})
req, _ := http.NewRequest("POST",
"https://api.lecturia.ai/v1/saas/tts:synthesize", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+os.Getenv("LECTURIA_API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var body struct {
AudioURL string `json:"audio_url"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
RequestID string `json:"request_id"`
} `json:"error"`
}
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
panic(err)
}
if body.Error != nil {
panic(fmt.Sprintf("%s: %s (request %s)",
body.Error.Code, body.Error.Message, body.Error.RequestID))
}
fmt.Println("listen:", body.AudioURL)
}// TypeScript (Node 18+, no dependencies)
const response = await fetch("https://api.lecturia.ai/v1/saas/tts:synthesize", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LECTURIA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
text: "Hello from Lecturia.",
ereader_id: process.env.LECTURIA_EREADER_ID,
format: "mp3",
}),
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message} (request ${error.request_id})`);
}
const { audio_url } = await response.json();
console.log("listen:", audio_url);// Rust — Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] },
// serde_json = "1"
use serde_json::{json, Value};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let response = reqwest::blocking::Client::new()
.post("https://api.lecturia.ai/v1/saas/tts:synthesize")
.bearer_auth(std::env::var("LECTURIA_API_KEY")?)
.json(&json!({
"text": "Hello from Lecturia.",
"ereader_id": std::env::var("LECTURIA_EREADER_ID")?,
"format": "mp3"
}))
.send()?;
let status = response.status();
let body: Value = response.json()?;
if !status.is_success() {
let error = &body["error"];
return Err(format!(
"{}: {} (request {})",
error["code"], error["message"], error["request_id"]
)
.into());
}
println!("listen: {}", body["audio_url"]);
Ok(())
}// Java 17+, standard library only (response printed raw — bring your own
// JSON library, e.g. Jackson, to unpack audio_url / the error envelope)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public final class FirstAudio {
public static void main(String[] args) throws Exception {
String json = """
{ "text": "Hello from Lecturia.", "ereader_id": "%s", "format": "mp3" }
""".formatted(System.getenv("LECTURIA_EREADER_ID"));
HttpRequest request = HttpRequest
.newBuilder(URI.create("https://api.lecturia.ai/v1/saas/tts:synthesize"))
.header("Authorization", "Bearer " + System.getenv("LECTURIA_API_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
// 200 carries audio_url; anything else is the error envelope.
System.out.println(response.statusCode() + ": " + response.body());
}
}# Ruby 3+, standard library only
require "json"
require "net/http"
uri = URI("https://api.lecturia.ai/v1/saas/tts:synthesize")
request = Net::HTTP::Post.new(uri, {
"Authorization" => "Bearer #{ENV.fetch("LECTURIA_API_KEY")}",
"Content-Type" => "application/json",
})
request.body = {
text: "Hello from Lecturia.",
ereader_id: ENV.fetch("LECTURIA_EREADER_ID"),
format: "mp3",
}.to_json
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
body = JSON.parse(response.body)
unless response.is_a?(Net::HTTPSuccess)
error = body["error"]
raise "#{error["code"]}: #{error["message"]} (request #{error["request_id"]})"
end
puts "listen: #{body["audio_url"]}"# Python 3.10+ (pip install httpx)
import os
import httpx
response = httpx.post(
"https://api.lecturia.ai/v1/saas/tts:synthesize",
headers={"Authorization": f"Bearer {os.environ['LECTURIA_API_KEY']}"},
json={
"text": "Hello from Lecturia.",
"ereader_id": os.environ["LECTURIA_EREADER_ID"],
"format": "mp3",
},
)
if response.is_error:
error = response.json()["error"]
raise RuntimeError(f"{error['code']}: {error['message']} (request {error['request_id']})")
print("listen:", response.json()["audio_url"])Open audio_url in a browser — that's your first Lecturia audio.
6. Going live
- Purchase credits at app.lecturia.ai (web only —
the API has no purchase endpoint by design). Live synthesis charges
credits; a
freeaccount that never purchased is blocked from minting keys with402 api_access_requires_credits. - Mint a live key (
"environment": "live") the same way as the sandbox key. - Register a webhook to get
material.ready/tts_sync.completedevents instead of polling. - For whole ebooks, use the materials pipeline:
POST /materials/upload-url→ upload →POST /materials/{id}/confirm-upload→ convert chunks. Full schemas in the API Reference.
Troubleshooting
| You see | Why | Fix |
|---|---|---|
401 invalid_api_key | Key unknown, or you sent a truncated/mangled value | Re-copy the key; check for whitespace |
401 missing_api_key | No Authorization header reached us | Send Authorization: Bearer lk_... |
403 bearer_not_allowed_on_saas | You sent a session JWT instead of an API key | /v1/saas/* takes API keys only |
402 api_access_requires_credits | Free account without a credit purchase | Buy credits at app.lecturia.ai |
403 consent_required | The account owner has not accepted updated terms or privacy policy | The owner signs in at app.lecturia.ai and accepts; see Authentication |
403 invite_invalid | The invite token can't be honoured | Request a fresh invitation |
Full list: Error codes. Every error carries a request_id —
include it when you contact support.