NetStacksNetStacks

API Authentication

Enterprise

Log in to the NetStacks Controller API, send the bearer token, refresh it, and manage sessions. Covers local, LDAP, and OIDC login endpoints.

Overview

The Controller API uses JSON Web Tokens (JWTs). You log in with a username and password and get two tokens back:

  • An access token. Send it on every request in the Authorization: Bearer header. It lasts 24 hours by default (the session.access_token_hours setting).
  • A refresh token. Exchange it for a new pair of tokens when the access token runs out. It lasts 30 days by default (the session.refresh_token_days setting).

There are no personal API keys or long-lived tokens. For automation, create a dedicated local user (for example automation), give it a role with only the permissions the script needs, and have the script log in and refresh.

Log in

POST /api/auth/login takes a local NetStacks username and password:

curl -s --cacert netstacks-ca.pem -X POST https://controller.example.com:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"automation","password":"<password>"}'
FieldTypeNotes
usernamestringRequired.
passwordstringRequired.
client_typestringOptional. admin_ui (the default) or terminal. A terminal login uses a license seat. Leave this out for scripts.
public_keystringOptional OpenSSH public key. If sent and the Controller has an SSH CA, the response also contains a signed SSH certificate (ssh_certificate).

A successful login returns 200:

{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "Xk3...64 characters...",
  "token_type": "Bearer",
  "expires_in": 86400
}

expires_in is the access token lifetime in seconds. Wrong credentials return 401 with {"error":"Invalid credentials"}. The message can also say how many attempts are left before the login lockout.

Send the token

Put the access token in the Authorization header of every /api request:

curl -s --cacert netstacks-ca.pem https://controller.example.com:3000/api/auth/me \
  -H "Authorization: Bearer $TOKEN"

GET /api/auth/me is a good first call. It returns who you are and what you can do:

{
  "id": "3f6c...",
  "org_id": "0000...0001",
  "home_org_id": "0000...0001",
  "is_platform_admin": false,
  "username": "automation",
  "email": "[email protected]",
  "display_name": "Automation",
  "roles": ["operator"],
  "permissions": ["devices.access", "..."],
  "is_active": true,
  "auth_provider": "local",
  "created_at": "2026-09-01T10:00:00Z",
  "updated_at": "2026-09-01T10:00:00Z"
}

Without a token you get 401 {"error":"Authentication required"}. With an expired or bad token you get 401 {"error":"Invalid or expired token"}.

Refresh the token

When the access token expires, exchange the refresh token for a new pair. No Authorization header is needed:

curl -s --cacert netstacks-ca.pem -X POST https://controller.example.com:3000/api/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh_token":"'"$REFRESH"'"}'

The response has the same shape as the login response. Refresh tokens are single use: each refresh revokes the old refresh token and returns a new one, so always save the new refresh_token. A refresh also puts a platform administrator back in their home organization.

Log out

POST /api/auth/logout (with the bearer token) ends your session. It revokes all of your refresh tokens, on every device, and frees the license seat if the session used one. It is recorded in the audit log as auth.logout.

{ "message": "Successfully logged out", "sessions_revoked": 2 }

To end only one session, use DELETE /api/auth/sessions/:id. To end every session except the current one, use DELETE /api/auth/sessions/other.

Endpoint reference

All paths are under /api/auth.

MethodPathPurposePermission
POST/loginLog in with a local username and passwordNone
POST/ldapLog in with LDAP / Active Directory credentialsNone
POST/refreshExchange a refresh token for a new token pair. Body: {"refresh_token"}None
GET/providersWhich login providers are configured: {"providers":[{"name":"local","enabled":true},…]}None
GET/oidc/authorizeStart an OpenID Connect browser loginNone
GET/oidc/callbackOIDC redirect target (called by your identity provider)None
GET/meCurrent user, roles, and computed permissionsSigned in
POST/logoutLog out and revoke your refresh tokensSigned in
PUT/passwordChange your own password. Body: {"current_password","new_password"}Signed in
GET/sessionsYour sessions: id, created_at, expires_at, user_agent, ip_addressSigned in
DELETE/sessions/:idRevoke one of your sessionsSigned in
DELETE/sessions/otherRevoke all your other sessionsSigned in
POST/switch-orgSwitch the active organization. Body: {"org_id"}platform.admin
POST/ldap/testTest the configured LDAP bindSigned in

