AgentConnect
Connect & Automate

Agent chat API

Put an agent behind your own chat UI, such as an Ask AI panel on a docs site or a support widget, with an API key and the AI SDK.

The agent chat API puts an agent behind any chat UI built on the AI SDK's useChat (UI message stream protocol v1). Each turn is one HTTP POST with your API key, answered as a server-sent event stream. The Ask AI panel on this site works this way.

It takes three steps:

  1. Add the API to the agent.
  2. Create a key limited to agent chat.
  3. Post turns to the chat endpoint with the key.

Add the API to the agent

An agent accepts API calls only after its owner adds the API. Open the agent, then Integrations → Add integration, pick API under Workflow, choose AI SDK UI and click Add API.

Add the API to an agent

The agent's Integrations tab now shows an API card. Its Quickstart shows the chat endpoint for this agent, examples for curl, Node and the browser, and the keys that reach the agent. Removing the API stops the agent accepting calls within a minute; keys that cover it stay valid for other agents.

The API card's Quickstart

Create a key

Click Create key in the Quickstart, or create one for a service account under Settings → Service accounts → Keys, so it does not depend on any one person, or for yourself under Profile → API keys. Either way:

  • Permission: Agent chat.
  • Agents: Selected agents, then pick the agent.

An Agent chat key works only on the chat endpoint, and only for the selected agents that have added the API; the Control Plane's REST API refuses it. Like any API key, it is shown once and expires after 90 days by default. Revoking it stops it within a minute.

Send a turn

Copy the chat endpoint from the Quickstart. Send POST to it with Authorization: Bearer <key> and the useChat request body ({ id, messages, trigger, … }):

curl -N "$CHAT_URL" \
  -H "Authorization: Bearer $AGENTCONNECT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"id":"chat-1","messages":[{"id":"m1","role":"user","parts":[{"type":"text","text":"How do I install the daemon?"}]}]}'

The chat endpoint is <relay-url>/ai-sdk/agents/<agent-id>/chat. On AgentConnect OSS, the relay URL is your relay's public URL.

The body's id, 1 to 128 characters, names the conversation:

  • The first turn with a new id opens a conversation; the same id continues it. useChat generates an id per chat and sends it on every turn.
  • Conversations belong to the key's owner. The same id sent with another member's key is a different conversation.
  • The relay reads only the text parts of the last user message. The agent's session already holds the history, so earlier messages, file parts and data-* parts are ignored. A request whose last message answers the agent's questions continues its turn instead; see Questions and approvals.

The response is text/event-stream with the header x-vercel-ai-ui-message-stream: v1. Its parts follow the AI SDK protocol, and the stream ends with [DONE].

  • A turn can run for minutes. A keepalive comment arrives every 15 seconds.
  • A request body may be up to 4 MiB, and a turn's text up to 128 KiB.
  • One turn runs at a time in a conversation.

Refusals arrive as a status before any stream, with a JSON body whose reason names the machine-readable cause where there is one:

StatusMeaning
400No id, or no user message with text.
401The key is unknown, revoked or expired.
403The key is not an Agent chat or full key, or the agent has not added the API (api_disabled).
404The key's agent selection does not include this agent, or you cannot see it.
409A turn is still running in this conversation (busy), the agent moved since the conversation ran (agent_moved), or the turn an answer belongs to has ended (turn_ended). Start a new conversation after agent_moved.
413The body or the turn's text is too large.
422The API's Decision gate declined the turn (declined).
503The agent is offline, paused or draining.

What the stream carries

PartContent
text-*The agent's reply.
reasoning-*The agent's thinking.
data-toolTool activity: title and status only.
data-planThe agent's plan.
data-noticeStatus notices, such as "Starting sandbox…".
message-metadataThe session title.
dynamic-toolThe agent's question (agentconnect_ask) or a tool approval (agentconnect_approval); see Questions and approvals.

A client that renders only text shows only the reply. stop() in useChat aborts only the fetch: the turn still finishes, and its output lands in the session's transcript in the console. An interrupted stream cannot be resumed yet.

Questions and approvals

A turn can stop on something only the caller can answer: the agent asks a question, or a tool it wants to run needs approval. The stream hands it over as a tool call, a dynamic-tool part, and ends with the finish reason tool-calls. The turn waits for the answer.

ToolWhat it asksHow to answer
agentconnect_askA question. input.message asks it, and input holds the options to choose from, a text or number field, several fields, or a url to open.addToolOutput with the chosen value, a list for a multi-select, a value per field for fields, or the url once opened. state: 'output-error' dismisses the question.
agentconnect_approvalApproval for one tool call. input.tool names the tool and input.detail summarizes it in one line, such as the command.addToolApprovalResponse({ id, approved }).

