# API Reference

> Every route on the platform, with its parameters, a runnable request and what comes back.

Section: Resources. Canonical page: https://routehook.ai/docs/api-reference

Every route, in one page. The endpoint headers elsewhere in these docs and the blocks below read from the same definition, so a route cannot be described as live on one page and planned on another. The cURL, JavaScript and Python snippets are generated from the sample body shown beneath each route. They are the request, not a paraphrase of it.

## Conventions

| ASPECT | VALUE |
| --- | --- |
| Base URL | https://api.routehook.ai/v1 |
| Authentication | Authorization: Bearer <key> |
| Anthropic route | x-api-key is accepted on POST /v1/messages |
| Request encoding | application/json |
| Response encoding | application/json, or text/event-stream when streaming |
| Idempotency | Idempotency-Key on a queue submit returns the existing job |
| Errors | { error: { code, message, request_id } } |

## GET /v1/key

Describe the key making the call: its label, spend cap, lifetime usage and rate limit.

Status: Available. Authentication: any live API key on the account.

> The route describes the key that called it. There is no way to read another key with this one. `limit_reset`, `is_management_key` and `byok_usage` are published as explicit constants rather than omitted. A client reading them gets a definite answer instead of a missing key it cannot tell from a typo. `key_id` is the key's own id, unprefixed; it is the id the dashboard shows and the one to quote in a support report.

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/key \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/key", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/key",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "data": {
    "label": "Production",
    "limit": null,
    "usage": 128.4419,
    "limit_remaining": null,
    "is_provisioning_key": false,
    "is_free_tier": false,
    "rate_limit": { "requests": 600, "interval": "60s" },
    "limit_reset": null,
    "is_management_key": false,
    "byok_usage": 0,
    "key_id": "3xK9mQ2p7vL4nR8t",
    "account_id": "acct_9f2c41be",
    "created_at": "2026-05-01T09:14:22Z",
    "last_used_at": "2026-08-23T11:02:47Z",
    "expires_at": null
  }
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 404 | `model_unavailable` | No such id, or it belongs to another account. Note the code. This route does not answer not_found. |
| 429 | `rate_limited` | Back off using the Retry-After header. |

## GET /v1/credits

Read the account balance, the amount held by requests in flight and what is left to spend.

Status: Available. Authentication: any live API key on the account.

> Figures here are JSON numbers, rounded to two decimal places. Per-token *rates* elsewhere are strings. See /docs/pricing for why.

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/credits \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/credits", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/credits",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "data": {
    "total_credits": 250.0,
    "total_usage": 128.44,
    "balance": 121.56,
    "held": 0.42,
    "available": 121.14,
    "credit_limit": 0,
    "currency": "USD"
  }
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 404 | `model_unavailable` | No such id, or it belongs to another account. Note the code. This route does not answer not_found. |
| 429 | `rate_limited` | Back off using the Retry-After header. |

## GET /v1/generation

Read what one request cost and how long it took.

Status: Available. Authentication: any live API key on the account.

> `total_cost` is what this call was charged to your balance. It is the only cost figure this route carries; what the platform pays upstream is never published.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | string | Required | — | The request id returned in the X-Routehook-Request-Id header, or on an error body. |

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/generation?id=req_7c41d9be \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/generation?id=req_7c41d9be", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/generation?id=req_7c41d9be",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "data": {
    "id": "req_7c41d9be",
    "model": "openai/gpt-5.4-mini",
    "streamed": false,
    "generation_time": 812,
    "created_at": "2026-08-23T11:02:47Z",
    "tokens_prompt": 9,
    "tokens_completion": 8,
    "native_tokens_prompt": 9,
    "native_tokens_completion": 8,
    "num_media_prompt": 0,
    "num_media_completion": 0,
    "origin": "api",
    "usage": 0.000103,
    "total_cost": 0.000103,
    "cache_discount": 0,
    "finish_reason": "stop",
    "latency": 812,
    "moderation_latency": 0,
    "request_id": "req_7c41d9be",
    "status": "succeeded",
    "error_code": null,
    "attempts": 1,
    "fallback_used": false
  }
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 404 | `model_unavailable` | No such id, or it belongs to another account. Note the code. This route does not answer not_found. |
| 429 | `rate_limited` | Back off using the Retry-After header. |

## POST /v1/chat/completions

Chat completions in OpenAI's shape, streamed or whole, with automatic failover.

Status: Available. Authentication: any live API key on the account.

> Unknown request fields are forwarded upstream rather than refused, so a vendor parameter this gateway has never heard of still works.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `model` | string | Required | — | Model slug, as returned by GET /v1/models. |
| `messages` | array | Required | — | Ordered turns, each with a role of system, user or assistant, and content. |
| `stream` | boolean | Optional | false | Emit server-sent events instead of one body. See /docs/streaming. |
| `max_tokens` | integer | Optional | model default | Ceiling on generated tokens. Caps the output side of the bill. |
| `temperature` | number | Optional | model default | Sampling randomness. Forwarded upstream unchanged. |

### Request

**cURL**

```bash
curl -X POST https://api.routehook.ai/v1/chat/completions \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-5.4-mini",
    "messages": [
      {
        "role": "user",
        "content": "Hello"
      }
    ]
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/chat/completions", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "model": "openai/gpt-5.4-mini",
    "messages": [
      {
        "role": "user",
        "content": "Hello"
      }
    ]
  }),
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.post(
    "https://api.routehook.ai/v1/chat/completions",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
    json={
      "model": "openai/gpt-5.4-mini",
      "messages": [
        {
          "role": "user",
          "content": "Hello"
        }
      ]
    },
)

data = res.json()
```

### Response

```json
{
  "id": "chatcmpl_5f81c0",
  "object": "chat.completion",
  "created": 1786312455,
  "model": "openai/gpt-5.4-mini",
  "choices": [
    {
      "index": 0,
      "message": { "role": "assistant", "content": "Hello. How can I help?" },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 9,
    "completion_tokens": 8,
    "total_tokens": 17,
    "cost": "0.000103"
  }
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | Malformed body, or a parameter outside its bounds. |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 402 | `insufficient_credits` | Top up the balance before retrying. |
| 429 | `rate_limited` | Back off using the Retry-After header. |
| 503 | `upstream_unavailable` | Upstream outage. Safe to retry. |
| 409 | `model_unavailable` | Model is listed but has no live endpoint to route to. |

## POST /v1/completions

OpenAI's legacy prompt completion, translated onto the same routing as chat.

Status: Available. Authentication: any live API key on the account.

> Almost nothing upstream still serves a true completion endpoint. The prompt is wrapped as one user message on the way out and the message content unwrapped back to `text` on the way in.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `model` | string | Required | — | Model slug, as returned by GET /v1/models. |
| `prompt` | string \| string[] | Required | — | The prompt, or a batch of them. A batch answers with one choice per prompt, in order. |
| `max_tokens` | integer | Optional | model default | Ceiling on generated tokens. |
| `stream` | boolean | Optional | false | Emit server-sent events instead of one body. |
| `temperature` | number | Optional | model default | Sampling randomness. Forwarded upstream unchanged. |

### Request

**cURL**

```bash
curl -X POST https://api.routehook.ai/v1/completions \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-5.4-mini",
    "prompt": "Write one sentence about harbour fog.",
    "max_tokens": 64
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/completions", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "model": "openai/gpt-5.4-mini",
    "prompt": "Write one sentence about harbour fog.",
    "max_tokens": 64
  }),
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.post(
    "https://api.routehook.ai/v1/completions",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
    json={
      "model": "openai/gpt-5.4-mini",
      "prompt": "Write one sentence about harbour fog.",
      "max_tokens": 64
    },
)

