HTTP endpoints
Every HTTP route Hubzoid serves, with its method, authentication and purpose, and where the full public API reference lives.
Every route a running Hub serves. Only the edge faces people. Everything else binds 127.0.0.1 and is reached through the edge or by Hubzoid's own processes on the same machine.
| Section | Routes |
|---|---|
| API reference | /docs and /openapi.json: the public Hubzoid API, for your own programs. |
| Edge | What the public port forwards and what it keeps private. |
| Web app | Chat, sign-in, conversations, branding and connections. |
| Chat inside your app | /api/agui, the AG-UI endpoint for embedded chat. |
| Bridge | /v1, uploads, downloads and health. |
| MCP server | /mcp and its OAuth routes. |
| Settings API | /portal/api: agents, access, people, workflows, connectors, activity. |
| Artifact viewer | Published artifacts, live reports, company and public links. |
| Connection pages | Links from connect_account. |
| Inbound receiver, workflow webhooks | WhatsApp, Telegram and webhooks. |
- 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. |
| Bridge | 127.0.0.1, the first free port from 8000 (BRIDGE_PORT) | Loopback only. Reached through the edge. |
| Inbound receiver | 127.0.0.1:8100 (HUBZOID_INBOUND_PORT) | Loopback only. /webhooks/<hub> is reachable through the edge. Runs only when WhatsApp, Telegram or the generic webhook is turned on. |
The Slack adapter uses Socket Mode and opens no port. With several Hubs, each Hub's bridge gets its own loopback port and the edge routes by Hub, as described in routes for several Hubs. With sign-in off, hubzoid run refuses a network --host unless HUBZOID_ALLOW_UNAUTHENTICATED_NETWORK=true, because every visitor would act as the local owner.
API reference
A running Hub serves a reference of its public API at /docs, with the schema at /openapi.json. It is titled Hubzoid API and carries the installed version. Signed-in people also reach the schema at /api/openapi.json. The SDK guide the Hubzoid API (opens in a new tab) has examples.
| What the reference covers | Routes |
|---|---|
| OpenAI-compatible chat | /v1/chat/completions, /v1/models |
| Chat inside your app | /api/agui and its thread routes |
| Conversations, files and search | /api/conversations, /api/chat/files, /api/chat/artifacts, /api/chat/search, /api/chat/models, /api/shares |
| Approvals and questions | /api/requests |
| Who is calling | /api/auth/session, /api/me/access |
- Public API only. The web app's own routes, Settings (
/portal) and administration are left out of the reference. - Address. Each agent has its own base,
https://<your hub>/b/<agent>. The first agent also answers at the root.GET /b/<agent>/v1/modelsnames the model id to send. - Sign-in.
Authorization: Bearer hzp_..., a personal API key a person creates in Settings under their account's security. It acts as that person, within their access, and may be limited to some agents. A key never signs in, changes passwords or two-step sign-in, creates other keys, or answers an approval or a question. An embedded app uses its short pass instead. - On a laptop with sign-in off, the agents answer on
http://127.0.0.1:3080with no key. - Errors are JSON with a
detail.401means no valid credential,403not allowed,409a conflict with a newer change,422an invalid body,503the agent is restarting.
Call any route as yourself from a terminal with hubzoid api.
Edge
The edge binds the public port and streams responses. Before it forwards anything:
- A path with a
.or..segment is refused with400, so no path can climb out of a forwarded prefix. - Client-sent
X-Hubzoid-*identity headers are removed. Only Hubzoid's own processes set identity headers, on loopback. - It keeps no cookies between visitors, and asserts
X-Forwarded-Proto: httpsupstream when the declared public URL is HTTPS and the request carries no scheme of its own. - An upstream that is not reachable returns
502. With several Hubs, a request body of up to 64 KiB is retried on the next bridge when the first refuses the connection. GET /healthz/workflowsis answered by the edge itself. See workflow health.
A single Hub's edge sends every path to the bridge, except:
| Path | Result |
|---|---|
/v1/chat/completions, /v1/models with a personal API key (Bearer hzp_...) | The bridge, which runs the call as the key's owner. Without a personal key they answer 401 with a message to send one. |
Every other path under /v1, /uploads or /otel | 404. The bridge's internal API stays on loopback. |
/webhooks/<hub>/<name> for a webhook declared in workflows/settings.yaml | The bridge's workflow webhook receiver. |
/webhooks/<hub> and below | The inbound receiver, when a file in the Hub's channels/ folder turns on WhatsApp, Telegram or the generic webhook. |
<hub> is the slug of the Hub folder name, or HUBZOID_HUB_SLUG. The bridge then serves the web app, Settings at /settings, MCP at /mcp, chat downloads and published artifacts. The edge also drops a client-sent X-Forwarded-Prefix header. HUBZOID_DISABLE_EDGE has no effect.
Routes for several Hubs
A hubzoid run of several Hubs runs one edge for all of them. Paths under /b/<hub> go to that Hub's bridge with the /b/<hub> prefix removed:
| Path prefix | Goes to | Notes |
|---|---|---|
/b/<hub>/api | that Hub's bridge | Chat calls that touch the Hub's files or runtime. |
/b/<hub>/branding | that Hub's bridge | The Hub's logo, favicon and stylesheet. |
/b/<hub>/portal/api | that Hub's bridge | Settings calls that must run where the Hub's engine and files are, such as running a schedule now or reading evals. |
/b/<hub>/artifacts | that Hub's bridge | Chat downloads. |
/b/<hub>/mcp | that Hub's bridge | Hubs whose own .env sets MCP_SERVER=true, with MCP_PUBLIC_URL ending in /b/<hub>/mcp. Their /.well-known/oauth-protected-resource/b/<hub>/mcp and /.well-known/oauth-authorization-server/b/<hub>/mcp/oauth documents go to the same bridge. |
/webhooks/<hub>/<name> | that Hub's bridge | Declared workflow webhooks. |
/webhooks/<hub> | that Hub's inbound receiver | Hubs whose channels/ folder turns on an inbound surface. |
| everything else | the first bridge, then the next |
The edge adds X-Forwarded-Prefix: /b/<hub> when it removes the prefix, so the bridge knows the call is scoped to that Hub. A chat call that reaches another Hub's bridge gets 409 with the code wrong_hub. Falling back to another bridge is not high availability for the deployment itself.
Workflow health
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET, HEAD | /healthz/workflows | none | {"ok": true, "checked": <hubs>} with 200 while every Hub's workflow engine is healthy, else 503. Checked every 15 seconds, so it answers 503 briefly after the edge starts. For an external uptime monitor. |
A Hub is unhealthy when its workflow settings are invalid, or when workflows are enabled and no engine holds a live, ready lease. The edge also sends the stale and recovered alerts.
Web app
The bridge serves the Hubzoid web app: chat at /, sign-in at /auth and Settings at /settings, one bundle and one sign-in. Every route below needs a signed-in person unless the table says otherwise. With sign-in off (HUBZOID_AUTH unset), every request is the local owner, admin@localhost.
- Session. An opaque
hz_sessioncookie, stored on the server only as a digest. It lasts at mostHUBZOID_SESSION_DAYS(30) and ends afterHUBZOID_SESSION_IDLE_DAYS(7) without use. Password, role and status changes, blocks and deletion end a person's sessions. - Same origin. Every request that changes something must carry an
OriginorRefererthat matches the request's host or one of the deployment's addresses (HUBZOID_PUBLIC_URL,HUBZOID_ALLOWED_ORIGINS), or it is refused with403. - Errors have the shape
{"detail": {"code": "...", "message": "..."}}.
Pages
| Method | Path | Purpose |
|---|---|---|
GET | /, /new, /c/<id> | Chat: a new conversation, or an existing one. |
GET | /s/<id> | A read-only shared conversation. |
GET | /auth, /auth/set-password?token= | Sign-in, and the page a one-time link opens. ?redirect= returns to a page after sign-in, and ?error=<code> explains a refused external sign-in. |
GET | /account, /account/connections | The person's account settings and their connections. |
GET | /settings | Settings, in two groups. Everyone signed in sees their personal sections: Account, Connected accounts, Sources, Memory and My schedules. People who manage an agent also see Manage: Agents, Activity, Team schedules, People, Usage and Organization. With sign-in off there are no People, Team schedules or sign-in pages. /portal/ and /portal/#/<page> redirect to /settings and /settings#/<page>. |
Sign-in API
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /api/auth/session | none | Whether someone is signed in, the mode (local or accounts), the external providers, whether password sign-in and sign-up are on, and the name to show. |
POST | /api/auth/login | none | Password sign-in. Body: email, password. Sets the session cookie. |
POST | /api/auth/logout | session | Sign out. Answers 204. |
POST | /api/auth/signup | none | Self sign-up when ENABLE_SIGNUP is on. Body: email, name, password. The account waits for an administrator's approval. |
GET | /api/auth/link/<token> | the link | Whether a one-time link is valid, its purpose and email. |
POST | /api/auth/link/<token> | the link | Set a password with the link and sign in. Body: password. A used or expired link answers 410. |
POST | /api/auth/password | session | Change your own password. Body: current_password, new_password. Answers 204. |
PATCH | /api/auth/me | session | Change your own name. Body: name. |
GET | /oauth/<provider>/login?redirect= | none | Start Google (google), Microsoft (microsoft) or OpenID Connect (oidc) sign-in. |
GET | /oauth/<provider>/callback | the provider's return | Finish it, then go to the redirect page. A refusal goes to /auth?error=<code>. |
GET, POST | /api/auth/mfa/setup | after the password | Set up two-step sign-in while signing in: the QR code and key, then the first code. The recovery codes come back once. |
POST | /api/auth/mfa/verify | after the password | The second step: an authenticator code or a recovery code. |
GET, PUT | /api/auth/mfa/policy | session, administrator to change | Who must use two-step sign-in: none, admins or all. |
POST | /api/auth/reauth | session | Confirm a sensitive action with the password, and a code with two-step sign-in. |
GET, POST, DELETE | /api/me/api-keys, /api/me/sessions, /api/me/mfa | session | Your own API keys, signed-in devices and two-step sign-in. |
- Password sign-in, sign-up and password changes answer
409withsign_in_offwhile sign-in is off. WithENABLE_LOGIN_FORMorENABLE_PASSWORD_AUTHset to false, password sign-in answers403. - Failed attempts are limited per client address and per email:
HUBZOID_AUTH_MAX_FAILURES(10) in 15 minutes locks for 15 minutes with429andRetry-After. Per-address limits depend onX-Forwarded-Forfrom a TLS proxy. - One-time links last
HUBZOID_LINK_HOURS(72) and work once. Administrators create them in Settings or withhubzoid admin. Nothing is emailed. - Register
<your address>/oauth/<provider>/callbackwith each provider, for every address people use. See authentication.
Chat API
| Method | Path | Purpose |
|---|---|---|
GET | /api/conversations?q=&archived=&limit=&cursor= | Your conversations, with search and pages. |
POST | /api/conversations | Start one with this bridge's agent. Body: agent, optional id. |
GET | /api/conversations/<id> | A conversation and its message tree. |
PATCH | /api/conversations/<id> | Change title, archived or head_id (the branch shown). |
DELETE | /api/conversations/<id> | Delete it with its messages, share link and files. |
POST | /api/chat | Send a message, or regenerate a reply, and stream the reply as server-sent events. Body: conversation_id, message or parent_id, optional assistant_message_id and agent. |
GET | /api/runs/<message_id> | A reply's status and content, for a page that reloads while a reply runs. |
POST | /api/runs/<message_id>/cancel | Stop a reply. Answers 202. |
POST | /api/conversations/<id>/files | Attach a file, sent as multipart file. Answers 201. |
GET | /api/conversations/<id>/files/<file_id> | Download an attachment. Raster images show inline and active content is never served with its own type. |
GET, POST, DELETE | /api/conversations/<id>/share | Read, create or refresh, and turn off a read-only share link. A share is a snapshot of the branch shown. |
GET | /api/shares/<share_id> | A shared conversation, for any signed-in person of the deployment. |
- Conversations are private to their owner. Anyone else gets
404. - Starting work needs Use this agent (
use_hub) on that agent. A reply runs on the server, so closing the page does not stop it. One reply runs per conversation at a time, and another request answers409withrun_in_progress. - Each file is limited to
HUBZOID_MAX_UPLOAD_BYTES(25 MiB) and each message toHUBZOID_MAX_FILES_PER_MESSAGEfiles (10). A larger upload answers413.
Agents, branding and connections
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /api/agents | session | The agents the person may use, and the default one. |
GET | /api/branding | none | The deployment's public brand: name, logos, favicon and sign-in text. |
GET | /api/branding/source, /api/branding/active | session | Where the brand is authored, and the brand in force. |
GET | /branding/<file> | none | An agent's logo or favicon, its avatar. |
GET | /api/connections | session | The person's connections: each connector they may connect, its status, since when, and the agents that use it. |
POST | /api/connections/<connector>/connect | session, same origin | Start connecting. Body: optional return_to, a path on this site. Answers {"authorize_url": ...}. |
DELETE | /api/connections/<connector> | session, same origin | Disconnect, revoking at the provider when possible. Answers 204. |
GET | /oauth/connectors/<connector>/callback | the provider's return | Finish a connector sign-in, then go to return_to or /account/connections?connected=<connector>, with ?error=<code> on a failure. |
The connection routes are also served under /portal/api/connections. With sign-in off and no HUBZOID_PUBLIC_URL or HUBZOID_ALLOWED_ORIGINS, they answer only requests addressed to a loopback address (403 with untrusted_host), so another web page cannot act as the local owner. See tools and connectors.
Chat inside your app
The AG-UI endpoint for apps that embed an agent's chat, registered under apps: in hubzoid.yaml. Every route takes Authorization: Bearer <pass or app JWT>. With several Hubs, prefix the agent's address, /b/<agent>.
| Method | Path | Purpose |
|---|---|---|
POST | /api/agui | Run: an AG-UI RunAgentInput. Answers with AG-UI events as server-sent events. |
GET | /api/agui/threads | This person's chats with this agent. |
GET | /api/agui/threads/<id> | One chat as AG-UI messages, whether a reply is running, and pending interrupts. |
GET | /api/agui/threads/<id>/run | Rejoin the reply being written. 204 when there is none. |
POST | /api/agui/threads/<id>/stop | Stop it. |
DELETE | /api/agui/threads/<id> | Delete the chat. |
- People. No Hubzoid account is created. A person's chats are kept under a private id made from the app and their user id, and nobody can reach someone else's chat by naming its id.
- History is Hubzoid's. Only the new user message is read from
messages. One reply runs per chat at a time, and a second answers409withrun_in_progress. - Approvals and questions come as
RUN_FINISHEDwith aninterruptoutcome. Answer with a new run whoseresumeholds the decision. - Page context in AG-UI
context, orforwardedProps.hubzoid.page, reaches the agent marked as data, at most 4,000 characters, and never decides access. - Security. Only the app's
originsmay call from a browser. These routes never read a Hubzoid cookie, and a request carrying both answers400withambiguous_credentials. A pass opens only these routes. Refused tokens are rate-limited per address and runs per person. - Limits of this release. Client-declared AG-UI
toolsare ignored, and messages are text only.
| Status and code | Why |
|---|---|
401 unknown_app | The token's issuer and audience match no app. |
401 invalid_token, token_expired | Wrong signature, algorithm or times, no user claim, or an expired pass. |
403 no_access, wrong_origin, app_disabled | The pass allows no agent here, the page's address is not in origins, or the app has enabled: false. |
429 too_many | Too many messages from one person in a minute, or too many refused tokens from one address. |
503 app_unavailable | The app's secret is not set, or its JWKS address cannot be fetched. |
Set up an app with hubzoid embed. The SDK guide chat inside your app (opens in a new tab) has the server and page code.
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>", "model": "<label>"}. Reachable through the edge. |
GET | /v1/models | bridge key, or a personal API key | One model, the Hub, in OpenAI list format. Public through the edge only with a personal API key. |
POST | /v1/chat/completions | bridge key, or a personal API key | Chat with the Hub's agent. OpenAI request and response shape, streaming or not. Public through the edge only with a personal API key. |
POST | /uploads/{chat_id}/{filename} | bridge key | Upload a file into a chat so the agent can read it. Loopback only. |
GET | /artifacts/{chat_id}/{filename} | signed link, session or bridge key | Download a file the agent saved. Public through the edge. |
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. Loopback only. |
GET | /openapi.json, /docs, /redoc | none | The API reference: the public Hubzoid API only. |
The bridge also serves the web app, the MCP server, the Settings API, the artifact viewer, the connection pages and the workflow webhook receiver.
POST /v1/chat/completions
The endpoint the Slack adapter and the inbound receiver use, with a bridge key on loopback. Your own programs call it through the edge with a personal API key, and it runs as that key's owner. With a key the answer holds the answer text only, without tool activity or thinking.
- 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.
X-Hubzoid-Usernames the caller,X-Hubzoid-Surfacenames the surface andX-Hubzoid-Groupsadds groups. They count only when a validX-Hubzoid-Assertioncovers exactly their values: an HMAC made with the deployment key by another Hubzoid process, for this Hub, in the last 60 seconds. Without one the request is anonymous, so a bridge key alone cannot act as somebody else.userin the body is never an identity.
| Status | Meaning |
|---|---|
400 | Invalid JSON, no messages, or an empty prompt. |
401 | Missing or wrong key. |
403 | No verified person, a blocked person, or a person without Use this agent (use_hub). |
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= and an expiry in ?e=. They last HUBZOID_ARTIFACT_LINK_TTL seconds, 7 days by default. The signed-in owner of the conversation can download its files even after the link expires. A request with neither 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": ...}. The web app attaches files through /api/conversations/<id>/files instead.
MCP server
With MCP on, 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 with several Hubs. Each request is self-contained (stateless). MCP is on for hubzoid run when the public address is loopback or HTTPS, at <address>/mcp, unless the Hub sets MCP_SERVER itself. With several Hubs it is on only for Hubs whose own .env sets MCP_SERVER=true. MCP_PUBLIC_URL must be that public /mcp address, HTTPS except on loopback.
| Aspect | Behaviour |
|---|---|
| Auth | OAuth only. Authorization: Bearer <access token> issued by Hubzoid after the person signs in and approves the client. Bridge keys, API keys and sign-in cookies are refused. A missing or invalid token answers 401 with OAuth discovery in WWW-Authenticate. |
| Gate | The person's account must be current, not blocked, and hold Use this agent (use_hub). It is checked on every request, so a change in Settings applies to the next call. |
| Tools | The Hub's tools except the chat-scoped ones (write_artifact, list_artifacts, read_upload, read_upload_full, record_feedback), remember and the Hub change tools, personal memory, ask_question, call_jev and delegates. Agent administrators also get the read-only agent_health, eval_status and recent_denials. Restricted tools and gated built-in 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 signed-in person on the mcp surface, and restricted tool decisions are audited. |
OAuth endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /.well-known/oauth-protected-resource/mcp | Protected resource metadata for the /mcp address. |
GET | /.well-known/oauth-authorization-server/mcp/oauth | Authorization server metadata. The issuer is <MCP_PUBLIC_URL>/oauth. |
POST | /mcp/oauth/register | Dynamic client registration. Public clients only (token_endpoint_auth_method none), with HTTPS or loopback redirect addresses. |
GET, POST | /mcp/oauth/authorize | Start authorization with PKCE (S256). resource must equal the MCP address and the scope is hub:access. |
GET, POST | /mcp/oauth/consent | The consent page. A signed-out person is sent to sign in first. Allow connection returns the client to its redirect address with a code. |
POST | /mcp/oauth/token | Exchange a code or a refresh token. resource must equal the MCP address. |
POST | /mcp/oauth/revoke | Revoke a token and its grant. |
GET, POST | /mcp/oauth/connections | The signed-in person's connected assistants for this Hub, each with Revoke connection. |
Access tokens last up to 10 minutes. Refresh tokens and grants last up to 30 days, and a refresh token works once. With several Hubs the same paths sit under /b/<hub>, for example /b/<hub>/mcp/oauth/connections, and the two metadata documents are at /.well-known/oauth-protected-resource/b/<hub>/mcp and /.well-known/oauth-authorization-server/b/<hub>/mcp/oauth.
Connecting Claude Code, Cursor and other clients is covered in connect an assistant.
Settings API
Settings is served at /settings and calls a JSON API under /portal/api. The same API is the management API for scripts. A client-sent identity header is never trusted. A caller authenticates with:
- The session. The Hubzoid session cookie. Requests that change anything must come from the same origin.
Most endpoints need Manage access (manage_access) for the organization or for at least one agent. Each response is limited to the agents the caller manages. Organization administrators hold it for the organization. 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"}. A signed-out caller gets 401, and a signed-in person who manages nothing gets 403 on these endpoints.
In the tables, manager means a person with Manage access somewhere, organization admin means an organization administrator, and signed in means any signed-in person.
Agents and access
| Method | Path | Who | Purpose |
|---|---|---|---|
GET | /portal/api/me?brief= | manager | The caller, whether they are an organization admin, and the agents they manage. Without brief=true it adds what they can grant, whether they can create accounts and how a new account can sign in. |
GET | /portal/api/hubs | manager | Manageable agents with key, name, folder, model id and whether the caller can chat with each. |
GET | /portal/api/permissions?hub= | manager | The capability catalog of an agent: built-ins, connectors and restricted tools, with labels, groups, sections and configuration status. |
GET | /portal/api/hub-mcp-servers?hub= | manager | The MCP servers in the agent's connectors/.mcp.json, read only: names, kind and the variables they need, never a value. |
GET | /portal/api/access?hub=&q=&offset=&limit= | manager | People and services with access to an agent, their direct and effective permissions, account status, what the caller may grant, and the policy revision. |
POST | /portal/api/access/grant | manager | Grant one permission. Body: subject, hub, permission, optional expected_revision. |
POST | /portal/api/access/revoke | manager | Revoke one permission. Same body. |
POST | /portal/api/access/apply | manager | Apply a set of grants and revokes for one person in one agent, all or nothing. Body: subject, hub, operations (each action and permission), optional expected_revision. |
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. Nobody can change their own access. Page sizes are capped at 200.
People and accounts
| Method | Path | Who | Purpose |
|---|---|---|---|
GET | /portal/api/people?q=&status=&role=&agent=&offset=&limit= | manager | People and services with account status, organization admin flag, reader-only flag and access per agent. Phone numbers are shown to organization admins only. |
GET | /portal/api/overview | manager | Counts of agents, grants and people. |
POST | /portal/api/people/refresh | organization admin | Re-read the account directory. |
POST | /portal/api/people/block | organization admin | Block or reactivate a person. Body: subject, suspended (true to block, false to reactivate). Settings offers only Reactivate, from a blocked person's details. Blocking removes every grant, and reactivating does not restore them. |
POST | /portal/api/accounts | manager | Create an account with its first access. Body: email, name, sign_in (password or google), optional password, grants (each hub and permission) and phone (organization admins). A password account created without a password comes with a one-time sign-in link, returned once as link and expires_at. An existing email answers 409 with account_exists. A partial result keeps the account and answers 502 with code partial. |
POST | /portal/api/accounts/grant | manager | 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, role, how they sign in and both halves of the Administrator role. |
POST | /portal/api/accounts/<subject>/password | organization admin | Reset a password and end their sessions. Body: password, optional. Leaving it out returns a one-time link. Refused with google_managed for a Google-only account. |
POST | /portal/api/accounts/<subject>/phone | organization admin | Record or clear the number the person's WhatsApp and Telegram messages come from. Body: phone, empty to clear. |
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 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 account and its chats. Body: confirm_email, which must equal the account's email. |
Workflows, schedules and reports
| Method | Path | Who | Purpose |
|---|---|---|---|
GET | /portal/api/workflows?hub= | manager | Workflows and markdown tasks with state, next run, the account each runs as and the input form of each code workflow. |
POST | /portal/api/workflows/run | manager with Run and control workflows, or organization admin | Run a workflow now as a team run. Body: hub, workflow, optional inputs. Returns run_id and already_running. |
GET | /portal/api/runs?hub=&workflow=&run_id=&status=&since=&until=&limit=&offset= | manager | Run history, across every managed agent when hub is left out. 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/schedules?hub= | manager | People's personal schedules on an agent: who, what, when, how it went and the model cost over 30 days. Never the report itself. |
GET | /portal/api/schedules/<id>/runs?hub= | manager | A personal schedule's latest runs, status and times only. |
POST | /portal/api/schedules/<id>/<action> | manager | pause, resume or stop a person's schedule. Body: hub. Administrators never edit its inputs. |
GET | /portal/api/reports?hub= | manager | Each live report link on an agent with its state, audience and company link, and whether company links are available. |
POST | /portal/api/reports/<id>/company | manager | copy, replace or turn off a report's company link. Body: hub, action. Copying and replacing are for team reports. A personal report's link can only be turned off here, and its owner is told. |
GET | /portal/api/webhooks?hub= | manager | Declared webhooks with their address, verification, the workflows they start, the last 24 hours of events by state and the latest failures with their redrive command. Never a payload, digest or header. |
GET | /portal/api/evals?hub= | manager | Eval cases with their latest result, history and the score. Prompts and checks of a case run as another account are left out. |
GET | /portal/api/evals/summary?hub= | manager | The latest score, for the Agents list. |
GET | /portal/api/evals/runs?hub=&offset=&limit= | manager | Eval runs, newest first. |
GET | /portal/api/evals/runs/<stamp>?hub= | manager | One eval run's case details. |
With several Hubs, the eval endpoints redirect to the agent's own bridge under /b/<hub>/portal/api.
Connectors
| Method | Path | Who | Purpose |
|---|---|---|---|
GET | /portal/api/connectors | organization admin | Registered connectors with their redirect address, number of connections and the agents that offer them. Never a secret. |
POST | /portal/api/connectors?hub= | organization admin | Add a connector, and with hub offer it in that agent. Answers 201. |
PATCH | /portal/api/connectors/<id> | organization admin | Change the fields sent. |
DELETE | /portal/api/connectors/<id> | organization admin | Delete a connector with every connection to it. Answers 204. |
PUT, DELETE | /portal/api/connectors/<id>/agents/<hub> | organization admin | Offer a connector in an agent, or stop offering it there, which also removes its grants there. Answers 204. |
POST | /portal/api/connectors/<id>/test | organization admin | Check the server and list its tools. Changes nothing. |
Connectors are files, connectors/<id>.yaml. Adding, changing, deleting and offering one answer 409 with in_app_editing_off unless browser editing is on, and never write files that come from an image. People's own connections are under /portal/api/connections with the same routes as /api/connections. The connector endpoints need the same origin for changes.
Your own work
These answer any signed-in person, for their own work only. A schedule that is not theirs answers 404.
| Method | Path | Purpose |
|---|---|---|
GET | /portal/api/my | Their agents, personal schedules, live report links, the reports they may run or schedule, and whether they have connections. |
GET | /portal/api/my/runs?hub=&run_id= | Their recent runs on an agent. |
GET | /portal/api/my/schedules/<id>?hub= | A schedule's saved inputs, timing and input form, for editing. |
PUT | /portal/api/my/schedules/<id> | Save it. Body: hub, inputs, cadence, timezone, revision. |
GET | /portal/api/my/schedules/<id>/runs?hub= | A schedule's latest runs. |
POST | /portal/api/my/schedules/<id>/<action> | pause, resume, run_now or stop it. Body: hub. run_now must reach the agent's own bridge. |
Activity and usage
| Method | Path | Who | Purpose |
|---|---|---|---|
GET | /portal/api/audit?hub=&user=&outcome=&tool=&surface=&since=&until=&limit=&offset= | manager | Restricted tool decisions. outcome is allow or deny. |
GET | /portal/api/access-changes?hub=&user=&actor=&action=&since=&until=&limit=&offset= | manager | Grant, revoke and account changes with who made them, and sign-in events for organization admins. |
GET | /portal/api/summary?period= | manager | Usage for 24h, 7d or 30d: chats, messages, active people, tokens, estimated cost, denials, runs and missed slots per agent, plus user_accounts, the accounts in the caller's scope whatever the period. |
Change requests
| Method | Path | Who | Purpose |
|---|---|---|---|
GET | /portal/api/change-requests/<id> | the person who proposed it | A change proposed by the access 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. An expired request answers 410. |
POST | /portal/api/change-requests/<id>/reject | the person who proposed it | Reject the proposal. |
What each section shows is described in the Settings 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 administration rights are involved: owners manage their own artifacts here. Identity comes from the session, verified on the server.
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /portal/artifacts/<id> | session | The viewer page. A signed-out visitor is sent to sign in and back to the same artifact. |
GET | /portal/artifacts/<id>/content, /portal/artifacts/<id>/content/<filename> | session | The file, shown inline and isolated. |
GET | /portal/artifacts/<id>/download | session | The file, as a download. |
GET | /portal/artifacts/api/<id> | session | Metadata, plus sharing settings for the owner. |
POST | /portal/artifacts/api/<id>/audience | owner, same origin | Share with only the owner (owner), named people (people, with a people list) or everyone who can use the agent (hub). |
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. |
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.
Live reports
A workflow that publishes with a key keeps one link per report that always shows the newest copy.
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /portal/latest/<agent>/<key> | session | A team report's live page, served at this address. It shows only the agent's own copies, never a person's. For an older keyed artifact without a report series, it opens the newest one the viewer may see. |
GET | /portal/latest/<agent>/<key>/by/<ref> | session | A person's own live report. |
GET | /portal/series/api/<id> | session | The report's state, when it updates next, and its newest copy. The owner of a personal report also gets its company link settings. |
GET | /portal/series/api/<id>/latest | session | The newest copy's id and time, polled by an open page. An unchanged report answers 304. |
POST | /portal/series/api/<id>/update | owner of a personal report, or Run and control workflows for a team report, same origin | Update now. Answers 429 when it was updated moments ago. |
POST | /portal/series/api/<id>/company | owner of a personal report, same origin | Turn its company link on or off. Body: action (on or off). Needs Share reports with the company domain. |
Company links
A company link lets anyone whose sign-in email is at HUBZOID_SHARE_DOMAINS read one report after signing in. A reader who does not use the agent becomes a reader-only account with no agent access.
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /portal/r/<token> | session at an allowed domain | The report page. A signed-out visitor is sent to sign in first. Every refusal shows the same page. |
GET | /portal/r/<token>/api, /portal/r/<token>/api/latest | the same | Metadata and the newest copy's id. |
GET | /portal/r/<token>/content/<filename>, /portal/r/<token>/download | the same | The file, inline or as a download. |
Public links
| Method | Path | Auth | Purpose |
|---|---|---|---|
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, same origin | 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. |
Connection pages
When an agent's connect_account tool sends a person a link, it points to /portal/connect/<id>. Every page needs the session of the account that 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, showing the account and the server before anything happens. |
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 stored connection. Parameters on the return address are never read. |
GET | /portal/connect/<id>/status | The journey's state as JSON, polled by the done page. |
GET | /portal/connections | A person's connections. It redirects to /account/connections. |
Links expire after HUBZOID_CONNECT_TTL seconds (600 by default).
Inbound receiver
The receiver starts with its Hub in hubzoid run when a file in the Hub's channels/ folder turns on WhatsApp, Telegram or the generic 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. A WhatsApp or Telegram sender must be known: by the phone number an organization administrator recorded for them in Settings, or by the Hub's own lookup in 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.
Workflow webhooks
A code workflow declared with @workflow(on_webhook="<name>") starts from a webhook named in the webhooks: section of workflows/settings.yaml. The Hub's own bridge receives it, whether the Hub runs alone or with others.
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /webhooks/<hub>/<name> | the webhook's secret, WEBHOOK_SECRET_<NAME> | Store one event for the workflow. Answers {"status": "accepted", "event_id": "..."}. |
<NAME> is the webhook name in upper case with - written as _. Each webhook verifies in one of three ways:
verify: header(default). The secret, unchanged, in the header named byheader(X-Hook-Secretwhen none is named).verify: hmac.sha256=and the hex HMAC-SHA256 of<timestamp>.<raw body>, insignature_header, with Unix seconds intimestamp_header, which is required. The timestamp must be within five minutes.verify: github. GitHub'sX-Hub-Signature-256, the HMAC-SHA256 of the raw body. GitHub'sX-GitHub-Deliveryidentifies repeats.
| Status | Meaning |
|---|---|
200 | The event is stored durably. That does not mean the workflow succeeded. A repeat with the same key and body answers 200 with the same event. |
400 | The body is not JSON, or the length header is invalid. |
401 | Verification failed, or no secret is set. |
404 | No webhook has this name. |
409 | The same event key arrived earlier with different content. An alert is raised. |
413 | The body is larger than 256 KiB. |
422 | The body is not a JSON object. |
503 | The workflow settings are invalid, the workflow engine is not ready, or the event could not be stored. The sender should retry. |
An event is identified by its event_key template when the declaration has one, then by a delivery id header, then by a hash of the body. Query parameters are never credentials. The names whatsapp, telegram and the Hub's WEBHOOK_INBOUND_NAME are refused, because the inbound receiver already serves them. See the workflow API and webhooks.

