Developer HubAPI Authentication
On this page0

Unfamiliar word? See Glossary: words used in these docs. Looking for the tasks in order? Begin at Start Here.

Reference

Not a developer? The step-by-step guides begin at Start Here, and a problem on your screen is in When something goes wrong.

API Authentication

The plain explanation is above. This part is for IT and developers: the exact settings, permission names, API addresses and data fields behind it.

Everything the dashboard does, it does over the same public API on your instance. A script calls it with a personal API token; a browser uses a session cookie.

There Is No Shared API Hostname

Every customer runs on their own dedicated instance, so there is no central api.zonitix.com to call. An instance answers on two addresses that matter to a program, both built from the instance name you were given at handover:

  • The API address, written in these docs as https://<your-instance>-api.zonitix.com. This is the base URL for a program: every /api/v1 route, SCIM at /api/v1/scim/v2, the device agent routes and the health routes are served here.
  • The app address, written as https://<your-instance>.zonitix.com. This is where people sign in. It serves the screens and the static API reference at /api/docs, and it does not answer /api/v1 calls. It is also the value of the Origin header described under Cookie Sessions And CSRF below.

Substitute your own instance name in both.

Personal API Tokens (For Scripts)

For anything unattended, create a personal API token and send it as Authorization: Bearer zx_…. A token carries only the permissions you tick when creating it, never more than you hold, and expires (90 days by default, one year at most). Each token is limited to 300 requests per minute. A token cannot call routes that need a sudo step-up or the administrator role, the sign-in, MFA and password routes, or token management itself. Steps: Create an API token.

curl -H "Authorization: Bearer $ZX_TOKEN" https://<your-instance>-api.zonitix.com/api/v1/user-management/users

Your instance also serves an OpenAPI 3.1 reference, a saved file built from the product’s route definitions and shipped with the release it runs, at https://<your-instance>.zonitix.com/api/docs. It covers the areas published for token use (users, groups, roles, workflows and runs, device reads, audit logs, API tokens), not every module — see the API Reference for what each list covers.

What Ends A Token

In plain words: a token belongs to one person’s account and stops when that account stops, when that person’s password changes, or when the token is revoked or runs out. Plan for that before you leave a script running unattended.

The token and its owner are checked on every request, in the order below. The first row that applies refuses the request with 401; the reply carries auth: false and the text shown as message.

What happenedmessageReason in the audit entry
The token is revoked, by its owner or by an administratorAPI token has been revokedrevoked
The token reaches its expiry dateAPI token has expiredexpired
The owner’s account no longer existsInvalid API tokenowner_missing
The owner is disabled, suspended or offboarded (any status that blocks sign-in)Account is not activeowner_inactive
The owner’s password was changed, set by an administrator, or expired by an administrator (Expire password on the Users page) after the token was createdAPI token was invalidated by a sign-out-everywhere or password changeowner_signed_out_everywhere
MFA is required of the owner and the owner has none set upMulti-factor authentication must be set up on the owner account to use API tokensowner_mfa_not_enrolled
  • The password row. The account records the moment its sessions were last ended, and a token created before that moment is refused and shown as Invalidated in the token list. Four things set that moment: the owner changes their own password, an administrator sets a new password for them, an administrator expires their password, or they accept an invitation and set a password. A token created afterwards is not affected. The message names a “sign-out-everywhere”, but there is no separate action of that name; the full list is under What Ends A Session Early below.
  • Losing a permission does not end the token. On each request the token carries the permissions ticked for it that its owner still holds. A permission taken from the owner stops working for the token within 15 seconds, and the routes that need it answer 403; the rest of the token keeps working. If the permission is given back, the token has it again.
  • A lapsed licence does not end a token. Nothing in the token check reads the licence. Routes of a module that is paused refuse a token as they refuse a session; see What Happens When A Subscription Lapses.
  • Each refusal is audited. The audit action is API_TOKEN_REJECTED, with the reason from the table, the token’s name and the first characters of the token. A script that keeps retrying a dead token adds one entry per reason every 10 minutes, not one per request.
  • A token always has an owner. Every token is tied to one user account; no token exists apart from an account, and there is no client-credentials grant. For an integration that must keep running, use an account kept for that purpose, with MFA set up where your policy requires it, and replace the token before its expiry date and after each change of that account’s password.

