Hubzoid
Reference

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.

  1. Browser or client
  2. Edge (PORT)
  3. Bridge (loopback)
  4. Hub runtime
The edge forwards a short list of path prefixes to the bridge and sends everything else to the web chat.
ProcessDefault addressExposure
Edge127.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.
Bridge127.0.0.1:8000 (BRIDGE_PORT)Loopback only. Selected paths are reachable through the edge.
Inbound receiver127.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 prefixGoes toPresent when
/artifactsbridgealways
/portalbridgealways
/mcpbridgeMCP_SERVER=true
/webhooks/<hub>inbound receiverhubzoid run was started with --whatsapp, --telegram or --webhook
everything elseweb chatalways

<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 with 400, so no path can climb out of a forwarded prefix.
  • Client-sent X-Hubzoid-* and X-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, PATCH or DELETE under /api/v1/models/) is refused with 403. Access for that Hub is managed in the Console.
  • With HUBZOID_LOCK_OWUI_ACCESS_UI=true, writes to /api/v1/groups (or the prefixes in HUBZOID_OWUI_LOCKED_PREFIXES) are refused with 403.
  • With HUBZOID_HIDE_OWUI_USERS on, 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 with 403. 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>/done page 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 prefixGoes toNotes
/b/<hub>/artifactsthat Hub's bridgeThe /b/<hub> prefix is removed before forwarding.
/b/<hub>/mcpthat Hub's bridgeHubs with MCP_SERVER=true only.
/webhooks/<hub>that Hub's inbound receiverHubs whose .env configures an inbound surface.
/portalthe first Hub's bridge, then the nextAny 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 elsethe shared web chat

Bridge

The bridge serves one Hub. Its API key check compares Authorization: Bearer <key> with BRIDGE_API_KEYS.

