> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.empiriolabs.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.empiriolabs.ai/_mcp/server.

# API Reference

EmpirioLabs speaks **OpenAI-, Anthropic-, and Google Gemini-compatible** request shapes. Drop in any of those SDKs, point it at `https://api.empiriolabs.ai`, and authenticate with your EmpirioLabs API key. Every endpoint below works against the matching client unchanged.

## Authentication

Every request requires a bearer token. Either header is accepted on every endpoint:

```
Authorization: Bearer $EMPIRIOLABS_API_KEY
x-api-key: $EMPIRIOLABS_API_KEY
```

Gemini-compatible routes additionally accept the key in the `x-goog-api-key` header or the `?key=` query parameter, matching Gemini client defaults.

**`Python (OpenAI SDK)`**

```python title="Python (OpenAI SDK)"
from openai import OpenAI

client = OpenAI(
    base_url="https://api.empiriolabs.ai",
    api_key="$EMPIRIOLABS_API_KEY",
)

resp = client.chat.completions.create(
    model="deepseek-v4-pro",
    messages=[{"role": "user", "content": "Hello!"}],
)
```

**`TypeScript (OpenAI SDK)`**

```typescript title="TypeScript (OpenAI SDK)"
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.empiriolabs.ai",
  apiKey: process.env.EMPIRIOLABS_API_KEY,
});

const resp = await client.chat.completions.create({
  model: "deepseek-v4-pro",
  messages: [{ role: "user", content: "Hello!" }],
});
```

**`Anthropic SDK`**

```typescript title="Anthropic SDK"
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({
  baseURL: "https://api.empiriolabs.ai",
  apiKey: process.env.EMPIRIOLABS_API_KEY,
});

const msg = await client.messages.create({
  model: "deepseek-v4-pro",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Hello!" }],
});
```

**`cURL`**

```bash title="cURL"
curl https://api.empiriolabs.ai/v1/chat/completions \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-v4-pro",
    "messages": [{"role": "user", "content": "Hello!"}]
  }'
```

## Endpoint surface

#### [Chat completions](#chat-completions)

OpenAI-compatible chat. Streaming, tool calling, vision, audio input, JSON mode, structured output, reasoning controls.

#### [Responses API](#responses-api)

OpenAI Responses-compatible. Same models as chat completions with the input / instructions request shape.

#### [Legacy completions](#legacy-completions)

OpenAI-compatible prompt completions for models that advertise `POST /v1/completions`.

#### [Anthropic Messages](#anthropic-messages)

Drop-in for Anthropic SDK clients. tool_use / tool_result blocks round-trip cleanly.

#### [Gemini-compatible](#gemini-compatible-endpoint)

Native Google Gemini generateContent, streaming, and countTokens for any chat model.

#### [Images](#image-generation)

Generate, edit, inpaint, image variations. Hosted CDN URLs, 7-day signed.

#### [Video](#video-generation)

Async video generation. Returns a job_id; poll the jobs endpoint for the URL.

#### [Generation templates](#generation-templates)

Apply curated effects to generations, or plan and render full short-form videos with Compose recipes.

#### [Audio (TTS, music, voices)](#audio-generation)

TTS plus real-time streaming TTS (Inworld), music / podcast / SFX generation, voice clone management.

#### [Realtime voice](#realtime-voice)

Full-duplex speech over a WebSocket. Speech-to-speech conversation and live transcription on one connection.

#### [Agents](#agents)

Long-running tool-using agent tasks. Start, poll, stream messages, stop early.

#### [Transcription](#transcription)

Whisper / Deepgram / Parakeet. Multipart upload or file_url.

#### [Document OCR](#document-ocr)

PDFs and images to Markdown, page by page, with tables, images, headers, and footers.

#### [Search and research](#search-and-research)

Exa, Tavily, Linkup, Perplexity Search. Domain filters, date ranges, geo bias.

#### [3D generation](#3d-generation)

Async image-to-3D asset generation. Returns a job_id; poll for the signed GLB URL.

#### [Detection](#detection)

POST /v1/detect for GPTZero AI-detection.

#### [Embeddings](#embeddings)

OpenAI-compatible embeddings. Multilingual text + multimodal embedders.

#### [Reranks](#reranks)

Semantic document reranking. Sort retrieval candidates by relevance for RAG and search refinement.

#### [Decisions](#decisions)

Calibrated probabilities for yes/no, choice, score, and tool questions about text, images, audio, and video.

#### [Batch API](#batch-api)

Bulk chat and embeddings as one asynchronous job. Upload a JSONL file, poll, download results.

#### [File URLs](#file-urls)

Pass any public URL on input fields. No upload, no re-sign. Generated outputs are valid for 7 days.

#### [Jobs](#async-jobs)

Poll the status / result of any async generation. State retained 1 hour after completion.

#### [Account usage](/account-usage-api)

Balance, spend by product, cache hit rate, and per-request usage history.

#### [GPU Cloud](/gpu-cloud)

Deploy managed GPU instances and one-click templates with the same API key.

#### [Hosted Agents](/hosted-agents)

Provision and manage always-on hosted agent runtimes.

#### [Models](#models)