data = res.json()
```

### Response

```json
{
  "id": "cmpl_5f81c0",
  "object": "text_completion",
  "created": 1786312455,
  "model": "openai/gpt-5.4-mini",
  "choices": [
    {
      "index": 0,
      "text": "Fog rolled off the water and swallowed the cranes one by one.",
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 8,
    "completion_tokens": 14,
    "total_tokens": 22,
    "cost": "0.0000038"
  }
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | Malformed body, or a parameter outside its bounds. |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 402 | `insufficient_credits` | Top up the balance before retrying. |
| 429 | `rate_limited` | Back off using the Retry-After header. |
| 503 | `upstream_unavailable` | Upstream outage. Safe to retry. |
| 409 | `model_unavailable` | Model is listed but has no live endpoint to route to. |

## POST /v1/messages

Anthropic's Messages API, so an Anthropic client works against this base URL unchanged.

Status: Available. Authentication: any live API key on the account.

> The key may arrive as `x-api-key` as well as a bearer token, so an Anthropic client needs no header rewriting. `model` on the way back is the gateway slug, never the upstream's.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `model` | string | Required | — | Model slug, as returned by GET /v1/models. |
| `messages` | array | Required | — | Anthropic's turns. Role user or assistant, and content. |
| `max_tokens` | integer | Required | — | Required by the Messages API, unlike chat completions. There is no default to fall back on. |
| `system` | string | Optional | — | System prompt, sent apart from the turns. |
| `stream` | boolean | Optional | false | Emit Anthropic's event framing: message_start through message_stop. |
| `stop_sequences` | string[] | Optional | — | Strings that end generation when produced. |
| `tools` | array | Optional | — | Tool definitions, in Anthropic's schema. |

### Request

**cURL**

```bash
curl -X POST https://api.routehook.ai/v1/messages \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "anthropic/claude-fable-5",
    "max_tokens": 256,
    "messages": [
      {
        "role": "user",
        "content": "Hello"
      }
    ]
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/messages", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "model": "anthropic/claude-fable-5",
    "max_tokens": 256,
    "messages": [
      {
        "role": "user",
        "content": "Hello"
      }
    ]
  }),
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.post(
    "https://api.routehook.ai/v1/messages",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
    json={
      "model": "anthropic/claude-fable-5",
      "max_tokens": 256,
      "messages": [
        {
          "role": "user",
          "content": "Hello"
        }
      ]
    },
)

data = res.json()
```

### Response

```json
{
  "id": "msg_5f81c0",
  "type": "message",
  "role": "assistant",
  "model": "anthropic/claude-fable-5",
  "content": [{ "type": "text", "text": "Hello. How can I help?" }],
  "stop_reason": "end_turn",
  "stop_sequence": null,
  "usage": { "input_tokens": 9, "output_tokens": 8 }
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | Malformed body, or a parameter outside its bounds. |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 402 | `insufficient_credits` | Top up the balance before retrying. |
| 429 | `rate_limited` | Back off using the Retry-After header. |
| 503 | `upstream_unavailable` | Upstream outage. Safe to retry. |
| 409 | `model_unavailable` | Model is listed but has no live endpoint to route to. |

## POST /v1/images/generations

Generate images and return their URLs. The connection is held until the images exist.

Status: Available. Authentication: any live API key on the account.

> The connection is held for the 20–60 seconds a generation takes. Video cannot work this way. Use the queue. Batching with `n` is a latency optimisation, not a discount.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `model` | string | Required | — | Model slug, as returned by GET /v1/models. |
| `prompt` | string | Required | — | What to generate. Up to 32,000 characters. |
| `n` | integer | Optional | 1 | Images per request, 1 to 10. Each bills at the unit rate. |
| `size` | string | Optional | model default | Dimensions (1024x1024), a ratio (16:9) or a tier (1K). Translated to whatever the model expects, and only the half it states: a ratio sets the shape, a tier sets the magnitude, dimensions set both. |
| `aspect_ratio` | string | Optional | model default | The shape, named directly: 16:9, 9:16, 1:1. Wins over anything derived from size. Snapped to the nearest ratio a model accepts rather than refused. |
| `resolution` | string | Optional | model default | The magnitude, named directly: 1K, 2K, 4K, 1024x1024. Wins over size. This is the field media rates are keyed on, so naming it names the rate you are billed at. |
| `quality` | string | Optional | — | Provider hint, same vocabulary as chat. Priced alongside resolution on models that sell both, such as GPT Image 2. |
| `reference_urls` | string[] | Optional | — | Publicly reachable input images, for models that take references. Ignored by models that do not. |

### Request

**cURL**

```bash
curl -X POST https://api.routehook.ai/v1/images/generations \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "google/nano-banana-2",
    "prompt": "isometric factory floor, technical illustration",
    "size": "1024x1024",
    "n": 2
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/images/generations", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "model": "google/nano-banana-2",
    "prompt": "isometric factory floor, technical illustration",
    "size": "1024x1024",
    "n": 2
  }),
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.post(
    "https://api.routehook.ai/v1/images/generations",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
    json={
      "model": "google/nano-banana-2",
      "prompt": "isometric factory floor, technical illustration",
      "size": "1024x1024",
      "n": 2
    },
)

data = res.json()
```

### Response

```json
{
  "created": 1786312455,
  "model": "google/nano-banana-2",
  "data": [
    { "url": "https://cdn.routehook.ai/img/9d21f4-0.png" },
    { "url": "https://cdn.routehook.ai/img/9d21f4-1.png" }
  ]
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | Malformed body, or a parameter outside its bounds. |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 402 | `insufficient_credits` | Top up the balance before retrying. |
| 429 | `rate_limited` | Back off using the Retry-After header. |
| 503 | `upstream_unavailable` | Upstream outage. Safe to retry. |
| 409 | `model_unavailable` | Model is listed but has no live endpoint to route to. |

## POST /v1/images

The same image generation under OpenRouter's path. One handler, one bill.

Status: Available. Authentication: any live API key on the account.

> The same handler as POST /v1/images/generations, at OpenRouter's spelling of the path. One implementation, one bill, one response shape. Which of the two you call is a matter of which client library you already have.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `model` | string | Required | — | Model slug, as returned by GET /v1/models. |
| `prompt` | string | Required | — | What to generate. Up to 32,000 characters. |
| `n` | integer | Optional | 1 | Images per request, 1 to 10. Each bills at the unit rate. |
| `size` | string | Optional | model default | Dimensions (1024x1024), a ratio (16:9) or a tier (1K). Translated to whatever the model expects, and only the half it states: a ratio sets the shape, a tier sets the magnitude, dimensions set both. |
| `aspect_ratio` | string | Optional | model default | The shape, named directly: 16:9, 9:16, 1:1. Wins over anything derived from size. Snapped to the nearest ratio a model accepts rather than refused. |
| `resolution` | string | Optional | model default | The magnitude, named directly: 1K, 2K, 4K, 1024x1024. Wins over size. This is the field media rates are keyed on, so naming it names the rate you are billed at. |
| `quality` | string | Optional | — | Provider hint, same vocabulary as chat. Priced alongside resolution on models that sell both, such as GPT Image 2. |
| `reference_urls` | string[] | Optional | — | Publicly reachable input images, for models that take references. Ignored by models that do not. |

### Request

**cURL**

```bash
curl -X POST https://api.routehook.ai/v1/images \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "google/nano-banana-2",
    "prompt": "isometric factory floor, technical illustration",
    "size": "1024x1024",
    "n": 2
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/images", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "model": "google/nano-banana-2",
    "prompt": "isometric factory floor, technical illustration",
    "size": "1024x1024",
    "n": 2
  }),
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.post(
    "https://api.routehook.ai/v1/images",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
    json={
      "model": "google/nano-banana-2",
      "prompt": "isometric factory floor, technical illustration",
      "size": "1024x1024",
      "n": 2
    },
)

data = res.json()
```

### Response

```json
{
  "created": 1786312455,
  "model": "google/nano-banana-2",
  "data": [
    { "url": "https://cdn.routehook.ai/img/9d21f4-0.png" },
    { "url": "https://cdn.routehook.ai/img/9d21f4-1.png" }
  ]
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | Malformed body, or a parameter outside its bounds. |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 402 | `insufficient_credits` | Top up the balance before retrying. |
| 429 | `rate_limited` | Back off using the Retry-After header. |
| 503 | `upstream_unavailable` | Upstream outage. Safe to retry. |
| 409 | `model_unavailable` | Model is listed but has no live endpoint to route to. |

## POST /v1/embeddings

Turn text into vectors for search, clustering and retrieval.

Status: Available. Authentication: any live API key on the account.

> Only models in the `embedding` category are accepted; anything else is refused with `invalid_request` before a hold is taken. Vectors are truncated in this example. Real ones are the model's full width.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `model` | string | Required | — | Model slug, as returned by GET /v1/models. |
| `input` | string \| string[] \| number[][] | Required | — | One string, a batch of strings, or pre-tokenised input to embed in a single call. |
| `encoding_format` | float \| base64 | Optional | float | How each vector is encoded in the response. |
| `dimensions` | integer | Optional | model default | Truncate vectors to this width, where the model supports it. |
| `user` | string | Optional | — | Opaque end-user identifier, forwarded upstream. |

### Request

**cURL**

```bash
curl -X POST https://api.routehook.ai/v1/embeddings \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/text-embedding-3-small",
    "input": [
      "first document",
      "second document"
    ]
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/embeddings", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "model": "openai/text-embedding-3-small",
    "input": [
      "first document",
      "second document"
    ]
  }),
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.post(
    "https://api.routehook.ai/v1/embeddings",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
    json={
      "model": "openai/text-embedding-3-small",
      "input": [
        "first document",
        "second document"
      ]
    },
)

