Skip to main content

Error envelope

Every non-2xx response has the same shape:
{
  "error": "Invalid agent_id format"
}
The HTTP status code carries the class of failure; the error string is a human-readable hint. Don’t pattern-match on the message text — it’s free-form and may change. Branch on the status code instead.

Status code reference

StatusMeaningWhat to do
400 Bad RequestMalformed request — invalid UUID, missing required field, oversized file.Fix the request; do not retry as-is.
401 UnauthorizedAPI key missing, malformed, or revoked.Mint a new key (see Authentication).
403 ForbiddenKey is valid but the agent/task lives in another workspace.Use a key scoped to the right workspace.
404 Not FoundAgent or task UUID doesn’t exist.Check the ID. Note: this is also returned for resources in other workspaces, so a stolen ID can’t be probed.
409 ConflictState-conflict (e.g. publishing an agent that has no draft changes).Inspect the message; the operation isn’t valid right now.
429 Too Many RequestsRate limit hit.Back off — see below.
500 Internal Server ErrorUnexpected server-side error.Safe to retry with exponential backoff.
503 Service UnavailableTemporary outage / deploy.Retry with backoff.

When to retry

ClassRetry?
4xx except 429No — the request itself is wrong. Fix and re-send.
429Yes, after a back-off. Respect any Retry-After header if present.
5xxYes — these are transient. Use exponential backoff (1s → 2s → 5s → 10s, cap at ~30s) with full jitter, and give up after 4–5 attempts.
A minimal retry helper:
Python
import os, time, random, requests

def with_retries(method, url, *, max_attempts=5, **kwargs):
    delay = 1.0
    for attempt in range(max_attempts):
        r = requests.request(method, url, **kwargs)
        if r.status_code < 500 and r.status_code != 429:
            return r
        if attempt == max_attempts - 1:
            r.raise_for_status()
        retry_after = r.headers.get("Retry-After")
        sleep = float(retry_after) if retry_after else min(delay, 30) * (1 + random.random())
        time.sleep(sleep)
        delay *= 2
Never retry POST /v1/agents/{agent_id}/run after a 5xx without checking whether the task was actually created. A 5xx during run-creation can occasionally mean “request reached us, then the connection died” — issue a GET /v1/tasks filtered by agent_id and created_after before retrying to avoid double-runs.

Pagination

List endpoints (GET /v1/tasks, GET /v1/agents, GET /v1/agents/{id}/versions) are cursor-paginated, newest-first. The response looks like:
{
  "tasks": [ /* … up to `limit` items … */ ],
  "next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wNS0xMlQwOToxNTowMFoiLCJpZCI6IjFhMmIzYy0..."
}
To fetch the next page, pass that cursor back unchanged as the cursor query parameter. When you reach the end, next_cursor is omitted (or empty).
# First page
curl "https://agents.nanonets.com/api/v1/tasks?limit=50" \
  -H "Authorization: Bearer $NANONETS_API_KEY"

# Subsequent page
curl "https://agents.nanonets.com/api/v1/tasks?limit=50&cursor=$NEXT_CURSOR" \
  -H "Authorization: Bearer $NANONETS_API_KEY"
Python
def iter_tasks(**filters):
    cursor = None
    while True:
        params = {"limit": 100, **filters}
        if cursor:
            params["cursor"] = cursor
        r = requests.get(f"{BASE}/api/v1/tasks", headers=HEADERS, params=params).json()
        for task in r["tasks"]:
            yield task
        cursor = r.get("next_cursor")
        if not cursor:
            return

for task in iter_tasks(agent_id="…", status="completed"):
    print(task["task_id"], task["title"])

Cursor opacity

next_cursor is opaque — don’t try to parse it. If you decode it today and rely on the shape, your code will break the next time the server changes its pagination key.

limit

Default is 20, max is 100. Larger pages mean fewer round-trips but a larger response body; for bulk export, 100 is usually the right choice.

Rate limits

Rate limits are enforced per-workspace, not per-key. If you’re running tight loops or fanning out many concurrent agents, watch for 429 and back off. We do not currently publish per-tier numbers — if you’re hitting limits in production, reach out.