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 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, Slack or WhatsApp and Telegram. For an assistant on someone's own computer, use MCP, 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, each hub's bridge has its own loopback port.
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.
BRIDGE_API_KEYS=<output of: openssl rand -hex 32>- With no value set, the bridge accepts the public key
devand logs a warning.hubzoid doctorreports 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
401with{"detail": "invalid api key"}. - Key changes apply when the hub restarts.
Quick example
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"{
"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.
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?" }
]
}'{
"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.
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.
Request fields
Prop
Type
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.
{
"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:
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.csvInline 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.
Prop
Type
Without identity headers a request runs without a verified identity: unrestricted tools and knowledge only, no restricted tools. Once a hub's access is managed in the Console, 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
WhatsApp and Telegram
Answer teammates on WhatsApp and Telegram through verified webhooks, with a contact list that decides who gets in, bounded history and attachments.
Console overview
The Console is Hubzoid's administration UI at /portal/. Learn who can open it, how the first administrator gets in, how to move around and what the totals on the Agents page mean.
