OAuth2 & OpenID Connect

Let third-party applications access your organization with OAuth2 access tokens instead of Core API Keys.

Xyte supports OAuth 2.0 (authorization code flow with PKCE) and OpenID Connect. A third-party application can use them to access your organization's data on your behalf, and to let your users sign in to the application with their Xyte account, without you creating or sharing a Core API Key.

This page is for two audiences:

  • Organization administrators and users who connect an application that integrates with Xyte through OAuth2. The first half explains what happens when you connect, what the application can do, and how to disconnect it.
  • Developers building such an application. The developer reference at the end lists every endpoint, parameter and error.
📘

Registering an application

Applications are registered by Xyte. To register yours, contact [email protected] with the application name, the partner that owns it and the exact redirect URIs it will use. You receive a client_id and a client_secret. Keep the secret on your server; it is never sent through the browser.

How connecting works

There is one flow, and it is the same for everyone in your organization. What it keeps apart is who you are and what the application may do:

WhatComes fromWhat the application receives
Authority over your dataThe organization chosen on the consent screenAn access token with the same reach as a Core API Key: full administrative access to the organization
The identity of the signed-in personThe user who signs inAn id_token identifying that user, whenever the application requests the openid scope
Who may sign in at allThe sign-in policy chosen when the application was approvedTokens only for the users that policy admits: anyone with access to the organization, the members of one group, or administrators only

An administrator approves the application for your organization once, and at the same time chooses who may sign in through it. Under the default — anyone with access to the organization — a user does not have to be an administrator to sign in, and the application can work with the organization's devices, spaces, incidents and so on, exactly as it could with a Core API Key. The approving administrator can narrow that to the members of one group, or to administrators only. The policy only ever narrows: an administrator of the organization is always admitted, and can always approve.

🚧

Approval decides the access, and who may sign in

Every access token the application receives carries the organization's full administrative access, whether an administrator or an ordinary member signed in to obtain it. Approving an application is what grants that access, and the same approval settles who may sign in through it — anyone with access to the organization, the members of one group, or administrators only. Approve only applications you trust, choose the narrowest sign-in policy that still lets the right people in, and disconnect them under Settings > Connected apps when you no longer use them.

The consent screen

When an application sends you to Xyte to connect, you land on a screen titled Authenticate application name:

  1. You sign in with your Xyte account, if you are not signed in already.
  2. You choose the organization. The screen lists every organization you can reach — nothing is hidden — in three groups:
    • Approved by tenant administrator — an administrator has already approved this application here. Pick one of these to sign in to that organization.
    • Available to approve — organizations you administer where the application has not been approved yet. Picking one approves the application for that organization.
    • Requires administrator approval — organizations you can reach but cannot use here: the application has not been approved and you do not administer them, or it is approved but its sign-in policy leaves you out. They are listed so you can see them, but you cannot pick them; ask an administrator of that organization to approve the application, or to let you in.
  3. If your choice approves the application for the first time, you also answer Who can sign in to application name through this organization?Anyone with access to the organization (the default), Only members of a group, and then which group, or Only organization administrators. An organization that is already approved keeps the policy chosen when it was approved, so this step is not shown for it, and signing in there does not change it.
  4. You review the request and submit, or deny it. The button reads Continue for an organization that is already approved, and Approve when your choice approves the application for the first time.

Approving sends you back to the application, which then exchanges the result for its tokens. Denying sends you back with an access_denied error and issues nothing.

If the request itself is invalid, for example an unknown application or a redirect address that was not registered, Xyte shows an error page and does not send you anywhere. This protects you from being redirected to an unverified address.

How tokens are used

The application calls the Organization API with the access token in the Authorization header, in place of a Core API Key:

GET /core/v1/organization/devices HTTP/1.1
Host: hub.xyte.io
Authorization: Bearer xoat_...

Endpoints, parameters, pagination and responses are identical to calls made with an API key; see Authentication & Authorization. An expired, revoked or malformed token is rejected with 401 and {"error":"Not authorized"}, plus the header WWW-Authenticate: Bearer error="invalid_token".

