# Server-Side Tool Calling over the API Source: https://docs.openwebui.com/reference/server-side-tool-calling **Send a prompt to Open WebUI, have Open WebUI run the tools, and get the finished answer back.** This page covers the exact requests you need when you want the _server_ to do the agentic work: the model decides to call a tool, Open WebUI executes it with your user's credentials and permissions, feeds the result back to the model, and repeats until the model is done. Your script never sees a `tool_calls` object it has to execute itself. That includes: - **[Built-in tools](/features/extensibility/plugin/tools#built-in-system-tools-nativeagentic-mode)**: web search, knowledge and file browsing, code interpreter, notes, channels, memory, calendar, automations, sub-agents, image generation. - **[Workspace tools](/features/extensibility/plugin/tools)**: your own Python tools. - **[MCP servers](/features/extensibility/mcp)** and OpenAPI tool servers configured in Open WebUI. - **[Open Terminal](/features/open-terminal)**: the terminal server the model or your user has access to. --- ## Before you start | You need | Where | | --- | --- | | An API key | API Keys. An admin has to enable the global API Keys toggle first, and non-admins also need the API Keys feature permission. | | The permissions the tools require | The key acts as you. Web search, code interpreter, image generation, notes, channels, memory and file upload are each gated by a user permission as well as by the global admin toggle. If your user cannot do it in the browser, the key cannot do it either. | | A model with Native function calling | Tool Calling Modes. Native is the default and the only mode that supports built-in tools and multi-round loops. | | A model whose capabilities are not switched off | Built-in tools respect the per-model Builtin Tools categories and capabilities. See Disabling Builtin Tools (Per-Model). | > **Reverse proxy already using Authorization?** > > Send the key in `x-api-key` instead, or rename the header with [`CUSTOM_API_KEY_HEADER`](/reference/env-configuration#custom_api_key_header). See [API Keys](/features/authentication-access/api-keys#behind-a-reverse-proxy-that-consumes-authorization). --- ## The one rule that decides your whole integration `POST /api/chat/completions` behaves in three different ways depending on which fields you send. Pick the row you want _before_ you write any code. | What you send | Who runs the tools | Where the answer comes back | Built-in tools | | --- | --- | --- | --- | | chat_id + id + stream: true (+ session_id) | Open WebUI, full multi-round native loop | The chat record (and the socket). The HTTP body is empty. | ✅ with session_id | | params.function_calling: "legacy" + tool_ids | Open WebUI, one round, before the model answers | The HTTP response body, normal OpenAI shape | ❌ | | Neither of the above | Nobody. Raw tool_calls are handed back to you | The HTTP response body | ❌ | The reason is structural, not a setting: the native tool-execution loop lives in the same code path that streams events into a chat, so it only runs when the request identifies a chat (`chat_id`) and an assistant message (`id`), and it only runs for `stream: true`. That path writes the result into the chat instead of the HTTP response. So there are two practical integrations, described below: - **[Path A](#path-a-full-agentic-loop)**, the full agentic loop. More calls, everything works. - **[Path B](#path-b-one-request-answer-in-the-body)**, one request in, one completion out. Simpler, but one round of tools and no built-in tools. --- ## Discover what you can call Run these once and cache the IDs. ``` # Models curl -s -H "Authorization: Bearer $OWUI_KEY" $OWUI_URL/api/models | jq '.data[].id' # Workspace tools you have access to (use .id as a tool_ids entry) curl -s -H "Authorization: Bearer $OWUI_KEY" $OWUI_URL/api/v1/tools/ | jq '.[] | {id, name}' # Terminal servers you have access to (use .id as terminal_id) curl -s -H "Authorization: Bearer $OWUI_KEY" $OWUI_URL/api/v1/terminals/ | jq '.[] | {id, name}' ``` MCP servers are addressed as tool IDs of the form `server:mcp:`. See [Using Open WebUI tools, including MCP, from the API](/reference/api-endpoints#using-open-webui-tools-including-mcp-from-the-api). > **The model's default terminal is a UI default, not a server default** > > When you pick a model in the browser, the frontend reads `meta.terminalId` off the model and puts it in the request for you. The backend does **not** do that. An API caller must send `terminal_id` explicitly, otherwise the model gets no terminal tools even if the model is configured with one. > > > > You can read a model's configured terminal with `GET /api/v1/models/model?id=` and use `meta.terminalId`, or just pick one from `/api/v1/terminals/`, which already lists only the terminals your user is allowed to use. --- ## Path A: full agentic loop Four calls. This is what the web UI does, minus the browser. ### A1. Create the chat The completion has to attach to a chat and an assistant message that already exist. Create both up front: ``` USER_MSG_ID=$(uuidgen) ASSISTANT_MSG_ID=$(uuidgen) TS=$(date +%s) CHAT_ID=$(curl -s -X POST $OWUI_URL/api/v1/chats/new \ -H "Authorization: Bearer $OWUI_KEY" \ -H "Content-Type: application/json" \ -d "{\"chat\": { \"title\": \"API run\", \"models\": [\"gpt-4o\"], \"history\": { \"currentId\": \"$ASSISTANT_MSG_ID\", \"messages\": { \"$USER_MSG_ID\": {\"id\": \"$USER_MSG_ID\", \"role\": \"user\", \"content\": \"Search the web for the latest Open WebUI release and summarise it.\", \"timestamp\": $TS, \"models\": [\"gpt-4o\"], \"childrenIds\": [\"$ASSISTANT_MSG_ID\"]}, \"$ASSISTANT_MSG_ID\": {\"id\": \"$ASSISTANT_MSG_ID\", \"role\": \"assistant\", \"content\": \"\", \"parentId\": \"$USER_MSG_ID\", \"childrenIds\": [], \"model\": \"gpt-4o\", \"modelName\": \"gpt-4o\", \"modelIdx\": 0, \"done\": false, \"timestamp\": $((TS + 1))} } } }}" | jq -r '.id') ``` The exact message-tree fields (`childrenIds`, `currentId`, `parentId`) matter if you also want the chat to render correctly in the browser. [Backend-Controlled API Flow](/reference/api-flow) documents that structure in full. ### A2. Fire the completion ``` curl -s -X POST $OWUI_URL/api/chat/completions \ -H "Authorization: Bearer $OWUI_KEY" \ -H "Content-Type: application/json" \ -d "{ \"model\": \"gpt-4o\", \"messages\": [{\"role\": \"user\", \"content\": \"Search the web for the latest Open WebUI release and summarise it.\"}], \"stream\": true, \"chat_id\": \"$CHAT_ID\", \"id\": \"$ASSISTANT_MSG_ID\", \"session_id\": \"api-$(uuidgen)\", \"features\": {\"web_search\": true, \"code_interpreter\": true, \"image_generation\": false, \"memory\": true}, \"tool_ids\": [\"my_workspace_tool\", \"server:mcp:my_mcp_server\"], \"terminal_id\": \"open-terminal\", \"background_tasks\": {\"title_generation\": false, \"tags_generation\": false, \"follow_up_generation\": false} }" ``` Response: ``` {"status": true, "task_ids": ["..."], "chat_id": "..."} ``` That is not the answer. It means the work was accepted and is running server-side. Field by field: | Field | Why it is there | | --- | --- | | stream: true | Required. The native tool loop only exists on the streaming path. | | chat_id + id | Required. Without both, no tools are executed server-side at all. | | session_id | The built-in tools switch. Any non-empty string works. Without it, built-in tools are not offered to the model. It also makes the request asynchronous (you get task_ids instead of blocking). | | features | Turns on the four togglable built-in groups: web_search, code_interpreter, image_generation, memory. The other built-ins (knowledge, files, notes, channels, calendar, automations, chats, time, tasks, sub-agents) need no flag and are offered whenever their global setting, your permission and the model's category allow it. | | tool_ids | Workspace tools and MCP servers. Optional. | | terminal_id | Open Terminal server. Optional, and independent of session_id. | | background_tasks | Turn title, tag and follow-up generation off unless you want the extra model calls. | > **Do not send your own tools array** > > If the request body contains a `tools` key, Open WebUI skips all server-side tool resolution and forwards your definitions to the model verbatim. Sending `"tools": []` is the documented way to opt out of built-in injection entirely. ### A3. Wait for it to finish ``` while [ "$(curl -s -H "Authorization: Bearer $OWUI_KEY" \ "$OWUI_URL/api/tasks/chat/$CHAT_ID" | jq '.task_ids | length')" != "0" ]; do sleep 2 done ``` ### A4. Read the answer ``` curl -s -H "Authorization: Bearer $OWUI_KEY" "$OWUI_URL/api/v1/chats/$CHAT_ID" \ | jq -r ".chat.history.messages[\"$ASSISTANT_MSG_ID\"].content" ``` `content` is the plain text. The same message also carries `output` (the structured item list: reasoning blocks, tool calls, code interpreter runs), `usage` and `sources`, if you want more than the prose. ### A5. If you did not want the chat saved Path A needs a real chat to write into, so the honest answer for "API request and response only" is to delete it when you are done: ``` curl -s -X DELETE -H "Authorization: Bearer $OWUI_KEY" "$OWUI_URL/api/v1/chats/$CHAT_ID" ``` > **The temporary: alternative** > > A `chat_id` of `temporary:` also runs the full loop and persists nothing at all. But then there is nowhere to read the result from over HTTP: the output only goes out over the socket. Use it only if your client is a real socket.io client. For plain HTTP, create and delete. ### Blocking variant (no `session_id`) Drop `session_id` and the request stops being asynchronous: the HTTP call blocks until the whole tool loop is finished, then returns `null`. You still read the answer from the chat, but you skip the polling in A3. The trade is that **built-in tools are not available** in this mode. Use it when `tool_ids`, `terminal_id` and MCP are all you need. --- ## Path B: one request, answer in the body If you want a single HTTP request that returns a normal OpenAI completion with tool results already baked in, switch that request to legacy function calling: ``` curl -s -X POST $OWUI_URL/api/chat/completions \ -H "Authorization: Bearer $OWUI_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o", "messages": [{"role": "user", "content": "What does the my_workspace_tool say about order 4711?"}], "stream": false, "params": {"function_calling": "legacy"}, "tool_ids": ["my_workspace_tool", "server:mcp:my_mcp_server"], "terminal_id": "open-terminal" }' ``` What happens: Open WebUI asks the task model which tools to call, executes them server-side with your permissions, injects the results into the prompt as context, then runs the real completion. Nothing is persisted, nothing needs a chat, and the response is the ordinary object your HTTP client expects, plus a top-level `sources` array holding each tool result. Set `"stream": true` for the same thing as SSE. The tool round happens before the first token, so the stream starts once the tools are done. The `sources` payload arrives as the first `data:` frame, ahead of the model deltas. Limits, in exchange for the simplicity: - **One round of tool calls.** The model cannot look at a result and decide to call something else. - **No built-in tools.** No web search, no code interpreter, no knowledge browsing, no terminal-driven agentic work. - Legacy mode is **deprecated** and depends on a task model that reliably emits JSON. See [Tool Calling Modes](/features/extensibility/plugin/tools#tool-calling-modes-default-vs-native). --- ## What you actually get back, by mode | Request | HTTP response | | --- | --- | | Path A with session_id | {"status": true, "task_ids": [...], "chat_id": "..."}, immediately | | Path A without session_id | null, after the full loop finishes | | Path B, stream: false | Normal chat completion object, plus sources | | Path B, stream: true | SSE: a sources frame, then standard OpenAI deltas, then [DONE] | | Native mode without chat_id/id | Standard OpenAI response containing unexecuted tool_calls | --- ## Reusable script Drop-in Python. Set the four constants at the top and call `run()`. ``` """Run a prompt through Open WebUI with server-side tool calling.""" import json import time import uuid import requests OWUI_URL = "http://localhost:3000" OWUI_KEY = "sk-..." MODEL = "gpt-4o" DELETE_CHAT_WHEN_DONE = True # False keeps the conversation in the user's chat list def _headers(): return {"Authorization": f"Bearer {OWUI_KEY}", "Content-Type": "application/json"} def run( prompt, tool_ids=None, terminal_id=None, features=None, system=None, builtin_tools=True, timeout=600, ): """Returns the finished assistant message as a dict (content, output, usage, sources).""" user_msg_id, assistant_msg_id = str(uuid.uuid4()), str(uuid.uuid4()) now = int(time.time()) chat = requests.post( f"{OWUI_URL}/api/v1/chats/new", headers=_headers(), json={ "chat": { "title": "API run", "models": [MODEL], "history": { "currentId": assistant_msg_id, "messages": { user_msg_id: { "id": user_msg_id, "role": "user", "content": prompt, "timestamp": now, "models": [MODEL], "childrenIds": [assistant_msg_id], }, assistant_msg_id: { "id": assistant_msg_id, "role": "assistant", "content": "", "parentId": user_msg_id, "childrenIds": [], "model": MODEL, "modelName": MODEL, "modelIdx": 0, "done": False, "timestamp": now + 1, }, }, }, } }, ) chat.raise_for_status() chat_id = chat.json()["id"] messages = ([{"role": "system", "content": system}] if system else []) + [ {"role": "user", "content": prompt} ] body = { "model": MODEL, "messages": messages, "stream": True, # required, the native tool loop is streaming-only "chat_id": chat_id, "id": assistant_msg_id, "background_tasks": { "title_generation": False, "tags_generation": False, "follow_up_generation": False, }, "features": features or { "web_search": True, "code_interpreter": True, "image_generation": False, "memory": False, }, } if builtin_tools: body["session_id"] = f"api-{uuid.uuid4()}" # unlocks the built-in tools if tool_ids: body["tool_ids"] = tool_ids if terminal_id: body["terminal_id"] = terminal_id started = requests.post( f"{OWUI_URL}/api/chat/completions", headers=_headers(), json=body, timeout=timeout ) started.raise_for_status() deadline = time.time() + timeout while time.time() < deadline: tasks = requests.get( f"{OWUI_URL}/api/tasks/chat/{chat_id}", headers=_headers() ).json() if not tasks.get("task_ids"): break time.sleep(2) else: raise TimeoutError(f"chat {chat_id} did not finish within {timeout}s") final = requests.get(f"{OWUI_URL}/api/v1/chats/{chat_id}", headers=_headers()) final.raise_for_status() message = final.json()["chat"]["history"]["messages"][assistant_msg_id] if DELETE_CHAT_WHEN_DONE: requests.delete(f"{OWUI_URL}/api/v1/chats/{chat_id}", headers=_headers()) return message def run_single_request(prompt, tool_ids=None, terminal_id=None, stream=False): """Path B: one call, completion in the body. One tool round, no built-in tools.""" body = { "model": MODEL, "messages": [{"role": "user", "content": prompt}], "stream": stream, "params": {"function_calling": "legacy"}, } if tool_ids: body["tool_ids"] = tool_ids if terminal_id: body["terminal_id"] = terminal_id if not stream: response = requests.post( f"{OWUI_URL}/api/chat/completions", headers=_headers(), json=body ) response.raise_for_status() return response.json() with requests.post( f"{OWUI_URL}/api/chat/completions", headers=_headers(), json=body, stream=True ) as response: response.raise_for_status() for line in response.iter_lines(decode_unicode=True): if line and line.startswith("data: "): payload = line[6:] if payload == "[DONE]": return yield json.loads(payload) if __name__ == "__main__": result = run( "Search the web for the newest Open WebUI release, then write a three-bullet summary.", terminal_id=None, tool_ids=None, ) print(result["content"]) ``` To adapt it: - **Different tools**: pass `tool_ids=["my_tool", "server:mcp:my_server"]`. - **Terminal work**: pass `terminal_id=""`. - **Keep the conversation**: set `DELETE_CHAT_WHEN_DONE = False`. It then shows up in the user's chat list exactly like a browser conversation. - **Multi-turn**: reuse the `chat_id`, add a new user message plus assistant placeholder to the tree, and send the full `messages` history. See [Backend-Controlled API Flow](/reference/api-flow#send-additional-messages-to-an-existing-chat). - **No built-in tools wanted**: `builtin_tools=False` makes the request blocking and skips the polling loop. --- ## Gotchas | Symptom | Cause | Fix | | --- | --- | --- | | Response contains tool_calls your script has to run | Native mode without chat_id + id | Use Path A, or switch that request to legacy mode | | No tools were called at all, and stream was false | The native loop is streaming-only | Send stream: true | | stream: true was sent but nothing streamed | The workspace model's Stream Chat Response parameter overrides the request | Clear it on the model, or accept the model's setting | | Web search, code interpreter, memory or image generation never offered | Missing session_id, missing features flag, missing user permission, disabled globally, or the model's Builtin Tools category is off | All five must line up. Check them in that order | | execute_code returns "WebSocket connection required" | The code interpreter engine is pyodide, which runs in the browser | Switch the engine to Jupyter in Settings > Admin > Tools > Code Interpreter, or drop code_interpreter from features | | File tools (view_file, grep_chat_files, ...) missing | They need files in the request body, the model's File Upload capability on, and File Context off | See Prompt Caching and Context Optimization | | 503 Terminal unavailable | The terminal server is unreachable, disabled, or your user has no access grant | Confirm the ID appears in GET /api/v1/terminals/ for that key | | MCP tool connection fails | An OAuth-protected MCP server the API key's user has not authorised | Complete the OAuth flow once in the browser as that user | | The chat is created but stays empty in the UI | Broken message tree | currentId is camelCase, and every message needs parentId and childrenIds. See Backend-Controlled API Flow | --- ## See also - [API Endpoints](/reference/api-endpoints): the full endpoint list, Anthropic Messages API, RAG and file uploads. - [Backend-Controlled API Flow](/reference/api-flow): the chat and message-tree structures used in Path A. - [Tools](/features/extensibility/plugin/tools): built-in tool reference, native vs legacy, per-model gating. - [Model Context Protocol (MCP)](/features/extensibility/mcp): adding MCP servers. - [Open Terminal](/features/open-terminal): setting up the terminal server. - [API Keys](/features/authentication-access/api-keys) and [Permissions](/features/authentication-access/rbac/permissions).