Session Sign-In Flow

Post your credentials to the login endpoint. On success the gateway sets a session cookie; the same session token can be sent as Authorization: Bearer <token> instead. This is the flow to use for anything that needs a step-up, which a personal API token cannot do. If the account has MFA enrolled or has not yet accepted terms, the response is a challenge rather than a session. The worked example below takes the second-factor challenge through to a step-up.

# 1. Authenticate against YOUR instance (not a shared endpoint):

curl -X POST https://<your-instance>-api.zonitix.com/api/v1/auth/login \

-H "Content-Type: application/json" \

-c cookies.txt \

-d '{ "username": "admin@your-company.com", "password": "your_password" }'

# 2. Call any authenticated endpoint with that session:

curl https://<your-instance>-api.zonitix.com/api/v1/auth/me -b cookies.txt

Worked Example: Sign In, Second Factor, Step-Up

The script signs in with a password and an authenticator code, shows a step-up-gated call being refused, steps up, and makes the call again. The gated call is the read-only diagnostics download, so running the script changes no setting; it does write audit entries. It needs curl and jq, and an account that signs in with a Zonitix password, has an authenticator app enrolled and holds manage_security.

# Set ZX_USER, ZX_PASSWORD and ZX_CODE (the current 6-digit code from your authenticator app) first.
API='https://<your-instance>-api.zonitix.com'
APP='https://<your-instance>.zonitix.com'

# 1. Password. 200 with a challenge when a second factor is due.
MFA_TOKEN=$(jq -n --arg u "$ZX_USER" --arg p "$ZX_PASSWORD" '{username:$u,password:$p}' \
  | curl -s -X POST "$API/api/v1/auth/login" \
      -H 'Content-Type: application/json' -c cookies.txt -d @- \
  | jq -r .mfaToken)

# 2. Second factor. 200 sets the session cookie.
jq -n --arg m "$MFA_TOKEN" --arg c "$ZX_CODE" '{mfaToken:$m,token:$c}' \
  | curl -s -X POST "$API/api/v1/auth/mfa/verify" \
      -H 'Content-Type: application/json' -c cookies.txt -d @- -o /dev/null -w '%{http_code}\n'

# 3. A step-up-gated call before the step-up. Prints 401; the body's "error" is SUDO_REQUIRED.
curl -s -b cookies.txt -o /dev/null -w '%{http_code}\n' \
  "$API/api/v1/user-management/support/diagnostics"

# 4. Step-up. A POST authenticated by the cookie must carry Origin. 200 replaces the session cookie.
jq -n --arg p "$ZX_PASSWORD" '{password:$p}' \
  | curl -s -X POST "$API/api/v1/auth/sudo" \
      -H 'Content-Type: application/json' -H "Origin: $APP" \
      -b cookies.txt -c cookies.txt -d @- -o /dev/null -w '%{http_code}\n'

# 5. The same call again. Prints 200 and saves the file.
curl -s -b cookies.txt -o diagnostics.json -w '%{http_code}\n' \
  "$API/api/v1/user-management/support/diagnostics"
StepRequestWhen it worksWhen it is refused
1POST /api/v1/auth/login200 with mfaRequired: true, mfaStatus: "challenge_pending" and mfaToken. No cookie is set yet.401 Invalid username or password
2POST /api/v1/auth/mfa/verify200 with auth: true and token; the session cookie is set.400 Invalid token for a wrong code
3GET /api/v1/user-management/support/diagnosticsRefused on purpose here: no step-up yet.401 with error: "SUDO_REQUIRED"
4POST /api/v1/auth/sudo200 with auth: true and a new token; the cookie is replaced.400 Invalid password; 403 CSRF check failed: missing or untrusted Origin. when the header is left out
5GET /api/v1/user-management/support/diagnostics200 and the diagnostics file.403 when the account does not hold manage_security
  • Step 1 can answer differently. When no second factor is due, the reply is auth: true with a token and the cookie is already set: skip step 2. The reply can also be a challenge this example does not cover: a second factor that still has to be set up (mfaStatus: "setup_pending"), terms that have not been accepted, or a password that must be changed.
  • The interim token is short-lived. mfaToken lasts 5 minutes and is refused as a session. The limits on wrong codes are on MFA Enforcement.
  • Origin, or a bearer. Only step 4 needs Origin in this script: it is the one request that changes something and is authenticated by the cookie alone. Its value is the app address, not the API address. To do without it, read token from the step 2 reply and send Authorization: Bearer <token> on the later requests; after step 4, use the new token from that reply, because the older one carries no step-up.
  • The refusal on other routes. A missing or expired step-up is refused with "error": "SUDO_REQUIRED" in the response body. The status beside it is 400 on most routes, because the gateway turns a 401 from a service behind it into 400. It stays 401 on the routes the gateway passes through unchanged: removing a passkey, the diagnostics bundle, and in Security Lab starting an aggressive scan, changing a scan scope and archiving findings. Key on the error string, not the status.

