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 applicationApplications 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_idand aclient_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:
| What | Comes from | What the application receives |
|---|---|---|
| Authority over your data | The organization chosen on the consent screen | An access token with the same reach as a Core API Key: full administrative access to the organization |
| The identity of the signed-in person | The user who signs in | An id_token identifying that user, whenever the application requests the openid scope |
| Who may sign in at all | The sign-in policy chosen when the application was approved | Tokens 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 inEvery 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:
- You sign in with your Xyte account, if you are not signed in already.
- 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.
- 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.
- 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
| Token | Lifetime |
|---|---|
| Authorization code | 60 seconds, single use |
| Access token | 60 minutes |
| Refresh token | 90 days from the original authorization, replaced on every use |
id_token | 60 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.
| Endpoint | Method | Purpose |
|---|---|---|
/oauth/authorize | GET | Start the authorization request (browser redirect) |
/oauth/token | POST | Exchange an authorization code, or a refresh token, for tokens |
/oauth/revoke | POST | Revoke a token and every other token issued to the same authorization |
/oauth/userinfo | GET, POST | Claims about the signed-in user (tokens whose authorization requested openid) |
/.well-known/openid-configuration | GET | OpenID Connect discovery document |
/oauth/.well-known/jwks.json | GET | Public 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.
| Parameter | Required | Description |
|---|---|---|
response_type | Yes | Must be code. |
client_id | Yes | Your application's client identifier. |
redirect_uri | Yes | Must match one of the registered redirect URIs byte for byte. |
state | Yes | Opaque value echoed back on the redirect, at most 512 characters. Use it to tie the response to the request (CSRF protection). |
code_challenge | Yes | PKCE challenge: base64url(SHA-256(code_verifier)), a 43-character unpadded base64url digest. Anything else is rejected with invalid_request. |
code_challenge_method | Yes | Must be S256. plain is rejected. |
scope | No | Space-separated subset of openid profile email. Any other value is rejected with invalid_scope on the redirect. |
nonce | Recommended with openid | Echoed 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.ioOutcomes:
- 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_idandredirect_uri: the browser is sent to302 {redirect_uri}?error=...&error_description=...&state=.... Theerrorisunsupported_response_typefor aresponse_typeother thancode,invalid_scopefor an unsupportedscope, andinvalid_requestfor a missingresponse_typeorstate, a badcode_challengeorcode_challenge_method, or anoncelonger than 512 characters. - Unknown or disabled
client_id, missing or unregisteredredirect_uri, orstatelonger than 512 characters: an error page on Xyte. The browser is never redirected to an unverified URI. The same error page, withserver_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_idandclient_secretas 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
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | authorization_code |
code | Yes | The code received on the redirect. |
redirect_uri | Yes | The exact value used in the authorization request. |
code_verifier | Yes | The 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
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | refresh_token |
refresh_token | Yes | The 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_SECRETThe 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 replayPresenting 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_grantonly. 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": "..."
}error | Status | When |
|---|---|---|
invalid_request | 400 | A parameter is missing or duplicated, or the body is not form-encoded. |
invalid_client | 401 | Unknown 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_grant | 400 | Bad, expired or already used code; PKCE mismatch; redirect_uri mismatch; revoked grant; expired or rotated refresh token. |
unsupported_grant_type | 400 | grant_type is not authorization_code or refresh_token. |
server_error | 500 | Xyte 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.
| Parameter | Required | Description |
|---|---|---|
token | Yes | An access token or refresh token issued to your application. |
token_type_hint | No | Accepted 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.
| Response | When |
|---|---|
200, empty body | Always, including unknown, expired, already revoked tokens and tokens of another application. |
400 invalid_request | token 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_client | Client 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
id_token claimsEvery 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.
| Claim | Value |
|---|---|
iss | https://hub.xyte.io |
sub | The Xyte user id (UUID string). |
aud | Your client_id. |
iat | Issue time (Unix seconds). |
exp | iat + 3600. |
nonce | The nonce from the authorization request. Present on the code exchange only, not on refresh. |
email | The user's email address. Requires the email scope. |
name | The user's name. Requires the profile scope. |
xyte_tenant_id | The id of the organization the user signed in to (UUID string). |
xyte_tenant_type | Always 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.
| Response | When |
|---|---|
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
| Endpoint | Limit |
|---|---|
/oauth/token | 60 per minute per IP address, 300 per minute per client_id |
/oauth/revoke | 60 per minute per IP address |
/oauth/userinfo | 120 per minute per IP address |
/.well-known/openid-configuration | 120 per minute per IP address |
/oauth/.well-known/jwks.json | 120 per minute per IP address |
| Consent screen | 30 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.
