Call the Embed API
Turn batches of text into vector embeddings through the gateway with POST /api/embed — same key, error envelope, and cost reporting as the Extract API.
Need vectors for search or similarity over text you've already extracted? POST /api/embed takes up to 100 short texts and returns one embedding per text, in order, priced and logged like every other model call the gateway makes. It uses the same bearer key as the Extract API, the same error envelope, and the same usage / cost shape, so a client that already talks to /api/extract needs nothing new.
It is synchronous. A full batch completes in a second or two, so there is no submit-and-poll variant.
Before you call it
- A bearer key from Manage Extraction API Keys. It's the same key
/api/extractuses. - An embedding deployment id. Only deployments whose Modality is Embedding can serve this endpoint — see Chat vs embedding deployments. Today that's
text-embedding-3-smallon OpenAI and on Azure OpenAI. Copy the id from the deployment's admin page. - A provider key linked to that deployment for your tenant, set up under Manage LLM Provider API Keys. Until a key is linked, the endpoint answers
model_unavailable.
Request
POST /api/embed
Authorization: Bearer <your tenant key>
Content-Type: application/json
{
"modelDeploymentId": "6b4f…", // required: an EMBEDDING model deployment
"dimensions": 1536, // optional; defaults to the deployment's setting
"inputs": [
{ "id": "3f2a…:0", "text": "This Master Services Agreement is entered into…" },
{ "id": "3f2a…:1", "text": "…continued text of the next window…" }
],
"metadata": { "file_processing_id": "…" } // optional; stored on the usage-ledger row for tracing
}| Field | Rule |
|---|---|
modelDeploymentId | The id of an embedding deployment. A chat deployment is rejected with deployment_modality_mismatch; an unknown, archived, or unlinked one with model_unavailable. |
inputs | 1 to 100 items. Each text is non-empty and at most 32,000 characters (the model's limit is 8,191 tokens). Each id is an opaque string you use to match results, unique within the request, at most 256 characters. |
dimensions | A positive integer no larger than the deployment's default (1,536 for text-embedding-3-small, which supports any smaller value natively — 512 is the other value we expect to use). Omit it to get the deployment's default. |
metadata | Any JSON object up to 4 KB. Echoed into the usage ledger, never sent to the provider. |
| Whole request | All or nothing. If any input is invalid, nothing is embedded and the error names the offending index. Retry the whole batch; there are no partial results. |
Response
{
"success": true,
"modelDeploymentId": "6b4f…",
"deploymentName": "text-embedding-3-small",
"provider": "OpenAI",
"dimensions": 1536,
"embeddings": [
{ "id": "3f2a…:0", "index": 0, "embedding": [0.0123, -0.0456, …] },
{ "id": "3f2a…:1", "index": 1, "embedding": [ … ] }
],
"usage": { "inputTokens": 74210, "outputTokens": 0, "totalTokens": 74210 },
"cost": { "inputCost": 0.001484, "outputCost": 0, "totalCost": 0.001484,
"inputPricePerMillion": 0.02, "outputPricePerMillion": 0 },
"durationMs": 812
}- Order is input order, and every input gets exactly one entry.
idandindexboth identify it. - Vectors are returned as the provider returns them — unit-normalised floats. The gateway doesn't normalise, quantise, or cache anything.
usageandcostuse the same keys as the Extract API, so you can pass them straight into whatever already consumes extract results.outputTokensis always0.- Size: 100 vectors of 1,536 floats is about 1.5 MB of JSON.
Errors
Same envelope as the Extract API. Branch on type and retryable, never on the message text.
{ "error": { "type": "invalid_request", "status": 400, "retryable": false,
"message": "inputs[37].text must be a non-empty string.", "index": 37 } }| Status | type | retryable | When |
|---|---|---|---|
| 400 | invalid_request | no | More than 100 inputs, empty text, over the character cap, duplicate ids, bad dimensions, oversized metadata, or a provider 400 (for example a single input over 8,191 tokens). index names the first offending input where one applies. |
| 400 | deployment_modality_mismatch | no | modelDeploymentId is a chat deployment. Carries expectedModality: "embedding" and actualModality: "chat". |
| 401 | auth | no | Missing or rejected bearer key. |
| 408 | timeout | yes | The request body did not fully arrive within 10 seconds. Nothing was parsed or sent. Resend. |
| 413 | payload_too_large | no | The request body exceeded the gateway's byte ceiling (about 20 MB, sized so a full batch at the character cap always fits). Rejected while streaming, before anything is parsed. Send fewer or shorter inputs. |
| 404 | model_unavailable | no | No enabled embedding deployment has that id, or no provider key is linked to it for your tenant. |
| 429 | rate_limit | yes | The provider throttled the call. Retry-After is set. |
| 502 | upstream_unavailable | yes | The provider errored or was unreachable, or answered with an unusable payload (wrong vector count, malformed indexes, non-numeric or wrong-width vectors). Nothing was applied. |
| 502 | auth | no | The provider rejected our key for it — the linked provider key is revoked or wrong. Fix it under Manage LLM Provider API Keys. |
| 502 | insufficient_credits | no | The provider account is out of quota or billing. |
| 503 | upstream_unavailable | yes | The gateway's own catalog lookup failed (database outage or pool timeout). The model was never asked. Same type as the provider case above, so one retry branch covers both. |
| 503 | at_capacity | yes | This replica is already handling its maximum number of concurrent embed calls and fast-failed rather than queue yours. The model was never asked. It's the embedding twin of the Extract API's extract_at_capacity. |
| 504 | timeout | yes | The provider call exceeded the gateway's 30-second budget. |
If your client already retries 429, 502, 503, and 504 with backoff and honours retryable, nothing changes for this endpoint.
Limits and behaviour
- Latency: dominated by the provider. A full batch of typical 750-token chunks completes in a few seconds. The gateway's provider timeout is 30 seconds; set your read timeout to 60.
- Concurrency: per replica, up to 32 provider calls in flight (
EMBED_MAX_CONCURRENCY) and up to 16 request bodies being received at once (EMBED_MAX_INGEST_CONCURRENCY); beyond either you getat_capacityand should retry. There is no per-tenant rate limit today. - Idempotent in effect: the same text, deployment, and dimensions return a vector you can treat as equivalent across calls. Bit-exactness is not promised — the provider doesn't promise it either.
- Logging: one structured log line per request with tenant, deployment, input count, tokens, cost, and latency. Never the text.
- Cost: every call, success or failure, writes a row to the usage ledger under operation Embed API, so it appears in Explore Costs alongside extractions.
Example
curl -sS https://evals.thelibrari.com/api/embed \
-H "Authorization: Bearer $EVALS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"modelDeploymentId": "6b4f…",
"dimensions": 512,
"inputs": [{ "id": "c:0", "text": "This Master Services Agreement is entered into…" }]
}'Related
- Call the Extract API — the same key, envelope, and cost shape for structured extraction.
- Browse Supported LLM Models — which deployments are embedding deployments.
- Manage LLM Provider API Keys — link a provider key to an embedding deployment.
- Explore Costs — embed spend shows up under its own operation.
Extract Large Documents with Async Jobs
Submit long-running extractions to /api/extract-async, poll a job id, and collect results that would time out a synchronous call.
Apply OCR to a Document
Flatten a scanned PDF into one with a selectable text layer via POST /api/ocr, then send the result to the Extract API.