Every call made with an OAuth2 token is recorded in the organization's API Call Log, in the Call Log tab under API Keys. The Credential column shows the application name with an OAuth badge in place of a key name, and the name of the user who signed in appears under the application.

Token lifetimes

TokenLifetime
Authorization code60 seconds, single use
Access token60 minutes
Refresh token90 days from the original authorization, replaced on every use
id_token60 minutes

Applications use the refresh token to obtain new access tokens without sending you back to the consent screen. Ninety days after the original authorization the refresh token expires and the application has to ask you to connect again.

What an application's token can do

Every access token behaves exactly like a Core API Key, whoever signed in to obtain it:

  • Lists of devices, spaces, assets, incidents, tickets, notes and commands cover the whole organization. They are not filtered down to what the signed-in user can see in the portal.
  • The endpoints only administrators can use in the portal — users, groups, and device merge or split — are open to the application too.

What an application can reach is therefore settled when an administrator approves it, not by which of your users signs in afterwards. That same approval settles which of your users may sign in at all: the sign-in policy admits anyone with access to the organization, the members of one group, or administrators only. It governs obtaining a token — tokens the application already holds keep working until they expire or the application is disconnected.

Disconnecting an application

Organization administrators can see every connected application under Settings > Connected apps, along with the sign-in policy it carries, and disconnect it there. The policy cannot be edited: changing it means revoking the application and approving it again, which signs out everyone currently using it. Revoking an application stops every token issued to it for your organization immediately, whichever of your users obtained them, and withdraws the approval: your users see the application under Requires administrator approval on the consent screen until an administrator approves it again. See Connected apps.

Applications can also give up their own tokens through the revocation endpoint. Xyte can disable an application entirely. Disabling revokes every grant the application holds, so its tokens stop working for every organization and stay revoked if the application is enabled again; it has to be authorized afresh.

Developer reference

All OAuth2 and OpenID Connect endpoints are served from https://hub.xyte.io. They sit outside the Organization API base path (/core/v1/organization), so they are not part of the OpenAPI specification or the interactive API explorer. This page is their reference.

EndpointMethodPurpose
/oauth/authorizeGETStart the authorization request (browser redirect)
/oauth/tokenPOSTExchange an authorization code, or a refresh token, for tokens
/oauth/revokePOSTRevoke a token and every other token issued to the same authorization
/oauth/userinfoGET, POSTClaims about the signed-in user (tokens whose authorization requested openid)
/.well-known/openid-configurationGETOpenID Connect discovery document
/oauth/.well-known/jwks.jsonGETPublic keys for verifying id_token signatures

Only the authorization code grant with PKCE (S256) is supported. Tokens are opaque strings with a type prefix: xoac_ for authorization codes, xoat_ for access tokens and xort_ for refresh tokens. Treat all of them as secrets.

Authorization request

Redirect the user's browser to GET https://hub.xyte.io/oauth/authorize with the query parameters below. Xyte answers with a 302 to the consent screen on the organization portal of the partner that owns the application, or on the default Xyte portal for applications not tied to a partner, carrying the same query.

ParameterRequiredDescription
response_typeYesMust be code.
client_idYesYour application's client identifier.
redirect_uriYesMust match one of the registered redirect URIs byte for byte.
stateYesOpaque value echoed back on the redirect, at most 512 characters. Use it to tie the response to the request (CSRF protection).
code_challengeYesPKCE challenge: base64url(SHA-256(code_verifier)), a 43-character unpadded base64url digest. Anything else is rejected with invalid_request.
code_challenge_methodYesMust be S256. plain is rejected.
scopeNoSpace-separated subset of openid profile email. Any other value is rejected with invalid_scope on the redirect.
nonceRecommended with openidEchoed in the id_token so you can bind it to the user's session. At most 512 characters.
GET /oauth/authorize?response_type=code
    &client_id=YOUR_CLIENT_ID
    &redirect_uri=https%3A%2F%2Fyour-app.example%2Fcallback
    &state=af0ifjsldkj
    &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
    &code_challenge_method=S256
    &scope=openid%20profile%20email
    &nonce=n-0S6_WzA2Mj HTTP/1.1
Host: hub.xyte.io

