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.

SectionRoutes
API reference/docs and /openapi.json: the public Hubzoid API, for your own programs.
EdgeWhat the public port forwards and what it keeps private.
Web appChat, 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 viewerPublished artifacts, live reports, company and public links.
Connection pagesLinks from connect_account.
Inbound receiver, workflow webhooksWhatsApp, Telegram and webhooks.
  1. Browser or client
  2. Edge (PORT)
  3. Bridge (loopback)
  4. Hub runtime
The edge sends the web app, Settings, MCP and downloads to the bridge and keeps the bridge's internal API private.
ProcessDefault addressExposure
Edge127.0.0.1:3080 (HUBZOID_HOST, PORT)The public front door.
Bridge127.0.0.1, the first free port from 8000 (BRIDGE_PORT)Loopback only. Reached through the edge.
Inbound receiver127.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 coversRoutes
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/models names 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:3080 with no key.
  • Errors are JSON with a detail. 401 means no valid credential, 403 not allowed, 409 a conflict with a newer change, 422 an invalid body, 503 the 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 with 400, 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: https upstream 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/workflows is answered by the edge itself. See workflow health.

A single Hub's edge sends every path to the bridge, except:

PathResult
/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 /otel404. The bridge's internal API stays on loopback.
/webhooks/<hub>/<name> for a webhook declared in workflows/settings.yamlThe bridge's workflow webhook receiver.
/webhooks/<hub> and belowThe 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 prefixGoes toNotes
/b/<hub>/apithat Hub's bridgeChat calls that touch the Hub's files or runtime.
/b/<hub>/brandingthat Hub's bridgeThe Hub's logo, favicon and stylesheet.
/b/<hub>/portal/apithat Hub's bridgeSettings calls that must run where the Hub's engine and files are, such as running a schedule now or reading evals.
/b/<hub>/artifactsthat Hub's bridgeChat downloads.
/b/<hub>/mcpthat Hub's bridgeHubs 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 bridgeDeclared workflow webhooks.
/webhooks/<hub>that Hub's inbound receiverHubs whose channels/ folder turns on an inbound surface.
everything elsethe 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