data = res.json()
```

### Response

```json
{
  "object": "list",
  "model": "openai/text-embedding-3-small",
  "data": [
    { "object": "embedding", "index": 0, "embedding": [0.0021, -0.0138, 0.0074] },
    { "object": "embedding", "index": 1, "embedding": [0.0019, -0.0142, 0.0069] }
  ],
  "usage": { "prompt_tokens": 6, "total_tokens": 6 }
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | Malformed body, or a parameter outside its bounds. |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 402 | `insufficient_credits` | Top up the balance before retrying. |
| 429 | `rate_limited` | Back off using the Retry-After header. |
| 503 | `upstream_unavailable` | Upstream outage. Safe to retry. |
| 409 | `model_unavailable` | Model is listed but has no live endpoint to route to. |

## POST /v1/responses

OpenAI's Responses API, served by every text model on the platform rather than one vendor's.

Status: Available. Authentication: any live API key on the account.

> Translated onto a chat completion rather than forwarded, so every text model in the catalogue answers here, not only the one vendor that implements this route. `previous_response_id` is refused rather than ignored: this gateway stores no conversations, and a reply that had silently forgotten the earlier turns would be worse than an error saying so.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `model` | string | Required | — | Model slug, as returned by GET /v1/models. |
| `input` | string \| object[] | Required | — | A prompt, or the conversation so far as a list of role/content items. |
| `instructions` | string | Optional | — | The system prompt. Sent as its own turn, not folded into the first user message. |
| `max_output_tokens` | integer | Optional | model default | Ceiling on the answer. |
| `stream` | boolean | Optional | false | Emit the answer as server-sent events. |
| `tools` | object[] | Optional | — | Function definitions, in the same shape chat takes. |
| `tool_choice` | string \| object | Optional | — | auto, none, required, or a named function. |
| `parallel_tool_calls` | boolean | Optional | — | Allow more than one tool call in a single turn. |
| `temperature` | number | Optional | — | 0 to 2. Forwarded unchanged. |
| `top_p` | number | Optional | — | 0 to 1. Forwarded unchanged. |
| `metadata` | object | Optional | — | Echoed back on the response object rather than forwarded upstream. It changes neither routing nor price. |
| `store` | boolean | Optional | — | Accepted and ignored. This gateway stores no conversations, so there is nothing for it to switch on. |
| `previous_response_id` | string | Optional | — | Refused with 400 rather than ignored, for the reason in the note below. |
| `user` | string | Optional | — | An opaque end-user id, forwarded upstream. |

### Request

**cURL**

```bash
curl -X POST https://api.routehook.ai/v1/responses \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-5.4-mini",
    "input": "Say exactly: parity works",
    "max_output_tokens": 20
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/responses", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "model": "openai/gpt-5.4-mini",
    "input": "Say exactly: parity works",
    "max_output_tokens": 20
  }),
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.post(
    "https://api.routehook.ai/v1/responses",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
    json={
      "model": "openai/gpt-5.4-mini",
      "input": "Say exactly: parity works",
      "max_output_tokens": 20
    },
)

data = res.json()
```

### Response

```json
{
  "id": "resp_A81FB265",
  "object": "response",
  "created_at": 1787654321,
  "status": "completed",
  "error": null,
  "incomplete_details": null,
  "model": "openai/gpt-5.4-mini",
  "output": [
    {
      "type": "message",
      "role": "assistant",
      "content": [{ "type": "output_text", "text": "parity works", "annotations": [] }]
    }
  ],
  "output_text": "parity works",
  "instructions": null,
  "max_output_tokens": 20,
  "temperature": null,
  "top_p": null,
  "parallel_tool_calls": true,
  "tool_choice": "auto",
  "tools": [],
  "metadata": {},
  "usage": { "input_tokens": 12, "output_tokens": 3, "total_tokens": 15 }
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | Malformed body, or a parameter outside its bounds. |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 402 | `insufficient_credits` | Top up the balance before retrying. |
| 429 | `rate_limited` | Back off using the Retry-After header. |
| 503 | `upstream_unavailable` | Upstream outage. Safe to retry. |
| 409 | `model_unavailable` | Model is listed but has no live endpoint to route to. |

## POST /v1/audio/speech

Turn text into spoken audio and stream the bytes back.

Status: Available. Authentication: any live API key on the account.

> The response is audio, not JSON. The bytes are streamed rather than buffered, so a long document does not sit in memory on either side. Billing is per character of input, and the charge is on X-Routehook-Cost because there is no body to put it in.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `model` | string | Required | — | Model slug, as returned by GET /v1/models. |
| `input` | string | Required | — | The text to speak. Capped at 4096 characters, which is also the axis this route is billed on. |
| `voice` | string | Optional | alloy | Vendor voice id. Not an enum here. A new voice should not need a release from us to be usable. |
| `response_format` | mp3 \| opus \| aac \| flac \| wav \| pcm | Optional | mp3 | Container for the returned audio. |
| `speed` | number | Optional | 1.0 | Playback rate, 0.25 to 4.0. |

### Request

**cURL**

```bash
curl -X POST https://api.routehook.ai/v1/audio/speech \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-transcribe",
    "input": "Routehook audio is live.",
    "voice": "alloy"
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/audio/speech", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "model": "openai/gpt-transcribe",
    "input": "Routehook audio is live.",
    "voice": "alloy"
  }),
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.post(
    "https://api.routehook.ai/v1/audio/speech",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
    json={
      "model": "openai/gpt-transcribe",
      "input": "Routehook audio is live.",
      "voice": "alloy"
    },
)

data = res.json()
```

### Response

```json
HTTP/1.1 200 OK
Content-Type: audio/mpeg
X-Routehook-Cost: 0.00045

<binary audio>
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | Malformed body, or a parameter outside its bounds. |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 402 | `insufficient_credits` | Top up the balance before retrying. |
| 429 | `rate_limited` | Back off using the Retry-After header. |
| 503 | `upstream_unavailable` | Upstream outage. Safe to retry. |
| 409 | `model_unavailable` | Model is listed but has no live endpoint to route to. |

## POST /v1/audio/transcriptions

Transcribe audio to text, with optional word and segment timestamps.

Status: Available. Authentication: any live API key on the account.

