Skip to main content
When you call POST /v1/agents/{agent_id}/run, the response returns immediately with a task_id and status: "running" — the agent is still working in the background. To get the final result you poll one of the task-read endpoints until status is terminal.

Pick the right endpoint to poll

EndpointUse when
GET /v1/tasks/{task_id}Polling. Returns status + a few timestamps. Cheapest call.
GET /v1/tasks/{task_id}/summaryThe task is completed and you only want the final answer.
GET /v1/tasks/{task_id}/resultDebugging or audit — returns the full reasoning feed (every tool call and intermediate message).
A typical production loop looks like: poll /v1/tasks/{task_id} on a backoff schedule; once status hits a terminal value, call /summary once.

Terminal vs non-terminal statuses

StatusTerminal?What to do
pendingNoKeep polling — the orchestrator hasn’t picked it up yet.
queuedNoKeep polling — the agent is at its concurrent-task limit.
runningNoKeep polling.
waiting_for_inputNoThe agent called ask_user. Reply via POST /v1/tasks/{task_id}/message, then resume polling. See Interactive tasks.
awaiting_reviewNoA human in the dashboard needs to approve the next step.
completedYesDone. Fetch /summary.
failedYesErrored. Fetch /result for the reasoning trail.
stoppedYesCancelled (by you or by the system).

A real polling loop

import os, time, requests

BASE = "https://agents.nanonets.com"
HEADERS = {"Authorization": f"Bearer {os.environ['NANONETS_API_KEY']}"}
TERMINAL = {"completed", "failed", "stopped"}

def wait_for_task(task_id: str, timeout_s: int = 600) -> dict:
    deadline = time.time() + timeout_s
    delay = 1.0
    while time.time() < deadline:
        r = requests.get(f"{BASE}/api/v1/tasks/{task_id}", headers=HEADERS, timeout=10)
        r.raise_for_status()
        body = r.json()
        if body["status"] in TERMINAL:
            return body
        if body["status"] == "waiting_for_input":
            raise RuntimeError(f"Task {task_id} needs a reply — see /interactive-tasks")
        time.sleep(delay)
        delay = min(delay * 1.5, 10.0)  # gentle exponential backoff, cap at 10s
    raise TimeoutError(f"Task {task_id} did not finish within {timeout_s}s")

final = wait_for_task("f1c2a3b4-...")
if final["status"] == "completed":
    summary = requests.get(
        f"{BASE}/api/v1/tasks/{final['task_id']}/summary", headers=HEADERS
    ).json()
    print(summary)
Start your polling interval at 1–2 seconds and back off to ~10 seconds. The status endpoint is cheap, but tight loops still burn through rate limit budget for no benefit — most tasks take seconds to minutes.

Cancelling

If you no longer want the result, send POST /v1/tasks/{task_id}/cancel. The task moves to stopped. The step currently executing on the worker is not interrupted — it finishes naturally, but no further steps are scheduled. Cancel is idempotent: calling on an already-terminal task returns the current state with 200.

Tasks that legitimately take a long time

Some agents do work that takes minutes (multi-step research, large document extraction). A few rules of thumb:
  • Keep your client-side timeout generous (≥10 minutes) for those agents.
  • Don’t poll faster than every couple of seconds — you won’t get the answer any sooner.
  • For very long jobs, persist the task_id and poll from a background worker, not a user-facing HTTP request.