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:
- Start the conversation — get back a
job_idand a usermessage_id. - Poll the user message until its
statusflips frompendingtocompleted(orfailed). - Read the assistant's reply from the message list.
- 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
doneasync 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
endimport 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 visiblemessages = 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-agentendpoint. - Hook the agent into Slack or Discord via the Channels API.