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/v1route, 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/v1calls. It is also the value of theOriginheader 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 happened | message | Reason in the audit entry |
|---|---|---|
| The token is revoked, by its owner or by an administrator | API token has been revoked | revoked |
| The token reaches its expiry date | API token has expired | expired |
| The owner’s account no longer exists | Invalid API token | owner_missing |
| The owner is disabled, suspended or offboarded (any status that blocks sign-in) | Account is not active | owner_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 created | API token was invalidated by a sign-out-everywhere or password change | owner_signed_out_everywhere |
| MFA is required of the owner and the owner has none set up | Multi-factor authentication must be set up on the owner account to use API tokens | owner_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"| Step | Request | When it works | When it is refused |
|---|---|---|---|
| 1 | POST /api/v1/auth/login | 200 with mfaRequired: true, mfaStatus: "challenge_pending" and mfaToken. No cookie is set yet. | 401 Invalid username or password |
| 2 | POST /api/v1/auth/mfa/verify | 200 with auth: true and token; the session cookie is set. | 400 Invalid token for a wrong code |
| 3 | GET /api/v1/user-management/support/diagnostics | Refused on purpose here: no step-up yet. | 401 with error: "SUDO_REQUIRED" |
| 4 | POST /api/v1/auth/sudo | 200 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 |
| 5 | GET /api/v1/user-management/support/diagnostics | 200 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: truewith atokenand 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.
mfaTokenlasts 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
Originin 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, readtokenfrom the step 2 reply and sendAuthorization: Bearer <token>on the later requests; after step 4, use the newtokenfrom 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-tokenand is setHttpOnly,Secure,SameSite=None,Path=/with noDomain, 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 happens | Effect on existing sessions | Audit action |
|---|---|---|
| The person signs out | That 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 password | Every 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 them | Every 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 age | Every 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 password | Any session token issued to that account earlier stops working. | USER_INVITE_ACCEPTED |
| An administrator disables, suspends or offboards the account | Refused 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 account | The 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 changes | Their 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 out | The 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/refreshexchanges 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.
| Claim | Type | Meaning |
|---|---|---|
| id | number | The user’s id. |
| username | string | The user’s email address. |
| firstName, lastName | string | As stored on the account. |
| roles | string[] | Role names: the roles assigned directly plus those inherited through groups. |
| groups | string[] | Group names. |
| permissions | string[] | The effective permission names at the moment the token was issued. |
| mfaEnabled | boolean | The account has a second factor enrolled. |
| mfaRequired | boolean | Policy requires a second factor for this account. |
| mfaVerified | boolean | A second factor was completed for this session. |
| isMfaSession | boolean | True only on the 5-minute interim token issued while a second factor is still owed. |
| isPasswordChangeSession | boolean | True only on the 5-minute interim token issued for a forced password change. |
| attributes | object | The account’s custom attributes. |
| sudoGrantedAt | number | Milliseconds since the epoch. Present only on a token issued by a step-up. |
| iat, exp | number | Issued-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
/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)
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.