MethodPathAuthPurpose
GET, HEAD/healthz/workflowsnone{"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_session cookie, stored on the server only as a digest. It lasts at most HUBZOID_SESSION_DAYS (30) and ends after HUBZOID_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 Origin or Referer that matches the request's host or one of the deployment's addresses (HUBZOID_PUBLIC_URL, HUBZOID_ALLOWED_ORIGINS), or it is refused with 403.
  • Errors have the shape {"detail": {"code": "...", "message": "..."}}.

Pages

MethodPathPurpose
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/connectionsThe person's account settings and their connections.
GET/settingsSettings, 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

MethodPathAuthPurpose
GET/api/auth/sessionnoneWhether 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/loginnonePassword sign-in. Body: email, password. Sets the session cookie.
POST/api/auth/logoutsessionSign out. Answers 204.
POST/api/auth/signupnoneSelf 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 linkWhether a one-time link is valid, its purpose and email.
POST/api/auth/link/<token>the linkSet a password with the link and sign in. Body: password. A used or expired link answers 410.
POST/api/auth/passwordsessionChange your own password. Body: current_password, new_password. Answers 204.
PATCH/api/auth/mesessionChange your own name. Body: name.
GET/oauth/<provider>/login?redirect=noneStart Google (google), Microsoft (microsoft) or OpenID Connect (oidc) sign-in.
GET/oauth/<provider>/callbackthe provider's returnFinish it, then go to the redirect page. A refusal goes to /auth?error=<code>.
GET, POST/api/auth/mfa/setupafter the passwordSet 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/verifyafter the passwordThe second step: an authenticator code or a recovery code.
GET, PUT/api/auth/mfa/policysession, administrator to changeWho must use two-step sign-in: none, admins or all.
POST/api/auth/reauthsessionConfirm 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/mfasessionYour own API keys, signed-in devices and two-step sign-in.
  • Password sign-in, sign-up and password changes answer 409 with sign_in_off while sign-in is off. With ENABLE_LOGIN_FORM or ENABLE_PASSWORD_AUTH set to false, password sign-in answers 403.
  • Failed attempts are limited per client address and per email: HUBZOID_AUTH_MAX_FAILURES (10) in 15 minutes locks for 15 minutes with 429 and Retry-After. Per-address limits depend on X-Forwarded-For from a TLS proxy.
  • One-time links last HUBZOID_LINK_HOURS (72) and work once. Administrators create them in Settings or with hubzoid admin. Nothing is emailed.
  • Register <your address>/oauth/<provider>/callback with each provider, for every address people use. See authentication.

Chat API

MethodPathPurpose
GET/api/conversations?q=&archived=&limit=&cursor=Your conversations, with search and pages.
POST/api/conversationsStart 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/chatSend 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>/cancelStop a reply. Answers 202.
POST/api/conversations/<id>/filesAttach 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>/shareRead, 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 answers 409 with run_in_progress.
  • Each file is limited to HUBZOID_MAX_UPLOAD_BYTES (25 MiB) and each message to HUBZOID_MAX_FILES_PER_MESSAGE files (10). A larger upload answers 413.

Agents, branding and connections

MethodPathAuthPurpose
GET/api/agentssessionThe agents the person may use, and the default one.
GET/api/brandingnoneThe deployment's public brand: name, logos, favicon and sign-in text.
GET/api/branding/source, /api/branding/activesessionWhere the brand is authored, and the brand in force.
GET/branding/<file>noneAn agent's logo or favicon, its avatar.
GET/api/connectionssessionThe person's connections: each connector they may connect, its status, since when, and the agents that use it.
POST/api/connections/<connector>/connectsession, same originStart connecting. Body: optional return_to, a path on this site. Answers {"authorize_url": ...}.
DELETE/api/connections/<connector>session, same originDisconnect, revoking at the provider when possible. Answers 204.
GET/oauth/connectors/<connector>/callbackthe provider's returnFinish 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>.

MethodPathPurpose
POST/api/aguiRun: an AG-UI RunAgentInput. Answers with AG-UI events as server-sent events.
GET/api/agui/threadsThis 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>/runRejoin the reply being written. 204 when there is none.
POST/api/agui/threads/<id>/stopStop 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 answers 409 with run_in_progress.
  • Approvals and questions come as RUN_FINISHED with an interrupt outcome. Answer with a new run whose resume holds the decision.
  • Page context in AG-UI context, or forwardedProps.hubzoid.page, reaches the agent marked as data, at most 4,000 characters, and never decides access.
  • Security. Only the app's origins may call from a browser. These routes never read a Hubzoid cookie, and a request carrying both answers 400 with ambiguous_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 tools are ignored, and messages are text only.
Status and codeWhy
401 unknown_appThe token's issuer and audience match no app.
401 invalid_token, token_expiredWrong signature, algorithm or times, no user claim, or an expired pass.
403 no_access, wrong_origin, app_disabledThe pass allows no agent here, the page's address is not in origins, or the app has enabled: false.
429 too_manyToo many messages from one person in a minute, or too many refused tokens from one address.
503 app_unavailableThe 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.

MethodPathAuthPurpose
GET/healthznoneLiveness. Returns {"status": "ok", "hub": "<folder>", "agent": "<name>", "model": "<label>"}. Reachable through the edge.
GET/v1/modelsbridge key, or a personal API keyOne model, the Hub, in OpenAI list format. Public through the edge only with a personal API key.
POST/v1/chat/completionsbridge key, or a personal API keyChat 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 keyUpload a file into a chat so the agent can read it. Loopback only.
GET/artifacts/{chat_id}/{filename}signed link, session or bridge keyDownload a file the agent saved. Public through the edge.
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. Loopback only.
GET/openapi.json, /docs, /redocnoneThe 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. 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. X-Hubzoid-User names the caller, X-Hubzoid-Surface names the surface and X-Hubzoid-Groups adds groups. They count only when a valid X-Hubzoid-Assertion covers 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. user in the body is never an identity.
StatusMeaning
400Invalid JSON, no messages, or an empty prompt.
401Missing or wrong key.
403No verified person, a blocked person, or a person without Use this agent (use_hub).
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= 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.

AspectBehaviour
AuthOAuth 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.
GateThe 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.
ToolsThe 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.
InstructionsThe mcp_instructions value from AGENTS.md frontmatter, or else the AGENTS.md body, sent when the client connects.
IdentityCalls run as the signed-in person on the mcp surface, and restricted tool decisions are audited.

OAuth endpoints

MethodPathPurpose
GET/.well-known/oauth-protected-resource/mcpProtected resource metadata for the /mcp address.
GET/.well-known/oauth-authorization-server/mcp/oauthAuthorization server metadata. The issuer is <MCP_PUBLIC_URL>/oauth.
POST/mcp/oauth/registerDynamic client registration. Public clients only (token_endpoint_auth_method none), with HTTPS or loopback redirect addresses.
GET, POST/mcp/oauth/authorizeStart authorization with PKCE (S256). resource must equal the MCP address and the scope is hub:access.
GET, POST/mcp/oauth/consentThe 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/tokenExchange a code or a refresh token. resource must equal the MCP address.
POST/mcp/oauth/revokeRevoke a token and its grant.
GET, POST/mcp/oauth/connectionsThe 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

MethodPathWhoPurpose
GET/portal/api/me?brief=managerThe 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/hubsmanagerManageable agents with key, name, folder, model id and whether the caller can chat with each.
GET/portal/api/permissions?hub=managerThe 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=managerThe 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=managerPeople 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/grantmanagerGrant one permission. Body: subject, hub, permission, optional expected_revision.
POST/portal/api/access/revokemanagerRevoke one permission. Same body.
POST/portal/api/access/applymanagerApply 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

MethodPathWhoPurpose
GET/portal/api/people?q=&status=&role=&agent=&offset=&limit=managerPeople 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/overviewmanagerCounts of agents, grants and people.
POST/portal/api/people/refreshorganization adminRe-read the account directory.
POST/portal/api/people/blockorganization adminBlock 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/accountsmanagerCreate 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/grantmanagerGive an existing account access. Body: email, grants. Never creates an account.
GET/portal/api/accounts/<subject>organization adminA person's account read live: name, role, how they sign in and both halves of the Administrator role.
POST/portal/api/accounts/<subject>/passwordorganization adminReset 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>/phoneorganization adminRecord or clear the number the person's WhatsApp and Telegram messages come from. Body: phone, empty to clear.
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 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 account and its chats. Body: confirm_email, which must equal the account's email.

Workflows, schedules and reports

MethodPathWhoPurpose
GET/portal/api/workflows?hub=managerWorkflows and markdown tasks with state, next run, the account each runs as and the input form of each code workflow.
POST/portal/api/workflows/runmanager with Run and control workflows, or organization adminRun 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=managerRun 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=managerPeople'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=managerA personal schedule's latest runs, status and times only.
POST/portal/api/schedules/<id>/<action>managerpause, resume or stop a person's schedule. Body: hub. Administrators never edit its inputs.
GET/portal/api/reports?hub=managerEach live report link on an agent with its state, audience and company link, and whether company links are available.
POST/portal/api/reports/<id>/companymanagercopy, 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=managerDeclared 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=managerEval 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=managerThe latest score, for the Agents list.
GET/portal/api/evals/runs?hub=&offset=&limit=managerEval runs, newest first.
GET/portal/api/evals/runs/<stamp>?hub=managerOne eval run's case details.

With several Hubs, the eval endpoints redirect to the agent's own bridge under /b/<hub>/portal/api.

Connectors

MethodPathWhoPurpose
GET/portal/api/connectorsorganization adminRegistered connectors with their redirect address, number of connections and the agents that offer them. Never a secret.
POST/portal/api/connectors?hub=organization adminAdd a connector, and with hub offer it in that agent. Answers 201.
PATCH/portal/api/connectors/<id>organization adminChange the fields sent.
DELETE/portal/api/connectors/<id>organization adminDelete a connector with every connection to it. Answers 204.
PUT, DELETE/portal/api/connectors/<id>/agents/<hub>organization adminOffer a connector in an agent, or stop offering it there, which also removes its grants there. Answers 204.
POST/portal/api/connectors/<id>/testorganization adminCheck 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.

MethodPathPurpose
GET/portal/api/myTheir 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

MethodPathWhoPurpose
GET/portal/api/audit?hub=&user=&outcome=&tool=&surface=&since=&until=&limit=&offset=managerRestricted tool decisions. outcome is allow or deny.
GET/portal/api/access-changes?hub=&user=&actor=&action=&since=&until=&limit=&offset=managerGrant, revoke and account changes with who made them, and sign-in events for organization admins.
GET/portal/api/summary?period=managerUsage 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

MethodPathWhoPurpose
GET/portal/api/change-requests/<id>the person who proposed itA change proposed by the access 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. An expired request answers 410.
POST/portal/api/change-requests/<id>/rejectthe person who proposed itReject 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.

MethodPathAuthPurpose
GET/portal/artifacts/<id>sessionThe 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>sessionThe file, shown inline and isolated.
GET/portal/artifacts/<id>/downloadsessionThe file, as a download.
GET/portal/artifacts/api/<id>sessionMetadata, plus sharing settings for the owner.
POST/portal/artifacts/api/<id>/audienceowner, same originShare 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>/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.

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.

MethodPathAuthPurpose
GET/portal/latest/<agent>/<key>sessionA 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>sessionA person's own live report.
GET/portal/series/api/<id>sessionThe 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>/latestsessionThe newest copy's id and time, polled by an open page. An unchanged report answers 304.
POST/portal/series/api/<id>/updateowner of a personal report, or Run and control workflows for a team report, same originUpdate now. Answers 429 when it was updated moments ago.
POST/portal/series/api/<id>/companyowner of a personal report, same originTurn its company link on or off. Body: action (on or off). Needs Share reports with the company domain.

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.

MethodPathAuthPurpose
GET/portal/r/<token>session at an allowed domainThe 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/latestthe sameMetadata and the newest copy's id.
GET/portal/r/<token>/content/<filename>, /portal/r/<token>/downloadthe sameThe file, inline or as a download.
MethodPathAuthPurpose
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 token, same originExchange 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.

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.

MethodPathPurpose
GET/portal/connect/<id>The connection page, showing the account and the server before anything happens.
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 stored connection. Parameters on the return address are never read.
GET/portal/connect/<id>/statusThe journey's state as JSON, polled by the done page.
GET/portal/connectionsA 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.

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

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.

MethodPathAuthPurpose
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 by header (X-Hook-Secret when none is named).
  • verify: hmac. sha256= and the hex HMAC-SHA256 of <timestamp>.<raw body>, in signature_header, with Unix seconds in timestamp_header, which is required. The timestamp must be within five minutes.
  • verify: github. GitHub's X-Hub-Signature-256, the HMAC-SHA256 of the raw body. GitHub's X-GitHub-Delivery identifies repeats.
StatusMeaning
200The 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.
400The body is not JSON, or the length header is invalid.
401Verification failed, or no secret is set.
404No webhook has this name.
409The same event key arrived earlier with different content. An alert is raised.
413The body is larger than 256 KiB.
422The body is not a JSON object.
503The 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.

Next steps

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.