NetStacksNetStacks

API Errors

Enterprise

The error format and HTTP status codes returned by the NetStacks Controller API, and how to handle them in scripts.

Error format

Most endpoints return errors as a JSON object with a single error field holding a readable message:

{ "error": "Device not found" }

There are no numeric error codes. Use the HTTP status code to decide what to do.

A few responses differ:

  • Rate limits (429) add retry_after_secs and a Retry-After header: {"error":"Too many requests","retry_after_secs":42}.
  • Plain-text errors. The scheduled task endpoints (/api/tasks/*) and the config snapshot endpoints (/api/devices/snapshots/*) return the message as plain text, for example Snapshot not found.
  • Request parsing errors are plain text from the web framework, for example when the JSON is malformed or a required field is missing (see the table below).
  • Some errors have no body. For example, the settings endpoints return only the status code.
Parse defensively

Check the status code first. Then try to parse the body as JSON and read error; if that fails, use the raw text.

Status codes

StatusMeaning in NetStacksWhat to do
200 OKSuccess with a body—
201 CreatedObject created (devices, templates, stacks, deployments, tasks, …), or a config pull that stored a backup—
202 AcceptedWork started in the background: snapshot collection, bulk config pull, task “run now”Poll the returned object for progress
204 No ContentDeleted—
400 Bad RequestThe request is invalid: a business rule failed (for example “Devices require a profile”), a template failed to render, a cron expression is invalid, or the JSON body could not be parsedFix the request; the message says what is wrong
401 UnauthorizedNo token, an expired or invalid token, or wrong login credentialsRefresh the token or log in again
403 ForbiddenYour roles lack the permission the endpoint needs (Insufficient permissions), or the action isn't allowed for you (for example revealing a profile secret without a reveal grant)Ask an administrator for the right role
404 Not FoundThe object doesn't exist, or it belongs to another organizationCheck the ID and the active organization
409 ConflictA duplicate (a name already in use), an organization that still has users or devices, or no free license seat for a terminal loginChoose another value, or free the resource
415 Unsupported Media TypeA JSON endpoint was called without Content-Type: application/jsonAdd the header
422 Unprocessable EntityThe JSON is well formed but doesn't match the expected fields (a required field is missing or has the wrong type)Compare the body with the field tables on the endpoint page
429 Too Many RequestsLogin rate limit or failed-login lockout on /api/auth/*Wait Retry-After seconds
500 Internal Server ErrorAn unexpected error inside the ControllerRetry once; if it persists, check the API logs
502 Bad GatewayThe Controller could not get a result from a device or upstream service, for example every method of a config pull failed. Also returned by the web proxy when the API container is downCheck device reachability and credentials, or the API container
503 Service UnavailableA feature's backing service is unavailable (for example session sharing without Valkey)Check the Controller's services

Common error messages

StatusMessageCause
401Authentication requiredNo Authorization: Bearer header
401Invalid or expired tokenThe access token expired, was signed with an old key, or is malformed
401Invalid credentialsWrong username or password at login (may add how many attempts remain)
403Insufficient permissionsMissing permission for this endpoint
429Too many failed login attempts. Please try again later.Failed-login lockout for your IP
409seat_limit_reached: … (seats_total: N, seats_used: N)No free license seat for a terminal login
409<field> "<value>" is already in use — choose a different value.Unique value already taken
400Can't apply this change: the selected profile no longer exists. …The request refers to a profile, site, or other record that was deleted
400A required field is missing.A required value was empty

Handling errors in scripts

import time, requests

def call(session, method, url, **kw):
    r = session.request(method, url, **kw)
    if r.status_code == 429:
        time.sleep(int(r.headers.get("Retry-After", "30")))
        r = session.request(method, url, **kw)
    if r.ok:
        return r.json() if r.content else None
    try:
        message = r.json().get("error", r.text)
    except ValueError:
        message = r.text  # plain-text error
    raise RuntimeError(f"{method} {url} -> {r.status_code}: {message}")

For 401 responses, refresh the token and retry once (see the example on Authentication). Don't retry 400, 403, 404, 409, or 422 without changing the request.