Password rules for PUT /password: at least 12 characters, and not one of the common passwords on the built-in deny list. It only works for local accounts.

LDAP and OIDC login

If an administrator has set up LDAP or OpenID Connect under Settings → Authentication, those users can get tokens too:

  • LDAP: POST /api/auth/ldap with {"username","password"} (and optional client_type). The response is the same as /login. This works from scripts.
  • OIDC: a browser flow. Send the browser to GET /api/auth/oidc/authorize?redirect_uri=<your URL>. After the identity provider login, the Controller redirects to your redirect_uri with the tokens in the URL fragment: #access_token=…&refresh_token=…&token_type=Bearer&expires_in=…. OIDC is not suitable for unattended scripts.
New directory users have no roles

The first LDAP or OIDC login creates a NetStacks user in the default organization with no roles. An administrator must assign a role before that user can call anything beyond /api/auth/*. Groups and claims are not mapped to roles automatically.

For automation, a local user is the simplest choice. See Authentication settings.

Switching organization

Platform administrators can work inside another organization. Call POST /api/auth/switch-org with {"org_id":"<uuid>"} to get a new access token scoped to that organization:

{
  "access_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 86400,
  "active_org_id": "a5e7..."
}

Use the new token for later calls. To switch back, send your home org_id, or refresh the token. Your refresh token stays the same. The switch is recorded in the audit log as auth.org_switched. To list organizations, use GET /api/admin/organizations.

Login rate limits

The /api/auth/* endpoints are rate limited per client IP address. No other endpoints are rate limited. The limits come from Settings → Security:

SettingDefaultEffect
security.rate_limit.requests_per_minute100Requests per minute to /api/auth/* from one IP
security.rate_limit.failed_login_limit5Failed logins allowed from one IP within the window
security.rate_limit.failed_login_window_minutes15Length of the failed-login window

Over the limit, the API returns 429 with a Retry-After header and a body such as:

{
  "error": "Too many failed login attempts. Please try again later.",
  "retry_after_secs": 840
}

Changes to these settings take effect after the API restarts.

Example: a script that stays logged in

A minimal Python pattern: log in once, retry once after refreshing on 401.

netstacks_client.pypython
import requests

BASE = "https://controller.example.com:3000/api"
CA = "netstacks-ca.pem"

class NetStacks:
    def __init__(self, username, password):
        r = requests.post(f"{BASE}/auth/login", verify=CA,
                          json={"username": username, "password": password})
        r.raise_for_status()
        self._save(r.json())

    def _save(self, tokens):
        self.access = tokens["access_token"]
        self.refresh = tokens["refresh_token"]

    def _refresh(self):
        r = requests.post(f"{BASE}/auth/refresh", verify=CA,
                          json={"refresh_token": self.refresh})
        r.raise_for_status()
        self._save(r.json())  # refresh tokens are single use

    def request(self, method, path, **kw):
        for attempt in (1, 2):
            r = requests.request(method, f"{BASE}{path}", verify=CA,
                                 headers={"Authorization": f"Bearer {self.access}"}, **kw)
            if r.status_code == 401 and attempt == 1:
                self._refresh()
                continue
            r.raise_for_status()
            return r.json() if r.content else None

ns = NetStacks("automation", "<password>")
for d in ns.request("GET", "/devices", params={"limit": 100})["devices"]:
    print(d["name"], d["host"])

Troubleshooting

  • 401 “Invalid credentials” for an LDAP user on /login: /api/auth/login only accepts local accounts. LDAP users must use /api/auth/ldap.
  • 401 after a refresh worked before: the refresh token was already used, or you logged out somewhere (logout revokes all refresh tokens). Log in again.
  • 403 “Insufficient permissions”: the user's roles lack the permission the endpoint needs. Check permissions in GET /api/auth/me.
  • 409 starting with “seat_limit_reached”: a client_type: "terminal" login found no free license seat. Scripts should leave client_type out.
  • SSL errors: download the Controller CA from /api/tls/ca-certificate and pass it to your client (see Base URL and TLS).

To try endpoints by hand, open the Swagger UI, click Authorize, and paste the access_token value.