MethodPathAuthPurpose
GET/healthznoneLiveness. Returns {"status": "ok", "hub": "<folder>", "agent": "<name>"}.
GET/v1/modelsbridge keyOne model, the Hub, in OpenAI list format.
POST/v1/chat/completionsbridge keyChat with the Hub's agent. OpenAI request and response shape, streaming or not.
GET/artifacts/{chat_id}/{filename}signed link or bridge keyDownload a file the agent saved. Public through the edge.
POST/uploads/{chat_id}/{filename}bridge keyUpload a file into a chat so the agent can read it.
POST/otel/v1/tracesnone (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/mcppersonal API keyThe Hub's MCP server, when MCP_SERVER=true. See MCP server.
GET/portal/noneThe Console web app.
various/portal/api/*Console session or Open WebUI API keyThe Console API. See Console API.
various/portal/artifacts/*, /portal/p/*web chat session, or a public linkThe artifact viewer and public links. See artifact viewer.
various/portal/connect/*web chat session of the person who askedConnection 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. messages is required. stream: true returns server-sent events in the OpenAI chunk format, ending with a usage chunk and data: [DONE]. Without it the reply is one JSON chat.completion object with usage. Attachments may be sent as data: 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, the X-Hubzoid-Chat-Id header, user, or a hash of the first user message.
  • Identity headers. X-OpenWebUI-User-Email or X-Hubzoid-User names the caller, X-Hubzoid-Surface names the surface (default owui), and X-Hubzoid-Groups adds groups. They are trusted because only holders of the bridge key can call the bridge.
StatusMeaning
400Invalid JSON, no messages, or an empty prompt.
401Missing or wrong bridge key.
403The 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.
413An attachment is larger than HUBZOID_MAX_UPLOAD_BYTES.
503The 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).

AspectBehaviour
AuthAuthorization: Bearer <key>, where the key is the caller's personal API key from the web chat. Bridge keys are not accepted.
GateOn 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.
ToolsThe 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.
InstructionsThe mcp_instructions value from AGENTS.md frontmatter, or else the AGENTS.md body, sent when the client connects.
IdentityCalls 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 token cookie with the web chat and uses the verified email. Requests that change anything must come from the same origin, checked with the Origin or Referer header.
  • 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 no Origin is needed. Any other Authorization value is ignored. Keys can be created where Open WebUI API keys are turned on, which MCP_SERVER=true does.

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"}.

MethodPathWhoPurpose
GET/portal/api/me?brief=adminThe 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/hubsadminManageable Hubs, whether each one's access is managed in the Console, and whether the caller can chat with it.
GET/portal/api/permissions?hub=adminThe capability catalog of a Hub, with labels, groups and configuration status.
GET/portal/api/access?hub=&q=&offset=&limit=adminPeople 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/grantadminGrant one permission. Body: subject, hub, permission, optional expected_revision.
POST/portal/api/access/revokeadminRevoke one permission. Same body.
POST/portal/api/access/applyadminApply 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=adminPeople and services with account status and access per Hub.
POST/portal/api/people/refreshorganization adminRe-read accounts from the web chat.
POST/portal/api/people/blockorganization adminBlock 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/accountsadmin who may create accountsCreate 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/grantadminGive an existing account access. Body: email, grants. Never creates an account.
GET/portal/api/accounts/{subject}organization adminA person's account read live: name, chat role, sign-in method and both halves of the Administrator role.
POST/portal/api/accounts/{subject}/passwordorganization adminReset a password. Body: password. Refused with google_managed for a Google-only user.
POST/portal/api/accounts/{subject}/approveorganization adminApprove a pending sign-up.
POST/portal/api/accounts/{subject}/roleorganization adminSet 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 adminDelete 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 itA change proposed by the management tools.
POST/portal/api/change-requests/{id}/confirmthe person who proposed it, with a web sessionApply the proposal exactly as proposed. Body: plan_hash, and password for a proposed account.
POST/portal/api/change-requests/{id}/rejectthe person who proposed itReject the proposal.
GET/portal/api/workflows?hub=adminWorkflows 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=adminRun 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=adminRestricted tool decisions.
GET/portal/api/access-changes?hub=&user=&actor=&action=&since=&until=&limit=&offset=adminGrant, revoke and account changes, with who made them.
POST/portal/api/syncorganization adminRe-mirror each person's visible Hubs into the web chat.
GET/portal/api/summary?period=adminUsage 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/overviewadminCounts of Hubs, grants and people, and how many Hubs are managed in the Console.
GET/portal/api/chat-accessany signed-in personWhich 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.

MethodPathAuthPurpose
GET/portal/artifacts/<id>web chat sessionThe viewer page. A signed-out visitor is sent to sign in and back to the same artifact.
GET/portal/artifacts/<id>/contentweb chat sessionThe file, shown inline and isolated.
GET/portal/artifacts/<id>/downloadweb chat sessionThe file, as a download.
GET/portal/artifacts/api/<id>web chat sessionMetadata, plus sharing settings for the owner.
POST/portal/artifacts/api/<id>/audienceowner, same originShare with only the owner, named people or groups, or everyone who can use the agent.
POST/portal/artifacts/api/<id>/linkowner, same originCreate 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 originDelete the artifact.
GET/portal/p/noneThe 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/openthe link's tokenExchange the token for a short-lived viewing cookie. A link that no longer works answers 404.
GET/portal/p/<id>/content, /portal/p/<id>/downloadthat cookieThe 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.

MethodPathPurpose
GET/portal/connect/<id>The connection page.
POST/portal/connect/<id>/startContinue to the provider's consent screen. Same origin only.
POST/portal/connect/<id>/cancelCancel the journey. Same origin only.
GET/portal/connect/<id>/doneThe result, checked against the connection the web chat stored. Parameters on the return address are never read.
GET/portal/connect/<id>/statusThe 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.

MethodPathAuthPurpose
GET/webhooks/<hub>/whatsappWHATSAPP_VERIFY_TOKENMeta's webhook verification challenge.
POST/webhooks/<hub>/whatsappX-Hub-Signature-256 signed with WHATSAPP_APP_SECRETIncoming WhatsApp messages. Acknowledged at once and answered in the background.
POST/webhooks/<hub>/telegramX-Telegram-Bot-Api-Secret-Token equal to TELEGRAM_WEBHOOK_SECRETIncoming Telegram updates.
POST/webhooks/<hub>/<name>shared secret or HMACGeneric 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> or X-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). Send X-Signature-256: sha256=<hex> or GitHub's X-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 is 200 ok once stored and 500 when storage failed, so the sender retries.
  • Duplicates. A delivery is recognized by X-GitHub-Delivery, X-Delivery-Id, X-Webhook-Id, Idempotency-Key or X-Request-Id, or else by an identical body in the same or the previous ten-minute window. A repeat answers 200 duplicate and is not stored again. A copy that arrives while the first is still being stored gets 503, so the sender retries later.

A markdown task with on_webhook: <name> runs when events wait in that inbox. See webhooks.

Next steps

Read this page as Markdown

Choose which cookies Hubzoid can use. You can change this at any time from Cookie settings in the footer. Read the Cookie Policy for details.