Lecturia Developers

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/saas

Every 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"
  }'

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

Troubleshooting

You seeWhyFix
401 invalid_api_keyKey unknown, or you sent a truncated/mangled valueRe-copy the key; check for whitespace
401 missing_api_keyNo Authorization header reached usSend Authorization: Bearer lk_...
403 bearer_not_allowed_on_saasYou sent a session JWT instead of an API key/v1/saas/* takes API keys only
402 api_access_requires_creditsFree account without a credit purchaseBuy credits at app.lecturia.ai
403 consent_requiredThe account owner has not accepted updated terms or privacy policyThe owner signs in at app.lecturia.ai and accepts; see Authentication
403 invite_invalidThe invite token can't be honouredRequest a fresh invitation

Full list: Error codes. Every error carries a request_id — include it when you contact support.