# Embeddings

> Vectors for search, clustering and retrieval, in OpenAI's shape.

Section: Core API. Canonical page: https://routehook.ai/docs/embeddings

`POST /v1/embeddings` takes a model and an input (one string, an array of strings, or an array of token arrays), and returns a list of embedding objects with the token usage alongside. It is OpenAI's request and OpenAI's response, so an OpenAI client's `embeddings.create` reaches it with nothing changed but the base URL and the model slug.

## A request

**cURL**

```bash
curl 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
import OpenAI from "openai";

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

const response = await client.embeddings.create({
  model: "openai/text-embedding-3-small",
  input: ["first document", "second document"],
});

// One entry per input, and index says which input it came from.
for (const item of response.data) {
  console.log(item.index, item.embedding.length);
}
```

**Python**

```python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.routehook.ai/v1",
    api_key=os.environ["ROUTEHOOK_API_KEY"],
)

response = client.embeddings.create(
    model="openai/text-embedding-3-small",
    input=["first document", "second document"],
)

# One entry per input, and index says which input it came from.
for item in response.data:
    print(item.index, len(item.embedding))
```

## POST /v1/embeddings

Turn text into vectors for search, clustering and retrieval.

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

## Request parameters

| PARAMETER | TYPE | REQUIRED | DESCRIPTION |
| --- | --- | --- | --- |
| model | string | required | A slug from GET /v1/models whose category is embedding. |
| input | string \| string[] \| number[][] | required | One string, a batch of strings, or pre-tokenised input. |
| encoding_format | float \| base64 | optional | How each vector comes back. Default float. |
| dimensions | integer | optional | Truncate vectors to this width, where the model supports it. 1 to 8192; a larger value is refused here rather than upstream. |
| user | string | optional | An opaque end-user identifier, forwarded upstream. |

## The response

**200 OK**

```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 }
}
```

> **Note: The vectors above are truncated**
>
> Three numbers fit on a page; a real vector is the model's full width, typically several hundred to a few thousand floats. Read the width from the response rather than assuming it: `dimensions`, the model, and an upstream's own defaults can all change it.

## Batching

Pass an array and every item is embedded in one call. The results come back in `data` with an `index` naming the input each one came from. Rely on `index` rather than array position if anything in your pipeline reorders. Batching saves round trips and nothing else: the bill is the sum of the tokens either way, so a batch of fifty costs what fifty separate calls cost, minus fifty HTTP handshakes.

## Limits worth knowing

Each item is embedded independently, so the model's context length applies to each one and not to the batch. A single document longer than the model's window is a `400`, however small the batch. Chunk long documents before you send them. Very large batches are also one all-or-nothing request: if it fails you retry the whole thing, which is a reason to keep batches in the hundreds rather than the tens of thousands.

## float or base64

`encoding_format: "float"` returns each vector as a JSON array of numbers. It is readable and it is large. A 1536-dimension vector is around 20KB of JSON. `encoding_format: "base64"` returns the same vector as a base64 string of little-endian float32s, roughly a third of the bytes and no decimal parsing on your side. Use base64 for anything at volume; use float when you are reading the response with your eyes.

## Decoding base64 vectors

**JavaScript**

```javascript
const response = await client.embeddings.create({
  model: "openai/text-embedding-3-small",
  input: ["first document"],
  encoding_format: "base64",
});

const bytes = Buffer.from(response.data[0].embedding, "base64");
const vector = new Float32Array(
  bytes.buffer,
  bytes.byteOffset,
  bytes.length / 4,
);

console.log(vector.length, vector[0]);
```

**Python**

```python
import base64
import numpy as np

response = client.embeddings.create(
    model="openai/text-embedding-3-small",
    input=["first document"],
    encoding_format="base64",
)

raw = base64.b64decode(response.data[0].embedding)
vector = np.frombuffer(raw, dtype="<f4")

print(vector.shape, vector[0])
```

## dimensions

`dimensions` truncates the vector, on models that support it. The shorter vector is still usable for similarity, and costs less to store and to search. Not every model offers it. Where one does not, the parameter is forwarded and the upstream decides what to do with it, so check the width you got back before you write it into an index that expects a fixed size.

> **Warning: A non-embedding model is a 400, not a wasted call**
>
> This route serves models whose category is `embedding`. Name a chat or an image model and the request is refused with `400 invalid_request`: before any upstream call is made, before credit is reserved, and before anything is spent. The message names the model and the category it actually is. Nothing is charged, so a mis-typed slug costs you a round trip and no money.

## Cost

Embeddings bill on one axis: the tokens you send. There is no output side, which is why `usage` carries `prompt_tokens` and `total_tokens` and no completion count. The published rate is on the model in `GET /v1/models`; what a specific call was charged is in `GET /v1/generation?id=req_…`, keyed on the request id returned in the `X-Routehook-Request-Id` header.

## Failures

| STATUS | CODE | MEANING |
| --- | --- | --- |
| 400 | invalid_request | Not an embedding model, an empty input, or an item over the context length |
| 402 | insufficient_credits | Refused before any upstream call |
| 409 | model_unavailable | No live endpoint can serve that embedding model |
| 503 | upstream_unavailable | Upstream outage. Safe to retry |

## Related

- [Models](/docs/models): The catalogue, filtered by category.
- [Pricing](/docs/pricing): The axis each category bills on.
- [Errors](/docs/errors): The eight codes, and what to do about each.
