---
title: "Chat Completions API"
description: "The full request and response specification for *.arkor.app inference endpoints."
---

Every Arkor endpoint serves the same OpenAI-compatible surface at `https://<slug>.arkor.app`. This page is the wire-level reference; it applies equally to [one-click endpoints](/docs/cloud/app-server) and [project endpoints](/docs/cloud/endpoints).

## Routes

| Method | Path | Purpose |
| ------ | ---- | ------- |
| `POST` | `/v1/chat/completions` | Chat completion (JSON or SSE streaming) |
| `POST` | `/v1/messages` | Anthropic-native Messages API |
| `GET` | `/v1/models` | List the model ids the endpoint serves |
| `GET` | `/v1/chat/completions/runs/{id}` | Replay a stored run as SSE |
| `GET` | `/healthz` | Liveness check, returns `{ "ok": true }` |

`GET /v1/models` returns the standard OpenAI list shape. For a [multi-model endpoint](/docs/cloud/endpoints#target) it lists the current public catalog (the list changes as models are published); for every other endpoint it lists the single pinned model. The route sits behind the same API-key auth as chat completions, and responses may be cached for up to 30 seconds.

Every model speaks the OpenAI chat completions surface this page documents, Anthropic's native Messages API at `POST /v1/messages` on the same endpoint, or both. To use the Messages route, point an Anthropic SDK at the base URL with the same API key.

- **Anthropic models** (`claude-*` ids) are Messages-only.
- **Arkor-hosted models** (including your own fine-tunes) speak the OpenAI surface always, and the Messages surface on the models where it has been enabled.

`GET /v1/models` lists every model the endpoint serves without saying which surfaces each one accepts, so the reliable signal is the response: sending a model to a surface it does not serve returns `400` with a message naming the route that does serve it.

## Authentication

Endpoints in **fixed API key** mode accept the key in either header:

```bash title="curl"
curl https://<slug>.arkor.app/v1/chat/completions \
  -H "Authorization: Bearer $ARKOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"Hello"}]}'
```

`x-api-key: <key>` works identically. Endpoints in **no auth** mode skip the check entirely.

Auth failures return `401` with a machine-readable `code`:

| Code | Meaning |
| ---- | ------- |
| `missing_key` | No key presented |
| `invalid_key` | Key does not match any enabled key |
| `no_keys` | The endpoint requires a key but has none configured |

These `code` values appear on the OpenAI-shaped routes only. The same auth failures on `POST /v1/messages` render in Anthropic's error envelope instead — `{ "type": "error", "error": { "type": "authentication_error", "message": "..." } }` — which carries no `code` field.

Revoked keys can keep authenticating for up to about 30 seconds while edge caches refresh.

## Request body

`POST /v1/chat/completions` takes the OpenAI chat completions shape. Requests larger than **4 MiB** are rejected with `413`.

| Field | Type | Notes |
| ----- | ---- | ----- |
| `messages` | array, required | At least one chat message. `content` is a plain string or OpenAI's content-part array of `text` parts (`{ "type": "text", "text": "..." }`) and — on **user** messages only — `image_url` parts. `image_url.url` must be an inline data URL, `data:image/<png\|jpeg\|webp\|gif>;base64,...`, with canonical base64; remote `http(s)` image URLs are rejected with `400`, as are `image_url` parts on other roles and `input_audio` / `file` parts. A single image's data URL is capped at ~6 MiB of characters (~4.5 MB of image data), though the whole request still has to fit the 4 MiB body limit — in practice keep images under ~2.5 MB before encoding |
| `model` | string | On single-model endpoints: accepted and **ignored** (the endpoint pins the model). On [multi-model endpoints](/docs/cloud/endpoints#target): selects the model by id (matched case-insensitively); omitting it uses the endpoint's default model, and an unknown id returns `404` with code `model_not_found`. `GET /v1/models` lists the valid ids |
| `stream` | boolean | Defaults to `false` (JSON response) |
| `stream_options.include_usage` | boolean | Opt in to a final usage chunk when streaming |
| `n` | integer | Only `1` (the OpenAI default) is accepted; any other value returns `400` — the pipeline serves a single choice |
| `temperature` | number | 0 to 2 |
| `top_p` | number | 0 to 1 |
| `max_tokens` | integer | Positive |
| `max_completion_tokens` | integer | OpenAI's newer name for the output-token cap. Both are accepted; when a request carries both, `max_completion_tokens` wins |
| `stop` | string or array | Stop sequences. Non-empty strings; a list holds up to 16 |
| `presence_penalty` | number | -2 to 2 |
| `frequency_penalty` | number | -2 to 2 |
| `seed` | integer | Best-effort deterministic sampling |
| `logprobs` | boolean | Return log-probabilities for output tokens |
| `top_logprobs` | integer | 0 to 20. Values above 0 need `logprobs: true` (enforced by the model server) |
| `logit_bias` | object | Stringified token id → bias, -100 to 100 |
| `tools` / `tool_choice` | OpenAI shapes | Function calling. Only automatic selection (`tool_choice` omitted or `"auto"`) needs a tool-parser-enabled model; a named function, `"required"`, and `"none"` work without one |
| `response_format` | OpenAI shape | Including `json_schema` structured outputs |
| `structured_outputs` | object | vLLM extension: `json` / `regex` / `choice` / `grammar` / `json_object` constraint fields, mutually exclusive |
| `enable_thinking` | boolean | Reasoning mode, on supported models |
| `reasoning_effort` | string | One of `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max` (anything else is a `400`). Gemma 4 only distinguishes thinking on/off: `none` turns thinking off, every other value turns it on |
| `user` | string | OpenAI's end-user attribution field. Feeds the same [Usage](/docs/cloud/usage) attribution as `X-Arkor-End-User-Id` (the header wins when both are present), so the value is stored in usage history and grouped by. Subject to the same limits as the attribution headers — up to 256 characters of printable Latin-1; anything else is dropped silently. Never used for authorization |

Messages use the four OpenAI roles `system`, `user`, `assistant`, and `tool` — the newer `developer` role is not accepted and returns `400`. Assistant messages may carry `tool_calls` (in which case `content` is optional), a `tool` message answers one via its required `tool_call_id`, and the optional `name` participant label is accepted on system/user/assistant messages.

Unknown fields are stripped rather than rejected, so OpenAI SDK extras do not cause errors. Tool definitions, JSON schemas, and `structured_outputs` constraints are forwarded verbatim to the model server.

## Responses and streaming

With `stream: false` (the default) you get a standard `chat.completion` JSON object. With `stream: true` you get OpenAI-style SSE chunks terminated by `data: [DONE]`. Usage numbers stream only when `stream_options.include_usage` was requested.

Models running with a reasoning parser emit their reasoning as `delta.reasoning_content` in streaming chunks, separate from the visible answer.

When run retention is enabled, responses persisted by Arkor carry an `X-Arkor-Run-Id` header identifying the stored run (see [Run history](/docs/cloud/runs)). Responses served by an external third-party provider (e.g. Gemini or Claude models on a multi-model endpoint) are never persisted and carry no run id, regardless of the retention setting.

## Replaying stored runs

`GET /v1/chat/completions/runs/{id}` re-emits a stored run's SSE stream. It supports resuming via `Last-Event-ID` or a `?from=` query parameter. Unknown, expired, or foreign run ids return `410` with code `run_gone`.

## Attribution headers

Two optional request headers label the request in [Usage](/docs/cloud/usage) breakdowns:

- `X-Arkor-End-User-Id` — your application's user id.
- `X-Arkor-Session-Id` — a conversation or session id.

Values are limited to 256 characters of printable Latin-1. Invalid values are dropped silently; they never cause a rejection and are never used for authorization.

The OpenAI `user` body field feeds the same end-user attribution; when both the field and the `X-Arkor-End-User-Id` header are present, the header wins. On the Anthropic Messages route, the standard `metadata.user_id` field plays the same role with the same precedence (the header wins). The limits above apply to these body fields too: a value over 256 characters or outside printable Latin-1 (for example Japanese text) is dropped silently, so the request succeeds but carries no end-user attribution. Whichever value survives is stored with the request's usage row, so do not send identifiers you do not want recorded.

## Errors

| Status | When |
| ------ | ---- |
| `400` | Invalid JSON, schema violation, or conflicting `response_format` / `structured_outputs` |
| `401` | Auth failure (see codes above) |
| `403` | The selected model requires an Arkor account (code `model_requires_signup`). Premium catalog models still appear in `GET /v1/models` on anonymous endpoints, but dispatching them there is rejected with a signup prompt |
| `404` | Unknown subdomain, disabled endpoint, or expired endpoint (single uniform message); on multi-model endpoints also an unknown `model` id (code `model_not_found`) |
| `410` | Stored run gone (replay route) |
| `413` | Body over 4 MiB |
| `429` | Rate limited (code `rate_limit_exceeded`), for either of two reasons: the endpoint owner's inference quota is exceeded (anonymous one-click endpoints have stricter per-minute and per-day limits than account-owned ones), or — on a multi-model request served by an external provider — the vendor's own rate limit was exhausted. Either way the response carries a `Retry-After` header (seconds, forwarded from the vendor in the external case); wait at least that long before retrying |
| `499` | Client closed the connection mid-request |
| `502` | Upstream model server error |
| `503` | Provider temporarily unavailable (code `provider_env_unavailable`); safe to retry |
| `504` | Every inference candidate timed out before responding (code `inference_upstream_timeout`); safe to retry |
| `529` | Every attempted external provider reported itself overloaded (code `overloaded`, mirroring Anthropic's convention). The vendor's `Retry-After` is forwarded when present; retry later |

On the OpenAI-compatible routes (`POST /v1/chat/completions` and `GET /v1/models`) error bodies use OpenAI's nested envelope:

```json title="Error body (OpenAI-compatible routes)"
{ "error": { "message": "...", "type": "...", "param": null, "code": "..." } }
```

The Arkor-specific run replay route (`GET /v1/chat/completions/runs/{id}`) keeps a flat envelope instead: `{ "error": string, "code"?: string }`.

## CORS

Endpoints send permissive CORS (`Access-Control-Allow-Origin: *`, no credentials), so browsers can call them directly. Allowed request headers include `Authorization`, `Content-Type`, `x-api-key`, `Last-Event-ID`, and the attribution headers above.
