HTTP endpoints
Every HTTP route Hubzoid 1.0.1 serves on the edge, bridge, MCP server, Console API, artifact viewer and webhook receiver, with method, authentication and purpose.
A running Hub listens on a small set of ports. Only the edge is meant to face users. Everything else binds 127.0.0.1 and is reached through the edge or by processes on the same machine.
- Browser or client
- Edge (PORT)
- Bridge (loopback)
- Hub runtime
| Process | Default address | Exposure |
|---|---|---|
| Edge | 127.0.0.1:3080 (HUBZOID_HOST, PORT) | The public front door. |
| Web chat (Open WebUI) | 127.0.0.1:43080 (PORT plus 40000, or HUBZOID_OWUI_PORT) | Reached through the edge. |
| Bridge | 127.0.0.1:8000 (BRIDGE_PORT) | Loopback only. Selected paths are reachable through the edge. |
| Inbound receiver | 127.0.0.1:8100 (HUBZOID_INBOUND_PORT) | Loopback only. /webhooks/<hub> is reachable through the edge. |
The Slack adapter uses Socket Mode and opens no port. In a gateway, each Hub's bridge gets its own loopback port and the edge routes by Hub, as described in gateway routes.
Edge
The edge binds the public port, streams responses and relays websockets. For a single Hub (hubzoid run) it forwards:
| Path prefix | Goes to | Present when |
|---|---|---|
/artifacts | bridge | always |
/portal | bridge | always |
/mcp | bridge | MCP_SERVER=true |
/webhooks/<hub> | inbound receiver | hubzoid run was started with --whatsapp, --telegram or --webhook |
| everything else | web chat | always |
<hub> is the slug of the Hub folder name, or HUBZOID_HUB_SLUG. /portal covers the Console and its API, the artifact viewer, public links and connection pages. The bridge's /v1, /uploads and /healthz routes are never forwarded. The edge also serves /hubzoid-portal-navigation.js, a small script that links the web chat to the Console.
Rules the edge applies before it forwards a request:
- A path with a
.or..segment is refused with400, so no path can climb out of a forwarded prefix. - Client-sent
X-Hubzoid-*andX-OpenWebUI-*headers are removed before forwarding. Only trusted local callers set identity headers. - In a gateway, a request that changes a migrated Hub's model access in the web chat (
POST,PUT,PATCHorDELETEunder/api/v1/models/) is refused with403. Access for that Hub is managed in the Console. - With
HUBZOID_LOCK_OWUI_ACCESS_UI=true, writes to/api/v1/groups(or the prefixes inHUBZOID_OWUI_LOCKED_PREFIXES) are refused with403. - With
HUBZOID_HIDE_OWUI_USERSon, the web chat's user list redirects to the Console's People page, its Admin Panel opens on Settings > Integrations, and browser writes to its account admin API are refused with403. When every agent is managed in the Console, its whole Users section, Groups included, opens Settings > Integrations. - The web chat's model list (
GET /api/models) is filtered by Hubzoid's entry decision for the signed-in person, read from/portal/api/chat-access, including for chat administrators. When that check cannot be made, the list is refused rather than shown unfiltered. - During a connection journey, the edge sends the browser back to the journey's
/portal/connect/<id>/donepage after the provider's consent, instead of the web chat's home page. - An upstream that is not reachable returns
502.
With HUBZOID_DISABLE_EDGE=1 there is no edge: the web chat binds the public port and none of the bridge paths are reachable from outside.
Gateway routes
hubzoid gateway runs one edge for all Hubs:
| Path prefix | Goes to | Notes |
|---|---|---|
/b/<hub>/artifacts | that Hub's bridge | The /b/<hub> prefix is removed before forwarding. |
/b/<hub>/mcp | that Hub's bridge | Hubs with MCP_SERVER=true only. |
/webhooks/<hub> | that Hub's inbound receiver | Hubs whose .env configures an inbound surface. |
/portal | the first Hub's bridge, then the next | Any bridge can serve the Console because they share one operational store. When a bridge refuses the connection, the edge tries the next one. This is not high availability for the gateway or the chat app. |
| everything else | the shared web chat |
Bridge
The bridge serves one Hub. Its API key check compares Authorization: Bearer <key> with BRIDGE_API_KEYS.
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /healthz | none | Liveness. Returns {"status": "ok", "hub": "<folder>", "agent": "<name>"}. |
GET | /v1/models | bridge key | One model, the Hub, in OpenAI list format. |
POST | /v1/chat/completions | bridge key | Chat with the Hub's agent. OpenAI request and response shape, streaming or not. |
GET | /artifacts/{chat_id}/{filename} | signed link or bridge key | Download a file the agent saved. Public through the edge. |
POST | /uploads/{chat_id}/{filename} | bridge key | Upload a file into a chat so the agent can read it. |
POST | /otel/v1/traces | none (loopback) | Present only with HUBZOID_OTEL_NORMALIZE=true, HUBZOID_OTEL_ENDPOINT set and claude-local. Receives the claude process's traces, normalizes them and forwards them. Always answers 200. |
POST | /mcp | personal API key | The Hub's MCP server, when MCP_SERVER=true. See MCP server. |
GET | /portal/ | none | The Console web app. |
| various | /portal/api/* | Console session or Open WebUI API key | The Console API. See Console API. |
| various | /portal/artifacts/*, /portal/p/* | web chat session, or a public link | The artifact viewer and public links. See artifact viewer. |
| various | /portal/connect/* | web chat session of the person who asked | Connection journey pages. See connection pages. |
POST /v1/chat/completions
The endpoint the web chat, Slack adapter and inbound receiver use. It is loopback only, so a client elsewhere reaches the Hub through one of those surfaces or through MCP.
- Body.
messagesis required.stream: truereturns server-sent events in the OpenAI chunk format, ending with a usage chunk anddata: [DONE]. Without it the reply is one JSONchat.completionobject withusage. Attachments may be sent asdata:URLs in message content, and each is saved to the chat's uploads. - Chat id. Files and history are scoped to a chat id taken from, in order:
chat_id,metadata.chat_id,metadata.conversation_id, theX-Hubzoid-Chat-Idheader,user, or a hash of the first user message. - Identity headers.
X-OpenWebUI-User-EmailorX-Hubzoid-Usernames the caller,X-Hubzoid-Surfacenames the surface (defaultowui), andX-Hubzoid-Groupsadds groups. They are trusted because only holders of the bridge key can call the bridge.
| Status | Meaning |
|---|---|
400 | Invalid JSON, no messages, or an empty prompt. |
401 | Missing or wrong bridge key. |
403 | The caller is blocked, or the Hub's access is managed in the Console and the caller has no use_hub grant or no verified identity. |
413 | An attachment is larger than HUBZOID_MAX_UPLOAD_BYTES. |
503 | The access check could not be completed. |
Using this API from your own code is covered in OpenAI-compatible API.
GET /artifacts/{chat_id}/{filename}
Links written by write_artifact carry a signature in ?t=, plus an expiry in ?e= when HUBZOID_ARTIFACT_LINK_TTL is set. A request without a valid signature must present a bridge key, and the public default key dev is refused on this route. Files are served inline.
POST /uploads/{chat_id}/{filename}
The raw request body is the file. The type comes from Content-Type, or from the file name when that header is missing or generic. A body over HUBZOID_MAX_UPLOAD_BYTES returns 413. The response is {"chat_id": ..., "filename": ..., "size": ...}.
MCP server
With MCP_SERVER=true, the bridge serves the Hub over MCP Streamable HTTP at /mcp. The edge exposes it at /mcp for a single Hub and at /b/<hub>/mcp in a gateway. Each request is self-contained (stateless).
| Aspect | Behaviour |
|---|---|
| Auth | Authorization: Bearer <key>, where the key is the caller's personal API key from the web chat. Bridge keys are not accepted. |
| Gate | On a Hub whose access is managed in the Console, the caller needs use_hub and must not be blocked. On a Hub with group-based access, MCP_ACCESS_GROUP, when set, limits access to members of that web chat group. A failed check answers 401. |
| Tools | The Hub's tools except the chat-scoped ones, remember and delegates. Restricted tools are listed only to callers who may use them, and are checked again on every call. |
| Instructions | The mcp_instructions value from AGENTS.md frontmatter, or else the AGENTS.md body, sent when the client connects. |
| Identity | Calls run as the key's owner on the mcp surface, and restricted tool decisions are audited. |
Connecting Claude Code, Cursor and other clients is covered in connect an assistant.
Console API
The Console's web app is served at /portal/ and calls a JSON API under /portal/api. The same API is the management API for scripts. A caller authenticates in one of two ways, and a client-sent identity header is never trusted:
- The web chat's session. The bridge verifies the
tokencookie with the web chat and uses the verified email. Requests that change anything must come from the same origin, checked with theOriginorRefererheader. - An Open WebUI API key, sent as
Authorization: Bearer sk-.... It is checked against the web chat's key table, the caller acts as the key's owner under the same rules, and noOriginis needed. Any otherAuthorizationvalue is ignored. Keys can be created where Open WebUI API keys are turned on, whichMCP_SERVER=truedoes.
Most endpoints need manage_access for the organization or for at least one Hub, and every response is limited to the Hubs the caller manages. One authorization service decides every change. A refusal has a safe message and a stable code, for example {"detail": "Outside your access: ...", "code": "outside_ceiling"}.
| Method | Path | Who | Purpose |
|---|---|---|---|
GET | /portal/api/me?brief= | admin | The signed-in admin, the Hubs they manage, what they can grant, and how a new account can sign in. brief=true returns only the first three fields. |
GET | /portal/api/hubs | admin | Manageable Hubs, whether each one's access is managed in the Console, and whether the caller can chat with it. |
GET | /portal/api/permissions?hub= | admin | The capability catalog of a Hub, with labels, groups and configuration status. |
GET | /portal/api/access?hub=&q=&offset=&limit= | admin | People and services with access to a Hub, their direct and effective permissions, account status, what the caller may grant, and the policy revision. |
POST | /portal/api/access/grant | admin | Grant one permission. Body: subject, hub, permission, optional expected_revision. |
POST | /portal/api/access/revoke | admin | Revoke one permission. Same body. |
POST | /portal/api/access/apply | admin | Apply a set of grants and revokes for one person in one Hub, all or nothing. Body: subject, hub, operations (each action and permission), optional expected_revision. |
GET | /portal/api/people?q=&status=&role=&agent=&offset=&limit= | admin | People and services with account status and access per Hub. |
POST | /portal/api/people/refresh | organization admin | Re-read accounts from the web chat. |
POST | /portal/api/people/block | organization admin | Block or unblock a person. Body: subject, suspended (true to block, false to unblock). The Console no longer offers Block or Reactivate, so this endpoint is how a user blocked earlier is unblocked. Blocking removes every grant, and unblocking does not restore them. |
POST | /portal/api/accounts | admin who may create accounts | Create an account with its first access. Body: email, name, sign_in (password or google), password for password, and grants (each hub and permission). A duplicate email changes nothing. A partial result keeps the account and answers 502 with code partial. |
POST | /portal/api/accounts/grant | admin | Give an existing account access. Body: email, grants. Never creates an account. |
GET | /portal/api/accounts/{subject} | organization admin | A person's account read live: name, chat role, sign-in method and both halves of the Administrator role. |
POST | /portal/api/accounts/{subject}/password | organization admin | Reset a password. Body: password. Refused with google_managed for a Google-only user. |
POST | /portal/api/accounts/{subject}/approve | organization admin | Approve a pending sign-up. |
POST | /portal/api/accounts/{subject}/role | organization admin | Set role to user or admin, which sets Hubzoid organization administration and the chat app's admin role together. A change that sets only one side answers 502 with role_partial. Repeating the request finishes it. |
DELETE | /portal/api/accounts/{subject} | organization admin | Delete a user: every grant, then the chat account and its chats. Body: confirm_email, which must equal the account's email. |
GET | /portal/api/change-requests/{id} | the person who proposed it | A change proposed by the management tools. |
POST | /portal/api/change-requests/{id}/confirm | the person who proposed it, with a web session | Apply the proposal exactly as proposed. Body: plan_hash, and password for a proposed account. |
POST | /portal/api/change-requests/{id}/reject | the person who proposed it | Reject the proposal. |
GET | /portal/api/workflows?hub= | admin | Workflows and markdown tasks with state, next run and the account each runs as. |
GET | /portal/api/runs?hub=&workflow=&run_id=&status=&since=&until=&limit=&offset= | admin | Run history. status accepts succeeded, failed, running and cancelled. A run's result and step outputs are returned only to the account it acted as. |
GET | /portal/api/audit?hub=&user=&outcome=&tool=&surface=&since=&until=&limit=&offset= | admin | Restricted tool decisions. |
GET | /portal/api/access-changes?hub=&user=&actor=&action=&since=&until=&limit=&offset= | admin | Grant, revoke and account changes, with who made them. |
POST | /portal/api/sync | organization admin | Re-mirror each person's visible Hubs into the web chat. |
GET | /portal/api/summary?period= | admin | Usage for 24h, 7d or 30d: chats, messages, active people, tokens, estimated cost, denials, runs and missed slots per Hub, plus user_accounts, the sign-in accounts in the caller's scope whatever the period. |
GET | /portal/api/overview | admin | Counts of Hubs, grants and people, and how many Hubs are managed in the Console. |
GET | /portal/api/chat-access | any signed-in person | Which agents the caller may enter. The edge uses it to filter the web chat's agent picker. Accepts the web chat's bearer token as well as its cookie. |
A change on a Hub with group-based access is refused with code legacy. An expected_revision that no longer matches returns 409, so an edit built on stale data is never applied. New access for everyone signed in (*) is refused. Page sizes are capped at 200. What each screen shows is described in the Console overview.
Artifact viewer
Published artifacts are served under /portal by any bridge. Access is decided on every request, so a removed share or link stops working at once. No Console rights are involved: owners manage their own artifacts here.
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /portal/artifacts/<id> | web chat session | The viewer page. A signed-out visitor is sent to sign in and back to the same artifact. |
GET | /portal/artifacts/<id>/content | web chat session | The file, shown inline and isolated. |
GET | /portal/artifacts/<id>/download | web chat session | The file, as a download. |
GET | /portal/artifacts/api/<id> | web chat session | Metadata, plus sharing settings for the owner. |
POST | /portal/artifacts/api/<id>/audience | owner, same origin | Share with only the owner, named people or groups, or everyone who can use the agent. |
POST | /portal/artifacts/api/<id>/link | owner, same origin | Create or turn off a public link. Body: action (create or revoke), optional days from 1 to 90. Needs Share artifacts publicly. |
DELETE | /portal/artifacts/api/<id> | owner, same origin | Delete the artifact. |
GET | /portal/p/ | none | The public link page. The link's token travels in the URL fragment, which the browser does not send with the page request. |
POST | /portal/p/open | the link's token | Exchange the token for a short-lived viewing cookie. A link that no longer works answers 404. |
GET | /portal/p/<id>/content, /portal/p/<id>/download | that cookie | The file, inline or as a download. |
HTML artifacts are served with a sandbox policy that gives them their own opaque origin, so their scripts cannot read the session or call the app. They make no network requests unless HUBZOID_ARTIFACT_ALLOW_ORIGINS names origins they may load from. File types the viewer does not preview are only downloaded. The chat download route /artifacts/{chat_id}/{filename} cannot reach published artifacts.
Connection pages
With HUBZOID_CONNECT_JOURNEY=true, the connect_account tool sends a person a link to /portal/connect/<id>. Every page needs the web chat session of the person who asked. A signed-out person is sent to sign in and back to the link, and another account gets 403, which is recorded in the access log.
| Method | Path | Purpose |
|---|---|---|
GET | /portal/connect/<id> | The connection page. |
POST | /portal/connect/<id>/start | Continue to the provider's consent screen. Same origin only. |
POST | /portal/connect/<id>/cancel | Cancel the journey. Same origin only. |
GET | /portal/connect/<id>/done | The result, checked against the connection the web chat stored. Parameters on the return address are never read. |
GET | /portal/connect/<id>/status | The journey's state as JSON, polled by the done page. |
Inbound receiver
The receiver runs with hubzoid inbound run or with hubzoid run --whatsapp, --telegram or --webhook. Each surface is served only when its variables are set.
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /webhooks/<hub>/whatsapp | WHATSAPP_VERIFY_TOKEN | Meta's webhook verification challenge. |
POST | /webhooks/<hub>/whatsapp | X-Hub-Signature-256 signed with WHATSAPP_APP_SECRET | Incoming WhatsApp messages. Acknowledged at once and answered in the background. |
POST | /webhooks/<hub>/telegram | X-Telegram-Bot-Api-Secret-Token equal to TELEGRAM_WEBHOOK_SECRET | Incoming Telegram updates. |
POST | /webhooks/<hub>/<name> | shared secret or HMAC | Generic webhook. <name> is WEBHOOK_INBOUND_NAME, default webhook. |
A request that fails verification gets 403 before anything else runs. WhatsApp and Telegram senders must also be in the Hub's roster, identity/access.csv or identity/access.py.
Generic webhook
- Shared secret mode (default). Send the secret as
Authorization: Bearer <secret>orX-Webhook-Secret: <secret>. A?token=query parameter is also accepted, but it lands in access logs, so prefer a header. - HMAC mode (
WEBHOOK_INBOUND_HMAC=true). SendX-Signature-256: sha256=<hex>or GitHub'sX-Hub-Signature-256, the HMAC-SHA256 of the raw body with the secret. - Storage. Each verified delivery is written to
<hub>/.inbound/webhooks/<name>/as one JSON file with the surface, name, receive time, query, content type and body. A non-JSON body is kept as text. The response is200 okonce stored and500when storage failed, so the sender retries. - Duplicates. A delivery is recognized by
X-GitHub-Delivery,X-Delivery-Id,X-Webhook-Id,Idempotency-KeyorX-Request-Id, or else by an identical body in the same or the previous ten-minute window. A repeat answers200 duplicateand is not stored again. A copy that arrives while the first is still being stored gets503, so the sender retries later.
A markdown task with on_webhook: <name> runs when events wait in that inbox. See webhooks.
Next steps
Built-in tools
Every tool the Hubzoid 1.0.1 runtime gives an agent, with arguments, limits, which surfaces receive it, and what the MCP server leaves out.
Doctor checks
What hubzoid doctor checks, its text and JSON output, every stable check id with its possible statuses, and how to fix each failure, including configuration secrets.
