Skip to main content
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 parameterTypeDescription
agentIdstring (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

FieldTypeRequiredDescription
messagesarrayYesConversation history as { "role": "user" | "assistant", "content": string } objects. Include prior turns to continue a conversation.
thread_idstringNoStable ID for the conversation thread. Reuse it across turns so the agent keeps session context (previous SQL, chart, and data).
payload.agent_idstringYesThe same agent ID as the path parameter.
payload.agent_versionnumberNoPin 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 typePayloadMeaning
text-startA new assistant text message begins
text-deltapayload.textNext chunk of assistant text
text-endThe current text message is complete
tool-callpayload.toolNameThe agent started a tool (e.g. generateSql, generateChart)
tool-resultpayload.toolName, payload.resultA 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.