---
title: OpenAI-compatible API
description: The bridge's /v1 chat completions API on 127.0.0.1, with its authentication, request fields, streaming, identity headers, attachments and uses.
canonical_url: https://hubzoid.com/docs/chat/openai-compatible-api
last_updated: 2026-09-27
---

# OpenAI-compatible API

The bridge's /v1 chat completions API on 127.0.0.1, with its authentication, request fields, streaming, identity headers, attachments and uses.

Every hub's bridge speaks the OpenAI chat completions format at `/v1`. It is how the web chat, the Slack adapter and the WhatsApp and Telegram server talk to the agent, and you can call it from your own code on the same machine.

## When to use this

Use the API for software that runs next to the hub: a script that produces a nightly stock summary, a `run:` script in a [scheduled task](https://hubzoid.com/docs/guides/markdown-tasks) that asks the agent to draft a message, or a local integration that needs the Hub's answer as text.

For people, use the [web chat](https://hubzoid.com/docs/chat/web-chat), [Slack](https://hubzoid.com/docs/chat/slack) or [WhatsApp and Telegram](https://hubzoid.com/docs/chat/whatsapp-and-telegram). For an assistant on someone's own computer, use [MCP](https://hubzoid.com/docs/guides/connect-an-assistant), which authenticates each person with their own key.

## Where it listens

The bridge binds `127.0.0.1` only, on `BRIDGE_PORT` (8000 by default, or `--bridge-port`). The public port forwards `/artifacts`, `/portal` and, when MCP is on, `/mcp` to the bridge, never `/v1`, `/uploads` or `/healthz`. The Docker image publishes only port 3080. Under a [gateway](https://hubzoid.com/docs/deploy/gateway), each hub's bridge has its own loopback port.

> **Warning: Keep /v1 on loopback**
>
> A holder of the bridge key can call the agent as any identity, because the bridge trusts the identity headers described below. That is why the API stays off the public port. Give remote tools and people the Hub through the chat surfaces or MCP instead.

## Authentication

Requests carry a bridge key as a Bearer token. Keys come from `BRIDGE_API_KEYS` in the hub's `.env`, a comma-separated list.

```bash title="my-hub/.env"
BRIDGE_API_KEYS=<output of: openssl rand -hex 32>
```

- With no value set, the bridge accepts the public key `dev` and logs a warning. `hubzoid doctor` reports it as a failure (`auth.bridge_keys`). Set a random key for anything beyond your own laptop.
- The chat app, the Slack adapter and the inbound server use the first key in the list, so you can add a second key for your scripts and rotate it separately.
- A missing or wrong key returns `401` with `{"detail": "invalid api key"}`.
- Key changes apply when the hub restarts.

## Quick example

```bash
export HUBZOID_BRIDGE_KEY=<a key from BRIDGE_API_KEYS>

curl -s http://127.0.0.1:8000/v1/models \
  -H "Authorization: Bearer $HUBZOID_BRIDGE_KEY"
```

```json
{
  "object": "list",
  "data": [
    { "id": "stock-desk", "object": "model", "created": 1790000000, "owned_by": "hubzoid" }
  ]
}
```

The single model is the hub's main agent. Its id is `MODEL_LABEL` when set, otherwise the `name:` from `AGENTS.md` in lowercase with hyphens.

```bash
curl -s http://127.0.0.1:8000/v1/chat/completions \
  -H "Authorization: Bearer $HUBZOID_BRIDGE_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Hubzoid-Surface: api" \
  -d '{
    "model": "stock-desk",
    "chat_id": "nightly-summary",
    "messages": [
      { "role": "user", "content": "Which items fell below their reorder point today?" }
    ]
  }'
```

```json
{
  "id": "chatcmpl-3f1c...",
  "object": "chat.completion",
  "created": 1790000000,
  "model": "stock-desk",
  "choices": [
    {
      "index": 0,
      "message": { "role": "assistant", "content": "Three items are below reorder point..." },
      "finish_reason": "stop"
    }
  ],
  "usage": { "prompt_tokens": 1840, "completion_tokens": 212, "total_tokens": 2052 }
}
```

### Streaming

Set `"stream": true` and read server-sent events. Use `curl -N` so output is not buffered.

```bash
curl -N http://127.0.0.1:8000/v1/chat/completions \
  -H "Authorization: Bearer $HUBZOID_BRIDGE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "stock-desk", "stream": true, "messages": [{"role": "user", "content": "Summarize yesterday"}]}'
```

The stream sends a first chunk with the assistant role, then one `chat.completion.chunk` per text delta, a chunk with `"finish_reason": "stop"`, a final chunk with empty `choices` and the token `usage`, and `data: [DONE]`.

## Endpoints

| Method and path | Auth | Purpose |
| --- | --- | --- |
| `GET /healthz` | none | Liveness. Returns `status`, `hub` and `agent`. |
| `GET /v1/models` | bridge key | The hub's one model. |
| `POST /v1/chat/completions` | bridge key | Run the agent on a conversation, blocking or streamed. |
| `POST /uploads/{chat_id}/{filename}` | bridge key | Store a file in a chat's uploads folder for the agent to read. |
| `GET /artifacts/{chat_id}/{filename}` | signed link or bridge key | Download a file the agent wrote. The public key `dev` is refused here. |

The full list, including the Console and MCP routes, is in [HTTP endpoints](https://hubzoid.com/docs/reference/http-endpoints).

## Request fields

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `messages` | `array` | Required | The conversation, oldest first. Roles are system, user and assistant. Content is a string or an array of parts. |
| `stream` | `boolean` | `false` | Stream the answer as server-sent events. |
| `chat_id` | `string` |  | Groups requests into one chat, so uploads and written files share a folder. Also read from metadata.chat\_id or the X-Hubzoid-Chat-Id header. |
| `model` | `string` |  | Not used for routing. The bridge serves one agent and answers with its own model id. |
| `user` | `string` |  | Used as a chat id when nothing else gives one. It never carries groups and is ignored as an identity once the hub is managed in the Console. |

The bridge keeps no conversation state. Send the whole conversation each turn. It flattens the messages into one prompt, with system messages first, and runs the hub's agent on it. A `system` message from the caller becomes part of that prompt. It does not replace the hub's own instructions, knowledge or tools. Other OpenAI parameters, such as `temperature`, `tools` or `response_format`, are not applied: the hub's model settings and tools decide.

## Chats, uploads and written files

Each request resolves a chat id, in this order: `chat_id`, `metadata.chat_id` (or `metadata.conversation_id`), the `X-Hubzoid-Chat-Id` header, `user`, and finally a hash of the first user message. Characters other than letters, digits, `.`, `_` and `-` become `-`, and the id is cut to 64 characters. Files for that chat live in `<hub>/.hubzoid/chats/<chat_id>/`.

Attach a file inline as a base64 data URL. Images use an `image_url` part. Other files use an `input_file` or `file` part with a `name`.

```json
{
  "role": "user",
  "content": [
    { "type": "text", "text": "Compare this count sheet with the system stock." },
    { "type": "input_file", "name": "count-sheet.csv", "data": "data:text/csv;base64,c2t1LGNvdW50ZWQK..." }
  ]
}
```

Or upload the raw file first and refer to it by name in the same chat:

```bash
curl -s -X POST http://127.0.0.1:8000/uploads/nightly-summary/count-sheet.csv \
  -H "Authorization: Bearer $HUBZOID_BRIDGE_KEY" \
  -H "Content-Type: text/csv" \
  --data-binary @count-sheet.csv
```

Inline attachments are announced to the agent automatically. Images are shown to the model and other files are read with `read_upload`. A file over `HUBZOID_MAX_UPLOAD_BYTES` (25 MiB by default) returns `413` and the request is refused as a whole. Files the agent saves with `write_artifact` come back as signed links in the answer and can also be fetched from `/artifacts/{chat_id}/{filename}` with the bridge key.

## Identity headers

The bridge trusts these headers because only holders of the bridge key can reach it. The public port strips them from outside requests.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `X-OpenWebUI-User-Email` | `string` |  | The caller's email. The bridge looks up their groups in the chat app and the hub contact list, and their grants in the Console. |
| `X-Hubzoid-User` | `string` |  | The user id recorded in audit and usage. Also accepted as the verified identity for Console-managed hubs. |
| `X-Hubzoid-Groups` | `string` |  | Comma-separated groups added to the ones looked up for the caller. |
| `X-Hubzoid-Surface` | `string` | `"owui"` | The surface the request comes from. Decides whether restricted tools are reachable and how the Console reports usage. |

Without identity headers a request runs without a verified identity: unrestricted tools and knowledge only, no [restricted tools](https://hubzoid.com/docs/guides/restrict-tools). Once a hub's access is managed in [the Console](https://hubzoid.com/docs/console/agents-and-access), the bridge requires a verified identity that holds **Use this agent**:

| Situation | Response |
| --- | --- |
| No `X-OpenWebUI-User-Email` or `X-Hubzoid-User` | `403` The hub requires sign-in. |
| The identity lacks **Use this agent** | `403` You do not have access to the hub. |
| The person is blocked in the Console | `403` Their agent access is blocked. |

Set `X-Hubzoid-Surface: api` on your own calls. `api` may reach restricted tools like the chat app, and usage is then recorded under the `api` channel instead of `web`.

## Next steps

- [HTTP endpoints](https://hubzoid.com/docs/reference/http-endpoints): Every route the edge, bridge, Console and inbound server serve.
- [Connect an assistant](https://hubzoid.com/docs/guides/connect-an-assistant): Give a remote assistant the Hub with a personal key instead of the bridge key.
- [Security model](https://hubzoid.com/docs/deploy/security-model): What the bridge trusts, what the edge exposes and why.