Live catalog with pricing, parameter schema, capability flags, regions.

#### [Errors](#errors)

OpenAI- and Anthropic-compatible error envelopes.

## Chat completions

`POST /v1/chat/completions`

Pass any chat-capable model from the catalog as `model`. Streaming uses Server-Sent Events with `data: ...` lines and a final `data: [DONE]`. `usage` arrives at the end of the stream on a chunk that also has `choices`. Set `stream_options.include_usage` to `true` to receive it the way OpenAI sends it, in its own chunk just before `[DONE]` whose `choices` is an empty list; some models still report it on a chunk with `choices`. Read `usage` from the last chunk that carries it.

**`Streaming completion`**

```bash title="Streaming completion"
curl https://api.empiriolabs.ai/v1/chat/completions \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-v4-pro",
    "messages": [{"role": "user", "content": "Summarize CRDT consistency in 3 bullets."}],
    "stream": true,
    "temperature": 0.7
  }'
```

**`Tool calling`**

```bash title="Tool calling"
curl https://api.empiriolabs.ai/v1/chat/completions \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "mistral-small-4",
    "messages": [{"role": "user", "content": "What is the weather in Paris?"}],
    "tools": [{
      "type": "function",
      "function": {
        "name": "get_weather",
        "parameters": {
          "type": "object",
          "properties": {"city": {"type": "string"}},
          "required": ["city"]
        }
      }
    }],
    "tool_choice": "auto"
  }'
```

**`Vision input`**

```bash title="Vision input"
curl https://api.empiriolabs.ai/v1/chat/completions \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen3-5-omni-plus",
    "messages": [{
      "role": "user",
      "content": [
        {"type": "text", "text": "What is in this image?"},
        {"type": "image_url", "image_url": {"url": "https://example.com/photo.jpg"}}
      ]
    }]
  }'
```

**`JSON mode`**

```bash title="JSON mode"
curl https://api.empiriolabs.ai/v1/chat/completions \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-v4-pro",
    "messages": [{"role": "user", "content": "Extract person from: John Doe, 32, NYC"}],
    "response_format": {"type": "json_object"}
  }'
```

> **Tip**
>
> Every model's accepted parameters live on its docs page (e.g. `temperature`, `top_p`, `enable_thinking`, `reasoning_effort`, `web_search_tier`). Browse them under [Authors and Models](/providers).

## Responses API

`POST /v1/responses`

OpenAI Responses-compatible. The same models available on `/v1/chat/completions` accept the Responses request shape: `input`, `instructions`, streaming, tool calling, and structured output via `text.format`.

```bash
curl https://api.empiriolabs.ai/v1/responses \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "seed-2-0-pro",
    "input": "Write a one-line launch checklist.",
    "instructions": "Be concise."
  }'
```

## Model parameters across endpoints

Model-specific parameters advertised on the model page and in `GET /v1/models/{id}` can be sent to `/v1/chat/completions`, `/v1/responses`, and `/v1/messages` when that model supports the endpoint. The gateway adapts request shapes so the same controls reach the underlying model.

For thinking-capable models, `enable_thinking` and `thinking_budget` are accepted on all three text endpoints. On `/v1/messages`, you can also use Anthropic-style thinking:

```json
{
  "thinking": {
    "type": "enabled",
    "budget_tokens": 1024
  }
}
```

That maps to the same `enable_thinking=true` and `thinking_budget=1024` controls used by Chat Completions and Responses.

## Legacy completions

`POST /v1/completions`

Use this endpoint for OpenAI-compatible clients that still send a raw `prompt` instead of chat `messages`. Only models that list `POST /v1/completions` in `supported_endpoints` accept this shape.

Streaming uses Server-Sent Events. `usage` arrives at the end of the stream on a chunk that also has `choices`. Set `stream_options.include_usage` to `true` to receive it the way OpenAI sends it, in its own chunk just before `[DONE]` whose `choices` is an empty list; some models still report it on a chunk with `choices`. Read `usage` from the last chunk that carries it.

```bash
curl https://api.empiriolabs.ai/v1/completions \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen3-5-9b",
    "prompt": "Write one concise launch sentence.",
    "max_tokens": 64,
    "stream": true
  }'
```

## Anthropic Messages

`POST /v1/messages`

Drop-in for any Anthropic SDK client: the same models accessible on `/v1/chat/completions` and `/v1/responses` are reachable here under the Anthropic Messages shape.

```bash
curl https://api.empiriolabs.ai/v1/messages \
  -H "x-api-key: $EMPIRIOLABS_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-v4-pro",
    "max_tokens": 1024,
    "messages": [{"role": "user", "content": "Hi!"}]
  }'
```

`tool_use` and `tool_result` blocks round-trip cleanly. Mixed text-plus-tool_use content arrays are preserved.

## Gemini-compatible endpoint

`POST /v1beta/models/{model}:generateContent`

Native Google Gemini request and response shapes for any chat model. `POST /v1beta/models/{model}:streamGenerateContent?alt=sse` streams Server-Sent Events (each event is a partial `GenerateContentResponse`; there is no `[DONE]` sentinel), and `POST /v1beta/models/{model}:countTokens` returns a fast token estimate.