> Billed per second of audio. Where a duration is reported that number is used; where it is not, the upload is measured and the request row records that the figure was estimated rather than metered.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `model` | string | Required | — | Model slug, as returned by GET /v1/models. |
| `input_audio` | object | Required | — | `{ data, format }`: base64 audio, with or without a data: prefix, and the container it is in. |
| `language` | string | Optional | — | ISO-639-1 hint. Improves both accuracy and latency when it is known. |
| `response_format` | json \| text \| srt \| verbose_json \| vtt | Optional | json | Shape of the transcript. Non-JSON formats are returned verbatim. |
| `timestamp_granularities` | string[] | Optional | — | `word` and/or `segment`. Only meaningful with verbose_json. |

### Request

**cURL**

```bash
curl -X POST https://api.routehook.ai/v1/audio/transcriptions \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-live-transcribe",
    "input_audio": {
      "data": "UklGRiQAAABXQVZF...",
      "format": "mp3"
    }
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/audio/transcriptions", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "model": "openai/gpt-live-transcribe",
    "input_audio": {
      "data": "UklGRiQAAABXQVZF...",
      "format": "mp3"
    }
  }),
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.post(
    "https://api.routehook.ai/v1/audio/transcriptions",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
    json={
      "model": "openai/gpt-live-transcribe",
      "input_audio": {
        "data": "UklGRiQAAABXQVZF...",
        "format": "mp3"
      }
    },
)

data = res.json()
```

### Response

```json
{
  "text": "Routehook audio is live."
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | Malformed body, or a parameter outside its bounds. |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 402 | `insufficient_credits` | Top up the balance before retrying. |
| 429 | `rate_limited` | Back off using the Retry-After header. |
| 503 | `upstream_unavailable` | Upstream outage. Safe to retry. |
| 409 | `model_unavailable` | Model is listed but has no live endpoint to route to. |

## POST /v1/rerank

Score a set of documents against a query and return them in relevance order.

Status: Available. Authentication: any live API key on the account.

> `index` is the position in the request's document list, not in this response, so a reordered result still points at what you sent. Billed on the query plus the documents.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `model` | string | Required | — | Model slug, as returned by GET /v1/models. |
| `query` | string | Required | — | What the documents are being scored against. |
| `documents` | string[] \| object[] | Required | — | Up to 1000 documents, as plain strings or as objects carrying a `text` field. |
| `top_n` | integer | Optional | all | Return only the highest-scoring N. |
| `return_documents` | boolean | Optional | false | Echo each document back beside its score. |

### Request

**cURL**

```bash
curl -X POST https://api.routehook.ai/v1/rerank \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "YOUR_MODEL_ID",
    "query": "how do holds work",
    "documents": [
      "A hold reserves the ceiling before the call.",
      "Invoices are issued monthly."
    ],
    "top_n": 2
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/rerank", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "model": "YOUR_MODEL_ID",
    "query": "how do holds work",
    "documents": [
      "A hold reserves the ceiling before the call.",
      "Invoices are issued monthly."
    ],
    "top_n": 2
  }),
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.post(
    "https://api.routehook.ai/v1/rerank",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
    json={
      "model": "YOUR_MODEL_ID",
      "query": "how do holds work",
      "documents": [
        "A hold reserves the ceiling before the call.",
        "Invoices are issued monthly."
      ],
      "top_n": 2
    },
)

