Harnix
Integrate

Streaming

SSE frames state|tool|token|approval|question|done|error, keepalive comments, reconnecting.

Once you've sent a message (widget, server-to-server, or POST /v2/conversations/{id}/messages), you have a run_id. Follow the answer live with:

GET https://api.harnix.vn/v2/runs/{run_id}/stream
Authorization: Bearer <token>

(or X-API-Key: <key> for server-to-server). The response is a Server-Sent Events stream, each frame a data: <json> line ending with a blank line.

EventSource can't carry Authorization

The browser's EventSource doesn't support custom headers, so it can't carry Bearer <token>. Use fetch and read the response's ReadableStream yourself (example below), or an SSE client that supports custom headers.

Frame types

typedataWhen
state{ "state": "running", "reason": "claimed" }The run changed state.
token{ "delta": "Hello" }A chunk of text the model just generated.
tool{ "call_id": "...", "name": "knowledge_search", "phase": "start" | "end", "is_error"?: true }A tool started or finished.
approval{ "run_id": "...", "expires_at": "...", "calls": [...] }The agent wants to call a tool the end-user must confirm; the run is paused. Not a last frame.
question{ "run_id": "...", "call_id": "...", "expires_at": "...", "questions": [...] }The agent calls ask_user to ask the end-user questions; the run pauses in waiting_input. Not a last frame.
done{ "result": {...}, "citations": [...] }The run finished successfully. Always the last frame.
error{ "message": "..." }The run failed or was cancelled. Always the last frame.

done.data.citations is always an array ([] when there are none) — the same value as done.data.result.citations, just placed somewhere more convenient for the UI. The error frame has no citations key at all.

The approval frame — an action awaiting confirmation

When the agent calls a connector tool that needs confirmation (by default: every tool the server doesn't mark read-only), the run pauses and the stream sends an approval frame:

{
  "type": "approval",
  "data": {
    "run_id": "0e2b...",
    "expires_at": "2026-10-09T08:05:00+00:00",
    "calls": [
      {
        "call_id": "call_1",
        "server_name": "Shop",
        "name": "create_order",
        "title": "Create order",
        "arguments": { "sku": "A-100", "quantity": 2 }
      }
    ]
  }
}

calls carries the real arguments the agent is about to send — that is what the user approves or declines; title is the human-readable name when the server declares one, server_name the connector name you chose in the console. Show them to the user (the assistant's text just before is the explanation), then answer with POST /v2/runs/{run_id}/approval before expires_at (5 minutes after the pause by default).

After an approval frame, the stream stays open: once a decision comes in, the run resumes and the same connection carries the following state frames (pending, then running), tool and token frames as usual. A declined or expired tool ends with a tool frame with phase: "end" and is_error: true; the assistant tells the user the action was not carried out, and the run ends with done as normal.

The question frame — questions awaiting user input

When the agent calls the built-in ask_user tool to ask the user questions, the run pauses in waiting_input and the stream sends a question frame:

{
  "type": "question",
  "data": {
    "run_id": "0e2b...",
    "call_id": "call_1",
    "expires_at": "2026-10-10T12:10:00+00:00",
    "questions": [
      {
        "header": "Branch",
        "question": "Which branch should prepare your order?",
        "multi_select": false,
        "options": [
          { "label": "District 1", "description": "Open until 22:00" },
          { "label": "District 3", "description": "Open until 21:00" }
        ]
      }
    ]
  }
}

Each question contains a concise title (header, up to 12 characters), question text (question, up to 500 characters), a multi-choice flag (multi_select), and 2–4 options (options). Display them to the user, then answer via POST /v2/runs/{run_id}/answer — or simply send a regular follow-up text message into the conversation — before expires_at (10 minutes after the pause by default).

Just like with approval, the stream stays open: once answered (or expired), the run resumes and the same connection carries the following state frames (pending, then running), tool (carrying the ask_user tool result), token and done frames as usual.

Between frames, the server may send : keepalive comment lines (not a frame — ignore any line starting with :) to keep the connection alive through an intermediate proxy while the model is thinking.

Example

curl -N https://api.harnix.vn/v2/runs/0e2b.../stream \
  -H "Authorization: Bearer $TOKEN"

Reconnecting

A finished run returns its full content straight from Postgres — nothing is lost whether you connect late or reconnect mid-run. If the connection drops while the run is still going, call the same URL again: the server replays every state/tool frame you missed (it doesn't replay old token deltas — just the final content in the done frame), then continues streaming live. An action still awaiting confirmation is replayed as an approval frame; a question still awaiting input is replayed as a question frame; one that has already been resolved is not — you see its outcome in the matching tool frame instead.

Streaming temporarily unavailable

If the streaming infrastructure (Redis) is temporarily unreachable, the endpoint returns 503 with code redis_unavailable. The run itself is not affected — it keeps running and gets recorded in full. Read the answer back with GET /v2/conversations/{id} after retrying the stream, or poll that conversation every few seconds.

Cancelling a running run

POST /v2/runs/{run_id}/cancel — see Idempotency & cancel. A run that is waiting for confirmation or waiting for user input can be cancelled too.

Next

  • Conversations — read history back once a run finishes, along with pending_approval and pending_question when reopening a waiting conversation.
  • Errors — run_not_found, run_not_waiting, redis_unavailable.

On this page