Send the answer as the next request. useChat does that on its own with sendAutomaticallyWhen set to lastAssistantMessageIsCompleteWithToolCalls or lastAssistantMessageIsCompleteWithApprovalResponses. The relay continues the same turn, and the rest of the reply streams into the same assistant message. Each tool call's callProviderMetadata names the turn it belongs to, so send the assistant message back as the stream built it.

A refused approval takes effect at once, and the agent's editors never see it. A granted one takes effect when the key's owner could approve it in the console: a Full access key whose owner can edit the agent. Granted with any other key, it goes to the agent's editors to approve in the console, and the turn waits for them. A public site can refuse every approval in code, so its visitors never reach the editors; this site's Ask AI panel does.

A turn does not wait forever:

  • Left unanswered for 10 minutes, it is cancelled.
  • A new message instead of an answer cancels it, and the message starts a turn of its own.
  • An answer to a turn that has already ended gets 409 with turn_ended.

From Node

The AI SDK's DefaultChatTransport sends turns and parses the stream. It needs AI SDK 6 or later (npm i ai):

import { DefaultChatTransport } from 'ai'

const transport = new DefaultChatTransport({
  api: process.env.CHAT_URL,
  headers: { Authorization: `Bearer ${process.env.AGENTCONNECT_API_KEY}` }
})
const stream = await transport.sendMessages({
  trigger: 'submit-message',
  chatId: 'chat-1', // the conversation; send the same id again to continue it
  messageId: undefined,
  abortSignal: undefined,
  messages: [{ id: '1', role: 'user', parts: [{ type: 'text', text: 'How do I install the daemon?' }] }]
})
for await (const chunk of stream) if (chunk.type === 'text-delta') process.stdout.write(chunk.delta)

From a browser

A browser must not hold the key, so put a route on your own server in front of the chat endpoint. It adds the key and forwards useChat's request unchanged. A Next.js route handler, app/api/chat/route.ts:

export async function POST(req: Request) {
  const upstream = await fetch(process.env.CHAT_URL!, {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.AGENTCONNECT_API_KEY}`, 'Content-Type': 'application/json' },
    body: await req.text(),
    signal: req.signal
  })
  return new Response(upstream.body, {
    status: upstream.status,
    headers: { 'Content-Type': 'text/event-stream', 'x-vercel-ai-ui-message-stream': 'v1' }
  })
}

On the page, point useChat at it (ai and @ai-sdk/react, AI SDK 6 or later):

'use client'
import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport } from 'ai'

export default function Chat() {
  const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat' }) })
  // Render messages, and call sendMessage({ text }) on submit.
}

Every visitor's conversations belong to the key's owner, and a visitor reaches only the conversations its own chat ids name; useChat's ids are random. Rate-limit visitors on your route, since AgentConnect limits turns per conversation, not per visitor.

This site's own route, in the agentconnect-md/docs repository, goes further: it chooses the chat id itself and keeps it in an HttpOnly cookie per open tab, and starts a new conversation when the panel is cleared. See lib/ask-ai.ts.

Gate turns by a Decision

The API card's Add decision gives the API the same rules a channel's By decision has: the agent answers a call only when the Decision matches the call's message, and the endpoint answers 422 otherwise. A call whose Decision cannot be evaluated is answered as if there were no gate. Recent evaluations in the rules shows each call the gate judged and why; only members who can edit the agent see it.

Sessions and safety

A conversation opened this way is an ordinary webchat session of the key's owner, and its source shows the key. A service account's session is visible to the whole organization; a personal key's is private to that user. Its usage is metered on the agent like any other turn; see Billing & usage.

The agent receives untrusted input on every turn. Configure it for that:

  • Give it no secrets and no write access to repositories.
  • Run it in a sandbox that isolates untrusted input: microsandbox or Kubernetes.
  • Keep it in an organization whose members may read those transcripts.

Troubleshooting

  • 403 "this agent does not accept API calls": add the API to the agent (above).
  • 403 "this API key cannot chat with agents": the key's permission is Read-only. Create an Agent chat key.
  • 404 "agent not found": the key's agent selection does not include this agent.
  • 409 on a turn: a turn is still running in this conversation. Wait for it to finish, or use a new chat id.
  • 409 turn_ended on an answer: the turn waited 10 minutes without one, or a new message replaced it. Ask again in a new message.
  • 503 right after a deployment: the agent's daemon is reconnecting. Retry.

How is this guide?

On this page

How is this guide?