data = res.json()
```

### Response

```json
{
  "id": "req_9f2c41be",
  "model": "YOUR_MODEL_ID",
  "results": [
    { "index": 0, "relevance_score": 0.97 },
    { "index": 1, "relevance_score": 0.04 }
  ],
  "usage": { "total_tokens": 24 }
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | Malformed body, or a parameter outside its bounds. |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 402 | `insufficient_credits` | Top up the balance before retrying. |
| 429 | `rate_limited` | Back off using the Retry-After header. |
| 503 | `upstream_unavailable` | Upstream outage. Safe to retry. |
| 409 | `model_unavailable` | Model is listed but has no live endpoint to route to. |

## POST /v1/queue/{model}

Submit a job and return immediately with the id and the URLs that follow it.

Status: Available. Authentication: any live API key on the account.

> Everything in the body other than the fields above is the model's own input and is passed through. Money is held at submit and settled when the job reaches a terminal state.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `model` | string | Required | — | Path parameter, and a wildcard. The whole slug, slashes included. |
| `webhook_url` | string | Optional | — | https URL to POST the terminal state to. `?fal_webhook=` is accepted for the same purpose. |
| `prompt` | string | Optional | — | Up to 32,000 characters. Read by the gateway for the request record, and forwarded. |
| `n` | integer | Optional | — | 1 to 10. Multiplies the reservation and the bill. |
| `size` | string | Optional | — | Vendor size string, forwarded as sent. |
| `duration` | number | Optional | — | Seconds, 1 to 600. The axis a video model is billed on, so it also sizes the hold. |
| `Idempotency-Key` | header | Optional | — | Repeating a submit with the same key returns the existing job instead of opening a second one. |

### Request

**cURL**

```bash
curl -X POST https://api.routehook.ai/v1/queue/google/veo-3.1 \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "drone shot over a harbour at dawn",
    "duration": 5,
    "webhook_url": "https://your.app/hooks/routehook"
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/queue/google/veo-3.1", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "prompt": "drone shot over a harbour at dawn",
    "duration": 5,
    "webhook_url": "https://your.app/hooks/routehook"
  }),
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.post(
    "https://api.routehook.ai/v1/queue/google/veo-3.1",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
    json={
      "prompt": "drone shot over a harbour at dawn",
      "duration": 5,
      "webhook_url": "https://your.app/hooks/routehook"
    },
)

data = res.json()
```

### Response

```json
{
  "request_id": "job_ab12cd34",
  "status": "IN_QUEUE",
  "queue_position": 3,
  "status_url": "https://api.routehook.ai/v1/queue/requests/job_ab12cd34/status",
  "response_url": "https://api.routehook.ai/v1/queue/requests/job_ab12cd34",
  "cancel_url": "https://api.routehook.ai/v1/queue/requests/job_ab12cd34/cancel"
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | Malformed body, or a parameter outside its bounds. |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 402 | `insufficient_credits` | Top up the balance before retrying. |
| 429 | `rate_limited` | Back off using the Retry-After header. |
| 503 | `upstream_unavailable` | Upstream outage. Safe to retry. |
| 409 | `model_unavailable` | Model is listed but has no live endpoint to route to. |

## GET /v1/queue/requests/{job_id}/status

Poll a job: queue position, terminal outcome, timings and optional logs.

Status: Available. Authentication: any live API key on the account.

> `status` carries fal's three values only (IN_QUEUE, IN_PROGRESS, COMPLETED), so a client polling until COMPLETED terminates. A job that failed or was cancelled is COMPLETED here and says which in `outcome`. `queue_position` is null once the job has left the queue rather than 0, because a running job reporting 0 reads as about to start. `metrics` is in seconds. Fal's unit, and the one place this API does not suffix a duration `_ms`. `GET /v1/queue/{model}/requests/{job_id}/status` is the same handler.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `job_id` | string | Required | — | Path parameter. The request_id returned by the submit call. |
| `logs` | 0 \| 1 | Optional | 0 | Include the job's log lines in the response. |

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/queue/requests/job_ab12cd34/status?logs=1 \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/queue/requests/job_ab12cd34/status?logs=1", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/queue/requests/job_ab12cd34/status?logs=1",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "request_id": "job_ab12cd34",
  "status": "IN_PROGRESS",
  "outcome": null,
  "queue_position": null,
  "logs": [
    { "message": "sampling frame 12/120", "level": "INFO", "timestamp": "2026-08-23T11:02:47Z" }
  ],
  "metrics": { "inference_time": null, "queue_time": 4.1 },
  "error": null
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 404 | `model_unavailable` | No such id, or it belongs to another account. Note the code. This route does not answer not_found. |
| 429 | `rate_limited` | Back off using the Retry-After header. |

## GET /v1/queue/requests/{job_id}

Fetch the output of a finished job.

Status: Available. Authentication: any live API key on the account.

> **Check the status code.** While the job is still running this route answers `202` with the status body — `request_id`, `status`, `queue_position` — not with the result above; 200 means finished. A client that parses the body without looking at the code reads a running job as a result with no payload. `payload` is the upstream's own result body, forwarded verbatim rather than normalised, so there is one shape per model. A job that failed answers with the standard error envelope and the failure's status code, not with a 200 carrying an error field. `GET /v1/queue/{model}/requests/{job_id}` is the same handler.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `job_id` | string | Required | — | Path parameter. The request_id returned by the submit call. |

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/queue/requests/job_ab12cd34 \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/queue/requests/job_ab12cd34", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/queue/requests/job_ab12cd34",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "request_id": "job_ab12cd34",
  "model": "google/veo-3.1",
  "created": 1786312455,
  "outcome": "succeeded",
  "payload": {
    "video": { "url": "https://cdn.routehook.ai/vid/ab12cd34.mp4" }
  },
  "gateway_request_id": "req_7c41d9be",
  "cost": "0.7500",
  "usage": { "video_seconds": "5" }
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 404 | `model_unavailable` | No such id, or it belongs to another account. Note the code. This route does not answer not_found. |
| 429 | `rate_limited` | Back off using the Retry-After header. |

## PUT /v1/queue/requests/{job_id}/cancel

Cancel a job that has not finished and release its hold.

Status: Available. Authentication: any live API key on the account.

> Cancelling releases the hold. The call is idempotent and answers 200 even for a job that had already finished: `canceled` is false there, and exists for the audit trail rather than for control flow. `outcome` is null in the window between the cancellation being recorded and the worker abandoning an attempt already in flight upstream.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `job_id` | string | Required | — | Path parameter. The request_id returned by the submit call. |

### Request

**cURL**

```bash
curl -X PUT https://api.routehook.ai/v1/queue/requests/job_ab12cd34/cancel \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/queue/requests/job_ab12cd34/cancel", {
  method: "PUT",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "PUT",
    "https://api.routehook.ai/v1/queue/requests/job_ab12cd34/cancel",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "request_id": "job_ab12cd34",
  "status": "COMPLETED",
  "outcome": "canceled",
  "canceled": true
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 404 | `model_unavailable` | No such id, or it belongs to another account. Note the code. This route does not answer not_found. |
| 429 | `rate_limited` | Back off using the Retry-After header. |

## GET /v1/queue/requests/{job_id}/stream

Server-sent stream of a job's status transitions, instead of polling.

Status: Available. Authentication: any live API key on the account.

> The response is `text/event-stream`; the object above is one event's `data:` payload. The stream ends when the job reaches a terminal state. Use it instead of a polling loop, not alongside one.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `job_id` | string | Required | — | Path parameter. The request_id returned by the submit call. |

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/queue/requests/job_ab12cd34/stream \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/queue/requests/job_ab12cd34/stream", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/queue/requests/job_ab12cd34/stream",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "request_id": "job_ab12cd34",
  "status": "IN_PROGRESS",
  "outcome": null,
  "queue_position": 0
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 404 | `model_unavailable` | No such id, or it belongs to another account. Note the code. This route does not answer not_found. |
| 429 | `rate_limited` | Back off using the Retry-After header. |

## GET /v1/queue/{model}/requests/{job_id}/status

The same poll, at fal's URL shape. The model segment is ignored.

Status: Available. Authentication: any live API key on the account.

> fal's path shape for GET /v1/queue/requests/{job_id}/status, reaching the same handler. It exists so a client that kept fal's URLs works without rewriting them; a new integration should use the short form.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `model` | string | Required | — | Path parameter, and a wildcard. Matched and then ignored — the job id alone identifies the job, and any slug here is accepted. |
| `job_id` | string | Required | — | Path parameter. The request_id returned by the submit call. |
| `logs` | 0 \| 1 | Optional | 0 | Include the job's log lines in the response. |

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/queue/google/veo-3.1/requests/job_ab12cd34/status?logs=1 \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/queue/google/veo-3.1/requests/job_ab12cd34/status?logs=1", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/queue/google/veo-3.1/requests/job_ab12cd34/status?logs=1",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "request_id": "job_ab12cd34",
  "status": "IN_PROGRESS",
  "outcome": null,
  "queue_position": null,
  "logs": [
    { "message": "sampling frame 12/120", "level": "INFO", "timestamp": "2026-08-23T11:02:47Z" }
  ],
  "metrics": { "inference_time": null, "queue_time": 4.1 },
  "error": null
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 404 | `model_unavailable` | No such id, or it belongs to another account. Note the code. This route does not answer not_found. |
| 429 | `rate_limited` | Back off using the Retry-After header. |

## GET /v1/queue/{model}/requests/{job_id}

The same result, at fal's URL shape.

Status: Available. Authentication: any live API key on the account.

> fal's path shape for GET /v1/queue/requests/{job_id}, reaching the same handler — including the 202 while the job is still running. It exists so a client that kept fal's URLs works without rewriting them; a new integration should use the short form.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `model` | string | Required | — | Path parameter, and a wildcard. Matched and then ignored — the job id alone identifies the job, and any slug here is accepted. |
| `job_id` | string | Required | — | Path parameter. The request_id returned by the submit call. |

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/queue/google/veo-3.1/requests/job_ab12cd34 \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/queue/google/veo-3.1/requests/job_ab12cd34", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/queue/google/veo-3.1/requests/job_ab12cd34",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "request_id": "job_ab12cd34",
  "model": "google/veo-3.1",
  "created": 1786312455,
  "outcome": "succeeded",
  "payload": {
    "video": { "url": "https://cdn.routehook.ai/vid/ab12cd34.mp4" }
  },
  "gateway_request_id": "req_7c41d9be",
  "cost": "0.7500",
  "usage": { "video_seconds": "5" }
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 404 | `model_unavailable` | No such id, or it belongs to another account. Note the code. This route does not answer not_found. |
| 429 | `rate_limited` | Back off using the Retry-After header. |

## PUT /v1/queue/{model}/requests/{job_id}/cancel

The same cancel, at fal's URL shape.

Status: Available. Authentication: any live API key on the account.

> fal's path shape for PUT /v1/queue/requests/{job_id}/cancel, reaching the same handler. It exists so a client that kept fal's URLs works without rewriting them; a new integration should use the short form.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `model` | string | Required | — | Path parameter, and a wildcard. Matched and then ignored — the job id alone identifies the job, and any slug here is accepted. |
| `job_id` | string | Required | — | Path parameter. The request_id returned by the submit call. |

### Request

**cURL**

```bash
curl -X PUT https://api.routehook.ai/v1/queue/google/veo-3.1/requests/job_ab12cd34/cancel \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/queue/google/veo-3.1/requests/job_ab12cd34/cancel", {
  method: "PUT",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "PUT",
    "https://api.routehook.ai/v1/queue/google/veo-3.1/requests/job_ab12cd34/cancel",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "request_id": "job_ab12cd34",
  "status": "COMPLETED",
  "outcome": "canceled",
  "canceled": true
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 404 | `model_unavailable` | No such id, or it belongs to another account. Note the code. This route does not answer not_found. |
| 429 | `rate_limited` | Back off using the Retry-After header. |

## POST /v1/run/{model}

Submit a job and hold the connection until it finishes or the wait ceiling is reached.

Status: Available. Authentication: any live API key on the account.

> Identical to submitting and polling, with the wait done for you. Past the platform's wait ceiling the call returns the job so you can poll it. The work is not thrown away.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `model` | string | Required | — | Path parameter, and a wildcard. The whole slug, slashes included. |

### Request

**cURL**

```bash
curl -X POST https://api.routehook.ai/v1/run/google/veo-3.1 \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "drone shot over a harbour at dawn",
    "duration": 5
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/run/google/veo-3.1", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "prompt": "drone shot over a harbour at dawn",
    "duration": 5
  }),
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.post(
    "https://api.routehook.ai/v1/run/google/veo-3.1",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
    json={
      "prompt": "drone shot over a harbour at dawn",
      "duration": 5
    },
)