Outcomes:

  • Approved: 302 {redirect_uri}?code=xoac_...&state=.... The code is valid for 60 seconds and can be exchanged once.
  • Denied: 302 {redirect_uri}?error=access_denied&state=....
  • Invalid request with a valid client_id and redirect_uri: the browser is sent to 302 {redirect_uri}?error=...&error_description=...&state=.... The error is unsupported_response_type for a response_type other than code, invalid_scope for an unsupported scope, and invalid_request for a missing response_type or state, a bad code_challenge or code_challenge_method, or a nonce longer than 512 characters.
  • Unknown or disabled client_id, missing or unregistered redirect_uri, or state longer than 512 characters: an error page on Xyte. The browser is never redirected to an unverified URI. The same error page, with server_error, appears when the partner that owns the application has no portal domain.

Token endpoint

POST https://hub.xyte.io/oauth/token. The body must be application/x-www-form-urlencoded; a JSON body is rejected with 400 invalid_request.

Authenticate the application in one of two ways:

  • client_secret_basic: Authorization: Basic base64(client_id:client_secret), with both parts form-url-encoded before they are joined (RFC 6749 section 2.3.1).
  • client_secret_post: client_id and client_secret as form fields in the body.

Sending both with different client ids fails with invalid_request. Responses carry Cache-Control: no-store and Pragma: no-cache; never cache them.

Exchange an authorization code

ParameterRequiredDescription
grant_typeYesauthorization_code
codeYesThe code received on the redirect.
redirect_uriYesThe exact value used in the authorization request.
code_verifierYesThe PKCE verifier the code_challenge was derived from: 43 to 128 characters of [A-Za-z0-9._~-]. Anything else is rejected with invalid_request.
POST /oauth/token HTTP/1.1
Host: hub.xyte.io
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=xoac_...&redirect_uri=https%3A%2F%2Fyour-app.example%2Fcallback&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
{
  "access_token": "xoat_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "xort_...",
  "scope": "openid profile email",
  "id_token": "eyJ..."
}

scope is present only when OpenID Connect scopes were requested. id_token is present whenever openid was requested, for every sign-in, including an administrator's.

Refresh an access token

ParameterRequiredDescription
grant_typeYesrefresh_token
refresh_tokenYesThe most recent refresh token of the grant.
POST /oauth/token HTTP/1.1
Host: hub.xyte.io
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=xort_...&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET

The response has the same shape as the code exchange and contains a new refresh token; the previous one stops working. If the grant includes openid, a new id_token is issued as well (without nonce).

Refresh tokens expire 90 days after the original code exchange. This is an absolute limit; refreshing does not extend it. After that the user has to go through the authorization request again.

🚧

Refresh token replay

Presenting a refresh token that has already been rotated is treated as a retry when it happens within 60 seconds of the rotation (for example after a lost response) and answers invalid_grant only. Presenting it later is treated as a leak and the whole grant is revoked: every token the application holds for that organization, including those obtained by its other users. An audit event is recorded. Store the new refresh token before you discard the old one.

Errors

Error responses are RFC 6749 section 5.2 objects:

{
  "error": "invalid_grant",
  "error_description": "..."
}
errorStatusWhen
invalid_request400A parameter is missing or duplicated, or the body is not form-encoded.
invalid_client401Unknown client, wrong secret, or the application was disabled by Xyte. When Basic authentication was used the response also carries WWW-Authenticate: Basic realm="oauth".
invalid_grant400Bad, expired or already used code; PKCE mismatch; redirect_uri mismatch; revoked grant; expired or rotated refresh token.
unsupported_grant_type400grant_type is not authorization_code or refresh_token.
server_error500Xyte could not sign the response (signing key not configured). Retry later or contact support.

The endpoint accepts 60 requests per minute per IP address and 300 per minute per client_id; above that it answers 429.

Revoke a token

POST https://hub.xyte.io/oauth/revoke (RFC 7009). Form-encoded, with the same client authentication as the token endpoint.

ParameterRequiredDescription
tokenYesAn access token or refresh token issued to your application.
token_type_hintNoAccepted and ignored; the token prefix identifies the type.
POST /oauth/revoke HTTP/1.1
Host: hub.xyte.io
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

token=xort_...