Authenticate with the `x-goog-api-key` header or the `?key=` query parameter. The google-genai SDKs work by pointing their base URL at `https://api.empiriolabs.ai`; pricing, parameters, and billing match `/v1/chat/completions`.

```bash
curl "https://api.empiriolabs.ai/v1beta/models/qwen3-7-max:generateContent" \
  -H "x-goog-api-key: $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contents": [{ "role": "user", "parts": [{ "text": "Hello!" }] }],
    "generationConfig": { "temperature": 0.7, "maxOutputTokens": 256 }
  }'
```

## Image generation

`POST /v1/images/generations`

```bash
curl https://api.empiriolabs.ai/v1/images/generations \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "wan-2-7-image",
    "prompt": "A glass cathedral at sunset, dramatic lighting",
    "aspect_ratio": "16:9",
    "resolution": "4K",
    "thinking_mode": true,
    "num_images": 4
  }'
```

Image generation returns the async `{job_id, poll_url}` envelope by default; poll the jobs endpoint for the finished result. Pass `sync: true` (alias `wait: true`) to hold the request open and receive the finished OpenAI-shaped `data[]` body directly, which is what OpenAI SDK `images.generate` integrations expect. A sync request that outlasts the wait window falls back to the async envelope with the job still running.

`POST /v1/images/edits` applies a text instruction to an existing image (send the image URL as `image`, the instruction as `prompt` and an image-edit model such as `grok-imagine-image-2-0` as `model`, which is required), and returns the same async job envelope.

Image-edit flows accept `image: ["https://..."]` with up to the model's documented limit (3 for `qwen-image-2-0`, 9 for `wan-2-7-image`, 14 for `seedream-5-0-lite`). Image-set modes generate cohesive series; see each model's page for the toggle.

Returned URLs live on `https://media.empiriolabs.ai` and expire after **7 days**. Save anything you want to keep before the URL expires.

`POST /v1/images/analysis` runs vision-only analysis (no generation) on one or more input images. Use for layout extraction, object detection, OCR, and similar inspection tasks where the model returns text or JSON describing the image rather than a new picture.

## Video generation

`POST /v1/videos/generations`

Always async: the endpoint returns a `job_id` and a polling URL.

**`Submit a job`**

```bash title="Submit a job"
curl https://api.empiriolabs.ai/v1/videos/generations \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "seedance-2-0-pro",
    "prompt": "A cinematic dolly shot of a city street at dusk in the rain",
    "resolution": "1080p",
    "aspect_ratio": "16:9",
    "duration": 8,
    "generate_audio": true
  }'
```

**`Initial response`**

```json title="Initial response"
{
  "job_id": "job_01HV3K...",
  "status": "processing",
  "poll_url": "/v1/jobs/job_01HV3K..."
}
```

**`Poll for result`**

```bash title="Poll for result"
curl https://api.empiriolabs.ai/v1/jobs/job_01HV3K... \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY"
```

**`Successful result`**

```json title="Successful result"
{
  "job_id": "job_01HV3K...",
  "status": "completed",
  "created_at": 1778000000,
  "completed_at": 1778000041,
  "endpoint": "/v1/videos/generations",
  "model": "seedance-2-0-pro",
  "result": {
    "data": [{ "url": "https://media.empiriolabs.ai/worker-outputs/..." }]
  }
}
```

## Generation templates

Effect templates apply curated creative treatments to normal image and video generation requests.

`GET /v1/templates` lists the public template catalog. Use `GET /v1/images/templates` or `GET /v1/videos/templates` when you already know the modality.

`GET /v1/videos/compose/recipes` lists full-production video recipes, caption styles, input requirements, and recommended model options.

`POST /v1/videos/compose` starts a Compose recipe job:

* `mode: "plan"` returns an editable storyboard from a brief or script.
* `mode: "render"` takes a storyboard and produces the final MP4.
* `mode: "edit"` takes a prior render manifest plus scene edit instructions.

**`Plan a Compose storyboard`**

```bash title="Plan a Compose storyboard"
curl https://api.empiriolabs.ai/v1/videos/compose \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recipe": "custom",
    "mode": "plan",
    "brief": "Explain why cold plunge clips went viral.",
    "aspect_ratio": "9:16",
    "scene_count": 5
  }'
```