data = res.json()
```

### Response

```json
{
  "request_id": "job_ab12cd34",
  "model": "google/veo-3.1",
  "created": 1786312455,
  "outcome": "succeeded",
  "payload": {
    "video": { "url": "https://cdn.routehook.ai/vid/ab12cd34.mp4" }
  },
  "gateway_request_id": "req_7c41d9be",
  "cost": "0.7500",
  "usage": { "video_seconds": "5" }
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | Malformed body, or a parameter outside its bounds. |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 402 | `insufficient_credits` | Top up the balance before retrying. |
| 429 | `rate_limited` | Back off using the Retry-After header. |
| 503 | `upstream_unavailable` | Upstream outage. Safe to retry. |
| 409 | `model_unavailable` | Model is listed but has no live endpoint to route to. |

## POST /v1/videos/generations

Submit a video generation and get a job id back. Answers 202: the work runs for minutes.

Status: Available. Authentication: any live API key on the account.

> **202, not 200.** The name looks synchronous and the behaviour is not: video generation runs for minutes and no connection is held for it. Poll `status_url`, or GET /v1/videos/{job_id}, which is the same job under a shorter path. Everything in the body other than the fields above is the model's own input and is forwarded untouched.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `model` | string | Required | — | Body field, not a path segment. This route has none to carry it, unlike POST /v1/queue/{model}. |
| `prompt` | string | Optional | — | Up to 32,000 characters. Read by the gateway for the request record, and forwarded. |
| `duration` | number | Optional | — | Seconds, 1 to 600. The axis a video model is billed on, so it also sizes the hold. |
| `size` | string | Optional | — | Vendor size string, forwarded as sent. |
| `n` | integer | Optional | — | 1 to 10. Multiplies the reservation and the bill. |
| `webhook_url` | string | Optional | — | https URL to POST the terminal state to. `?fal_webhook=` is accepted for the same purpose. |
| `Idempotency-Key` | header | Optional | — | Repeating a submit with the same key returns the existing job instead of opening a second one. |

### Request

**cURL**

```bash
curl -X POST https://api.routehook.ai/v1/videos/generations \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "google/veo-3.1",
    "prompt": "drone shot over a harbour at dawn",
    "duration": 5
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/videos/generations", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "model": "google/veo-3.1",
    "prompt": "drone shot over a harbour at dawn",
    "duration": 5
  }),
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.post(
    "https://api.routehook.ai/v1/videos/generations",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
    json={
      "model": "google/veo-3.1",
      "prompt": "drone shot over a harbour at dawn",
      "duration": 5
    },
)

data = res.json()
```

### Response

```json
{
  "request_id": "job_ab12cd34",
  "status": "IN_QUEUE",
  "queue_position": 3,
  "status_url": "https://api.routehook.ai/v1/queue/requests/job_ab12cd34/status",
  "response_url": "https://api.routehook.ai/v1/queue/requests/job_ab12cd34",
  "cancel_url": "https://api.routehook.ai/v1/queue/requests/job_ab12cd34/cancel"
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | Malformed body, or a parameter outside its bounds. |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 402 | `insufficient_credits` | Top up the balance before retrying. |
| 429 | `rate_limited` | Back off using the Retry-After header. |
| 503 | `upstream_unavailable` | Upstream outage. Safe to retry. |
| 409 | `model_unavailable` | Model is listed but has no live endpoint to route to. |

## POST /v1/videos

The same submit under OpenRouter's path.

Status: Available. Authentication: any live API key on the account.

> The same handler as POST /v1/videos/generations, at OpenRouter's spelling of the path. **202, not 200** — poll `status_url` or GET /v1/videos/{job_id}.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `model` | string | Required | — | Body field, not a path segment. This route has none to carry it, unlike POST /v1/queue/{model}. |
| `prompt` | string | Optional | — | Up to 32,000 characters. Read by the gateway for the request record, and forwarded. |
| `duration` | number | Optional | — | Seconds, 1 to 600. The axis a video model is billed on, so it also sizes the hold. |
| `size` | string | Optional | — | Vendor size string, forwarded as sent. |
| `n` | integer | Optional | — | 1 to 10. Multiplies the reservation and the bill. |
| `webhook_url` | string | Optional | — | https URL to POST the terminal state to. `?fal_webhook=` is accepted for the same purpose. |
| `Idempotency-Key` | header | Optional | — | Repeating a submit with the same key returns the existing job instead of opening a second one. |

### Request

**cURL**

```bash
curl -X POST https://api.routehook.ai/v1/videos \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "google/veo-3.1",
    "prompt": "drone shot over a harbour at dawn",
    "duration": 5
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/videos", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "model": "google/veo-3.1",
    "prompt": "drone shot over a harbour at dawn",
    "duration": 5
  }),
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.post(
    "https://api.routehook.ai/v1/videos",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
    json={
      "model": "google/veo-3.1",
      "prompt": "drone shot over a harbour at dawn",
      "duration": 5
    },
)

