Send a message to one of your project agents and stream the response. The
agent answers with text and, when the question calls for a visualization, a
generated chart configuration you can render, save, and add to dashboards.
This endpoint streams its response. It is not part of the OpenAPI playground
— use the examples below to integrate it.
Endpoint
POST https://api.upsolve.ai/v1/api/project-agents/{agentId}/chat
| Path parameter | Type | Description |
|---|
agentId | string (UUID) | The project agent to chat with. Found in the agent’s URL in the hub: /projects/{projectId}/agents/{agentId} |
Authentication
Authenticate with a project user token obtained from
Get Project User Token:
Authorization: Bearer <project_user_token>
The token determines which organization’s data the agent can access, so each
of your end users sees only their own data.
Request body
| Field | Type | Required | Description |
|---|
messages | array | Yes | Conversation history as { "role": "user" | "assistant", "content": string } objects. Include prior turns to continue a conversation. |
thread_id | string | No | Stable ID for the conversation thread. Reuse it across turns so the agent keeps session context (previous SQL, chart, and data). |
payload.agent_id | string | Yes | The same agent ID as the path parameter. |
payload.agent_version | number | No | Pin a specific agent version. Defaults to the latest version. |
{
"messages": [{ "role": "user", "content": "Show me revenue by month for 2025" }],
"thread_id": "thread-4f2a1c",
"payload": {
"agent_id": "AGENT_ID"
}
}
Response stream
The response is a text/event-stream of newline-separated events, each on a
data: {json} line. Every event has a type; the ones you need are:
| Event type | Payload | Meaning |
|---|
text-start | — | A new assistant text message begins |
text-delta | payload.text | Next chunk of assistant text |
text-end | — | The current text message is complete |
tool-call | payload.toolName | The agent started a tool (e.g. generateSql, generateChart) |
tool-result | payload.toolName, payload.result | A tool finished; result holds its output |
Other event types may appear (workflow steps, finish markers); ignore any
type you don’t handle.
The chart result
When the agent builds a chart, a tool-result event arrives with
payload.toolName === "generateChart". Its payload.result contains:
{
"chartConfig": {
// A complete chart configuration, including the SQL that powers it
"title": "Monthly Revenue 2025",
"type": "bar",
"dimensions": [ { "name": "Month", "field": "month", "type": "date", "bucketing": "month" } ],
"measures": [ { "name": "Revenue", "field": "revenue", "aggregation": "sum" } ],
"settings": { "layout": "vertical" },
"sqlOverride": "SELECT ..." // baked in by the agent — required for saving
},
"chartData": [ /* the aggregated rows the chart plots */ ],
"generatedChartRef": "d5e0…", // session-scoped reference — not a saved chart ID
"explanation": "A bar chart best shows the month-over-month comparison…"
}
chartConfig is ephemeral — the chat endpoint does not persist charts.
To keep a chart (and add it to a dashboard), pass chartConfig to
Save Chart from Chat. Do not
strip sqlOverride; a chart saved without it cannot load data on a
dashboard. generatedChartRef is a transient session ID, not a chart ID.
Example: call and parse the stream
const response = await fetch(
`https://api.upsolve.ai/v1/api/project-agents/${agentId}/chat`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${projectUserToken}`,
},
body: JSON.stringify({
messages: [{ role: "user", content: "Show me revenue by month for 2025" }],
thread_id: threadId,
payload: { agent_id: agentId },
}),
}
);
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buffer = "";
let assistantText = "";
let chart: { chartConfig: unknown; chartData: unknown[] } | null = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const event = JSON.parse(line.slice(6));
if (event.type === "text-delta") {
assistantText += event.payload?.text ?? "";
}
if (
event.type === "tool-result" &&
event.payload?.toolName === "generateChart" &&
event.payload?.result?.chartConfig
) {
chart = event.payload.result; // { chartConfig, chartData, ... }
}
}
}
Rate limits
Requests are rate limited at 180 requests per minute per organization + user.