Compose returns the same async job envelope as video generation. Poll `GET /v1/jobs/{job_id}` until the result is ready. See [Generation Templates](/generation-templates#compose) for the end-to-end plan, render, edit, and usage workflow.

## Audio generation

`POST /v1/audio/speech` synchronous, returns a hosted URL by default; pass `response_format: "b64_json"` for inline audio bytes.

`POST /v1/audio/speech:stream` real-time TTS. Returns Server-Sent Events as the model synthesizes. Sub-130ms time-to-first-byte on Inworld TTS Mini, sub-250ms on Max. Use for voice agents and interactive playback. Currently supported on Inworld TTS Mini / Max; other TTS models use the synchronous endpoint.

`POST /v1/audio/generations` music, podcast, and sound-effect generation. Covers Stable Audio, GLM TTS, MOSS, SoulX Podcast where the prompt-to-audio shape differs from TTS.

`GET /v1/voices` list and manage voices, including custom voice clones for Inworld TTS. Use the returned `voice_id` on either speech endpoint.

**`Single voice (Gemini TTS)`**

```bash title="Single voice (Gemini TTS)"
curl https://api.empiriolabs.ai/v1/audio/speech \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gemini-2-5-flash-tts",
    "input": "Hello from EmpirioLabs.",
    "voice": "Charon",
    "output_format": "WAV"
  }'
```

**`Streaming TTS (Inworld)`**

```bash title="Streaming TTS (Inworld)"
curl -N https://api.empiriolabs.ai/v1/audio/speech:stream \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "inworld-tts-mini",
    "input": "Streaming audio arrives in real time.",
    "voice": "Ashley",
    "output_format": "mp3"
  }'
```

**`Multi-speaker podcast`**

```bash title="Multi-speaker podcast"
curl https://api.empiriolabs.ai/v1/audio/speech \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "soulx-podcast",
    "input": "[S1] Welcome to the show. [S2] Glad to be here. [S1] Lets dive in.",
    "voice_s1": "arthur",
    "voice_s2": "lj",
    "output_format": "mp3"
  }'
```

**`Music generation`**

```bash title="Music generation"
curl https://api.empiriolabs.ai/v1/audio/generations \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "stable-audio-2-5",
    "prompt": "Lo-fi hip hop, mellow piano, gentle vinyl crackle, 90 BPM",
    "duration": 60,
    "steps": 8,
    "cfg_scale": 1
  }'
```

## Transcription

`POST /v1/audio/transcriptions`

Accepts either a multipart `file` upload or a JSON payload with `file_url`.

**`Multipart upload`**

```bash title="Multipart upload"
curl https://api.empiriolabs.ai/v1/audio/transcriptions \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -F "model=openai-whisper-1" \
  -F "file=@meeting.mp3" \
  -F "response_format=verbose_json" \
  -F "timestamp_granularities=word,segment"
```

**`From URL`**

```bash title="From URL"
curl https://api.empiriolabs.ai/v1/audio/transcriptions \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepgram-nova-3",
    "file_url": "https://example.com/recording.wav",
    "diarize": true,
    "smart_format": true
  }'
```

Every transcription request returns `202` with a `job_id` and a `poll_url`, whatever the length of the recording. Poll `GET /v1/jobs/<job-id>` until `status` is `completed`; the transcript is in `result`.

## Document OCR

`POST /v1/ocr`

Reads a PDF or an image and returns every page as Markdown, with image regions, tables, headers, and footers. `document` takes a `document_url` or an `image_url`, each a public URL or a base64 data URI; `pages` chooses zero-based pages. A request still running after about four and a half minutes answers `202` with a `job_id`, and `Prefer: respond-async` asks for it at once.

**`Read two pages of a PDF`**

```bash title="Read two pages of a PDF"
curl https://api.empiriolabs.ai/v1/ocr \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "lightonocr-3-4b",
    "document": {"type": "document_url", "document_url": "https://arxiv.org/pdf/2412.13663"},
    "pages": [0, 1]
  }'
```

**`Response shape`**

```json title="Response shape"
{
  "pages": [
    {
      "index": 0,
      "markdown": "# ModernBERT\n\n![img-0.jpeg](img-0.jpeg)\n\n...",
      "images": [{"id": "img-0.jpeg", "top_left_x": 182, "top_left_y": 410, "bottom_right_x": 1401, "bottom_right_y": 902, "image_base64": null}],
      "tables": [],
      "header": null,
      "footer": null,
      "dimensions": {"dpi": 186, "height": 2048, "width": 1583}
    }
  ],
  "model": "lightonocr-3-4b",
  "usage_info": {"pages_processed": 2, "doc_size_bytes": 445479}
}
```

See the [Document OCR API guide](/ocr) for page selection, image and table options, long documents, and billing.

## Realtime voice

`WEBSOCKET /v1/realtime`

A WebSocket endpoint for full-duplex audio: the model can be interrupted mid-response, and live transcription returns results while the speaker is still talking. `stream: true` on the HTTP endpoints streams output only and cannot carry microphone audio to the model.

```text
wss://api.empiriolabs.ai/v1/realtime?model=<model>
```

Authenticate with your API key in an `Authorization: Bearer` header on the handshake. A web page cannot set that header, so it connects with a short-lived client secret that your server creates with `POST /v1/realtime/client_secrets`; see [Connect from a browser](/realtime#connect-from-a-browser).

```python
import asyncio, json, os, websockets

async def main():
    async with websockets.connect(
        "wss://api.empiriolabs.ai/v1/realtime?model=stepaudio-3-realtime",
        additional_headers=[("Authorization", f"Bearer {os.environ['EMPIRIOLABS_API_KEY']}")],
        max_size=None,
    ) as ws:
        print(json.loads(await ws.recv())["type"])   # session.created

        await ws.send(json.dumps({
            "type": "conversation.item.create",
            "item": {"type": "message", "role": "user",
                     "content": [{"type": "input_text", "text": "Say hello."}]},
        }))
        await ws.send(json.dumps({"type": "response.create"}))

        async for raw in ws:
            event = json.loads(raw)
            if event["type"] == "response.audio.delta":
                ...  # base64 16-bit PCM chunk, append to your playback buffer
            elif event["type"] == "response.done":
                break

asyncio.run(main())
```

Speech-to-speech models bill each completed turn from that turn's token usage; live transcription models bill by the duration of audio sent. Models, per-model voices, the event table and the transcription protocol are on [Realtime Voice API](/realtime).

## Search and research

`POST /v1/search` unified search surface for retrieval-style models. The exact accepted params per model live on each model's page (e.g. `exa-search` exposes 28 params including `category`, `livecrawl`, `subpages`, `summary_query`, `code_tokens`).

`POST /v1/research` deep research / multi-step retrieval models (Exa Research, Perplexity Deep Research, Linkup Deep Search). Generates a structured research report with cited sources.

`POST /v1/answer` direct question-answering models (Exa Answer). Returns a concise answer plus citations without the full report shape.

**`Tavily search with crawl`**

```bash title="Tavily search with crawl"
curl https://api.empiriolabs.ai/v1/search \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "tavily-search",
    "query": "latest CRDT research papers 2026",
    "search_depth": "advanced",
    "include_answer": "advanced",
    "max_results": 10,
    "topic": "general"
  }'
```

**`Perplexity Sonar with geo + date filter`**

```bash title="Perplexity Sonar with geo + date filter"
curl https://api.empiriolabs.ai/v1/chat/completions \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "perplexity-sonar-pro",
    "messages": [{"role": "user", "content": "Recent FDA approvals"}],
    "search_context_size": "high",
    "search_recency_filter": "week",
    "country": "US",
    "search_mode": "default"
  }'
```

**`Exa neural search with subpages`**

```bash title="Exa neural search with subpages"
curl https://api.empiriolabs.ai/v1/search \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "exa-search",
    "query": "alternatives to vector databases",
    "search_type": "neural",
    "category": "research paper",
    "subpages": 3,
    "summary": true
  }'
```

## Agents

Long-running, tool-using agent tasks (currently routed to Manus). Submit once, then poll for status and step-by-step messages, or stop early.

`POST /v1/agents/run` does double duty:

* With no `task_id` it starts a fresh task. The response carries the new `task_id`.
* With `task_id` it sends a follow-up message to an existing task. The agent picks it up on its next reasoning step.

`GET /v1/agents/{task_id}` retrieve the task's current status and final result.

`GET /v1/agents/{task_id}/messages` list every step the agent has emitted so far. Useful for rendering a live reasoning trace alongside the final answer.

`POST /v1/agents/{task_id}/stop` stop a running task. Billing settles for the work the agent already completed.

**`Start a task`**

```bash title="Start a task"
curl https://api.empiriolabs.ai/v1/agents/run \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "manus",
    "input": "Summarize the top 5 humanoid robotics startups by funding raised in 2025-2026"
  }'
```

**`Poll the task`**

```bash title="Poll the task"
curl https://api.empiriolabs.ai/v1/agents/task_01HV3K... \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY"
```

**`Stream task messages`**

```bash title="Stream task messages"
curl https://api.empiriolabs.ai/v1/agents/task_01HV3K.../messages \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY"
```

## 3D Generation

`POST /v1/3d/generations`

Image-to-3D generation is async. The endpoint returns a `job_id` and a polling URL; poll the jobs endpoint to retrieve the final signed GLB URL.

**`Submit a 3D job`**

```bash title="Submit a 3D job"
curl https://api.empiriolabs.ai/v1/3d/generations \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "trellis-2-4b",
    "image_url": "https://example.com/product-photo.png",
    "resolution": "1024",
    "texture_size": "2048",
    "decimation_target": 500000,
    "seed": 42
  }'
```

**`Initial response`**

```json title="Initial response"
{
  "job_id": "job_01HV3K...",
  "status": "processing",
  "poll_url": "/v1/jobs/job_01HV3K..."
}
```

**`Successful result`**

```json title="Successful result"
{
  "job_id": "job_01HV3K...",
  "status": "completed",
  "created_at": 1778000000,
  "completed_at": 1778000095,
  "endpoint": "/v1/3d/generations",
  "model": "trellis-2-4b",
  "result": {
    "data": [{
      "url": "https://media.empiriolabs.ai/worker-outputs/...",
      "content_type": "model/gltf-binary"
    }]
  }
}
```

`trellis-2-4b` exposes the full image, resolution, sampler, texture, and mesh export parameter surface on its model page.

## Detection

`POST /v1/detect`

Specialized text-classification endpoint. Currently powers GPTZero AI-detection. Each model's `scan_type` enum picks the scan; see the per-model docs for the full parameter surface.

**`GPTZero AI-detection`**

```bash title="GPTZero AI-detection"
curl https://api.empiriolabs.ai/v1/detect \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gptzero",
    "input": "The quick brown fox jumps over the lazy dog.",
    "scan_type": "ai_detection",
    "multilingual": false
  }'
```

GPTZero is also reachable via `/v1/chat/completions` and `/v1/responses`: pass the text on the message body and the gateway adapts the call. The detection summary comes back as the assistant message; pass `disable_formatting: true` to receive the raw upstream JSON instead.

## Embeddings

`POST /v1/embeddings`

OpenAI-compatible embeddings. Multilingual text and multimodal (text + image + video) embedders are available.

**`Text embedding`**

```bash title="Text embedding"
curl https://api.empiriolabs.ai/v1/embeddings \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "text-embedding-v4",
    "input": ["Sentence one.", "Sentence two."],
    "encoding_format": "float"
  }'
```

**`Multimodal embedding (text + image)`**

```bash title="Multimodal embedding (text + image)"
curl https://api.empiriolabs.ai/v1/embeddings \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "tongyi-embedding-vision-plus",
    "input": [
      {"type": "text", "text": "a glass cathedral at sunset"},
      {"type": "image", "url": "https://example.com/photo.jpg"}
    ]
  }'
```

## Reranks

`POST /v1/reranks`

Sort candidate `documents` by semantic relevance to a `query`. Returns each document's original index plus a 0-1 relevance score (higher = more relevant). Use this to tighten the output of a vector store / BM25 / hybrid retriever before passing the top hits to a language model, the standard last step in a RAG pipeline.

**`Rerank a candidate list`**

```bash title="Rerank a candidate list"
curl https://api.empiriolabs.ai/v1/reranks \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen3-rerank",
    "query": "What is a rerank model?",
    "documents": [
      "Rerank models sort candidate documents by relevance.",
      "Quantum computing is a cutting-edge field of computer science.",
      "Pre-trained language models advanced rerank models."
    ],
    "top_n": 2,
    "return_documents": true
  }'
```

**`Response shape`**

```json title="Response shape"
{
  "output": {
    "results": [
      {"index": 0, "relevance_score": 0.93, "document": {"text": "Rerank models sort candidate documents by relevance."}},
      {"index": 2, "relevance_score": 0.34, "document": {"text": "Pre-trained language models advanced rerank models."}}
    ]
  },
  "usage": {"total_tokens": 79, "cost_usd": 0.0000079},
  "request_id": "85ba5752-..."
}
```

The optional `instruct` parameter swaps between Q\&A retrieval (default) and pure semantic-similarity sorting; see the [qwen3-rerank model page](/models/qwen3-rerank) for the full parameter table.

## Decisions

`POST /v1/decisions`

Answer typed questions about a `state` with calibrated probabilities. Decision models read text, JSON, images, audio, and video, and return a probability distribution and a confidence value for each question instead of generated text. Question types are `noul` (yes or no), `choice` (2 to 255 named options), `score` (2 to 10 ordered levels), and `tool` (which function to call). Usage counts input tokens only.

**`Ask two questions about a message`**

```bash title="Ask two questions about a message"
curl https://api.empiriolabs.ai/v1/decisions \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "aplomb-1",
    "state": "I was charged twice for my subscription this month. Please refund the duplicate charge today.",
    "questions": {
      "is_urgent": {"type": "noul", "instructions": "Does the customer need a resolution today?"},
      "department": {
        "type": "choice",
        "instructions": "Which team should handle this message?",
        "criteria": {"billing": null, "technical": null, "sales": null}
      }
    }
  }'
```

**`Response shape`**

```json title="Response shape"
{
  "model": "aplomb-1",
  "answers": {
    "is_urgent": {"type": "noul", "noul": 0.9412, "confidence": 0.8824},
    "department": {
      "type": "choice",
      "choice": "billing",
      "probabilities": {"billing": 0.88, "technical": 0.11, "sales": 0.01},
      "confidence": 0.82
    }
  },
  "usage": {
    "input_tokens": 41,
    "output_tokens": 0,
    "input_tokens_details": {"text_tokens": 41, "image_tokens": 0, "audio_tokens": 0, "video_tokens": 0}
  }
}
```

See the [Decisions API guide](/decisions) for every question type, media input, limits, and billing.

## Batch API

Process large request sets asynchronously. Batch runs chat and embeddings requests for batch-eligible models; check the `batch_supported` field on `GET /v1/models` (eligible model pages also show a Batch API row). Each line bills at the model's list price less its `batch_discount_pct`, which is `0` when the model runs batch at list price. On a model with a batch discount, batch requests cannot use web search or built-in tools; send those requests to the real-time endpoint. Results arrive within the 24-hour completion window.

1. Upload a JSONL input file with `POST /v1/files` (multipart, `purpose=batch`). Each line is `{"custom_id": "...", "method": "POST", "url": "/v1/chat/completions", "body": {...}}`; `/v1/embeddings` lines work the same way. Up to 50,000 requests per file.
2. Create the batch with `POST /v1/batches`, passing the returned `input_file_id`.
3. Poll `GET /v1/batches/{batch_id}` until the batch reaches a terminal status. `GET /v1/batches` lists recent batches, and `POST /v1/batches/{batch_id}/cancel` cancels one.
4. Download results with `GET /v1/files/{output_file_id}/content`. Each output line carries the `custom_id` plus the full response body; failed lines land in the error file.

**`Upload the input file`**

```bash title="Upload the input file"
curl https://api.empiriolabs.ai/v1/files \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -F "purpose=batch" \
  -F "file=@requests.jsonl"
```

**`Create the batch`**

```bash title="Create the batch"
curl https://api.empiriolabs.ai/v1/batches \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input_file_id": "file_20545d7f6bfb4594af68ee90f28c6833",
    "endpoint": "/v1/chat/completions",
    "completion_window": "24h"
  }'
```

**`Poll, then download results`**

```bash title="Poll, then download results"
curl https://api.empiriolabs.ai/v1/batches/batch_123 \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY"

curl https://api.empiriolabs.ai/v1/files/file_output_456/content \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY"
```

## Usage object

Every endpoint that bills usage returns a `usage` field on the response. A streamed Chat Completions or Completions response carries it at the end of the stream, on a chunk that also has `choices` or, when the request sets `stream_options.include_usage: true`, in its own chunk with an empty `choices` list; read it from the last chunk that carries it. Messages, Responses and Gemini streams report it in `message_delta`, in `response.completed` and in the last event's `usageMetadata`. Base shape:

* `cost_usd`: what your account was charged for the request, after any discount, plan allowance or add-on. It equals `cost.amount` on the request's [Account Usage](/account-usage-api) row. A stream carries it on its final usage event. It is omitted when the charge is not final yet; Account Usage then shows it.
* `prompt_tokens` / `completion_tokens` / `total_tokens`: for chat-style models.
* Cache fields (`cache_read_input_tokens`, `cache_creation_input_tokens`): when prompt caching applies.

`POST /v1/decisions` uses its own shape: `input_tokens`, `output_tokens` (always 0), and `input_tokens_details` by modality, without `cost_usd`. See [Decisions API](/decisions#usage-and-billing).

Models with tiered, per-call, or variant pricing stamp extra fields on `usage` so you can see which rate was applied:

* **Tier / variant pricing.** A tier discriminator appears on `usage` when the same dimension has more than one rate. The primary field is `pricing_tier_label` (human-readable, e.g. `"Medium context"` / `"Pro"` / `"2K"`). Some models stamp the raw dimension directly instead (`resolution`, `quality`, `mode`, `rate_tier`). The dashboard renders the badge from whichever is present.
* **Per-call pricing.** Models that bill per tool invocation (search, fetch, code execution, etc.) report counts under `tool_calls_details.<tool>.invocation` or `tool_usage.<tool>`. The dashboard expands these into a per-tool breakdown automatically.
* **Per-dimension pricing.** Models that bill multiple dimensions in one request (e.g. citation tokens + reasoning tokens + search queries on deep-research models) report each dimension as its own field (`citation_tokens`, `reasoning_tokens`, `num_search_queries`, etc.).

The same fields drive the tier badge and per-tool breakdown on the dashboard usage logs, and they are also returned by the [`GET /v1/account/usage`](/api-reference/api-reference/account/retrieve-account-usage) history endpoint under each event's `metadata.worker_usage`, plus a structured top-level `tool_breakdown` array on events from per-call models. So whether you read live response usage, account-usage history, or your dashboard, the tier and billing breakdown match exactly. See [Account Usage API](/account-usage-api) for the full response shape, including the account balance, per-product spend, and page-level `cache_hit_rate`.

## File URLs

EmpirioLabs does not host user uploads. Pass any public URL directly on the input field of the model endpoint:

| Endpoint family                     | Input field                                                                            | Accepts                                              |
| ----------------------------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| Chat completions (vision)           | `content[].image_url.url`                                                              | Any public image URL or `data:` URI                  |
| Chat completions (audio)            | `content[].audio_url.url`                                                              | Any public audio URL                                 |
| Image generation (edit / variation) | `image: ["https://..."]`                                                               | Up to N URLs (model-specific limit)                  |
| Video generation (i2v / r2v / edit) | `image: "https://..."` / `video: "https://..."`                                        | Public URLs                                          |
| Audio TTS / music                   | n/a (text-only input)                                                                  | -                                                    |
| Audio transcription                 | `file_url: "https://..."` **or** multipart `file=@local.mp3`                           | Public URL or direct upload of short clips           |
| Search                              | n/a (query text)                                                                       | -                                                    |
| Embeddings (multimodal)             | `input[].url` (image/video item)                                                       | Public URLs                                          |
| Reranks                             | n/a (text query + text documents)                                                      | -                                                    |
| Decisions                           | Media nodes anywhere in `state`, for example `{"type": "image", "url": "https://..."}` | Public `https://` URLs, or base64 `data` with `mime` |

For audio transcription specifically, the multipart-direct upload on `/v1/audio/transcriptions` is the supported path for private clips that aren't on a URL; those bytes flow straight to the speech-to-text model service without persistent storage.

> **Warning**
>
> Generated output URLs are signed and expire **7 days** after creation. There is no re-sign endpoint. Save anything you need to keep (both the URL and the binary) within that window.

## Async jobs

`GET /v1/jobs/<job-id>` polls the status / final result of any async generation or transcription job. `GET /v1/jobs` lists your last 100 jobs, filterable by `status`.

Job state is retained for **1 hour** after completion.

**`Job state shape`**

```json title="Job state shape"
{
  "job_id": "job_01HV...",
  "status": "processing | completed | failed | cancelled",
  "created_at": 1778000000,
  "completed_at": 1778000041,
  "endpoint": "/v1/videos/generations",
  "model": "<model id>",
  "result": null,
  "error": null
}
```

`created_at` and `completed_at` are Unix timestamps in seconds. `completed_at` appears once the job has finished.

When `status` is `completed`, the `result` field carries the full response in the same shape the synchronous endpoint would have returned.

> **Warning**
>
> A request stays open for up to **15 minutes**, and a request without `stream: true` must return within **5 minutes**. Set `stream: true` on chat requests that may take longer. Longer generation work runs as an async job: poll it with `GET /v1/jobs/<job-id>`.

## Models

`GET /v1/models` lists every available model.

`GET /v1/models/<model-id>` returns the full schema for one model, including its parameter table.

Each model returns:

| Field                 | Description                                                                                                                                                                                     |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                  | Canonical slug (e.g. `wan-2-7-image`)                                                                                                                                                           |
| `description`         | Short marketing description                                                                                                                                                                     |
| `category`            | text / image / video / audio / transcription / research / tools / embedding / reranker                                                                                                          |
| `input_modalities`    | What the model accepts                                                                                                                                                                          |
| `output_modalities`   | What the model emits                                                                                                                                                                            |
| `context_window`      | Tokens (chat) or null (media)                                                                                                                                                                   |
| `region`              | Server region                                                                                                                                                                                   |
| `logo`                | CDN URL to the model logo                                                                                                                                                                       |
| `pricing_rows`        | Per-token, per-image, per-second, or per-call rates                                                                                                                                             |
| `pricing`             | USD per token (`prompt`, `completion`, `input_cache_read`) and per image or request, as strings; `"-1"` where the model is not priced per token, with its rates in `unit_pricing`               |
| `unit_pricing`        | The rates in `pricing_rows` that have one exact price per unit, as `{unit, usd, label}`, for example `{"unit": "minute", "usd": "0.03"}`; tiered and ranged rates appear in `pricing_rows` only |
| `supported_endpoints` | Which `/v1/...` endpoints accept this model                                                                                                                                                     |
| `parameters`          | **Full schema**: name, type, default, min/max, allowed values, descriptions                                                                                                                     |
| `features`            | Tags like streaming, vision, tool_calling, voice_cloning                                                                                                                                        |
| `structured_output`   | `json_schema` (strict schema enforcement), `json_object` (JSON mode), or `none`                                                                                                                 |
| `batch_supported`     | Present when the model is eligible for the Batch API                                                                                                                                            |
| `batch_discount_pct`  | Percent off list price for Batch API requests; `0` when batch runs at list price                                                                                                                |

```bash
curl https://api.empiriolabs.ai/v1/models | jq '.data[0]'
curl https://api.empiriolabs.ai/v1/models/wan-2-7-image | jq '.parameters'
```

## disable_formatting flag

Many chat, search, research, and rerank endpoints accept a `disable_formatting=true` flag. When set on a supporting model, the worker skips EmpirioLabs server-side formatting (citation rewriting, References block, thinking-block Markdown, etc.) and returns the upstream payload shape verbatim.

Coverage is advertised per-model. Check `supports_passthrough` in `GET /v1/models/{id}` to confirm a specific model honors the flag. Models that advertise `supports_passthrough: true` also accept the aliases `raw=true`, `passthrough=true`, and `raw_response=true`. Models without that field accept only the canonical `disable_formatting=true` form (or do not honor passthrough at all). The model card lists which aliases each model accepts.

Image, video, audio-generation, transcription, and embedding endpoints do not accept this flag, since there is no formatting layer to disable on those endpoints.

## Generated media retention

Generated images, videos, and audio are returned as signed URLs that are valid for **7 days**. After that, the URL stops working and the asset is gone; there is no re-sign endpoint. Save anything you want to keep before the 7-day window expires.

## Errors

OpenAI envelope on chat / responses / images / videos / audio / search / embeddings / reranks:

```json
{
  "error": {
    "message": "Aspect ratio is not supported by this model. Allowed: 16:9, 9:16, 1:1, ...",
    "type": "invalid_request_error",
    "code": "invalid_parameter",
    "param": "aspect_ratio"
  }
}
```

Anthropic envelope on `/v1/messages`:

```json
{ "type": "error", "error": { "type": "invalid_request_error", "message": "..." } }
```

## Headers reference

| Header                        | Required                    | Purpose                                            |
| ----------------------------- | --------------------------- | -------------------------------------------------- |
| `Authorization` / `x-api-key` | yes                         | Bearer token authentication                        |
| `Content-Type`                | yes on POST                 | `application/json` or `multipart/form-data`        |
| `Accept`                      | no                          | `text/event-stream` to force SSE on chat endpoints |
| `anthropic-version`           | when calling `/v1/messages` | Anthropic API version (e.g. `2023-06-01`)          |

> **Note**
>
> Browse the per-model parameter schemas under [Authors and Models](/providers). When you click into a specific model, every parameter the model accepts (type, default, range, allowed values, conditional flags) is documented in a table generated from the live database.