Version 2026-08-01
API reference
One REST API, four official SDKs and no surprises. Every endpoint is idempotent where it can be, every error is machine-readable, and every response tells you exactly what it cost.
Introduction
The Lumen API is organised around three resources: runs (a model call, with optional tools and retrieval), collections (indexed documents) and keys. Requests and responses are JSON. All timestamps are RFC 3339 in UTC.
usage.input_tokens, usage.output_tokens and
usage.cached_tokens appear on every successful call, so you can
attribute cost per request without a separate billing query.
Authentication
Authenticate with a bearer token in the Authorization header. Keys are
scoped to a single workspace and carry one of three roles: read,
run or admin.
Authorization: Bearer lum_live_7f4c9a2e8b1d6035
Content-Type: application/json
Lumen-Version: 2026-08-01
lum_live_ carry full workspace permissions. For
client-side apps, mint a short-lived session token from your backend with
POST /v1/sessions instead.
Key roles
| Role | Can do | Cannot do |
|---|---|---|
| read | Query collections, read runs and usage | Create runs, mutate collections |
| run | Everything read can, plus create runs and call tools |
Rotate keys, change billing, delete collections |
| admin | Full workspace access including key rotation and budgets | Cross-workspace access — keys never span workspaces |
Quickstart
The shortest useful call: one prompt, one model, one answer. Pick your language.
curl https://api.lumen.example.com/v1/runs \
-H "Authorization: Bearer $LUMEN_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "lumen-pro",
"input": "Summarise our refund policy in two sentences.",
"collections": ["handbook"],
"max_output_tokens": 300
}'
from lumen import Lumen
client = Lumen() # reads LUMEN_API_KEY
run = client.runs.create(
model="lumen-pro",
input="Summarise our refund policy.",
collections=["handbook"],
)
print(run.output_text)
print(run.usage.output_tokens, "tokens")
import { Lumen } from "@lumen/sdk";
const lumen = new Lumen();
const run = await lumen.runs.create({
model: "lumen-pro",
input: "Summarise our refund policy.",
collections: ["handbook"],
});
console.log(run.outputText);
console.log(run.usage.outputTokens);
package main
import (
"context"
"fmt"
"github.com/lumen/lumen-go"
)
func main() {
c := lumen.New()
run, err := c.Runs.Create(context.Background(), lumen.RunParams{
Model: "lumen-pro",
Input: "Summarise our refund policy.",
})
if err != nil {
panic(err)
}
fmt.Println(run.OutputText)
}
Create a run
A run is one turn of an agent: optional retrieval, zero or more tool calls, and a final answer. Runs are durable — fetch one again by id for 30 days.
Body parameters
| Parameter | Required | Description |
|---|---|---|
| modelstring | Required | One of lumen-flash, lumen-pro, lumen-max. See models. |
| inputstring | array | Required | A prompt string, or an array of message objects for multi-turn conversations. |
| collectionsstring[] | Optional | Collection ids to retrieve from before answering. Retrieval is skipped when omitted. |
| toolsobject[] | Optional | Tool definitions the model may call. Up to 128 per run. |
| streamboolean | Optional | When true, responds with text/event-stream. Defaults to false. |
| max_output_tokensinteger | Optional | Hard ceiling on generated tokens. Defaults to the model maximum. |
| temperaturenumber | Optional | Between 0 and 2. Defaults to 0.7. Use 0 for extraction. |
| response_formatobject | Optional | Pass {"type":"json_schema","schema":{…}} to force schema-valid JSON. |
| idempotency_keystring | Optional | Replays return the original run instead of billing twice. Retained 24 hours. |
Unknown fields are rejected with 400 invalid_request rather than ignored — typos fail loudly.
Response
{ "id": "run_9f2ab41c", "object": "run", "model": "lumen-pro", "created_at": "2026-08-01T09:14:22Z", "status": "completed", "output_text": "Refunds are available for 30 days…", "citations": [ { "source": "handbook.pdf", "page": 14, "score": 0.912 } ], "usage": { "input_tokens": 1842, "cached_tokens": 1408, "output_tokens": 96, "cost_eur": 0.00213 }}
Streaming
Set stream: true to receive Server-Sent Events. Tokens, tool calls and
citations arrive on the same channel, each as a typed event.
| Event | Payload | Meaning |
|---|---|---|
| run.started | { id, model } |
The run is accepted. Emitted once. |
| token | { text } |
A fragment of the answer. Concatenate in order received. |
| tool.call | { name, arguments } |
The model wants a tool run. Arguments are already schema-validated. |
| tool.result | { name, output, ms } |
Your handler returned. Emitted for hosted tools only. |
| citation | { source, page, score } |
A source that grounded the sentence just streamed. |
| run.completed | { usage, finish_reason } |
Terminal event. Always emitted, including after an error. |
event: run.started
data: {"id":"run_9f2ab41c","model":"lumen-pro"}
event: token
data: {"text":"Refunds "}
event: tool.call
data: {"name":"lookup_order","arguments":{"id":"A-8812"}}
event: citation
data: {"source":"handbook.pdf","page":14}
event: run.completed
data: {"finish_reason":"stop","usage":{"output_tokens":96}}
with client.runs.stream(
model="lumen-pro",
input=question,
tools=[lookup_order],
) as stream:
for event in stream:
if event.type == "token":
print(event.text, end="")
elif event.type == "citation":
sources.append(event.source)
final = stream.get_final_run()
print(final.usage.cost_eur)
const stream = await lumen.runs.stream({
model: "lumen-pro",
input: question,
tools: [lookupOrder],
});
for await (const event of stream) {
switch (event.type) {
case "token":
write(event.text);
break;
case "tool.call":
ui.showToolChip(event.name);
break;
}
}
Tool calling
Declare tools as JSON Schema. Lumen validates arguments before they reach your handler, so a malformed call never becomes a runtime exception in your code. Independent calls run in parallel automatically.
{
"name": "lookup_order",
"description": "Fetch an order by its reference.",
"parameters": {
"type": "object",
"properties": {
"id": { "type": "string", "pattern": "^A-[0-9]{4}$" }
},
"required": ["id"],
"additionalProperties": false
},
"timeout_ms": 4000,
"retries": 2
}
Retrieval
Query a collection directly when you want the passages without an answer — for a search UI, an evaluation harness, or your own prompt assembly.
curl ".../v1/collections/handbook/query" \
-H "Authorization: Bearer $LUMEN_KEY" \
-d '{
"query": "parental leave",
"top_k": 5,
"rerank": true,
"filter": { "region": "eu", "updated_after": "2026-01-01" }
}'
# → { "matches": [ { "text": "…", "score": 0.91, "page": 14 } ] }
Errors
Errors use standard HTTP status codes with a stable machine-readable
code. Match on code, never on message — the
prose changes, the code does not.
| Status | Code | What happened | What to do |
|---|---|---|---|
| 400 | invalid_request |
A field is missing, malformed or unknown. | Fix the payload. The param field names the offender. |
| 401 | invalid_api_key |
The key is missing, revoked or from another workspace. | Check the header. Do not retry — it will not start working. |
| 403 | insufficient_scope |
A read key tried to create a run. |
Mint a key with the run role. |
| 404 | not_found |
No such run, collection or document. | Verify the id. Runs are retained for 30 days. |
| 409 | idempotency_conflict |
The same key was reused with a different body. | Use a fresh key, or resend the identical payload. |
| 413 | context_length_exceeded |
Input plus retrieved context exceeds the window. | Lower top_k, or move to a longer-context model. |
| 429 | rate_limited |
Requests or tokens per minute exceeded. | Back off using Retry-After. See limits. |
| 429 | budget_exceeded |
The workspace hit its monthly spend cap. | Raise the cap in settings. Retrying will not help. |
| 500 | internal_error |
Our fault. Already paged. | Retry with jittered backoff, up to three attempts. |
| 503 | upstream_unavailable |
Every provider for that model is degraded. | Retry, or fall back to a different model family. |
{
"error": {
"code": "invalid_request",
"message": "temperature must be between 0 and 2",
"param": "temperature",
"request_id": "req_c81f0a44"
}
}
Always log request_id. It is the fastest way for us to find your call
in support.
Versioning
The API is dated, not numbered. Pin a version with the Lumen-Version
header; without it you get the version your workspace was created on, frozen.
Breaking changes ship under a new date and old dates stay live for 18 months.
| Version | Status | Notes |
|---|---|---|
| 2026-08-01 | Current | Parallel tool calls, cached_tokens in usage, 1M context on max. |
| 2026-02-14 | Supported | Adds citations to streamed runs. Sunsets 14 August 2027. |
| 2025-09-30 | Deprecated | Sunsets 30 March 2027. Migrate off completions. |
Get a key and make your first call
Free tier, no card. The quickstart above works verbatim once you paste your key.