# 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](/api/quickstart/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 [#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).

<Tabs items="[&#x22;bash&#x22;, &#x22;TypeScript&#x22;, &#x22;Ruby&#x22;, &#x22;Python&#x22;, &#x22;Go&#x22;]">
  <Tab value="bash">
    ```bash
    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)
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    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;
    };
    ```
  </Tab>

  <Tab value="Ruby">
    ```ruby
    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"]
    ```
  </Tab>

  <Tab value="Python">
    ```python
    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"]
    ```
  </Tab>

  <Tab value="Go">
    ```go
    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)
    ```
  </Tab>
</Tabs>

## Step 2: Poll for completion [#step-2-poll-for-completion]

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

<Tabs items="[&#x22;bash&#x22;, &#x22;TypeScript&#x22;, &#x22;Ruby&#x22;, &#x22;Python&#x22;, &#x22;Go&#x22;]">
  <Tab value="bash">
    ```bash
    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
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    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));
    }
    ```
  </Tab>

  <Tab value="Ruby">
    ```ruby
    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
    ```
  </Tab>

  <Tab value="Python">
    ```python
    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)
    ```
  </Tab>

  <Tab value="Go">
    ```go
    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)
    }
    ```
  </Tab>
</Tabs>

## Step 3: Read the assistant's reply [#step-3-read-the-assistants-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.

<Tabs items="[&#x22;bash&#x22;, &#x22;TypeScript&#x22;, &#x22;Ruby&#x22;, &#x22;Python&#x22;, &#x22;Go&#x22;]">
  <Tab value="bash">
    ```bash
    curl -s -H "$AUTH" $BASE/conversations/$JOB_ID/messages \
      | jq '.messages[] | select(.role != "progress")'
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    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);
    ```
  </Tab>

  <Tab value="Ruby">
    ```ruby
    messages = get("/conversations/#{job_id}/messages")["messages"]
    visible = messages.reject { |m| m["role"] == "progress" }
    puts visible
    ```
  </Tab>

  <Tab value="Python">
    ```python
    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)
    ```
  </Tab>

  <Tab value="Go">
    ```go
    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)
    	}
    }
    ```
  </Tab>
</Tabs>

## Step 4: Send a follow-up [#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.

<Tabs items="[&#x22;bash&#x22;, &#x22;TypeScript&#x22;, &#x22;Ruby&#x22;, &#x22;Python&#x22;, &#x22;Go&#x22;]">
  <Tab value="bash">
    ```bash
    curl -s -X POST $BASE/conversations/$JOB_ID/continue \
      -H "$AUTH" -H "$CT" \
      -d '{"prompt": "Also send the customer an apology email."}'
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    await fetch(`${BASE}/conversations/${jobId}/continue`, {
      method: "POST",
      headers: HEADERS,
      body: JSON.stringify({ prompt: "Also send the customer an apology email." }),
    });
    ```
  </Tab>

  <Tab value="Ruby">
    ```ruby
    post("/conversations/#{job_id}/continue", {
      prompt: "Also send the customer an apology email.",
    })
    ```
  </Tab>

  <Tab value="Python">
    ```python
    requests.post(
        f"{BASE}/conversations/{job_id}/continue",
        headers=HEADERS,
        json={"prompt": "Also send the customer an apology email."},
    )
    ```
  </Tab>

  <Tab value="Go">
    ```go
    if _, err := post("/conversations/"+jobID+"/continue", map[string]any{
    	"prompt": "Also send the customer an apology email.",
    }); err != nil {
    	panic(err)
    }
    ```
  </Tab>
</Tabs>

<Callout type="info">
  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`.
</Callout>

## Where to go next [#where-to-go-next]

* See the whole flow stitched together on the &#x2A;*[Full example](/api/quickstart/full-example)** page.
* Read the full &#x2A;*[Conversations](/api/conversations/overview)** reference for ask mode, message roles, and the cursor-paginated `list-for-agent` endpoint.
* Hook the agent into Slack or Discord via the &#x2A;*[Channels](/api/channels/overview)** API.