data = res.json()
```

### Response

```json
{
  "request_id": "job_ab12cd34",
  "status": "IN_QUEUE",
  "queue_position": 3,
  "status_url": "https://api.routehook.ai/v1/queue/requests/job_ab12cd34/status",
  "response_url": "https://api.routehook.ai/v1/queue/requests/job_ab12cd34",
  "cancel_url": "https://api.routehook.ai/v1/queue/requests/job_ab12cd34/cancel"
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | Malformed body, or a parameter outside its bounds. |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 402 | `insufficient_credits` | Top up the balance before retrying. |
| 429 | `rate_limited` | Back off using the Retry-After header. |
| 503 | `upstream_unavailable` | Upstream outage. Safe to retry. |
| 409 | `model_unavailable` | Model is listed but has no live endpoint to route to. |

## GET /v1/videos/{job_id}

Poll a video job: its status while it runs, its result once it finishes.

Status: Available. Authentication: any live API key on the account.

> The queue's result route under a shorter path, and the same handler. While the job is running it answers **202** with the status body — `request_id`, `status`, `queue_position` — and once it finishes, 200 with the result above. Poll it until the status code changes. The finished video's bytes are at GET /v1/videos/{job_id}/content.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `job_id` | string | Required | — | Path parameter. The request_id returned by the submit call. |

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/videos/job_ab12cd34 \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/videos/job_ab12cd34", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/videos/job_ab12cd34",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "request_id": "job_ab12cd34",
  "model": "google/veo-3.1",
  "created": 1786312455,
  "outcome": "succeeded",
  "payload": {
    "video": { "url": "https://cdn.routehook.ai/vid/ab12cd34.mp4" }
  },
  "gateway_request_id": "req_7c41d9be",
  "cost": "0.7500",
  "usage": { "video_seconds": "5" }
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 404 | `model_unavailable` | No such id, or it belongs to another account. Note the code. This route does not answer not_found. |
| 429 | `rate_limited` | Back off using the Retry-After header. |

## GET /v1/videos/{job_id}/content

Stream the finished video's bytes.

Status: Available. Authentication: any live API key on the account.

> The same proxy as GET /v1/files/{id}, reached by job id instead of by media reference. A job that has not finished answers 400 rather than 404: the bytes are on their way, and a client told 'not found' would stop polling.

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/videos/job_ab12cd34/content \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/videos/job_ab12cd34/content", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/videos/job_ab12cd34/content",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
HTTP/1.1 200 OK
Content-Type: video/mp4
Accept-Ranges: bytes

<binary media>
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 404 | `not_found` | No such id, or it belongs to another account. |
| 429 | `rate_limited` | Back off using the Retry-After header. |
| 410 | `gone` | The reference expired. Permanent. Do not retry. |

## GET /v1/files/{id}

Fetch generated media by reference. Proxied at request time, never redirected upstream.

Status: Available. Authentication: any live API key on the account.

> The origin copy is fetched at request time and streamed through. It never redirects: a 302 would publish the upstream URL, which is the one thing the indirection exists to hide. References expire in minutes, and an expired one answers 410 rather than a retryable code.

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/files/file_9f2c41be \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/files/file_9f2c41be", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/files/file_9f2c41be",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
HTTP/1.1 200 OK
Content-Type: video/mp4
Accept-Ranges: bytes
Cache-Control: private, max-age=280, must-revalidate

<binary media>
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 404 | `not_found` | No such id, or it belongs to another account. |
| 429 | `rate_limited` | Back off using the Retry-After header. |
| 410 | `gone` | The reference expired. Permanent. Do not retry. |

## GET /v1/jobs

List this account's jobs, newest first, keyset-paged.

Status: Available. Authentication: any live API key on the account.

> This route is ours rather than fal's (fal has no job list), so it is the one place to find a job whose id was lost. A row carries no payload: fifty jobs' worth of upstream JSON is a megabyte of response to render a table, so the output stays behind `response_url`. `cost` is null until the hold is settled. An open job has been reserved against, not charged.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `status` | string | Optional | all | Filter to queued, running, succeeded, failed or canceled. The stored vocabulary, not the three wire statuses. |
| `model` | string | Optional | all | Filter to one model slug, matched exactly. |
| `category` | string | Optional | all | Filter to one modality: text, image, video, audio, embedding or rerank. |
| `cursor` | string | Optional | — | Keyset cursor. The `next_cursor` value from the previous page. |
| `limit` | integer | Optional | 50 | Rows per page, 1 to 200. |

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/jobs?limit=50&status=running \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/jobs?limit=50&status=running", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/jobs?limit=50&status=running",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "data": [
    {
      "request_id": "job_ab12cd34",
      "model": "google/veo-3.1",
      "category": "video",
      "status": "IN_PROGRESS",
      "outcome": null,
      "queue_position": null,
      "gateway_request_id": "req_7c41d9be",
      "cost": null,
      "error_code": null,
      "created_at": "2026-08-23T11:02:47Z",
      "started_at": "2026-08-23T11:02:51Z",
      "completed_at": null
    }
  ],
  "next_cursor": null
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 429 | `rate_limited` | Back off using the Retry-After header. |

## GET /v1/models

Every model with its category, status, published price and per-token rates.

Status: Available. Authentication: none — this route answers anonymously.

> Anonymous and cached, no key needed. Per-token rates are strings, because a client that sums a million float64 rates drifts. The headline `price` stays a number.

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/models \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/models", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/models",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "data": [
    {
      "id": "openai/gpt-5.4-mini",
      "canonical_slug": "openai/gpt-5.4-mini",
      "name": "GPT-4o mini",
      "category": "text",
      "status": "available",
      "unit": "per 1M tokens",
      "price": 0.15,
      "context_length": 128000,
      "pricing": {
        "prompt": "0.00000015",
        "completion": "0.0000006",
        "request": null,
        "image": null
      },
      "supported_parameters": ["tools", "temperature", "max_tokens"]
    }
  ]
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 429 | `rate_limited` | Back off using the Retry-After header. |

## GET /v1/models/count

How many models are servable right now.

Status: Available. Authentication: none — this route answers anonymously.

> Counts models whose status is `available`. Everything listed but not yet servable — `coming_soon` — is excluded, so this is the number of slugs a request can actually route to rather than the length of `GET /v1/models`.

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/models/count \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/models/count", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/models/count",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "data": { "count": 214 }
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 429 | `rate_limited` | Back off using the Retry-After header. |

## GET /v1/models/user

The catalogue as this caller sees it.

Status: Available. Authentication: none — this route answers anonymously.

> Returns the same catalogue as GET /v1/models. OpenRouter narrows this route by an account's provider preferences; there is no such per-account filtering here — every published model is available to every key — so the honest answer is the whole catalogue rather than a stub. The route exists so a client that calls it gets a catalogue instead of a 404, and it will narrow on its own the day per-account preferences do.

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/models/user \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/models/user", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/models/user",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "data": [
    {
      "id": "openai/gpt-5.4-mini",
      "canonical_slug": "openai/gpt-5.4-mini",
      "name": "GPT-4o mini",
      "category": "text",
      "status": "available",
      "unit": "per 1M tokens",
      "price": 0.15,
      "context_length": 128000,
      "pricing": {
        "prompt": "0.00000015",
        "completion": "0.0000006",
        "request": null,
        "image": null
      },
      "supported_parameters": ["tools", "temperature", "max_tokens"]
    }
  ]
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 429 | `rate_limited` | Back off using the Retry-After header. |

## GET /v1/model/{author}/{slug}

One model by slug. Singular 'model'. OpenRouter's spelling for this route.

Status: Available. Authentication: none — this route answers anonymously.

> Singular `model`, which is OpenRouter's spelling for this route and not a typo for the plural one. An unknown slug answers 404 with `model_unavailable` rather than `not_found` — note the code, and see the header of this file for why the two differ.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `author/slug` | string | Required | — | Path parameter, and two segments. Catalogue slugs are namespaced, so `openai/gpt-5.4-mini` is the whole of it. |

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/model/openai/gpt-5.4-mini \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/model/openai/gpt-5.4-mini", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/model/openai/gpt-5.4-mini",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "data": {
    "id": "openai/gpt-5.4-mini",
    "canonical_slug": "openai/gpt-5.4-mini",
    "name": "GPT-4o mini",
    "category": "text",
    "status": "available",
    "unit": "per 1M tokens",
    "price": 0.15,
    "context_length": 128000,
    "architecture": {
      "modality": "text->text",
      "input_modalities": ["text"],
      "output_modalities": ["text"],
      "tokenizer": null,
      "instruct_type": null
    },
    "pricing": {
      "prompt": "0.00000015",
      "completion": "0.0000006",
      "request": null,
      "image": null
    },
    "supported_parameters": ["tools", "temperature", "max_tokens"],
    "alias_target": null,
    "reasoning": { "supported": false },
    "discount": null
  }
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 429 | `rate_limited` | Back off using the Retry-After header. |
| 404 | `model_unavailable` | No such id, or it belongs to another account. Note the code. This route does not answer not_found. |

## GET /v1/embeddings/models

The catalogue narrowed to embedding models, with a total count.

Status: Available. Authentication: none — this route answers anonymously.

> Note the envelope: `total_count` sits beside `data` here, and does not on GET /v1/models. That is OpenRouter's shape for the narrowed listings and ours matches it per route, so a client written against theirs parses this without a branch. Anonymous and cached.

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/embeddings/models \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/embeddings/models", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/embeddings/models",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "data": [
    {
      "id": "openai/text-embedding-3-small",
      "name": "Text Embedding 3 Small",
      "category": "embedding",
      "status": "available",
      "unit": "per 1M tokens",
      "price": 0.02,
      "pricing": { "prompt": "0.00000002", "completion": null, "request": null, "image": null },
      "supported_parameters": ["dimensions", "encoding_format"]
    }
  ],
  "total_count": 6
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 429 | `rate_limited` | Back off using the Retry-After header. |

## GET /v1/images/models

The catalogue narrowed to image models.

Status: Available. Authentication: none — this route answers anonymously.

> Note the envelope: `total_count` sits beside `data` here, and does not on GET /v1/models. That is OpenRouter's shape for the narrowed listings and ours matches it per route, so a client written against theirs parses this without a branch. Anonymous and cached.

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/images/models \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/images/models", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/images/models",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "data": [
    {
      "id": "google/nano-banana-2",
      "name": "Nano Banana 2",
      "category": "image",
      "status": "available",
      "unit": "per image, 1024x1024",
      "price": 0.039,
      "pricing": { "prompt": null, "completion": null, "request": null, "image": "0.039" },
      "supported_parameters": ["size", "n", "reference_urls"]
    }
  ],
  "total_count": 24
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 429 | `rate_limited` | Back off using the Retry-After header. |

## GET /v1/videos/models

The catalogue narrowed to video models.

Status: Available. Authentication: none — this route answers anonymously.

> Note the envelope: `total_count` sits beside `data` here, and does not on GET /v1/models. That is OpenRouter's shape for the narrowed listings and ours matches it per route, so a client written against theirs parses this without a branch. Anonymous and cached. Every model listed here is served through the queue, not through a synchronous route — see /docs/queue.

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/videos/models \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/videos/models", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/videos/models",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "data": [
    {
      "id": "google/veo-3.1",
      "name": "Veo 3",
      "category": "video",
      "status": "available",
      "unit": "per 5s clip, 720p",
      "price": 0.75,
      "pricing": { "prompt": null, "completion": null, "request": "0.75", "image": null },
      "supported_parameters": ["duration", "size", "prompt"]
    }
  ],
  "total_count": 18
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 429 | `rate_limited` | Back off using the Retry-After header. |

## GET /v1/catalog

The discovery catalogue behind the public site: names, blurbs, families and headline rates.

Status: Available. Authentication: none — this route answers anonymously.

> camelCase, unlike every other /v1 route, because this one feeds the public site rather than an OpenAI-shaped client. Build against /v1/models instead unless you are rendering a catalogue.

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/catalog \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/catalog", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/catalog",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "data": [
    {
      "id": "openai/gpt-5.4-mini",
      "slug": "openai-gpt-4o-mini",
      "name": "GPT-4o mini",
      "description": "Small, fast general-purpose text model.",
      "category": "text",
      "status": "available",
      "unit": "per 1M tokens",
      "price": 0.15,
      "reference": 0.30,
      "speed": "fast",
      "featured": true,
      "capabilities": ["tools", "vision"],
      "contextLength": 128000,
      "addedAt": "2026-05-01T09:14:22Z"
    }
  ],
  "generatedAt": "2026-08-23T11:02:47Z"
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 429 | `rate_limited` | Back off using the Retry-After header. |

## GET /v1/stats

Platform totals: accounts, tokens billed over the trailing 30 days, providers and live models.

Status: Available. Authentication: none — this route answers anonymously.

> Four counts and nothing derived from them. `tokens` is input plus output over the trailing thirty days; cached-input and reasoning tokens are excluded on purpose, so the figure matches what an account would recognise from its own usage page. Cached for five minutes.

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/stats \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/stats", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/stats",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "accounts": 1284,
  "tokens": 91744812,
  "providers": 17,
  "models": 214,
  "generatedAt": "2026-08-23T11:02:47Z"
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 429 | `rate_limited` | Back off using the Retry-After header. |

## POST /v1/mcp

JSON-RPC 2.0 over Streamable HTTP: initialise a session and call the tools.

Status: Available. Authentication: any live API key on the account.

> The initialize reply carries an `Mcp-Session-Id` header. Send it on every later call. `notifications/initialized` answers 202 with no body. Tools that spend money say so in their own description.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `jsonrpc` | string | Required | — | Always "2.0". |
| `method` | string | Required | — | initialize, tools/list, tools/call, resources/list, resources/read or prompts/get. |
| `params` | object | Optional | — | Method arguments, per the MCP specification. |
| `MCP-Protocol-Version` | header | Optional | 2025-06-18 | Negotiated protocol version. 2025-06-18, falling back to 2025-03-26. |

### Request

**cURL**

```bash
curl -X POST https://api.routehook.ai/v1/mcp \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
      "protocolVersion": "2025-06-18",
      "capabilities": {},
      "clientInfo": {
        "name": "your-client",
        "version": "1.0.0"
      }
    }
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/mcp", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
      "protocolVersion": "2025-06-18",
      "capabilities": {},
      "clientInfo": {
        "name": "your-client",
        "version": "1.0.0"
      }
    }
  }),
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.post(
    "https://api.routehook.ai/v1/mcp",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
    json={
      "jsonrpc": "2.0",
      "id": 1,
      "method": "initialize",
      "params": {
        "protocolVersion": "2025-06-18",
        "capabilities": {},
        "clientInfo": {
          "name": "your-client",
          "version": "1.0.0"
        }
      }
    },
)

data = res.json()
```

### Response

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": { "listChanged": false },
      "resources": { "subscribe": false, "listChanged": false },
      "prompts": {}
    },
    "serverInfo": { "name": "routehook", "version": "1.0.0" }
  }
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | Malformed body, or a parameter outside its bounds. |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 402 | `insufficient_credits` | Top up the balance before retrying. |
| 429 | `rate_limited` | Back off using the Retry-After header. |
| 503 | `upstream_unavailable` | Upstream outage. Safe to retry. |
| 200 | `-32601` | JSON-RPC error, not an HTTP one: unknown method. Transport errors stay HTTP. |

