Chat with AI agents via REST. For SDK helpers (sendMessage, run, streaming), see the Agent SDK.
Most integrations use POST /agents/run: it creates a chat when needed, sends the user message, and returns assistant output. For a pure chat-first flow (create a chat, then send messages separately), use POST /chats followed by POST /chats/:id/messages, the same pattern the web app and React AgentChatProvider use for new conversations (client.agent().sendMessage() still uses POST /agents/run). Use the tool endpoints below for client tools, widgets, and human-in-the-loop (HIL) approval.
Run agent (recommended)
POST /agents/run
Send a message to a template or ad-hoc agent. Creates a new chat when chat_id is omitted. For a new template-backed chat in the agent's home workspace, the resolved agent version's permissions (for example allow_all_tools) are copied into the chat once at creation.
Request
| Field | Type | Required | Description |
|---|---|---|---|
chat_id | string | No | Existing chat ID (omit to start a new chat) |
agent | string | No | Template agent ref (namespace/name or namespace/name@version) |
agent_id | string | No | Stored agent ID |
agent_version_id | string | No | Version of agent_id to run (full or short ID). Without it the agent's current version runs |
agent_config | object | No | Ad-hoc agent config (tools, system prompt, core app) |
agent_name | string | No | Display name for ad-hoc chats |
context | object | No | Per-chat context variables (string values). Stored on a new chat, or on an existing chat that has none |
input | object | Yes | The message (see below) |
stream | boolean | No | If true, response is SSE instead of JSON |
Name the agent exactly one way: agent, agent_id (with optional agent_version_id), or agent_config. Or name none and send only chat_id to post into a chat that already has its agent. The API returns 400 validation_error when the agent is named more than one way, when agent_version_id comes without agent_id, or when neither an agent nor chat_id is given. An agent_version_id that does not exist or that you cannot read returns 404, and nothing is written.
A body that still carries channel_context is accepted and the field is ignored. See Channel context. trigger_id and tool_invocation_id in a client's body are ignored too: the server sets them for messages that triggers and sub-agent calls send.
input fields (MessageInput):
| Field | Type | Description |
|---|---|---|
text | string | User message text |
attachments | array | File refs from the Files API (POST /files): uri, filename, content_type, optional id and size. Images reach the model as image input |
images | array | Deprecated. Image URIs, read only when attachments is empty |
files | array | Deprecated. File URIs, read only when attachments is empty |
Other fields older clients send in input (role, context, system_prompt, context_size, reasoning, tools, settings) are accepted and ignored. Settings, the system prompt, tools and history come from the agent and the chat.
Template agent (new chat):
1{2 "agent": "myteam/support-agent@latest",3 "context": {4 "project_id": "proj_456"5 },6 "input": {7 "text": "Summarize open tickets"8 }9}Continue chat:
1{2 "chat_id": "chat_abc123",3 "input": {4 "text": "What about priority ones?"5 }6}Response
The response body is the DTO under data:
1{2 "user_message": { "id": "msg_...", "chat_id": "chat_abc123", "role": "user", "content": [...] },3 "assistant_message": { "id": "msg_...", "chat_id": "chat_abc123", "role": "assistant", "content": [...], "tool_invocations": [...] }4}See REST overview — response format.
Set stream: true to receive message updates over SSE. See Stream chat updates. Streaming applies when a new agent run starts — if the chat already has an active run, the message is auto-queued instead (no SSE stream).
The turn runs on the server independently of the request. Closing a streaming POST /agents/run connection does not stop the turn: reconnect with Stream chat updates or poll List chat messages to pick it up. Use Stop chat to end a turn.
Queuing while a run is active
When chat_id points to a chat with a non-terminal agent run (status: busy), POST /agents/run and POST /agents/messages do not start a new run. The user message is queued for the agent's next turn, the same behavior as Send chat message.
Each chat has at most one active agent run. Rapid follow-up messages are queued rather than starting a parallel run or stopping the in-progress response — use Stop chat to cancel explicitly.
Remote harness exception. When the active run uses a profile with capabilities.steer: true (for example Claude Code or Codex, as reported at registration), mid-turn messages are forwarded into the running harness turn at once. The user message is ready immediately and is tied to the active run — not held until the next turn. Profiles with steer: false (typical ACP harnesses) keep the queued behavior below. See Remote harness runs — Mid-turn messages.
The response returns immediately with only user_message (status: "queued" for most runs, or "ready" when a steerable remote harness accepted the message mid-turn). assistant_message is omitted. Poll or stream the chat to see when the agent picks up the message and generates a reply.
1{2 "user_message": {3 "id": "msg_...",4 "chat_id": "chat_abc123",5 "role": "user",6 "status": "queued",7 "content": [{ "type": "text", "text": "Focus on P0 tickets only" }]8 }9}Use POST /chats/:id/messages when you only need to send a message without other run parameters. See Chat message status.
Send message (alternate)
POST /agents/messages or POST /agents/message
Both are the same endpoint as Run agent: same request body (including stream) and same response.
1{2 "agent_id": "agent_abc123",3 "agent_version_id": "ver_xyz789",4 "input": { "text": "Hello" }5}Create chat
POST /chats
Create a chat, optionally assigning a template agent before the first message. Pair with Send chat message for a two-step new conversation: create the chat, then post the first user message.
Use POST /agents/run when you want one call that creates the chat, sends the message, and returns assistant output — recommended for scripts, ad-hoc agent_config, per-chat context, attachments, and A2A integrations. Use the chat-first flow when you need a chat ID before sending (for example to open a stream or navigate in a SPA).
Requires conversations:write scope.
Request
| Field | Type | Required | Description |
|---|---|---|---|
agent | string | No | Template agent ref (namespace/name or namespace/name@version) |
name | string | No | Display name for the chat |
context | object | No | Per-chat context variables (string values) |
When agent is provided, the API resolves the template and pins agent_id and agent_version_id on the chat (same version pinning as Run agent). When the chat is in the agent's home workspace, the pinned version's permissions (for example allow_all_tools) are copied once into this chat's agent_data when the chat is created; chats another workspace starts with a published agent do not copy those defaults. The chat owns copied values from then on (see Chat settings). Omit agent to create an empty chat and assign an agent later with Set agent.
Response
Returns the chat DTO (same shape as Get chat).
Errors
| HTTP status | Code | When |
|---|---|---|
| 400 | invalid_request | Invalid request body |
| 404 | not_found | Agent ref not found |
Example — two-step new conversation
1# 1. Create chat with agent2curl -X POST https://api.inference.sh/chats \3 -H "Authorization: Bearer inf_your_key" \4 -H "Content-Type: application/json" \5 -d '{"agent": "myteam/support-agent@latest", "name": "Support"}'67# 2. Send first message (starts the agent run)8curl -X POST https://api.inference.sh/chats/chat_abc123/messages \9 -H "Authorization: Bearer inf_your_key" \10 -H "Content-Type: application/json" \11 -d '{"message": "Summarize open tickets"}'JavaScript SDK: AgentChatProvider / useAgentActions().sendMessage() on a template agent with no bound chat ID calls POST /chats, then POST /chats/{id}/messages; call reset() before the next message to start a new chat. client.agent().sendMessage() still uses POST /agents/run.
List chats
GET /chats or POST /chats/list
Cursor-paginated list of chats. POST /chats/list accepts a JSON body with limit, cursor, direction, and filters (for example status in busy, awaiting_input).
Each item includes the default chat projection: id, name, status, active_run_id, and channel_context when the chat originated from an external messaging channel. Use Get chat for the full metadata shape (output, context, active_run, …).
Get chat
GET /chats/:id
Returns chat metadata — status, structured output, per-chat context, optional channel_context, and active run. Does not include message history or tool invocations.
Response
| Field | Type | Description |
|---|---|---|
id | string | Chat ID |
name | string | Display name in the chat list and settings |
short_id | string | Short id used in public share links (https://1nf.sh/chats/{short_id}) |
visibility | string | Who can open the chat: private (only you), team (your workspace), or public (anyone with the link) |
status | string | busy, idle, awaiting_input, or completed |
agent_data | object | Per-chat agent state: memory (key–value notes from the agent's memory tools), allow_all_tools (when true, every tool runs without asking), and disable_hooks (when true, lifecycle hooks do not run in this chat). Per-tool approvals are stored as chat rules, not in agent_data |
output | object | Structured result from the agent's finish tool (when set) |
context | object | Per-chat context variables, used as {{context.NAME}} in tool calls and URL templates (see Per-chat context) |
channel_context | object | Present when the chat came through an external messaging channel (Slack thread, Telegram chat, …): channel_type plus opaque channel_metadata used to route replies back. Omitted for chats started in the app or SDK. See Channel context. |
work_dir | string | When set, the folder a remote harness works in for this chat |
harness_session_id | string | When set, the harness's own session id on the remote (for example to resume with claude --resume) |
active_run | object | Current agent run, when one is in progress |
work_dir | string | When a remote harness drives this chat, the working directory on that machine (set at launch). Harness sessions are folder-bound; each chat can use a different folder with the same harness agent. |
harness_session_id | string | null | When a remote harness profile drives this chat, the harness vendor's session id (for example to resume on that machine). Omitted when the platform loop runs the agent. |
work_dir | string | When set, the folder on the remote where the harness works for this chat (from launch or assignment). Omitted when empty. |
forked_from_message_id | string | null | Message id this chat was branched from. Not the same as parent_id on sub-agent chats. |
context and channel_context are unrelated: context holds the context variables you pass when starting a chat; channel_context is set by the platform when a chat is created from a messaging channel webhook.
1{2 "id": "chat_abc123",3 "status": "awaiting_input",4 "output": null,5 "context": { "project_id": "proj_123" },6 "channel_context": {7 "channel_type": "slack",8 "channel_metadata": { "channel": "C01234567", "thread_ts": "1234567890.123456" }9 },10 "active_run": {11 "id": "run_abc123",12 "state": "awaiting_input"13 }14}channel_context is omitted on chats that did not originate from an external channel. channel_metadata shape is provider-specific and opaque to callers — treat it as pass-through data for the platform's outbound delivery.
output is populated when the agent calls the internal finish tool. See Structured Output.
For message history and tool invocations, use List chat messages. For live updates, use Stream chat updates or GET /chats/:id/status for status-only polling.
Change name, visibility, per-chat tool approval defaults, or whether lifecycle hooks run with Chat settings. In the workspace, open chat settings from the gear icon in the chat header; see Chat settings. When a chat is created in the agent's home workspace, allow_all_tools is copied from the agent version's permissions.allow_all_tools when set; after that, only chat settings change it for that conversation.
Remote placement on chats and runs
When an agent is bound to a remote harness profile, responses include where thinking happens and which harness session backs the chat:
| Field | On | Meaning |
|---|---|---|
harness | Agent | What drives the agent: inference (platform loop, default for deployed templates) or an agentprotocol registry id (claude, codex, …) for a harness on a remote. The UI uses this to decide what is editable; the registry describes display name, vendor, and capabilities. Harness agents use the harness's own instructions, tools, and versions — not AgentVersionDTO config. |
profile_id | Agent, active_run | Harness profile that thinks for the agent or run. Omitted or null when the platform loop runs the agent. |
remote_id | Agent, active_run | Remote machine id. Terminal and remote internal tools use this host by default. |
work_dir | Chat | Folder on the remote for this harness session (from Launch harness chat or the workspace). |
harness_session_id | Chat | Vendor session id on the remote, stable for the chat's lifetime. |
forked_from_message_id | Chat | Branch point when this chat was forked from another conversation. |
On AgentDTO, harness, profile_id, and remote_id are deployment fields on the agent record (not on AgentVersionDTO config). Changing them affects new runs only. When harness is not inference, the agent is backed by an external harness — platform instructions, tools, and version history are not used for execution. Harness agents are created on first harness launch per profile; you cannot add new versions to them.
On AgentRunDTO, profile_id and remote_id are a snapshot taken when the run is created. Updating the agent later does not rewrite past runs; the turn driver is chosen from the run record.
Remote harness runs
When active_run.profile_id is set, the API drives a harness session on that profile's remote instead of calling a hosted model in the platform loop. The chat wire format, streaming endpoints, approval interrupts, and queue behavior match a normal agent run — only execution moves to your machine.
| Phase | Behavior |
|---|---|
| Start | Opens or resumes the chat's harness session on the remote in work_dir (harness_session_id when resuming), then sends the user prompt. A new session in a chat that already has messages receives prior conversation text first so the harness continues rather than starts over. |
| Streaming | Harness output becomes assistant messages and broadcasts on the chat stream. Tool calls the harness makes on the host appear as tool invocations and tool messages with type: harness — they are recorded for the transcript and are not executed again on inference.sh. |
| Approvals | Harness permission requests surface as the standard approval interrupt (input_required). Allow or deny through the same REST/SDK paths as platform tools; the decision is forwarded to the harness once. If the harness withdraws a request before you answer (for example you interrupted the turn, or the harness moved on), it sends approval.resolved while the invocation is still awaiting_approval. The API cancels the tool call, resolves the interrupt, and the run is no longer blocked. Your own allow/deny that arrives after the invocation has already left awaiting_approval is ignored — the harness already handled it. |
| Attachments | Images and files on the message go with the prompt. The daemon downloads them to the machine and the harness gets images as images where it supports them (capabilities.images), other files by local path. A remote whose daemon predates belt v1.21.0 (no capabilities.files) gets the files' URLs in the prompt text. See Remotes guide, Attachments. |
| Mid-turn messages | Depends on the profile's capabilities.steer from registration. When steer is true, follow-up user messages during a working turn are sent to the harness immediately and appear as ready on the active run. When steer is false, messages are queued for the next turn (same as queuing while a run is active). |
| Stop | Stop chat sends an interrupt to the harness turn in flight and keeps the session for later messages. |
| End | When the harness finishes the turn, the run completes and queued messages drain into the next turn. Session errors fail the run. |
Chat rules on harness runs. The chat's permission rules (including rules added with Always allow tool) apply to harness permission prompts the same way as to platform loop tools. If the harness asks to run a tool already covered by a chat allow rule for that harness tool, the run does not enter input_required: the invocation is recorded as in progress and the API answers the harness with remote_session_resolve (decision: allow, scope: session) so it can stop re-prompting for that tool name during the current harness session. Other tools still park for approval. When agent_data.allow_all_tools is true (see Chat settings), every harness permission prompt is auto-approved the same way as platform tool approvals — turn it off again from chat settings when you want prompts back.
When you resolve a parked harness request, the API forwards your decision on the remote WebSocket:
| Action | remote_session_resolve.scope |
|---|---|
POST /tools/:id/invoke (allow once) | once |
POST /tools/:id/reject | once |
POST /chats/:chatId/tools/:toolInvocationId/always-allow | session |
See Remotes API — remote_session_resolve.
Start failures (the run does not begin; error text is returned to the client):
| Condition | Typical message |
|---|---|
| Remote not connected | the remote this agent runs on is not connected |
Daemon without --agents | this remote does not run agent sessions; start it with belt remote start --agents |
| Profile missing, logged out, or not drivable | the harness this agent runs on is not available on its remote |
Remote disconnect. When a remote goes disconnected or stopped, every non-terminal harness run on that machine fails with the remote disconnected. Those turns are not retried automatically — the harness may already have changed files locally. Exec runs on the same remote fail the same way (remote disconnected before the command finished).
Setup: Remotes guide — Agent harness sessions, Profile-backed agent chats, and Launch harness chat.
List chat messages
GET /chats/:id/messages or POST /chats/:id/messages/list
Returns cursor-paginated messages for a chat. Requires the conversations:read scope (same as Get chat). The chat must exist and belong to your workspace.
Returns every message in the chat. Access is verified when loading the chat ID. Message rows are not filtered by creator or workspace.
Default sort is order descending (newest first) — load older pages as users scroll up. POST /chats/:id/messages/list accepts the same fields in a JSON body (cursor, limit, direction, sort, filters).
| Query / body field | Type | Description |
|---|---|---|
limit | integer | Page size (default 50). Pass -1 to return up to 10,000 messages in one response — see Cursor pagination. |
cursor | string | Opaque cursor from next_cursor (ignored when limit=-1) |
direction | string | forward or backward (ignored when limit=-1) |
sort | array | Sort orders (default [{ "field": "order", "dir": "desc" }]) |
filters | array | Filter expressions (same pattern as other list endpoints) |
Uses the same cursor pagination shape as List skills. Each items entry is a ChatMessageDTO with role, status, content, order, and optional tool_invocations. Transcripts can include role: "event" rows when lifecycle hooks run — see Chat message roles.
Message order: Integer sequence within a chat (unique per chat_id, allocated by the server on insert). Sort transcripts by order, not created_at — messages written in the same turn can share a timestamp. While status is queued, order is provisional; when the message becomes ready on the agent's next turn, the server may assign a new order so it follows any tool results that completed while it was queued. See Chat message status.
Example (newest page):
1curl "https://api.inference.sh/chats/chat_abc123/messages?limit=50" \2 -H "Authorization: Bearer inf_your_key"Example response:
1{2 "items": [3 {4 "id": "msg_xyz789",5 "chat_id": "chat_abc123",6 "role": "assistant",7 "status": "ready",8 "order": 4,9 "content": [{ "type": "text", "text": "Here is the summary." }],10 "tool_invocations": []11 }12 ],13 "next_cursor": "msg_abc456",14 "has_next": true,15 "has_previous": false,16 "items_per_page": 50,17 "total_items": 1218}Pass cursor from next_cursor to fetch older messages.
Example (full transcript — no pagination):
1curl "https://api.inference.sh/chats/chat_abc123/messages?limit=-1" \2 -H "Authorization: Bearer inf_your_key"When limit=-1, has_next is false and up to 10,000 matching messages are returned in items (sorted and filtered as requested). Prefer cursor pagination for UI scroll-back; use limit=-1 for exports or debugging on chats with fewer than 10,000 messages.
JavaScript SDK (@inferencesh/sdk agent module, v0.6.40+): AgentChatProvider loads one page of history using the server default limit (50) when GET /chats/:id omits chat_messages. v0.6.41+: useAgentActions().loadOlderMessages() fetches the next page via cursor and prepends deduplicated messages to useAgentChat().messages; hasOlderMessages is true when another page exists. For custom UIs outside AgentChatProvider, call this endpoint directly with cursor or limit=-1.
For live updates while the agent runs, use Stream chat updates instead of polling this endpoint.
Get chat trace
GET /chats/:id/trace
Returns the execution graph for an agent chat: nodes (assistant turns, tool invocations, sync barriers, and long-running work such as app tasks and remote_exec exec runs), edges (dependencies), and step counts by status. Requires the conversations:read scope (same as other chat endpoints).
Long-running tools add execution nodes linked to their backing resource (for example resource_type task for app tools, exec_run for remote commands). The tool invocation node stays in progress until that work finishes.
Use this to debug agent runs programmatically: see which tools ran, how long each step took, and where failures occurred. In the workspace, open chat settings → agent → Show trace for the same execution graph (Chatting guide).
Response
| Field | Type | Description |
|---|---|---|
graph_id | string | Graph identifier (same as the chat ID) |
nodes | array | Execution steps (type, label, status, resource_id, started_at, completed_at, duration_ms, …) |
edges | array | Dependencies between nodes (from_node, to_node, type) |
total_steps | number | Total nodes in the graph |
completed_steps | number | Steps that finished successfully |
running_steps | number | Steps still in progress |
failed_steps | number | Steps that failed |
Example:
1curl https://api.inference.sh/chats/chat_abc123/trace \2 -H "Authorization: Bearer inf_your_key"JavaScript SDK: client.chats.getTrace('chat_abc123').
→ Observability · Stream chat updates
Chat status values
| Status | Meaning |
|---|---|
busy | Agent is working — LLM generation, or automated tool execution (apps, sub-agents, hooks, client tools, MCP) |
idle | Agent finished; chat can accept new messages |
awaiting_input | A human must act on this chat: HIL tool approval or an interactive widget |
completed | Terminal state (archived chat) |
Client tools and hooks keep the chat in busy while they execute. Use chat.status === "awaiting_input" when you need a human on the conversation; use tool invocation status for per-tool progress.
Chat message roles
Each message has a role that controls how the platform stores it and whether it is sent to the LLM on later turns.
| Role | Sent to LLM | Typical use |
|---|---|---|
user | Yes | End-user input |
assistant | Yes | Model output and tool calls |
tool | Yes | Tool results |
system | Yes | System instructions |
event | No | System info for the transcript (for example a lifecycle hook handler run) |
injection | Folded in | Internal context; folded into the user turn when context is built, not sent as its own message |
compaction | Replaced | Compaction marker; replaced by its summary when context is built |
Messages with injection, compaction or event roles still appear in List chat messages and on the chat stream with status: "ready". Custom UIs can render them; integrations that rebuild LLM context from the transcript must skip event rows and treat injection and compaction as described above. Clients can only create user messages; every other role is written by the platform.
Event content: Event messages use a content block with type: "event". The block includes short text plus structured event data (today event.type is "hook" with a hook object). See Lifecycle hooks — Hook runs in the transcript.
SDK types: ChatMessageRoleEvent, ChatEvent, and ChatHookEvent in @inferencesh/sdk / generated Python types.
Chat message status
Each entry in chat_messages has its own status, separate from the chat-level status above.
| Status | Meaning |
|---|---|
pending | Assistant message still being generated (LLM stream in progress) |
queued | User message queued while the agent is still running — via Send chat message or auto-queue on run/message — waiting for the next turn |
ready | Complete — included in LLM context on subsequent turns |
failed | Generation failed, or the run could not start |
cancelled | Cancelled — via Cancel message or when the chat run is stopped |
Messages from POST /agents/run or POST /agents/messages start as status: ready when no run is active. When a run is already active, they are auto-queued with status: queued instead. When the run cannot start (for example its agent version is gone or you cannot read it), the message is marked failed and the request returns the error. Messages sent via POST /chats/:id/messages (or auto-queue) transition to ready when the agent begins its next turn, before context is built.
Poll List chat messages or stream chat updates to detect queued messages — the stream emits chat_messages events as status transitions from queued to ready, often including an updated order in the same partial update. To withdraw a queued message before the agent consumes it, use Cancel message.
While a message is still queued, sorting the transcript by order alone can place it between an in-flight tool call and its result; that position is corrected when the message is readied.
SDK types: Import ChatMessageStatusQueued from @inferencesh/sdk (JavaScript) or ChatMessageStatus.QUEUED from inferencesh.types (Python) when comparing message status values. JavaScript SDK (v0.6.36+): client.chats.cancelMessage(messageId) and useAgentActions().cancelMessage(messageId) with AgentChatProvider cancel queued messages. Python SDK does not expose cancelMessage yet.
Delete chat
DELETE /chats/:id
Permanently deletes a chat, its messages, and tool history. Requires conversations:write scope. The workspace delete chat action in chat settings calls this endpoint.
JavaScript SDK: client.chats.delete('chat_abc123').
Stop chat
POST /chats/:id/stop
Stop the current agent run and cancel pending tool invocations.
JavaScript SDK: client.chats.stop('chat_abc123') — or agent.stopChat() when using client.agent().
Turn budget
Each chat has a turn budget that caps one uninterrupted stretch of agent work. The runtime counts the messages added since the last user message, ordered by message order. When the count reaches 500, the run ends without dispatching further tool calls.
| Behavior | Detail |
|---|---|
| What counts | Assistant and tool messages since the last user message |
| Limit | 500 consecutive non-user messages per chat |
| On exceed | Pending tool calls are dropped. The run ends the same way as when the model stops calling tools |
| User message | A new user message resets the counter for that chat |
| Scope | Per chat. Budget does not accumulate across user turns |
If the model's last response has no text, the runtime sets it to a message saying that the task was not finished and asking the user how to continue. Sub-agent and agent tool chats are separate chats with their own budgets.
The budget stops tool-calling loops that never converge from running and consuming credits indefinitely. Use Stop chat to end a run earlier.
Context auto-compaction
Long chats can exceed the agent core app's context size (the model window configured on the agent version). Before each LLM turn, the platform estimates how many tokens the next prompt would send — system prompt, tool definitions, any prior compaction summary, and live transcript messages (including reasoning blocks, tool calls, tool results, and file or image references). When that estimate passes 80% of the context size, the runtime compacts the oldest live turns so the newest 25% of live content stays in full.
| Behavior | Detail |
|---|---|
| Trigger | Estimated prompt tokens > 80% of the agent's context size |
| What stays | The newest live messages that fit in ~25% of the context budget |
| What is summarized | Older live turns, plus the text of any earlier compaction marker when compacting again |
| Cut boundary | Never splits a tool call from its tool result — both stay or both retire |
| When it runs | At the start of a turn, after queued user messages are readied and before the model call |
| Agent event | context.compacted on the agent run event stream (before_tokens, after_tokens in the payload) |
Compaction inserts a chat message with role: "compaction" and an extractive summary (user requests and assistant decisions; large tool outputs are dropped). When the agent version has a core app, the platform may call that app with a structured summarization prompt instead; on failure it falls back to extractive text. The marker's compaction_meta records compacted_up_to_order, compacted_count, estimated_tokens, and method (extractive or llm).
List chat messages returns compaction markers in transcript order. BuildContext replaces each marker with its summary text when calling the model — markers are bookkeeping, not an extra user turn.
Lifecycle hooks: agent.pre_compact runs immediately before compaction; a synchronous webhook that returns deny or stop skips compaction for that turn. agent.post_compact fires after a successful compaction. Per-chat disable_hooks turns both off for that chat.
→ Long conversations in the workspace
Set agent
POST /chats/:id/agent
Change which template agent handles an existing chat. Message history and per-chat context are preserved; the next run uses the new agent's tools and system prompt.
Agent switching is explicit — call this endpoint to change agents. Sending a message via POST /agents/run, POST /agents/message, or POST /chats/:id/messages does not change which agent owns the chat, and there is no per-message agent binding.
Requires conversations:write scope. In the JavaScript SDK this is useAgentActions().switchAgent(ref). Only agents the platform loop runs can take over a chat; a chat on a remote harness keeps its agent (409).
Request
| Field | Type | Required | Description |
|---|---|---|---|
agent | string | Yes | Template agent ref (namespace/name or namespace/name@version) |
Response
Returns what the switch changed, not the chat:
| Field | Type | Description |
|---|---|---|
chat_id | string | The chat |
agent_id | string | The agent the chat now runs on |
agent | object | That agent |
agent_version_id | string | The agent version the next run uses |
agent_version | object | That version |
Merge it into the chat you hold. Messages and the rest of the chat are unchanged.
Errors
| HTTP status | Code | When |
|---|---|---|
| 400 | invalid_request | Missing or empty agent |
| 404 | not_found | Chat or agent not found |
| 409 | conflict | The chat or the agent runs on a remote harness, or the chat is a review branch |
Example
1curl -X POST https://api.inference.sh/chats/chat_abc123/agent \2 -H "Authorization: Bearer inf_your_key" \3 -H "Content-Type: application/json" \4 -d '{"agent": "myteam/escalation-agent@latest"}'Chat settings
POST /chats/:id/settings
Change a chat's settings. A field left out is left as it is. Requires conversations:write scope. In the JavaScript SDK this is useAgentActions().updateChatSettings(settings).
Request
| Field | Type | Description |
|---|---|---|
name | string | Rename the chat. Cannot be empty. |
visibility | string | private, team or public |
allow_all_tools | boolean | Run every tool call without asking. Switching it on also approves the calls already waiting. |
disable_hooks | boolean | Stop the agent's lifecycle hooks firing in this chat |
forget_memory | string[] | Remove these keys from the chat's memory. A key that is not there is ignored. |
Response
Returns the chat's settings after the change, not the chat:
| Field | Type | Description |
|---|---|---|
chat_id | string | The chat |
name | string | Chat name |
visibility | string | private, team or public |
allow_all_tools | boolean | agent_data.allow_all_tools on the chat |
disable_hooks | boolean | agent_data.disable_hooks on the chat |
memory | object | agent_data.memory on the chat, after forget_memory |
Merge it into the chat you hold. Calls approved by switching allow_all_tools on arrive on the chat stream.
Example
1curl -X POST https://api.inference.sh/chats/chat_abc123/settings \2 -H "Authorization: Bearer inf_your_key" \3 -H "Content-Type: application/json" \4 -d '{"name": "release notes", "forget_memory": ["draft"]}'1{2 "chat_id": "chat_abc123",3 "name": "release notes",4 "visibility": "team",5 "allow_all_tools": false,6 "disable_hooks": false,7 "memory": { "goal": "ship the release" }8}Send chat message
POST /chats/:id/messages
Send a user message to an existing chat. If the agent is idle, this starts a new run. If a run is already active (status: busy), the message is queued and picked up on the agent's next turn — the same auto-queue behavior as POST /agents/run with an existing chat_id.
Use this endpoint when you only need to send a message without other run parameters (for example a follow-up while tools are executing, or continuing a chat without specifying agent config again).
Legacy alias: POST /chats/:id/steer — same request and response. Prefer POST /chats/:id/messages.
Requires conversations:write scope and write access to the chat. Someone who can only read the chat (for example through a read share or a public link) gets 403; posting to a chat you cannot read returns 404. JavaScript SDK: used internally by AgentChatProvider / useAgentActions().sendMessage() (client.agent().sendMessage() still uses POST /agents/run). Python SDK: call the REST endpoint directly.
Request
1{2 "message": "Also prioritize tickets from enterprise customers",3 "attachments": [4 { "uri": "https://cloud.inference.sh/t/abc123/tickets.csv", "filename": "tickets.csv", "content_type": "text/csv" }5 ]6}| Field | Type | Required | Description |
|---|---|---|---|
message | string | No | User message text |
attachments | array | No | File refs from the Files API, same shape as input.attachments on Run agent |
Send message, attachments, or both. A message may be attachments alone.
Response
Returns the created message DTO with role: "user". When a run is active, status is "queued" and agent_run_id is set; when idle, a new run starts and the message is "ready". Queued messages transition to ready when the agent begins its next turn, before context is built. See Chat message status.
Errors
| HTTP status | Code | When |
|---|---|---|
| 400 | invalid_request | Neither message nor attachments is set |
| 404 | not_found | Chat does not exist |
| 409 | conflict | Harness-backed chat and the remote profile's harness is below the driver's minimum (profiles[].support.level is older-than-supported). The message explains what is missing and how to upgrade on the host. Nothing is sent to the machine. |
Example
1curl -X POST https://api.inference.sh/chats/chat_abc123/messages \2 -H "Authorization: Bearer inf_your_key" \3 -H "Content-Type: application/json" \4 -d '{"message": "Focus on P0 tickets only"}'Cancel message
POST /chats/messages/:messageId/cancel
Cancel a queued user message before the agent picks it up — for example a message from Send chat message or an auto-queued message you want to withdraw. The message status becomes cancelled and content is cleared.
Only messages with status: "queued" can be cancelled. Messages that are ready, failed, or already cancelled return 409 not_cancellable.
Requires conversations:write scope. JavaScript SDK: client.chats.cancelMessage('msg_abc123') — or useAgentActions().cancelMessage(messageId) with AgentChatProvider. Python SDK: call the REST endpoint directly.
Response
Returns the updated message DTO with status: "cancelled". The chat stream emits a chat_messages event when the cancellation succeeds.
Errors
| HTTP status | Code | When |
|---|---|---|
| 400 | missing_id | messageId path parameter is missing |
| 404 | not_found | Message does not exist |
| 409 | not_cancellable | Message is not queued |
Example
1curl -X POST https://api.inference.sh/chats/messages/msg_abc123/cancel \2 -H "Authorization: Bearer inf_your_key"Tool invocations
Tool calls on assistant messages expose tool_invocations. Each invocation has an id used by the endpoints below.
Tool invocation status
| Status | Meaning |
|---|---|
pending | Queued |
in_progress | Executing — apps, sub-agents, client tools, hooks, MCP |
awaiting_input | Interactive widget or form needs human input |
awaiting_approval | HIL approval needed before execution |
completed | Done |
failed | Error |
cancelled | Cancelled |
Status transitions
The API enforces valid transitions server-side. Terminal states (completed, failed, cancelled) cannot change again. A typical successful path is pending → in_progress → completed, with optional stops at awaiting_approval or awaiting_input. Skipping required steps is rejected — for example pending cannot go directly to awaiting_input.
If a chat run is cancelled while a tool is executing, the invocation moves to failed and any late completion attempt from the tool is rejected.
Tool invocation updates are delivered on the chat stream as chat_messages events (the server republishes the parent message when an invocation changes).
Submit tool result
POST /tools/:toolInvocationId
Submit a result for client tools (in_progress) or interactive widgets (awaiting_input).
Request
1{2 "result": "{\"temperature\": 72}"3}| Field | Type | Required | Description |
|---|---|---|---|
result | string | Yes | The tool's output, as text |
attachments | array | No | Files the tool returns: file refs uploaded first with POST /files (uri, filename, content_type, optional id and size). Each needs a uri; at most 20 |
The tool message keeps the text and the files. Images reach the model as image input when the model accepts images.
1{2 "result": "{\"url\": \"https://example.com\", \"note\": \"Screenshot attached\"}",3 "attachments": [4 {5 "id": "file_abc123",6 "uri": "https://cloud.inference.sh/t/abc123/screenshot.png",7 "filename": "screenshot.png",8 "content_type": "image/png"9 }10 ]11}A request with more than 20 attachments, or an attachment without a uri, returns 400 validation_error.
For widget actions, JSON-serialize { "action": {...}, "form_data": {...} } into result, or use the deprecated POST /tools/:id/widget endpoint.
The JavaScript and Python SDKs call this via submitToolResult / submit_tool_result. See Client Tools.
Approve tool (HIL)
POST /tools/:toolInvocationId/invoke
Approve a tool with status: awaiting_approval and execute it. Returns 400 invalid_state if the invocation is not awaiting approval. Approving, rejecting, always-allowing, and submitting a tool result all require write access to the call's chat; being able to read the chat is not enough.
1curl -X POST https://api.inference.sh/tools/ti_abc123/invoke \2 -H "Authorization: Bearer inf_your_key"→ Approval settings · Human-in-the-loop
Reject tool (HIL)
POST /tools/:toolInvocationId/reject
Reject a pending approval. The agent continues with the rejection reason.
Request
1{2 "reason": "Not authorized to delete files"3}reason is optional.
Always allow tool
When a tool call is awaiting_approval, always allow approves it once and saves chat allow rules so matching calls in this chat skip the interrupt (platform loop and remote harness runs). List and revoke rules with Chat permissions rules.
On a harness-backed chat, always allow also tells the harness it may stop asking for that tool for the remainder of the harness session (remote_session_resolve with scope: session). A one-time allow via POST /tools/:id/invoke sends scope: once instead.
Requires the conversations:read scope for options and conversations:write to save.
List always-allow options
GET /chats/:chatId/tools/:toolInvocationId/always-allow/options
Returns the choices the API suggests for this call, narrowest first (for example an exact remote_exec command, a command prefix, every command on the remote, or the whole tool). The client does not build rules — each option includes the PolicyRuleDTO rows it would save, a label, optional description (when the engine cannot narrow further), and a stable key for POST .../always-allow.
Response (AlwaysAllowOptionsDTO):
| Field | Description |
|---|---|
options | AlwaysAllowOptionDTO[] with key, scope (exact, prefix, folder, remote, tool), label, optional description, and rules |
default | Key of the narrowest option, or empty when there is none |
unavailable | When options is empty, a sentence explaining why (for example governance allows only admin rules, or no chat rule could allow this call) |
For HTTP tools, the engine resolves the request URL to a host. When approval waits on WebFetch(domain:…) (governance ask or default ask on the host), chat rules cannot answer that question — options is empty, unavailable names the host, and POST .../always-allow without option does not write a whole-tool allow. When only the tool's built-in approval asked, the whole-tool option is returned as for other tools.
1curl https://api.inference.sh/chats/chat_abc123/tools/ti_abc123/always-allow/options \2 -H "Authorization: Bearer inf_your_key"Save always-allow
POST /chats/:chatId/tools/:toolInvocationId/always-allow
Body (AlwaysAllowRequest):
| Field | Description |
|---|---|
option | Option key from the options endpoint. If the key no longer matches a current option (the person waited too long or the call changed), the API returns 409 conflict. |
tool_name | Accepted for older clients and ignored — the invocation names its own tool. |
Omit option to keep the pre-options default: for remote_exec, save allow rules for each piece of the command exactly; for other tools, save a whole-tool allow rule.
Response (AlwaysAllowResultDTO): option (when one was chosen) and rules (the chat rules now in force for it).
1curl -X POST https://api.inference.sh/chats/chat_abc123/tools/ti_abc123/always-allow \2 -H "Authorization: Bearer inf_your_key" \3 -H "Content-Type: application/json" \4 -d '{"option":"exact:RemoteExec:git status"}'Errors (stale approval): If the invocation is no longer awaiting_approval (already answered, rejected, or timed out), GET .../always-allow/options, POST .../always-allow, and Explain tool call return 400 invalid_request with message Tool is not awaiting approval. POST /tools/:id/invoke and POST /tools/:id/reject still use 400 invalid_state for the same condition.
Explain tool call
POST /chats/:chatId/tools/:toolInvocationId/explain
Plain-language summary of a call that is awaiting_approval (similar to Claude Code's permission explainer): what it does, why the agent appears to be making it, and how risky it is. Requires conversations:write.
The model is the chat agent's core app (the same model family the loop runs on). Harness-backed chats have no platform model for the harness itself, so explain is not available there.
The answer is generated once per invocation and stored; asking again returns the cached result and does not bill twice. Concurrent requests share one generation. The call runs as the person asking, under their usage policy and team billing.
Response (ToolExplanationDTO):
| Field | Description |
|---|---|
risk_level | low, medium, or high |
explanation | What the call does, in one or two sentences |
reasoning | Why the agent appears to be making it |
risk | What could go wrong, in a few words |
Arguments are redacted before they reach the model; chat history is not included.
1curl -X POST https://api.inference.sh/chats/chat_abc123/tools/ti_abc123/explain \2 -H "Authorization: Bearer inf_your_key"Agent and remote permission rules
Agents and connected remotes can carry their own allow, ask, and deny rules. Edit them in the web app (Permissions on the agent editor; Rules on the remote detail page) or with the REST routes below. Requires agents:read / agents:write for agents and remotes:read / remotes:write for remotes. Writing an agent's rules needs write access to the agent; writing a remote's rules is for the remote's owner only (a teammate who can see the remote gets 403).
| Subject | List | Add | Update enforcement | Revoke |
|---|---|---|---|---|
| Agent | GET /agents/{id}/rules | POST /agents/{id}/rules | PUT /agents/{id}/rules/{ruleId} | DELETE /agents/{id}/rules/{ruleId} |
| Remote | GET /remotes/{id}/rules | POST /remotes/{id}/rules | PUT /remotes/{id}/rules/{ruleId} | DELETE /remotes/{id}/rules/{ruleId} |
| User (your own) | GET /me/rules | POST /me/rules | PUT /me/rules/{ruleId} | DELETE /me/rules/{ruleId} |
Response (PolicyLayerDTO): rules[] on that subject plus rule_kinds[] describing which kind values you may target (same shapes as chat rules). Agent rules may use RemoteExec, Workspace, Harness, or Tool; remote rules RemoteExec, Workspace, or Harness.
Scope:
- Agent rules apply in every chat that runs the agent. An agent allow rule works only in the agent's own workspace; elsewhere that layer may ask or deny but not allow.
- Remote rules apply to every
remote_execcommand and harness call on that machine (selector defaults to the remote id when omitted on remote-owned rules).
Add and revoke behave like chat rules: only narrowing allows are stored; duplicate allows are ignored; invalid allows return 400. POST /agents/{id}/rules/explain and POST /remotes/{id}/rules/explain test a call against that subject's rules together with yours, with the same body as Explain a call (there is no explain route on /me/rules). The rules API also has POST …/rules/preview and GET …/rules/recent on chats, agents, and remotes, and GET /rules/suggestions; they are not described here.
1curl https://api.inference.sh/agents/agent_abc/rules \2 -H "Authorization: Bearer inf_your_key"The agent's allow_all_tools template flag (see Chat settings) is separate from this layer: it is saved on the agent version and copied into new chats in the agent's own workspace when they start.
Chat permissions rules
Stacked allow, ask, and deny rules decide whether a call runs, pauses for approval, or is refused. Layers are evaluated most specific first: chat (editable in this conversation), then agent and user, then the governance layer in force (team or org rules written by admins). Chat rules can only narrow what layers above allow — they cannot loosen a governance ask or deny, and when governance is the only layer that may allow, chat rules may ask or deny but not allow.
The workspace Permissions pane in chat settings shows the same layers as GET /chats/:id/rules.
Requires write access to the chat (same check as Chat settings).
List rules
GET /chats/:id/rules
Requires the conversations:read scope.
Response (ChatRulesDTO): layers[] ordered most specific first. Each layer has subject_type (chat, agent, remote, user, team, org), subject_id, label, governance, editable (only the chat layer), and rules[].
Each rule (PolicyRuleDTO) has:
| Field | Description |
|---|---|
id | Rule id |
effect | allow, ask, or deny |
enforcement | How the rule participates: default (decides in its layer unless a more specific admin layer overrides), enforced (admin-only — final governance rule), evaluate (never decides; the decision feed records what it would have done), or disabled (stored but ignored). Omitted on older rows means default. |
kind | RemoteExec, Workspace, Harness, or Tool (chat-editable kinds in this release) |
selector | Optional remote id, or tag:<name> to match remotes by tag |
specifier | Kind-specific pattern (see below). Empty means the whole kind (for example every harness tool). |
label | The rule in words (for example git commit commands on Laptop), the same wording always-allow options use |
created_at | Set for stored rules; null for synthesized rows such as legacy always_allowed_tools entries or the allow all tools setting |
created_by | User id for stored rules; empty for synthesized rows |
1curl https://api.inference.sh/chats/chat_abc123/rules \2 -H "Authorization: Bearer inf_your_key"Add a chat rule
POST /chats/:id/rules
Requires the conversations:write scope. Body (ChatRuleCreateRequest):
| Field | Required | Description |
|---|---|---|
effect | Yes | allow, ask, or deny |
kind | Yes | RemoteExec, Workspace, Harness, or Tool |
selector | No | Remote id or tag:<name> |
specifier | Yes | Pattern for the kind (may be empty only when the kind allows a whole-kind rule) |
enforcement | No | default, evaluate, or disabled on chat rules. enforced is reserved for admin governance layers (usage policy). |
Specifier examples (typed form Kind[selector](specifier)):
| Kind | Example specifier | Matches |
|---|---|---|
RemoteExec | npm test:* | Commands starting with npm test on the selected remote(s) |
RemoteExec | rm:* | Commands starting with rm |
Workspace | ~/inference/** | Paths under that folder on the remote |
Tool | search | One platform loop tool by name |
Harness | Bash(git status:*) | Harness Bash calls matching git status |
Harness | Read | The harness Read tool (any target) |
Compound commands. A remote_exec line is split into pieces at &&, ||, ;, newlines, and |. Every piece must be allowed for the line to run; a deny or ask on any piece wins. Prefix rules such as git clone:* apply per piece. Commands the policy engine cannot fully analyse ask for approval. Workspace rules also apply to the paths a command touches; see Remotes — security model for how ~ and symlinks are resolved.
Returns the created PolicyRuleDTO. Duplicate allows for the same target are ignored (idempotent).
Validation (400): Chat allow rules that could not take effect are rejected — for example an allow while governance is the only layer that may allow, or an allow for a target a higher layer already asks about or denies.
1curl -X POST https://api.inference.sh/chats/chat_abc123/rules \2 -H "Authorization: Bearer inf_your_key" \3 -H "Content-Type: application/json" \4 -d '{"effect":"allow","kind":"Tool","specifier":"search"}'Update a rule's enforcement
PUT /chats/:id/rules/:ruleId
PUT /agents/{id}/rules/{ruleId}
PUT /remotes/{id}/rules/{ruleId}
PUT /me/rules/{ruleId}
Requires the same write scope as adding a rule on that subject (conversations:write, agents:write, remotes:write, or user:write for /me/rules). Body (PolicyRuleUpdateRequest):
| Field | Required | Description |
|---|---|---|
enforcement | Yes | default, enforced (governance layers only), evaluate, or disabled |
Only enforcement changes in place. To change a rule's target or effect, delete it and add a new rule. Governance routes on teams, orgs, and members use the same PUT .../rules/{ruleId} shape — see Usage policy API.
Returns the updated PolicyRuleDTO.
1curl -X PUT https://api.inference.sh/chats/chat_abc123/rules/rule_xyz \2 -H "Authorization: Bearer inf_your_key" \3 -H "Content-Type: application/json" \4 -d '{"enforcement":"evaluate"}'Revoke a chat rule
DELETE /chats/:id/rules/:ruleId
Requires the conversations:write scope. Only rules stored on the chat layer may be deleted (not synthesized legacy rows — remove those by changing chat settings or clearing the legacy list).
1{ "status": "ok" }Explain a call
POST /chats/:id/rules/explain
Requires the conversations:read scope. Test how the stacked rules would treat one call without running it. Body (ChatRuleExplainRequest):
| Field | Description |
|---|---|
kind | RemoteExec, Workspace, Harness, or Tool |
target | The call: shell command (RemoteExec), path (Workspace), harness tool call such as Bash(git status) or Read (Harness), or loop tool name (Tool) |
remote | Optional remote id when testing selector-scoped rules |
cwd | Optional working directory for relative paths |
Response (ChatRuleExplainDTO): effect, the rung and rule that decided (both null when no single rule decided: the kind's default, a command the engine cannot analyse, or every piece of a compound command allowed), a one-line reason, and would_have when rules set to evaluate would have decided otherwise.
1curl -X POST https://api.inference.sh/chats/chat_abc123/rules/explain \2 -H "Authorization: Bearer inf_your_key" \3 -H "Content-Type: application/json" \4 -d '{"kind":"RemoteExec","target":"rm -rf /","remote":"r_laptop"}'Defaults (no matching rule)
| Call | Default when nothing matches |
|---|---|
Platform loop tool with require_approval | Ask (approval interrupt) |
Platform loop tool without require_approval | Allow |
remote_exec | Ask — commands do not run until approved unless a chat or governance allow rule matches |
| Harness permission prompt | Ask, unless a matching allow rule (or allow all tools) auto-answers |
→ Approval settings · Human-in-the-loop workflow
Human-in-the-loop workflow
- Run an agent with tools marked
require_approval(see Approval settings). - Poll or stream the chat until
statusisawaiting_input. - In List chat messages (or the chat stream), find invocations with
status: awaiting_approval. - Call
POST /tools/:id/invoketo approve orPOST /tools/:id/rejectto reject. - Poll or stream again until the agent reaches
idleorcompleted.
List chats with status in busy or awaiting_input to find conversations needing action. The belt CLI wraps this flow with belt chat pending, belt chat approve, and belt chat reject — see CLI Setup.
Interrupt gates
When an agent run needs external input — tool approval, widget input, missing credentials, or a lifecycle hook gate — the platform creates an interrupt record. Interrupts provide a unified gate model alongside the tool-invocation endpoints above.
→ Lifecycle hooks — gate handlers for hook-driven interrupts
GET /chats/:id includes active_run when a run is in progress. When the run is interrupted, active_run.state is input_required or auth_required and active_run may include:
| Field | Description |
|---|---|
interrupt_reason | Why the run paused (see Interrupt reasons) |
interrupt_tool_id | Related tool invocation ID (when applicable) |
interrupt_meta | Additional context (JSON) |
List pending interrupts for a run
GET /agent-runs/:runId/interrupts
Returns pending interrupts for the given agent run. Requires the conversations:read scope.
Example:
1curl https://api.inference.sh/agent-runs/ar_abc123/interrupts \2 -H "Authorization: Bearer inf_your_key"JavaScript SDK: client.agents.listRunInterrupts('ar_abc123')
CLI: belt interrupt list ar_abc123 — see CLI setup — Interrupt.
Python SDK: use the REST endpoint with typed responses — import InterruptDTO from inferencesh.types (see Python SDK — Interrupt gates). Client helpers are not yet exposed in Python.
Resolve interrupt
POST /interrupts/:id/resolve
Resolve a pending interrupt with allow or deny. Requires the conversations:write scope and write access to the interrupt's chat; read access (for example a read share) is not enough. When all pending interrupts for the run are resolved, the agent run resumes automatically.
Request
1{2 "decision": "allow",3 "data": { "reviewer": "security-bot" }4}decision must be "allow" or "deny". Optional data (any JSON object) is stored on the interrupt as resolved_data and returned in the response — useful for audit metadata on hook gate resolutions.
Example:
1curl -X POST https://api.inference.sh/interrupts/int_abc123/resolve \2 -H "Authorization: Bearer inf_your_key" \3 -H "Content-Type: application/json" \4 -d '{"decision": "allow"}'JavaScript SDK: client.agents.resolveInterrupt('int_abc123', 'allow') — or agent.resolveInterrupt() when using client.agent().
CLI: belt interrupt resolve int_abc123 allow (or deny).
Python SDK: POST /interrupts/:id/resolve with {"decision": "allow"} or "deny" — type the response as InterruptDTO from inferencesh.types.
For tool approval interrupts (reason: tool_approval), resolving with allow is equivalent to POST /tools/:id/invoke; deny is equivalent to POST /tools/:id/reject.
To answer an interrupt from an external system, call POST /interrupts/:id/resolve with credentials that can write to the chat. A resolve_interrupt trigger answers only with the interrupt's default resolution on its schedule (this is how gate expiry works); see Triggers API — resolve_interrupt action.
InterruptDTO fields
| Field | Type | Description |
|---|---|---|
id | string | Interrupt ID |
run_id | string | Agent run ID |
chat_id | string | Chat ID |
reason | string | Why the run paused |
source | string | Gate origin (runtime component) |
resource_id | string | Related resource ID (for example a tool invocation) |
resource_type | string | tool_invocation (tool approval gates) or hook_event (lifecycle hook gates) |
status | string | pending, resolved, expired, or cancelled |
resolution | string | allow or deny (set when resolved) |
resolved_data | object | Optional JSON from the resolve request data field |
expires_at | string | RFC3339 expiry (if set) |
meta | object | Additional context (JSON). For hook_gate, contains the hook payload. |
Interrupt reasons
| Reason | When |
|---|---|
tool_approval | Tool requires human approval before execution |
client_tool | Client-side tool must run in your app or browser |
widget | Interactive widget needs user input |
auth | Missing credentials |
confirmation | Agent is waiting for explicit user confirmation |
hook_gate | Lifecycle hook gate awaiting an external decision |
→ Approval settings · Lifecycle hooks
List internal tool definitions
GET /agents/internal-tools
Returns the registry of built-in internal tool categories (plan, memory, widget, finish, skills, artifacts, agent, remotes, knowledge, meta). Use it to build agent configuration UIs. Each item includes the default the runtime applies when that category's flag is unset.
Requires the agents:read scope.
Response:
1[2 {3 "id": "plan",4 "name": "Plan",5 "description": "Task planning tools for creating, updating, and tracking multi-step plans. Helps the agent organize complex tasks into manageable steps.",6 "tools": ["plan_create", "plan_update", "plan_load"],7 "scope": "all",8 "default_enabled": true9 },10 {11 "id": "agent",12 "name": "Agent",13 "description": "Run a copy of this agent on a self-contained side task and get its answer back, ...",14 "tools": ["agent"],15 "scope": "all",16 "default_enabled": false17 }18]| Field | Type | Description |
|---|---|---|
id | string | Category key in internal_tools (for example plan, meta, knowledge) |
name | string | Display name |
description | string | What the category does |
tools | string[] | Tool names in this category |
scope | string | all, top_level, or sub_agent |
default_enabled | boolean | What the runtime applies when the agent's flag for this category is unset. false for the opt-in categories (meta, artifact, agent, remote, knowledge), true for the rest |
→ Internal Tools · JavaScript SDK: list internal tool definitions
List hook events
GET /agents/hook-events
Returns the canonical list of lifecycle hook events with metadata. Requires the agents:read scope.
Response:
1[2 {3 "event": "agent.tool_call",4 "description": "Fires before a tool is executed",5 "can_gate": true6 },7 {8 "event": "agent.tool_result",9 "description": "Fires after a tool completes",10 "can_gate": false11 }12]Use can_gate to choose between a synchronous webhook (decision: deny) and a gate handler (type: "gate") that creates an interrupt. See Lifecycle hooks.
cURL example
1# Start or continue a chat2curl -X POST https://api.inference.sh/agents/run \3 -H "Authorization: Bearer inf_your_key" \4 -H "Content-Type: application/json" \5 -d '{6 "agent": "myteam/support-agent@latest",7 "input": { "text": "Hello!" }8 }'910# Get chat (includes tool invocations)11curl https://api.inference.sh/chats/chat_abc123 \12 -H "Authorization: Bearer inf_your_key"1314# Approve a tool awaiting HIL15curl -X POST https://api.inference.sh/tools/ti_abc123/invoke \16 -H "Authorization: Bearer inf_your_key"Messaging channel webhooks
Connect external chat platforms so users can talk to an agent where they already work. Inbound platform events create agent messages; agent replies are delivered back through the same channel.
Receive events
POST /agents/{id}/channel/{provider}
| Parameter | Description |
|---|---|
id | Agent ID |
provider | Integration provider slug (currently slack) |
Legacy route (Slack only): POST /agents/{id}/slack — same behavior as provider=slack.
No API key is required. The platform verifies the request (for Slack, using your app's Signing Secret from the Slack integration). URL verification challenges (for example Slack url_verification) are answered automatically.
Example (Slack event subscription Request URL):
1https://api.inference.sh/agents/agent_abc123/channel/slackOn a valid inbound message, the API acknowledges immediately (200 with {"status":"ok"}) and processes the message asynchronously. Thread history is fetched when the channel supports it, so the agent has prior context.
Supported providers are registered per deployment; check integration guides for availability. See Chatting with Agents for the web app workflow.
Channel context
channel_context records where a conversation lives outside inference.sh so assistant replies can be delivered back through the same transport (Slack thread, Telegram chat, a wearable tag, …).
| Field | Type | Description |
|---|---|---|
channel_type | string | Provider slug — for example slack, telegram, teams, discord |
channel_metadata | object | Transport-specific routing data (JSON object on the wire) |
Webhook ingress. Inbound messaging channel webhooks set channel_context on the first user message automatically. The API persists it on the chat; later turns in that chat inherit routing without resending metadata.
Set by the platform only. channel_context comes only from messages that arrive through the platform's channel integrations. POST /agents/run, /agents/messages and /agents/message ignore a channel_context field in the body: the run goes ahead and the chat gets no channel.
For Slack, channel_metadata matches the fields the platform stores when events arrive through the channel webhook (channel, thread_ts, and optionally team_id, user_id, event_ts). Other providers use their own metadata shape; replies are dispatched only when a registered transport exists for channel_type.
GET /chats/:id returns channel_context on channel-originated chats. Chats started in the web app or through the API omit the field.
Get A2A agent card
GET /agents/{id}/card
GET /agents/{namespace}/{name}/card
Returns an Agent2Agent (A2A) protocol agent card for agent discovery and marketplace listings (for example GCP AI Agents Producer Portal). Requires agents:read.
The response is raw A2A JSON — not wrapped in the usual { "success": true, "data": … } envelope. Resolve the agent by ID or by namespace/name ref.
1curl https://api.inference.sh/agents/myteam/support-agent/card \2 -H "Authorization: Bearer inf_your_key"Example response:
1{2 "name": "myteam/support-agent",3 "description": "Customer support assistant",4 "version": "1.0.0",5 "url": "https://api.inference.sh/agents/myteam/support-agent/a2a",6 "documentationUrl": "https://app.inference.sh/agents/myteam/support-agent",7 "protocolVersion": "1.0",8 "preferredTransport": "JSONRPC",9 "provider": { "name": "inference shell", "url": "https://inference.sh" },10 "capabilities": { "streaming": true, "pushNotifications": false },11 "defaultInputModes": ["text/plain"],12 "defaultOutputModes": ["text/plain", "application/json", "application/a2ui+json"],13 "skills": [14 { "id": "search-tickets", "name": "Search tickets", "tags": ["app"] }15 ],16 "securitySchemes": [{ "scheme": "bearer" }]17}url is the agent's A2A endpoint (see A2A protocol); documentationUrl is its page in the web app. Skills are derived from the agent's current version tools (apps, sub-agents, MCP servers, HTTP hooks) and attached knowledge skills. In the web app, open an agent and use a2a card on the detail header to copy this JSON to the clipboard.
JavaScript SDK (@inferencesh/sdk ≥ v0.6.12): client.agents.getA2ACard('agent_abc123') — returns the same raw card object. See JavaScript SDK — Agent SDK.
A2A protocol
Every agent speaks A2A under its own path. The agent is the resource; the protocol is a dialect of it:
1/agents/{id}/a2a2/agents/{namespace}/{name}/a2a| Method | Path | Scope |
|---|---|---|
POST | /agents/{ref}/a2a | agents:execute — JSON-RPC 2.0 entry point (message/send, message/stream, tasks/get, tasks/cancel…) |
POST | /agents/{ref}/a2a/message:send | agents:execute |
POST | /agents/{ref}/a2a/message:stream | agents:execute — SSE |
GET | /agents/{ref}/a2a/tasks | conversations:read |
GET | /agents/{ref}/a2a/tasks/{runID} | conversations:read |
POST | /agents/{ref}/a2a/tasks/{runID}:cancel | conversations:write |
GET | /agents/{ref}/a2a/tasks/{runID}:subscribe | conversations:read — SSE |
Requests may carry an A2A-Version header; the response echoes the supported version. An A2A task maps to an agent run, not to a platform task.
1curl https://api.inference.sh/agents/myteam/support-agent/a2a/message:send \2 -H "Authorization: Bearer inf_your_key" \3 -H "Content-Type: application/json" \4 -d '{"message": {"messageId": "m1", "role": "user", "parts": [{"text": "hello"}]}}'OpenAI-compatible dialect
The same idea for OpenAI clients: /agents/{namespace}/{name}/openai/chat/completions runs one turn of the agent over the Chat Completions wire format, with channel, continue, wait and max_chars for clients that only send their latest utterance. See OpenAI-Compatible API — Agents.
Create or update agent template
POST /agents
Creates a saved agent template, or adds a new version when an agent with the same name already exists in your workspace's namespace. Requires the agents:write scope.
This mirrors app deploy semantics: the namespace is assigned server-side from your workspace username, so redeploying only requires the agent name and a version payload. You do not need to look up the agent ID first, and the namespace field in the request is optional.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Agent name (unique per workspace namespace) |
namespace | string | No | Optional. The server uses your workspace username |
id | string | No | Agent ID — include to create a new version of a known agent (alternative to name-based upsert) |
version | object | No | Version config (description, system_prompt, core_app, tools, skills, context, internal_tools, permissions, hooks, output_schema, …). version.description is limited to 200 characters |
When name matches an existing agent, the API creates a new version and sets it as current (same outcome as POST /agents/{id} with a version payload). Omitting visibility on redeploy preserves the existing visibility setting.
Agent version permissions
Set defaults on the version under permissions. They describe what new chats in the agent's home workspace may do without asking; each value is copied into the chat when it is created there (POST /chats, POST /agents/run without chat_id, and other chat-creation paths). Chats another workspace starts with a published agent do not copy these defaults. The chat owns copied settings from then on — the same allow_all_tools switch in chat settings and POST /chats/:id/settings. Team and org policy layers can still ask or deny over a chat's rules. Allow rules on the agent template apply only in the agent's home workspace; in another workspace's chat, template ask and deny rules still apply but allow rules do not. See also Approval settings — Agent template defaults.
| Field | Type | Description |
|---|---|---|
allow_all_tools | boolean | When true, each new chat in the agent's home workspace starts with allow all tools on so unattended runs (webhooks, cron, triggers) are not blocked on tool approvals. Existing chats are unchanged when you deploy a new version |
1"permissions": {2 "allow_all_tools": true3}In agent YAML (belt agent deploy), permissions is a top-level key next to system_prompt.
Example — first deploy:
1curl -X POST https://api.inference.sh/agents \2 -H "Authorization: Bearer inf_your_key" \3 -H "Content-Type: application/json" \4 -d '{5 "name": "support-agent",6 "version": {7 "description": "Customer support assistant",8 "system_prompt": "You help users with billing questions.",9 "core_app": { "ref": "openrouter/claude-sonnet-46" }10 }11 }'Example — unattended agent (allow every tool in new chats):
1curl -X POST https://api.inference.sh/agents \2 -H "Authorization: Bearer inf_your_key" \3 -H "Content-Type: application/json" \4 -d '{5 "name": "oncall-runner",6 "version": {7 "system_prompt": "Run the playbook when paged.",8 "core_app": { "ref": "openrouter/claude-sonnet-46" },9 "permissions": { "allow_all_tools": true }10 }11 }'Example — redeploy same name (creates new version):
Send the same request shape with updated version fields — no agent ID lookup required.
To update by ID instead (for example when you already have agent_abc123), use POST /agents/{id} with a version object. To change visibility or store images without a new version, use POST /agents/{id}/visibility or include only agent-level fields in POST /agents/{id}.
CLI: belt agent deploy <path> and belt agent create both use this endpoint. See CLI setup — Agents.
This endpoint is for new versions of an existing template. To fork a template into a separate agent (new ID, new version row, default private visibility), use Duplicate agent instead.
Duplicate agent
POST /agents/{id}/duplicate
Creates a new saved agent from a template you can read. Requires the agents:write scope. Returns the new Agent object.
| Behavior | Detail |
|---|---|
| Name | First free <name>-copy, then <name>-copy-2, … in your workspace namespace |
| Visibility | Always private on the copy |
| Title | Appends (copy) when the source has a title |
| Version | Copies the source's current version config into a new version row; the original agent does not gain a version |
| Workspace | The copy is created in the caller's active workspace. When the source lives in another workspace (for example a public agent), project, profile, and remote bindings are omitted |
Unlike POST /agents with an existing name (which appends a version to the same agent), duplicate always creates a distinct agent ID — the same outcome as Duplicate in the web app. Flows use the same -copy naming rule; see Duplicate flow.
JavaScript SDK
1const { data: copy } = await client.agents.duplicate('agent_abc123');Delete agent template
DELETE /agents/{id}
Permanently deletes a saved agent template (workspace definition). Requires the agents:write scope. This does not delete chat history from past runs.
1curl -X DELETE https://api.inference.sh/agents/agent_abc123 \2 -H "Authorization: Bearer inf_your_key"CLI equivalent: belt agent delete <ref> (accepts agent ID or namespace/name). See CLI setup — Agents.