Revoking a token revokes every access token, refresh token and pending authorization code issued to the same authorization. The organization's approval of the application stays in place; a new authorization request, including the consent screen, issues fresh tokens without needing an administrator again. Only an administrator revoking the application under Connected apps withdraws the approval itself.

ResponseWhen
200, empty bodyAlways, including unknown, expired, already revoked tokens and tokens of another application.
400 invalid_requesttoken is missing, the body is not form-encoded, or client credentials were sent both in the header and in the body with different values.
401 invalid_clientClient authentication failed: unknown client, wrong secret, or the application was disabled by Xyte.

The endpoint accepts 60 requests per minute per IP address.

OpenID Connect

Discovery

GET https://hub.xyte.io/.well-known/openid-configuration (cacheable for 1 hour):

{
  "issuer": "https://hub.xyte.io",
  "authorization_endpoint": "https://hub.xyte.io/oauth/authorize",
  "token_endpoint": "https://hub.xyte.io/oauth/token",
  "userinfo_endpoint": "https://hub.xyte.io/oauth/userinfo",
  "jwks_uri": "https://hub.xyte.io/oauth/.well-known/jwks.json",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "subject_types_supported": ["public"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "scopes_supported": ["openid", "profile", "email"],
  "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"],
  "code_challenge_methods_supported": ["S256"],
  "claims_supported": ["iss", "sub", "aud", "exp", "iat", "nonce", "email", "name", "xyte_tenant_id", "xyte_tenant_type"]
}

The revocation endpoint is not advertised in the discovery document yet.

JSON Web Key Set

GET https://hub.xyte.io/oauth/.well-known/jwks.json (cacheable for 5 minutes):

{
  "keys": [
    { "kty": "RSA", "use": "sig", "alg": "RS256", "kid": "...", "n": "...", "e": "AQAB" }
  ]
}

When no signing key is configured the set is empty: {"keys":[]}.

Pick the key whose kid matches the id_token header. If the kid is unknown, fetch the set again before rejecting the token, in case the signing key was rotated.

id_token claims

Every sign-in that requested openid receives an id_token, including an administrator's. It is a JWT signed with RS256; the header carries the kid of the signing key. Validate the signature, iss, aud, exp and, on the code exchange, nonce.

ClaimValue
isshttps://hub.xyte.io
subThe Xyte user id (UUID string).
audYour client_id.
iatIssue time (Unix seconds).
expiat + 3600.
nonceThe nonce from the authorization request. Present on the code exchange only, not on refresh.
emailThe user's email address. Requires the email scope.
nameThe user's name. Requires the profile scope.
xyte_tenant_idThe id of the organization the user signed in to (UUID string).
xyte_tenant_typeAlways organization.

Userinfo

GET https://hub.xyte.io/oauth/userinfo with Authorization: Bearer xoat_... (any token whose authorization requested openid). It describes the user who signed in:

{
  "sub": "...",
  "email": "[email protected]",
  "name": "Jane Doe",
  "xyte_tenant_id": "...",
  "xyte_tenant_type": "organization"
}

POST is accepted as well (OpenID Connect Core 5.3.1): send the same Authorization header with an application/x-www-form-urlencoded body, which may be empty. The response is the same as for GET.

email and name are included according to the granted scopes. The response carries Cache-Control: no-store.

ResponseWhen
403 {"error":"insufficient_scope"}The token's authorization did not request openid. Who signed in makes no difference. Also carries WWW-Authenticate: Bearer error="insufficient_scope".
401 {"error":"invalid_token"}The token is missing, expired, revoked or malformed. Also carries WWW-Authenticate: Bearer error="invalid_token".

The endpoint accepts 120 requests per minute per IP address.

Rate limits

EndpointLimit
/oauth/token60 per minute per IP address, 300 per minute per client_id
/oauth/revoke60 per minute per IP address
/oauth/userinfo120 per minute per IP address
/.well-known/openid-configuration120 per minute per IP address
/oauth/.well-known/jwks.json120 per minute per IP address
Consent screen30 per minute per user

Requests above a limit are answered with 429 and the JSON body {"error":"Too many requests"}. No Retry-After header is sent; back off and retry after the minute window.