## GET /v1/mcp

Open the server-sent stream for server-initiated MCP messages.

Status: Available. Authentication: any live API key on the account.

> `text/event-stream`; the object above is one event's `data:` payload. Only server-initiated messages arrive here. Tool calls go over POST. A session id this server does not know answers `404` carrying a JSON-RPC `-32600`, not the gateway's `{ error: { code } }` envelope: an MCP client is a JSON-RPC client, and the specification makes a 404 on a request bearing a session id mean "that session is gone, call initialize again".

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `Mcp-Session-Id` | header | Required | — | The session id returned by initialize. |

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/mcp \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/mcp", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/mcp",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "jsonrpc": "2.0",
  "method": "notifications/message",
  "params": { "level": "info", "data": "job_ab12cd34 finished" }
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 429 | `rate_limited` | Back off using the Retry-After header. |

## DELETE /v1/mcp

End an MCP session.

Status: Available. Authentication: any live API key on the account.

> There is no response body. Ending a session drops its stream; the API key itself is untouched. An unknown session id answers `404` with a JSON-RPC `-32600` rather than the gateway envelope, exactly as on the stream above.

### Parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `Mcp-Session-Id` | header | Required | — | The session to end. |

### Request

**cURL**

```bash
curl -X DELETE https://api.routehook.ai/v1/mcp \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/mcp", {
  method: "DELETE",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "DELETE",
    "https://api.routehook.ai/v1/mcp",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
204 No Content
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `invalid_api_key` | Key missing, malformed or revoked. |
| 429 | `rate_limited` | Back off using the Retry-After header. |

## GET /v1/status

Platform health: component states, current incidents and recent uptime.

Status: Available. Authentication: none — this route answers anonymously.

> camelCase, unlike the OpenAI-shaped routes. `overall` and each component's `status` are upper case (OPERATIONAL, DEGRADED, DOWN or UNCONFIGURED), while a history day's `state` is lower case: operational, degraded, down or none. They are different vocabularies and a client that lower-cases one to match the other will be wrong about the day it did it to. There are four components, keyed `api`, `providers`, `storage` and `payments`, and `history` carries one entry per day for the whole 90-day window, oldest first (trimmed to one above). `monitored` is false when no probe has ever recorded a sample, which is a different claim from "everything is fine" and must not render the same way. `uptime` is a 0–1 fraction or null, never a percentage. The route deliberately keeps answering during maintenance (a status page that 503s while the platform is down is worse than none), and publishes measurements only: no vendor names, no topology.

### Request

**cURL**

```bash
curl https://api.routehook.ai/v1/status \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/status", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}` },
});

const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.request(
    "GET",
    "https://api.routehook.ai/v1/status",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
)

data = res.json()
```

### Response

```json
{
  "overall": "OPERATIONAL",
  "monitored": true,
  "windowDays": 90,
  "generatedAt": "2026-08-23T11:02:47Z",
  "components": [
    {
      "key": "api",
      "name": "API",
      "description": "The gateway and the dashboards behind it.",
      "status": "OPERATIONAL",
      "uptime": 0.9997,
      "latencyMs": 84,
      "latencyMeaning": "Median round trip to the database.",
      "sampleCount": 129600,
      "checkedAt": "2026-08-23T11:02:41Z",
      "history": [
        { "date": "2026-05-26", "state": "operational", "uptime": 1, "sampleCount": 1440 }
      ]
    }
  ]
}
```

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 429 | `rate_limited` | Back off using the Retry-After header. |

> **Note: Quote the request id**
>
> Every response carries a request id, and every error body repeats it. It is the key every lookup here is made on. A support thread that starts with one is a lookup, and one that starts without it is an investigation.