Session Tokens

A session token is a JWT signed with HS256 using a secret held on your instance. Its lifetime is the Absolute Timeout an administrator sets under Security Settings → Sessions (the sessionTimeout setting): 8 hours on a new instance, and any value from 5 minutes to 24 hours. Activity does not extend it. Signing in again, or completing a step-up, issues a new token.

  • In production the cookie is named __Host-token and is set HttpOnly, Secure, SameSite=None, Path=/ with no Domain, so it is bound to your instance’s API address. Its max-age is the time the token has left, capped at 24 hours.
  • The interim token returned by a sign-in that still needs a second factor or a forced password change lasts 5 minutes and is refused as a session.
  • The server re-reads the account on every request, so disabling a user or expiring their password ends their sessions at once, whatever the token’s own expiry says. Every case is in the table under What ends a session early, below.
  • Changing the timeout affects tokens issued after the change; tokens already issued keep their original expiry.

What Ends A Session Early

A session token is not stored on the server, so it cannot be deleted. It is cut short in one of two ways: that one token is listed as signed out, or the account records a moment (sessionsInvalidatedAt) and every token issued before it is refused with 401 and the message Session expired — please sign in again.

What happensEffect on existing sessionsAudit action
The person signs outThat one session token is put on a signed-out list until the token’s own expiry. Their sessions in other browsers keep working.LOGOUT
The person changes their own passwordEvery session token issued to them before that moment stops working, on every device. Their trusted devices are removed too, so the next sign-in asks for the second factor again.PASSWORD_CHANGED
An administrator sets a new password for themEvery session token issued to them before that moment stops working, and they must choose their own password at the next sign-in.USER_UPDATED
Their password reaches its expiry ageEvery session token issued to them before that moment stops working, and the next sign-in makes them choose a new password.USER_PASSWORD_EXPIRED
They accept an invitation and set a passwordAny session token issued to that account earlier stops working.USER_INVITE_ACCEPTED
An administrator disables, suspends or offboards the accountRefused on the very next request with 401, code SESSION_NOT_ACTIVE and the message "Account is not active". The account status is read from the database on every request, never from the token. The statuses that block are disabled, inactive, suspended, terminated, offboarded, archived and deleted.USER_UPDATED when the status is changed by editing the user; AUTH_REJECTED for each refused request
An administrator deletes the accountThe account is removed or, when it has history that must be kept, stripped of personal details and given the status deleted. Either way no token for it works again.USER_DELETED / USER_ANONYMIZED
A role, group or permission of theirs changesTheir sessions stay signed in. The new permissions apply within 15 seconds, which is how long the permission list is cached per person.—
Support access is revoked or runs outThe Zonitix support account is refused on its next request, to the second, with 401, code SESSION_NOT_ACTIVE and the message "Support access is not active".SUPPORT_ACCESS_REVOKED / SUPPORT_ACCESS_EXPIRED; AUTH_REJECTED for each refused request
  • There is no separate “sign out everywhere” action. To end every session of one person, change or reset their password, or disable the account.
  • There is no refresh token. POST /api/v1/auth/refresh exchanges a session token that is still valid for a new one with a fresh expiry. It refuses a signed-out token, a blocked account and a token issued before the account’s invalidation moment. The token that was exchanged is not signed out by the exchange; it stays usable until its own expiry.
  • Personal API tokens follow the same moment. A token created before the owner’s sessions were last invalidated stops working and is shown as Invalidated in the token list. A blocked owner’s tokens stop on the next request. Every case, with the message each one returns, is under What Ends A Token above; the steps are in Create an API token.
  • Where the signed-out list is kept. In Redis, each entry expiring when the token itself would. If Redis cannot be reached at that moment the entry is kept in the identity service’s own memory instead, and both places are checked on every request. An entry held only in memory is lost if that service restarts before the token expires.

