OpenClaw OpenResponses API: Let Existing Clients Talk to Your Gateway
Read from search, close with the playbook
If this post helped, here is the fastest path into the full operator setup.
Search posts do the first job. The preview, homepage, and full playbook show how the pieces fit together when you want the whole operating system.
A lot of agent infrastructure dies at the integration layer. The agent works in chat. The workflow works in the terminal. Then somebody asks whether an existing app, internal dashboard, RAG tool, or OpenAI-compatible client can call the same operator without learning OpenClaw's native protocol.
That is where the OpenResponses HTTP endpoint is useful. OpenClaw's Gateway can serve an OpenResponses-compatible POST /v1/responses endpoint on the same Gateway port as the normal WebSocket and HTTP surfaces. It is disabled by default, and that default is correct. Once enabled, requests run through the normal Gateway agent path, so routing, permissions, sessions, tools, and config are the same control plane your operator already uses.
The important part is not "OpenAI-compatible." The important part is that existing clients can talk to your Gateway without turning the Gateway into a random public chatbot. This is an operator-access surface. Treat it that way.
Enable it deliberately
The docs name the switch directly: gateway.http.endpoints.responses.enabled. OpenClaw reads config from ~/.openclaw/openclaw.json by default, and the config docs remind operators that strict validation is enforced. Unknown keys or malformed values can keep the Gateway from starting, so this is not a place for guesswork.
{
gateway: {
http: {
endpoints: {
responses: {
enabled: true
}
}
}
}
} After this is enabled, the endpoint is available at http://<gateway-host>:<port>/v1/responses. On the default local Gateway port, that means http://127.0.0.1:18789/v1/responses.
I would start on loopback, then move to tailnet or private ingress only after the local smoke test is boring. If the endpoint will be reachable by a browser or service outside the host, solve authentication, network exposure, origin policy, and logging before you wire a production client into it.
Use agent targets, not raw provider models
The OpenAI-compatible Gateway surface uses an agent-first model contract. The model field selects an OpenClaw agent target, not a backend model id. The documented targets are openclaw, openclaw/default, and openclaw/<agentId>. You can also use x-openclaw-agent-id as a compatibility override.
That is a good design. Your client should not need to know whether the selected agent uses GPT, Claude, a local model, or a future fallback chain. The client is asking the operator named support or the default agent to do work. The Gateway decides how that agent runs.
curl -sS http://127.0.0.1:18789/v1/responses \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-H 'x-openclaw-agent-id: main' \
-d '{
"model": "openclaw",
"input": "Summarize the current operator handoff."
}' If you need to override the backend model for a request, the docs put that in x-openclaw-model. On shared-secret auth paths, that is treated as an owner-level capability. On identity-bearing HTTP paths such as trusted proxy auth, the caller needs operator.admin for owner-level controls like model override. Do not hand that header to random app clients.
Route sessions on purpose
By default, the OpenResponses endpoint is stateless per request. OpenClaw generates a new session key for each call. That is the safest default for one-off integrations, but it is not what you want for a real workflow where the same dashboard user keeps asking follow-up questions about one ticket or one account.
The docs give you two stable routes. A request can include an OpenResponses user string, and OpenClaw derives a stable session key from it. Or the caller can set x-openclaw-session-key explicitly. Use explicit keys when you want clear isolation, reproducible debugging, or per-tenant routing from an internal backend.
curl -N http://127.0.0.1:18789/v1/responses \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-H 'x-openclaw-agent-id: support' \
-H 'x-openclaw-session-key: acme-support-demo' \
-H 'x-openclaw-message-channel: webchat' \
-d '{
"model": "openclaw/support",
"stream": true,
"input": "Draft a reply for this support ticket."
}' x-openclaw-message-channel is also useful when your agent policies care about ingress context. It lets the HTTP request carry a synthetic channel label instead of pretending every call came from the same default surface.
The request shape is item-based
The supported OpenResponses request shape centers on input. That input can be a string or an array of item objects. Message items can use system, developer, user, or assistant roles. System and developer items are appended to the system prompt; the most recent user or function_call_output item becomes the current message, and earlier user or assistant items are included as history.
The endpoint also accepts instructions, tools, tool_choice, stream, max_output_tokens, temperature, top_p, and user. Temperature and top-p are best-effort provider fields; the docs call out that the ChatGPT-based Codex Responses backend uses fixed server-side sampling and ignores those sampling knobs.
Some fields are accepted for schema compatibility but ignored: max_tool_calls, reasoning, metadata, store, and truncation. That matters when you are porting an existing client. If your application depends on one of those fields having provider-native semantics, do not assume the Gateway has adopted that behavior just because the request is accepted.
Client tools are a turn loop
OpenResponses client tools are caller-supplied function tools. The docs show tools entries shaped as { type: "function", name, description?, parameters? }. If the agent decides to call one, the response returns a function_call output item. Your client runs the function, then sends a follow-up request with function_call_output and the matching call_id.
{
"model": "openclaw/default",
"input": [
{
"type": "message",
"role": "user",
"content": "Check order 1842 and tell me what to do next."
}
],
"tools": [
{
"type": "function",
"name": "lookup_order",
"description": "Look up order status by id",
"parameters": {
"type": "object",
"properties": {
"order_id": { "type": "string" }
},
"required": ["order_id"]
}
}
],
"tool_choice": "auto"
} tool_choice can be auto, none, required, or a pinned function. For required and pinned function choices, the endpoint narrows the exposed client function-tool set and rejects the turn if the model does not produce a matching structured client-tool call. Non-streaming requests return a 502 with an api_error; streaming requests emit response.failed.
That contract applies to the HTTP caller's supplied tools, not every internal OpenClaw agent tool. Keep that boundary clear. Client tools are how your app participates in the turn. Internal OpenClaw tools still belong to the selected agent and its normal policy.
Turning a Gateway endpoint into a real operator surface?
ClawKit gives you the practical runbooks for auth boundaries, routing, session proof, cron safety, and buyer-ready agent workflows. Get ClawKit for $9.99.
Files and images are useful, but not casual
The OpenResponses endpoint supports input_image with base64 or URL sources. Current image MIME types include JPEG, PNG, GIF, WebP, HEIC, and HEIF, with a 10MB default max. HEIC and HEIF sources are accepted when a system converter is available and are normalized to JPEG before provider delivery.
input_file supports base64 or URL sources for text, Markdown, HTML, CSV, JSON, and PDF. The current default file max is 5MB with a 200k character limit. File content is decoded and added to the system prompt, not persisted as normal user-message history. The live docs also say decoded file text is wrapped as untrusted external content with explicit boundary markers and a Source: External metadata line.
That last detail is worth caring about. Files are data, not trusted instructions. If your client lets a user upload a PDF, that PDF should not get to rewrite the agent's rules just because it contains persuasive text.
{
gateway: {
http: {
endpoints: {
responses: {
enabled: true,
maxBodyBytes: 20000000,
maxUrlParts: 8,
files: {
allowUrl: true,
urlAllowlist: ["cdn.example.com"],
maxBytes: 5242880,
maxChars: 200000
},
images: {
allowUrl: true,
urlAllowlist: ["images.example.com"],
maxBytes: 10485760
}
}
}
}
}
} URL fetch defaults are permissive enough to be useful: files.allowUrl and images.allowUrl default to true, with maxUrlParts at 8 across URL-based file and image parts. Requests are guarded with DNS resolution, private IP blocking, redirect caps, and timeouts. You can add hostname allowlists for files and images. Exact hosts and wildcard subdomains are supported, and wildcard subdomains do not match the apex.
If this endpoint is reachable beyond localhost, I would use allowlists. The docs explicitly warn that allowlisting a hostname does not bypass private or internal IP blocking, and that internet-exposed Gateways should use network egress controls in addition to app-level guards.
Stream only when the client can handle it
Set stream: true to receive Server-Sent Events. The response uses Content-Type: text/event-stream, emits event lines with JSON payloads, and ends with data: [DONE]. Current event types include response.created, response.in_progress, response.output_item.added, response.content_part.added, response.output_text.delta, response.output_text.done, response.content_part.done, response.output_item.done, response.completed, and response.failed.
Streaming is great for chat-style user experience. It is also one more state machine. If your integration is a nightly backend task or a webhook that just needs a final answer, start non-streaming. Add SSE after you have retry, cancellation, timeout, and error handling nailed down.
Authentication is the real product decision
The docs are direct: treat this endpoint as full operator access for the Gateway instance. Shared-secret auth uses Authorization: Bearer <token-or-password> for gateway.auth.mode="token" or "password". With those shared-secret modes, OpenClaw ignores narrower bearer-declared x-openclaw-scopes values and restores the full default operator scope set.
Trusted identity-bearing HTTP modes behave differently. Trusted proxy auth, or private-ingress gateway.auth.mode="none", can honor x-openclaw-scopes when present and otherwise fall back to the normal operator defaults. The docs say owner semantics are lost only when the caller explicitly narrows scopes and omits operator.admin.
In plain English: if you hand a normal app a Gateway token, you handed it operator power. If you want narrower app behavior, put a trusted backend or identity-aware proxy in front of the Gateway, scope what that backend can do, and keep the Gateway off the public internet.
How I would ship it
For an internal dashboard, I would put the Gateway on loopback or private network access, enable /v1/responses, route calls through a backend that owns the Gateway credential, and expose only the app-specific actions to the browser. The browser should not hold the Gateway operator token.
For Open WebUI-style clients, I would first test /v1/models and use openclaw/default as the model id. If the client needs an agent-specific target, use openclaw/<agentId>. If it needs OpenResponses rather than Chat Completions, point it at /v1/responses and keep the same auth boundary.
For production, I would log the selected agent, session key, synthetic channel, stream/non-stream mode, and whether files or URL parts were included. I would not log raw secrets, full uploaded file bytes, or bearer tokens. The goal is enough evidence to debug a bad run without turning the audit trail into a data leak.
The OpenResponses endpoint is a bridge. It lets existing clients reach the same OpenClaw operator you already trust. Just remember what is on the other side of the bridge: sessions, tools, files, model routing, and real operator authority. Build the guardrails first.
Want the complete guide? Get ClawKit — $9.99