Skip to content

Authentication

Every jtWMS endpoint requires authentication, apart from a few public ones such as sign-up and the plan list. You can authenticate in two ways:

Method Header Use it for
User access token Authorization: Bearer <access-token> Acting as a person: scripts and apps where someone signs in.
API key X-Api-Key: <key> Machine-to-machine integrations, such as an ERP or e-commerce connector.

Either way, the request is limited to the data of one tenant (your organisation), and to the permissions the token or key carries. See Permissions and scopes.

All examples use https://<your-jtwms-host>. On a local development stack, the API is served at http://localhost:5050.

jtWMS issues OAuth 2.0 tokens at POST /connect/token. It supports three grant types:

  • password: exchange a user’s email and password for tokens.
  • refresh_token: exchange a refresh token for new tokens.
  • client_credentials: authenticate an OAuth client rather than a user (see Client credentials).

Other grant types, including authorization code and PKCE, are not supported, and return 400 unsupported_grant_type.

Your jtWMS operator registers the OAuth client you use and gives you its client_id. A public client, such as the one the jtWMS web app uses, sends no client secret.

Send the user’s email as username. Request the offline_access scope to receive a refresh token as well as an access token.

Terminal window
curl -X POST "https://<your-jtwms-host>/connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=password" \
--data-urlencode "client_id=<client-id>" \
--data-urlencode "username=alex@example.com" \
--data-urlencode "password=<password>" \
--data-urlencode "scope=offline_access"
{
"access_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "eyJhbGciOi..."
}

A wrong email or password, or a user who is deactivated, gets 400 with the OAuth error invalid_grant. The /connect/* endpoints use the OAuth error format (error and error_description), not the problem details the rest of the API uses.

Send the access token in the Authorization header:

Terminal window
curl "https://<your-jtwms-host>/api/v1/facilities" \
-H "Authorization: Bearer <access-token>"

The access token carries the user’s tenant, permissions and the features of the tenant’s plan. Treat it as opaque: its contents can change, so don’t build on them.

Token Lifetime
Access token 15 minutes
Refresh token 14 days, sliding: each refresh issues a new refresh token valid for another 14 days

Before the access token expires, or when a request gets 401, exchange the refresh token for new tokens:

Terminal window
curl -X POST "https://<your-jtwms-host>/connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=refresh_token" \
--data-urlencode "client_id=<client-id>" \
--data-urlencode "refresh_token=<refresh-token>"

Refresh tokens rotate. Every refresh redeems the token you send and returns a new one, so store the new refresh_token from each response and discard the old one. If your client makes requests in parallel, refresh once and share the result rather than refreshing from each request.

A refresh re-reads the user, so the new access token reflects their current permissions and plan features. A refresh for a user who has been deactivated fails with invalid_grant.

If your organisation has a sandbox (POST /api/v1/sandbox), a user holding identity.sandbox.manage can switch their session into it, and back, without signing in again. The web app does this from Settings → Sandbox (see Sandbox). Exchange the current access token (RFC 8693 token exchange):

Terminal window
curl -X POST "https://<your-jtwms-host>/connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
--data-urlencode "client_id=<client-id>" \
--data-urlencode "subject_token=<access-token>" \
--data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
--data-urlencode "scope=offline_access" \
--data-urlencode "jtwms_target=sandbox"

The response holds new access and refresh tokens for the sandbox. Requests made with them act in the sandbox, which sees none of your organisation’s data, and the token’s tenant_kind claim reads sandbox. To go back, exchange a sandbox access token the same way with jtwms_target=home. Revoke the refresh token of the session you leave, as when signing out.

The switch fails with invalid_grant when the user lacks the permission, the organisation has no sandbox, or the session is already where it asked to go. A sandbox session ends when the user is deactivated, loses the permission or resets their password, and when the sandbox is reset or deleted.

Revoke the refresh token at POST /connect/revoke (RFC 7009), then discard both tokens:

Terminal window
curl -X POST "https://<your-jtwms-host>/connect/revoke" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "client_id=<client-id>" \
--data-urlencode "token=<refresh-token>" \
--data-urlencode "token_type_hint=refresh_token"

jtWMS ends a user’s existing access tokens when their access changes: when their roles or a role’s permissions change, when they are deactivated, or when their password is reset. A request made with an access token issued before the change gets 401 with errorCode token_revoked:

{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.2",
"title": "Unauthorized",
"status": 401,
"detail": "Your session permissions have changed. Please log in again.",
"errorCode": "token_revoked",
"traceId": "00-6f1c0a9d2b6e4c1f8a3b5d7e9f0a1b2c-4d5e6f7a8b9c0d1e-00",
"correlationId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301"
}

Don’t retry the request with the same access token. Refresh the session once: after a role or permission change, the refresh succeeds and the new access token carries the user’s current permissions. A password reset also ends the user’s refresh tokens, and a deactivated user can’t refresh, so if the refresh fails with invalid_grant, sign the user in again with the password grant.

A confidential OAuth client can get a token with grant_type=client_credentials, its client_id and its client_secret. That token identifies the client only: it carries no user, no tenant and no permissions, so it gives no access to any tenant’s data.

For machine-to-machine access to your tenant’s data, use an API key.

An API key authenticates a system as your tenant, with a fixed set of permissions chosen when the key is created. Create and revoke keys in the app (see API keys) or through /api/v1/api-keys with a user token. Each tenant starts with one key, “Default API Key”, which can read but not change data.

Send the key in the X-Api-Key header:

Terminal window
curl "https://<your-jtwms-host>/api/v1/products?pageSize=20" \
-H "X-Api-Key: jtwms_live_<rest-of-key>"

A key is jtwms_live_ followed by 32 URL-safe characters (A-Z, a-z, 0-9, - and _), 43 characters in all. jtWMS shows the full key once, in the response that creates it, and stores only a hash of it. Afterwards a key is identified by its keyPrefix, its first 16 characters, so store the key in a secret store as soon as you create it. A lost key cannot be recovered: create a new one and revoke the old one.

  • Send exactly one X-Api-Key header. A request with more than one is refused.
  • A request that carries both an Authorization header and X-Api-Key is authenticated by the bearer token, and the key is ignored.
  • A key’s expiry (expiresAt, if you set one) and its revocation take effect within about a minute. A revoked or expired key can’t be reactivated.
  • Requests made with a key have no user, so actions are recorded in the audit trail against the key.
Status errorCode Meaning What to do
401 invalid_api_key The key is unknown, revoked or expired, or the tenant is no longer active. The response carries WWW-Authenticate: ApiKey. Check the key. Don’t retry with the same key.
503 auth_unavailable jtWMS couldn’t check the key just now. The response carries Retry-After: 5. Retry after the Retry-After seconds.