Quickstart

2. Drive a conversation

Start a conversation, wait for the reply, and send a follow-up.

Once the agent is deployed, you talk to it through the Conversations API. The pattern is always the same:

  1. Start the conversation — get back a job_id and a user message_id.
  2. Poll the user message until its status flips from pending to completed (or failed).
  3. Read the assistant's reply from the message list.
  4. Continue the same conversation by sending follow-ups against the job_id.

This page assumes the agent slug from Configure and deploy is available as agent_id / AGENT_ID. If you skipped that page, set it to whatever agent_id you got back from POST /agents.

The same BASE / HEADERS / helper setup from the previous page applies here.

Step 1: Start the conversation

POST /conversations/start creates a new job and dispatches the first message. The response gives you a job_id (use it for follow-ups and listing messages) and a message_id (use it to poll for completion).

RESP=$(curl -s -X POST $BASE/conversations/start \
  -H "$AUTH" -H "$CT" \
  -d "{
    \"agent_id\": \"$AGENT_ID\",
    \"prompt\": \"Refund the failed charge for customer cus_123.\",
    \"ask_mode\": false
  }")

JOB_ID=$(echo "$RESP" | jq -r .job_id)
MSG_ID=$(echo "$RESP" | jq -r .message_id)
const startRes = await fetch(`${BASE}/conversations/start`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({
    agent_id: agentId,
    prompt: "Refund the failed charge for customer cus_123.",
    ask_mode: false,
  }),
});
const { job_id: jobId, message_id: messageId } = (await startRes.json()) as {
  job_id: string;
  message_id: string;
};
started = post("/conversations/start", {
  agent_id: agent_id,
  prompt: "Refund the failed charge for customer cus_123.",
  ask_mode: false,
})
job_id = started["job_id"]
message_id = started["message_id"]
started = requests.post(
    f"{BASE}/conversations/start",
    headers=HEADERS,
    json={
        "agent_id": agent_id,
        "prompt": "Refund the failed charge for customer cus_123.",
        "ask_mode": False,
    },
).json()
job_id = started["job_id"]
message_id = started["message_id"]
started, err := post("/conversations/start", map[string]any{
	"agent_id": agentID,
	"prompt":   "Refund the failed charge for customer cus_123.",
	"ask_mode": false,
})
if err != nil {
	panic(err)
}
jobID := started["job_id"].(string)
messageID := started["message_id"].(string)

Step 2: Poll for completion

GET /messages/{message_id} returns the user message and its current status. Poll until it's completed or failed.

while true; do
  STATUS=$(curl -s -H "$AUTH" $BASE/messages/$MSG_ID | jq -r .status)
  case "$STATUS" in
    completed|failed) break ;;
  esac
  sleep 2
done
async function getJson<T>(path: string): Promise<T> {
  const res = await fetch(`${BASE}${path}`, { headers: HEADERS });
  return (await res.json()) as T;
}

while (true) {
  const msg = await getJson<{ status: string }>(`/messages/${messageId}`);
  if (msg.status === "completed" || msg.status === "failed") break;
  await new Promise((r) => setTimeout(r, 2000));
}
def get(path)
  uri = URI("#{BASE}#{path}")
  req = Net::HTTP::Get.new(uri, HEADERS)
  res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
  JSON.parse(res.body)
end

loop do
  status = get("/messages/#{message_id}")["status"]
  break if %w[completed failed].include?(status)
  sleep 2
end
import time

while True:
    status = requests.get(
        f"{BASE}/messages/{message_id}",
        headers=HEADERS,
    ).json()["status"]
    if status in ("completed", "failed"):
        break
    time.sleep(2)
import "time"

func get(path string) (map[string]any, error) {
	req, err := http.NewRequest("GET", base+path, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("NAIRI_API_KEY"))
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	var out map[string]any
	return out, json.NewDecoder(res.Body).Decode(&out)
}

for {
	msg, err := get("/messages/" + messageID)
	if err != nil {
		panic(err)
	}
	status, _ := msg["status"].(string)
	if status == "completed" || status == "failed" {
		break
	}
	time.Sleep(2 * time.Second)
}

Step 3: Read the assistant's reply

GET /conversations/{job_id}/messages returns the full message timeline. Most integrators filter out intermediate progress messages — they're useful for live UIs but noise for backend code.

curl -s -H "$AUTH" $BASE/conversations/$JOB_ID/messages \
  | jq '.messages[] | select(.role != "progress")'
type Message = { role: string; content: string };
const { messages } = await getJson<{ messages: Message[] }>(
  `/conversations/${jobId}/messages`,
);
const visible = messages.filter((m) => m.role !== "progress");
console.log(visible);
messages = get("/conversations/#{job_id}/messages")["messages"]
visible = messages.reject { |m| m["role"] == "progress" }
puts visible
messages = requests.get(
    f"{BASE}/conversations/{job_id}/messages",
    headers=HEADERS,
).json()["messages"]
visible = [m for m in messages if m["role"] != "progress"]
print(visible)
convo, err := get("/conversations/" + jobID + "/messages")
if err != nil {
	panic(err)
}
messages, _ := convo["messages"].([]any)
for _, m := range messages {
	msg, _ := m.(map[string]any)
	if msg["role"] != "progress" {
		fmt.Println(msg)
	}
}

Step 4: Send a follow-up

To continue the same conversation, POST /conversations/{job_id}/continue with the next prompt. The agent has full context of the prior turn.

curl -s -X POST $BASE/conversations/$JOB_ID/continue \
  -H "$AUTH" -H "$CT" \
  -d '{"prompt": "Also send the customer an apology email."}'
await fetch(`${BASE}/conversations/${jobId}/continue`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({ prompt: "Also send the customer an apology email." }),
});
post("/conversations/#{job_id}/continue", {
  prompt: "Also send the customer an apology email.",
})
requests.post(
    f"{BASE}/conversations/{job_id}/continue",
    headers=HEADERS,
    json={"prompt": "Also send the customer an apology email."},
)
if _, err := post("/conversations/"+jobID+"/continue", map[string]any{
	"prompt": "Also send the customer an apology email.",
}); err != nil {
	panic(err)
}

Only conversations started via the API can be continued via the API. Trying to continue a job created from Slack, Discord, or the web UI returns 400 job is not an API job.

Where to go next

  • See the whole flow stitched together on the Full example page.
  • Read the full Conversations reference for ask mode, message roles, and the cursor-paginated list-for-agent endpoint.
  • Hook the agent into Slack or Discord via the Channels API.

On this page