Token Claims

The token is signed and not encrypted, so anyone who holds it can read these claims. It is a bearer credential: sent in the Authorization header it is worth exactly what the cookie is worth. Every place that verifies it accepts HS256 only. There is no iss, aud or jti claim.

ClaimTypeMeaning
idnumberThe user’s id.
usernamestringThe user’s email address.
firstName, lastNamestringAs stored on the account.
rolesstring[]Role names: the roles assigned directly plus those inherited through groups.
groupsstring[]Group names.
permissionsstring[]The effective permission names at the moment the token was issued.
mfaEnabledbooleanThe account has a second factor enrolled.
mfaRequiredbooleanPolicy requires a second factor for this account.
mfaVerifiedbooleanA second factor was completed for this session.
isMfaSessionbooleanTrue only on the 5-minute interim token issued while a second factor is still owed.
isPasswordChangeSessionbooleanTrue only on the 5-minute interim token issued for a forced password change.
attributesobjectThe account’s custom attributes.
sudoGrantedAtnumberMilliseconds since the epoch. Present only on a token issued by a step-up.
iat, expnumberIssued-at and expiry, in seconds since the epoch.

The roles and permissions in the token are a snapshot. The gateway does not authorise from them: it asks the identity service to validate the token and re-read the account, and forwards that answer to the service doing the work (see Transport & Service Trust). The gateway reuses an answer for the same token for at most one second. How passwords are hashed and when an account locks are on the same page.

Cookie Sessions And CSRF

A request that changes something (any method other than GET, HEAD or OPTIONS) and is authenticated by the session cookie alone must carry an Origin header (or, failing that, a Referer) naming your instance’s app address (https://<your-instance>.zonitix.com, not the API address). Without one the gateway answers 403 with CSRF check failed: missing or untrusted Origin. and records the attempt in the audit log. A request that carries an Authorization header is not subject to this check, so a script that uses a personal API token never meets it. The one exception is the SAML assertion endpoint, POST /api/v1/auth/sso/saml/acs, which your identity provider posts to from its own origin.

Path Conventions

Versioned Prefix

Prefer /api/v1/… for everything. What the code gives an integrator is that prefix: every route group is served under it, there is no other version, and no request header selects one. The gateway also mounts each route group without the prefix (e.g. /auth/…), but nothing is published about how long those stay: write integrations against the versioned prefix. No deprecation policy and no route-by-route record of changes are published. The detail, and a check you can run yourself before and after an update, are under API Versioning.

Step-Up (Sudo)

High-impact routes require a recent sudo re-authentication on top of your permissions. Call POST /api/v1/auth/sudo first; it opens a step-up window and is rate-limited to 5 failed attempts per 15 minutes. A passkey can satisfy the step-up instead of a password, through GET /api/v1/auth/webauthn/sudo/options and POST /api/v1/auth/webauthn/sudo/verify. Step-up needs a signed-in session — a personal API token is refused on these routes. A step-up counts for 15 minutes. The server reads the window from a stored setting, sudoTimeoutMinutes, and holds any value between 1 and 60 minutes, but no screen and no API route writes that setting, so every instance uses 15 minutes. A missing or expired step-up is refused with "error": "SUDO_REQUIRED" in the response body. The status beside it is 400 on most routes, because the gateway turns a 401 from a service behind it into 400. It stays 401 on the routes the gateway passes through unchanged: removing a passkey, the diagnostics bundle, and in Security Lab starting an aggressive scan, changing a scan scope and archiving findings. Key on the error string, not the status.

Permissions, Not Roles

Authorisation is checked against atomic permissions, not role names. The API Reference lists the exact permission(s) each endpoint accepts. The gateway forwards to each service only the permissions that belong to that service’s area, so a permission an account holds can be missing on another area’s routes: see Gateway-To-Service Trust.

Licensed Modules

Some routes (Security Lab, cloud mapping) are additionally gated on your license entitlements and return an error when the module is not active on your license.

Related reference