The GoClaw gateway exposes the Web UI, web chat over Server-Sent Events, WebSocket chat, REST administration APIs, webhook ingress, media endpoints, and an OpenAI-compatible chat API.
Routes are registered in cmd/gateway/startup.go (registerAPIRoutes, which
also mounts the trace routes of internal/gateway/handler_traces.go) and in
cmd/gateway/main.go (the Web UI's files, /canvas/ and /uploads/), and
implemented under internal/gateway.
Every route on this page needs the gateway token, gateway.auth.token. The
gateway does not start without one. These are the exceptions:
| Route | How it is checked |
|---|---|
/health, /healthz, /ready, /readyz |
Not checked. |
The Web UI's own files (/, its scripts, styles and images) |
Not checked; every API call the page makes sends the token. |
/goclaw/hook/… and the channel webhooks (/hook/…) |
Each checks its sender's own credential. See Webhook receiver. |
/goclaw/view/<slug> |
The page's own view token (?token=<view_token> or an X-View-Token header), or the gateway token. |
/api/app/*, /ws/app |
The App API's own credential. See App API. |
Send the token in any of these:
X-Goclaw-Token: <token> header;Authorization: Bearer <token> header;?token=<token> query parameter, for clients that cannot set a header
(EventSource, a browser WebSocket);X-Goclaw-Token cookie. The gateway sets it when a browser opens the
one-time login link it logs at startup (Web UI: http://…/?auth_code=…): the
code is valid for 5 minutes, and for 30 seconds after its first use. The
cookie lasts 7 days, and the gateway redirects to the same path without the
code. The code is not the token. The cookie is taken only from the dashboard
itself: a request to /api/, /v1/ or /plugins/ that carries it alone and
that the browser marks as started by another site (Sec-Fetch-Site: same-site or cross-site — a page on another port of the same host is
same-site) is answered 403. Send the token in a header there instead.A missing or wrong token is answered 401 {"error":"unauthorized"}. When
gateway.auth.token is a ${secret:…} reference the vault cannot resolve, every
request is answered 500.
Requests that carry the token are not counted against gateway.rate_limit;
neither are health probes and the Web UI's files. When webhooks share the main
port (no gateway.ingress.port), gateway.ingress.allow_ips / allow_preset
apply to every route on it; loopback is always let through.
POST /api/chatThe Web UI's chat. It runs one message on the default agent as the admin (role
system) and streams the reply as Server-Sent Events.
{
"session_id": "admin-session",
"content": "Hello",
"attachments": []
}
| Field | Notes |
|---|---|
session_id |
Optional; defaults to admin-session. |
content |
The message (message is accepted as an alias). Required unless attachments has an item. |
attachments |
Files uploaded first with /api/upload-workspace or /api/media/upload, each as {media_id, media_url, local_path, filename, mime_type, size}. |
A message sent while an earlier one in the same session is still being answered
waits for it. If the request ends while it waits, the answer is 409.
The response is text/event-stream. Each frame is event: <name> with a JSON
data: line:
| Event | Data |
|---|---|
token |
content (also delta): the next piece of the reply. |
tool_start |
tool: the tool the agent called. |
tool_result |
tool: the tool that finished. |
canvas |
title, content (HTML): a canvas artifact. |
buffer_reset |
The reply is being retried: discard the text received so far. |
error |
error: what went wrong. No done follows. |
done |
content (the whole reply), attachments (files the agent produced, served under /canvas/), prompt_tokens, output_tokens, elapsed_ms, session_id. |
A reset trigger (session.reset_triggers, by default /new and /reset)
clears the session, and @config:, @notes:, @sop: directives and
/subagents commands are answered without the model; each ends with done, or error when it fails.
The directives need an administrator, which this endpoint always is. If
the client disconnects, the agent still finishes (for up to 10 minutes) and the
reply is saved to the session.
GET /gatewayUpgrades to a WebSocket connection, for chat clients other than the Web UI (the
Web UI uses POST /api/chat). When gateway.allowed_origins
is set, only those origins may connect.
Message envelope:
{
"id": "msg-123",
"kind": "inbound",
"session_id": "admin-session",
"payload": {
"channel": "web",
"peer_id": "web-tab",
"content": "Hello"
},
"timestamp": "2026-06-20T10:00:00Z"
}
Supported message kinds:
| Kind | Direction | Purpose |
|---|---|---|
inbound |
client -> gateway | User message and optional attachments. |
outbound_chunk |
gateway -> client | Streaming token chunk. |
outbound |
gateway -> client | Final assistant response. |
event |
gateway -> client | Tool lifecycle and buffer-reset events. |
canvas |
gateway -> client | Rich canvas artifact update. |
error |
gateway -> client | User-facing processing error. |
Payloads:
| Kind | Payload |
|---|---|
inbound |
channel, account_id, peer_id, content, and optional attachments, each {media_id, media_url, local_path, filename, mime_type, size}. |
outbound_chunk |
content: the next piece of the reply. |
outbound |
content, and attachments the agent produced (served under /canvas/). |
event |
{"event_type":"tool_start","tool_name","args"}, {"event_type":"tool_result","tool_name","result","error","elapsed_ms"} or {"event_type":"buffer_reset"} (discard the text received so far). |
canvas |
title, content (HTML). |
error |
error, and ref_id: the id of the message it answers. |
Sessions: on channel web, a session_id that starts with admin- is used as
given; any other value, or none, gives the connection its own session,
admin-conn-<16 hex digits>. Web messages run as the admin (role system). On
any other channel the session_id is used as given, and without one the
session is p2p:<peer_id>:<default agent>.
A connection's messages are handled one at a time. Reset triggers, directives
and /subagents commands work as in POST /api/chat. Off the
web channel a message runs with the role GoClaw resolves for its channel and
peer_id, and @config:, @notes: and @sop: need a peer paired as admin:
anyone else is told so and nothing is changed (the message and the refusal stay
in the session, as for any directive).
GET /api/statsReturns gateway health, provider/model information, session count, connected channels, runtime ingress status, and version metadata.
GET /api/gatewayReturns detailed runtime status, ingress settings, rate-limit state, and recent
access logs. Supports ?limit=N.
GET /api/doctorRuns health checks and returns diagnostic results. With ?stream=1 the results
arrive as Server-Sent Events, one check event per check as it finishes and a
final done event with {"total", "ok"}.
GET /api/inspectReturns a combined diagnostic view with sessions, memory snippets, database path, state directory, and server time.
GET|POST /api/configReads or mutates active configuration. GET returns the whole configuration
keyed as config.yaml is (tools.http.allow), with secrets masked;
GET ?key=ai.provider returns a single key. POST supports mutation actions
such as set, append, and remove.
GET|POST|DELETE /api/secretsManages the encrypted secret vault. Secret values are masked when listed.
GET|POST /api/integrationsLists, tests, saves and deletes the external services the tools use: GitLab,
SonarQube, Grafana, Graylog, NetBox, Uptrace, Zabbix, Wazuh and datasources.
GET returns each definition with credentials masked (${secret:…} references
are shown as written). POST takes {"action", "type", "name", "config"}:
test probes the definition as the default agent and succeeds only when the
credentials are accepted. warnings names agents that override the enclosing
block (and so will not see it) or whose HTTP policy blocks the host.save merges config onto the stored definition — a key left out keeps its
stored value, and the mask keeps a stored secret only while the URL or host is
unchanged. create: true refuses an existing name (409);
store_secrets: true moves typed credentials into the vault as
integrations.<type>.<name>.<field> and writes a ${secret:…} reference.delete removes the definition; a datasource named by
org_directory.sql.datasource is refused (409).GET|POST /api/pairingLists pending/approved channel pairings and approves, revokes, or updates user
roles. GET takes an optional ?channel=. POST takes an action:
{"action":"approve","code","role"} approves a pairing code,
{"action":"revoke","user_id"} removes a user, and
{"action":"update_role","user_id","role"} changes a user's role. Approvals made
here take effect in the running gateway at once.
GET|POST|DELETE /api/sessionsGET: Lists persisted sessions, each as {id, title, message_count, last_seen}.
The messages themselves are served by GET /api/session.POST {"action":"create","id":"admin-name","name":"Label"}: Creates an admin
Web UI session. The id must start with admin-; without one it is
admin-<unix milliseconds>. name is optional.POST {"action":"rename","id":"admin-name","name":"New label"}: Renames a
session.POST {"action":"clear"}: Deletes all sessions; the response says how many.DELETE ?id=<session-id>: Deletes one session.Session creation is persisted immediately, including empty sessions, so Web UI refreshes do not make newly created sessions disappear.
Each of these changes waits until a message being answered in that session is
done, so the reply cannot write the old session back over the change. A request
that ends while it waits is answered 503; clear then reports how many
sessions it had already deleted.
GET /api/sessionReturns paginated message history for one session.
Query parameters:
| Parameter | Required | Notes |
|---|---|---|
id |
yes | Session ID. |
limit |
no | Defaults to 50; a value that is not a number from 1 to 500 gives 50. |
offset |
no | Offset from the newest message. |
The response includes history, parallel meta, total, limit, offset,
and username. Web chat attachment previews are stored in meta[].attachments.
GET /api/memoryReturns memory records grouped for the Web UI.
GET|POST /api/memory/coreReads or updates an agent's core instruction: ?agent=<id>, the default
agent's when left out. The ID is used as given: where the default agent has an
ID of its own and no agent is configured as main, ?agent=main reads an
instruction stored under main, which no run follows.
GET|POST /api/memory/bootstrap-fileReads or updates controlled bootstrap files such as AGENTS.md, TOOLS.md, and
other admin-managed prompt files, in the workspace of ?agent=<id> (the default
agent's when left out).
| Endpoint | Purpose |
|---|---|
/api/memory/replay |
Replay archived/session memory into the memory engine. |
/api/memory/reindex |
Rebuild memory indexes. |
/api/memory/stats |
Return memory statistics. |
/api/memory/prune |
Prune old or low-value memory. |
/api/memory/dedupe |
Deduplicate memory records. |
/api/memory/forget |
Delete selected memory. |
/api/memory/clear |
Clear memory data. |
/api/memory/relearn |
Trigger relearning workflows. |
/api/memory/revise |
POST {"dry_run"}: weigh each learned fact or preference against older ones on the same subject and retire those it replaces (tagged superseded, not deleted). goclaw memory revise calls it. |
/api/memory/revisions |
GET ?run=&record=&limit=: the recorded decisions that one learned record replaces another, newest first (goclaw memory revisions). |
/api/memory/revisions/undo |
POST {"record":"<memory-id>"} or {"run":"<run-id>"}: restore what a decision retired; an undone decision is not made again (goclaw memory revisions undo). |
/api/memory/backfill-metadata |
Populate missing memory metadata. |
/api/memory/notes |
Read or update agent notes. |
/api/memory/export |
Export memory. |
/api/memory/import |
Import memory. |
/api/memory/graph |
Return knowledge graph data. |
GET /api/searchSearches memory and archived material. Common parameters include q and
source.
| Endpoint | Purpose |
|---|---|
GET /api/agents |
List configured agents and bindings. Each agent carries tool_policy: its effective tool, shell, HTTP and git policy and the keys it overrides. |
GET|POST|DELETE /api/skills |
GET lists installed and built-in skills. POST takes an action: install, toggle (turn a skill on or off), verify or save. DELETE ?name=<skill> removes a user-installed skill. |
GET|POST /api/skills/quality |
A skill's quality score: GET ?name=<skill> returns the cached score or evaluates it; POST {"name","force":true} evaluates it again. |
GET /api/tools |
List registered tools. |
POST /api/rag-test |
{"query"}: show which tools and skills Tool RAG and Skill RAG would select for it, with their scores. |
GET|POST /api/mcp |
GET lists the configured MCP servers. POST takes an action: test connects a draft definition and lists its tools, call runs one tool, save writes the definition into config.yaml, delete removes it. |
| Endpoint | Purpose |
|---|---|
/api/cron |
GET lists cron jobs with their next run. POST {"action","id"} with run, enable, disable, update (with "schedule") or delete; a system job (sys-…) cannot be deleted, only disabled. Jobs are created with the agent's cron tool; this API has no create. |
/api/sop |
Manage SOP definitions. GET lists them, or with ?id= returns one with cron_schedule, its cron job's schedule. POST saves one: on an existing SOP, a field the body leaves out (top level, or in a step with the same id) keeps its stored value, and a changed definition archives the previous version. A cron_schedule in the body creates or reschedules the SOP's cron job; one that cannot be applied is returned as warning. DELETE ?id= removes one. |
/api/sop/executions |
List SOP executions. |
/api/sop/run |
POST {"id"}: start a run of an SOP in the background; the answer has its execution_id. A paused SOP, or one overdue for review, is answered 409 with the reason. |
/api/sop/approve |
POST {"job_id","action":"approve"|"reject","reason"}: approve or reject an SOP step that is waiting for approval. A step nobody decides fails after 1 hour. |
/api/sop/validate |
Validate an SOP definition. |
/api/sop/channels |
Pin SOP notification channels. |
/api/capability-requests |
GET lists capability requests; ?status= filters them. |
/api/capability-requests/resolve |
POST {"id","status","resolved_by","note"}, with status resolved or rejected. |
| Endpoint | Purpose |
|---|---|
POST /api/webhook |
Generic inbound message endpoint. See POST /api/webhook. |
GET /api/hooks |
Registered gateway hook events (built-in and Go extensions) with their handlers. The Hooks page's rules are /api/hooks/rules (see Other Routes). |
GET|POST /api/webhooks |
List webhook routes (GET); create, update, enable, disable or delete a stored route (POST with action). See Webhook routes API. |
GET|DELETE /api/plugins |
GET lists the installed agent plugins (the Plugins page); DELETE ?name=<plugin> turns one off for every agent and removes it. |
GET /api/extensions |
Discovered Go/OpenClaw extensions (extensions/ directories) and which are active. |
GET /api/audit |
Query webhook audit logs: ?channel=, ?event=, ?user=, ?limit= (default 100, at most 500). With ?kind=admin, the administrative trail (SOP deletes, channel pins) instead: ?action=, ?actor=, ?resource=, ?limit=. |
POST /goclaw/hook/<type>[/<key>] |
The webhook receiver outside systems (GitLab, GitHub, Graylog, Zabbix, …) deliver events to. See Webhook receiver. |
When the Teams or Facebook channel, or the Telegram channel in webhook mode, is
enabled, its webhook is mounted on the webhook listener at /hook/msteams,
/hook/facebook or /hook/telegram, or at /hook/<webhook_secret_slug>/<channel>
when gateway.webhook_secret_slug is set.
POST /api/webhookRuns one message on the default agent and answers with the whole reply. It needs
the gateway token on every port, a separate gateway.ingress.port included:
{"channel": "discord", "account_id": "discord", "peer_id": "u_123456", "content": "Hello"}
{"content": "Hi! How can I help?"}
The session is p2p:<peer_id>:<default agent>, and the message runs with the
role GoClaw resolves for that channel and peer_id, as for a message from that
channel, or, when account_id is cron:<user ID> (as cron agent jobs send it),
with that user's role. With
"channel": "web" it runs in admin-session as the admin (role system).
With "subject_ref": "<user ID>" (a cron agent … job with a subject sends
it) the message runs as that person instead, at role user with a
delegation, on every channel, web included: account_id, the peer's pairing
and memory.write_access do not set its role, so directives are refused. Its
session is p2p:<peer_id>:<default agent>:for:<user ID>.
goclaw message <channel> <text> posts here. A message sent while an earlier one
in the same session is still being answered waits for it; if the request ends
while it waits, the answer is 409. Reset triggers, directives and /subagents
commands work as in POST /api/chat, except that off the web
channel @config:, @notes: and @sop: need that role to be admin.
goclaw message <channel> sends as peer cli, which is paired with no one, so
a directive goes through it only on web (the default). An agent error is
answered 500 with the error text.
/api/webhooks needs the gateway token (X-Goclaw-Token: <token> or
Authorization: Bearer <token>).
GET lists every route: the stored ones, and the config.yaml ones with
"source": "config" and "id": 0, which cannot be changed here. Secrets are
masked; secret_set says whether the route has a secret or token at all.
hit_count, last_hit_at and last_error are kept for stored routes only.
last_error is the latest refusal or drop (its time, status and why: a
delivery refused, or taken and then dropped unrun); the next delivery that gets
through clears it.
POST takes a JSON body with an action:
curl -X POST https://<gateway>/api/webhooks \
-H "Authorization: Bearer $GOCLAW_TOKEN" -H "Content-Type: application/json" \
-d '{"action":"create","type":"graylog","key":"payments-errors","agent":"main","sop_id":"incident-triage"}'
{"ok": true, "id": 7, "agent": "main", "generated_secret": "whsec_…"}
| Field | Actions | Meaning |
|---|---|---|
action |
all | create, update, enable, disable or delete. |
id |
update, enable, disable, delete |
The stored route's id from GET. |
type |
create (required), update |
First path segment of /goclaw/hook/<type>/<key>. On update, empty keeps the stored type. |
key |
create, update |
Second path segment. Empty makes the type's route for any key that has no route of its own. |
agent |
create, update (required) |
Agent ID or name, any case, or * for every agent. An unknown agent is refused with 400, naming the agents there are. |
secret, token |
create, update |
On create, leave both out to have a whsec_… secret generated: it is returned once, as generated_secret, and not shown again. On update, an empty value keeps the stored one. |
events |
create, update |
Comma-separated event names, e.g. "push,merge_request"; empty takes every event. |
sop_id, batch_mode, batch_window |
create, update |
Run a SOP per event, or per batch. On update, an empty batch_mode or a batch_window of 0 keeps the stored value. |
The fields mean what they mean in config.yaml: see
Webhook Routes.
update writes type, key, agent, events and sop_id as sent, so send
the route's current values along with the change: an empty key, events or
sop_id clears it. It does not change whether the route is paused.disable pauses a route: its deliveries are answered 404, and each is noted
on the route. enable resumes it.delete removes the route and what was set up for it on the sender's side: the
GitLab project hook goclaw gitlab hook registered, or the Graylog event
definition and notification graylog_alert created. The response's notice
says what was removed, or what is left to delete by hand.POST /goclaw/hook/<type>[/<key>] is where outside systems deliver events. It is
served on the webhook listener: gateway.ingress.port when that is set, else the
main port. The route is the one for this type and exact key, else the type's route
without a key; between a stored route and a config.yaml route for the same key,
the stored one. How each type authenticates (GitLab token, GitHub HMAC, otherwise
X-Token, ?token= or an X-Signature HMAC) is in
Webhook Routes.
| Status | When |
|---|---|
200 {"status":"ok", …} |
The event was taken: a SOP run started (or the event queued, for a bulk route or behind the run in progress for the same object), or the agent will answer it. dispatched counts the agents it reached. |
200 {"status":"filtered"} |
The route's events leave this event out. Nothing runs. |
200 {"status":"duplicate"} |
The route took a delivery with this ID before (a retry, a redelivery). Nothing runs again. |
200 {"status":"test"} |
Graylog's test notification, on a graylog route. Nothing runs. |
401 |
The credential is missing or does not match, or the route has none; the body says which. |
403 |
The sender's IP is not on gateway.ingress.allow_ips / allow_preset (only when an allowlist is set; loopback is always let through). |
404 |
No route for the type and key, or the route is paused. |
409 |
The route's SOP cannot run: it was deleted or disabled, or it is overdue for review. |
413 |
The body is larger than 4 MB. |
429 |
The sender's IP is over gateway.rate_limit. |
500 |
The event was not taken, for example the SOP run could not be started. Send it again. |
503 |
Too many deliveries are in progress, or 500 events already wait for the same object's run (Retry-After: 30), or another request is taking this same delivery right now (Retry-After: 10); sent again, it is a repeat or it runs. Also when the delivery's ID could not be recorded (Retry-After: 10): run unrecorded, a resend could not be told from it. |
A refused delivery to a stored route is noted on the route (last_error, and on
the Webhooks page); for a config.yaml route, only the gateway log has it
([webhook] lines). The 503 for a delivery another request is taking is only
in the gateway log: sent again, that delivery is a repeat or runs, as the other
request ended.
POST /api/media/uploadUploads a file, sent as the multipart form field file, to the media store
(<state dir>/media) and returns an ID, URL, filename, MIME type, size, and
creation time.
GET|HEAD /media/<id>Serves a media-store object. Images, audio, video, PDF and plain text are served
inline with their stored content type; anything else (HTML, SVG, XML, scripts,
unknown types) is served as a download (application/octet-stream).
POST /api/upload-workspaceUploads a file, sent as the multipart form field file, directly into
workspace/uploads. Web chat uses this endpoint so tools can access uploaded
files by workspace-relative path.
The response includes:
{
"id": "timestamp-filename.png",
"local_path": "uploads/timestamp-filename.png",
"filename": "filename.png",
"url": "/uploads/timestamp-filename.png",
"mime_type": "image/png",
"size": 12345
}
GET /uploads/<file>Serves files from workspace/uploads. Images, audio, video, PDF, .txt and
.log files are served inline; any other file, or one without an extension, is
served as a download.
GET /api/canvasLists canvas artifacts, each as {name, size, mod_time}.
GET /canvas/<file>Serves generated canvas artifacts from <state dir>/canvas, inline or as a
download by the same rule as /uploads/. Artifacts are removed after 48 hours.
GET /goclaw/view/<slug>Serves an HTML page the agent made (generate_html). It needs the page's view
token (?token=<view_token> or an X-View-Token header) or the gateway token.
/goclaw/view/ without a slug lists the pages and needs the gateway token.
The page is served sandboxed (Content-Security-Policy: sandbox allow-scripts …, without allow-same-origin), so the browser treats it as an origin of its
own: its scripts run, but no dashboard cookie goes with its requests, and it
cannot post forms, open frames, use fetch, or read localStorage or
document.cookie.
POST /api/sendSends a message to a connected channel, without the agent:
{"channel": "discord", "target": "<channel or user ID>", "text": "Hello"}
All three fields are required. It needs the gateway token and accepts only
requests from a loopback address (127.0.0.1, ::1); any other is answered
403. A channel that is not connected is answered 404, with the connected
channels in available.
GET /api/metrics/tokensReturns token usage summaries. Optional filters include agent_id, channel,
provider, model, date_from, date_to, and limit (1 to 500, default
50).
| Endpoint | Purpose |
|---|---|
GET /api/traces |
Agent traces, newest first. Filters: session_id, agent_id, channel, status, tag, sop_id, from and to (unix seconds, RFC 3339 or YYYY-MM-DD), min_tokens (prompt and output together), before (unix seconds, for paging) and limit (default 50, at most 200). sort=tokens lists the traces that used the most tokens first, paged by offset instead of before. A SOP run's trace is filed under the agent that ran it; sop_id finds a SOP's runs. A scheduled built-in or shell command, or a schedule whose SOP is gone, has no agent: find its job's runs by session_id (cron:<job id>); GET /api/traces/<id> also gives the job as cron_id. |
GET /api/traces/stats |
Trace counts (total, done, error) and averages of duration, tool calls and tokens. |
GET /api/traces/<id> |
One trace with its steps, tool calls and timings. |
GET /api/traces/<id>.csv |
The same trace as a CSV download. |
POST /api/traces/<id>/feedback |
{"score": 1 or -1, "comment", "user_id"}: rate a trace. |
GET /api/logs |
The gateway log as Server-Sent Events, one line per event. |
| Endpoint | Purpose |
|---|---|
GET /api/sbom, GET /api/admin/sbom |
The software bill of materials: ?format=cyclonedx (default), spdx or summary; ?download=1 sends it as a file. See SBOM. |
Web UI only: /api/agents/profiles, /api/agents/profiles/draft, /api/agents/instructions, /api/agents/instructions/draft, /api/agents/persona/draft, /api/agents/constraints, /api/agents/preview, /api/hooks/rules, /api/plugins/{inspect,install,agents,approve,inputs,fetch,install-fetched,marketplaces,marketplaces/browse,check-updates,export}, /api/memory/wiki and /api/memory/wiki/{history,restore,distill,attachments,search} |
The Agents, Instructions, Hooks, Plugins and Wiki pages of the Web UI. Their request shapes follow those pages and are not described here. They need the gateway token like the rest. |
POST /api/app/chat and GET /api/app/events serve the Enterprise App API
channel. They check the app's own credential, not the gateway token, and
/ws/app answers 410 Gone. See
App API.
POST /v1/chat/completionsOpenAI-compatible chat completions endpoint. See OpenAI Compatibility.
GET /health, GET /healthz, GET /ready, GET /readyzLightweight health probes that return {"status":"ok","uptime_seconds":…}.
These endpoints are intentionally unauthenticated. When gateway.ingress.port
gives webhooks their own listener, they are served there too.