Every error returned by the Xyte OAuth2 endpoints, and by the Organization Core API for bearer tokens, with the recommended client action.
This page lists every error the Xyte OAuth2 endpoints return, grouped by endpoint. For the flow itself, see Connect your app with Xyte OAuth2.
The token and revocation endpoints use the RFC 6749 section 5.2 error object:
{
"error": "invalid_grant",
"error_description": "..."
}error is stable and meant for your code; error_description is human-readable and may change. Branch on error and the HTTP status only.
Authorization endpoint
GET /oauth/authorize reports errors in two ways, depending on how far validation got.
Error page, no redirect
Xyte validates client_id, redirect_uri and the length of state before anything else. When one of these checks fails, or the consent screen cannot be hosted, the user sees an error page hosted by Xyte and the browser is not redirected. Your application receives nothing.
| Status | error | Condition | Action |
|---|---|---|---|
400 | invalid_client | Unknown client_id: the value is not a registered application. | Check the id your deployment uses. |
400 | invalid_client | Disabled application: Xyte has disabled your application. Its grants are revoked. | Contact Xyte. Once re-enabled, every End Customer has to connect again. |
400 | invalid_request | Missing redirect_uri. | Always send redirect_uri. |
400 | invalid_request | Unregistered redirect_uri: the value does not match a registered URI byte for byte (scheme, host, port, path, trailing slash, query). | Fix the URI in your code, or ask Xyte to register the new one. |
400 | invalid_request | state longer than 512 characters. | Shorten the value. A random 32-byte value encoded as hex or base64url is plenty. |
500 | server_error | The Partner that owns your application has no Customer Portal domain to host the consent screen. | Contact Xyte. |
Errors returned on the redirect URI
Once client_id and redirect_uri are verified, errors come back to your redirect URI as query parameters, together with your state. Validation errors also carry an error_description parameter with a human-readable explanation; only access_denied comes without one:
HTTP/1.1 302 Found
Location: https://app.example.com/oauth/callback?error=access_denied&state=1f7c2d8e9a4berror | Cause | Action |
|---|---|---|
access_denied | The user declined on the consent screen. | Show a neutral message and offer to try again. Do not retry automatically. |
invalid_scope | scope contains a value outside openid profile email. | Fix the scope list. |
unsupported_response_type | response_type is present but is not code. | Fix the request. |
invalid_request | response_type, state or code_challenge is missing, code_challenge is malformed, code_challenge_method is not S256, or nonce is longer than 512 characters. Unlike an over-long state, an over-long nonce is reported on the redirect URI. | Fix the request. These are integration bugs, not runtime conditions. |
Always verify state before reading error or code. A callback with an unknown state is not yours; discard it.
Users with no End Customer they can pickThe consent screen lists every End Customer the user can reach, but a row is selectable only when the user administers that End Customer, or when your application is already approved there and the connection's sign-in policy admits the user. An approval can be narrowed to one group or to administrators only, so a user who signed in before may find nothing selectable after an administrator changed the policy. Either way the screen cannot be submitted and no code is issued; there is no redirect and no error on your side.
Token endpoint
POST /oauth/token. Error responses carry Cache-Control: no-store and Pragma: no-cache.
| Status | error | Cause | Action |
|---|---|---|---|
400 | invalid_request | A parameter is missing; the body is not application/x-www-form-urlencoded (for example JSON); client credentials were sent both in the Authorization header and in the body with different values. | Fix the request. Do not retry it unchanged. |
401 | invalid_client | Unknown client_id, wrong client_secret, or the application is disabled. When HTTP Basic was used, the response also carries WWW-Authenticate: Basic realm="oauth". | Check the deployed credentials. If the secret may have leaked, ask Xyte to rotate it. If the application was disabled, contact Xyte; disabling revoked its grants, so every End Customer has to connect again once it is re-enabled. |
400 | invalid_grant | With authorization_code: the code is unknown, expired (60 seconds), already used, code_verifier does not match code_challenge, or redirect_uri differs from the authorization request. With refresh_token: the token is expired (90 days), already rotated, or its grant was revoked (by the End Customer, by you, by a replay, or because Xyte disabled the application; those grants stay revoked after it is re-enabled). | For a code: start a new authorization request. For a refresh token: delete the stored tokens and send the user through authorization again. |
400 | unsupported_grant_type | grant_type is not authorization_code or refresh_token. | Fix the request. |
500 | server_error | Xyte could not sign the id_token (signing key not configured). | Retry with backoff. Contact Xyte if it persists. |
429 | More than 60 requests per minute from one IP address, or 300 per minute for one client_id. The body is the JSON {"error":"Too many requests"}; there is no Retry-After header. | Back off with your own schedule and retry later. Refresh only when the access token is about to expire. |
Refresh token replay
Reusing a refresh token that has already been rotated returns 400 invalid_grant in both cases below, but the consequences differ:
| When | What Xyte does | What to do |
|---|---|---|
| Within 60 seconds of the rotation | Treats it as a retry of a request whose response was lost. Only invalid_grant is returned; the grant stays valid. | Use the refresh token you received from the first, successful call. |
| Later than 60 seconds | Treats it as a stolen token. Revokes the whole grant — every access token, refresh token and pending code your application holds for that End Customer, including those obtained by its other users — and records an audit event. | Delete every stored token for the grant and ask the user to connect again. Find out how the old token was reused. |
Revocation endpoint
POST /oauth/revoke.
| Status | error | Cause | Action |
|---|---|---|---|
200 | Success: every token issued to the same authorization is revoked. The End Customer's approval of your application is untouched, so re-authorizing issues fresh tokens without another administrator approval; only a Customer Portal revoke (Settings → Connected apps) withdraws the approval. Also returned for unknown, expired and already-revoked tokens and for tokens issued to another client (RFC 7009). The body is empty. | Treat the token as gone. | |
400 | invalid_request | token is missing; the body is not application/x-www-form-urlencoded; or client credentials were sent both in the Authorization header and in the body with different values. | Fix the request. |
401 | invalid_client | Bad client credentials. | Same as for the token endpoint. |
429 | More than 60 requests per minute from one IP address. The body is the JSON {"error":"Too many requests"}; there is no Retry-After header. | Back off with your own schedule and retry. |
Userinfo endpoint
GET /oauth/userinfo (also POST). Errors follow RFC 6750: the status and the WWW-Authenticate header carry the error, and the body repeats it.
| Status | Body | WWW-Authenticate | Cause | Action |
|---|---|---|---|---|
401 | {"error":"invalid_token"} | Bearer error="invalid_token" | Missing, expired, revoked or malformed access token. | Refresh the token and retry once. If the refresh fails, re-authorize. |
403 | {"error":"insufficient_scope"} | Bearer error="insufficient_scope" | The token's authorization did not request the openid scope. Who signed in makes no difference: userinfo answers for an administrator's token as readily as for anyone else's. | Do not retry with this token. Include openid in the next authorization request. |
429 | {"error":"Too many requests"} | More than 120 requests per minute from one IP address. There is no Retry-After header. | Back off with your own schedule. The id_token already carries the same claims, so most applications never need to poll userinfo. |
Discovery and JWKS endpoints
GET /.well-known/openid-configuration and GET /oauth/.well-known/jwks.json. Neither takes authentication.
| Status | Body | Cause | Action |
|---|---|---|---|
429 | {"error":"Too many requests"} | More than 120 requests per minute from one IP address, for either endpoint. There is no Retry-After header. | Cache the discovery document for 1 hour and the JWKS for 5 minutes instead of fetching them on every sign-in. |
Organization Core API with a bearer token
Calls to https://hub.xyte.io/core/v1/organization/... with Authorization: Bearer xoat_... return the same responses as with an API key, plus this authentication error:
| Status | Body | WWW-Authenticate | Cause | Action |
|---|---|---|---|---|
401 | {"error":"Not authorized"} | Bearer error="invalid_token" | The access token is expired, revoked or not recognized. | Refresh once and retry. If the refresh returns invalid_grant, delete the stored tokens and ask the user to connect again. |
Authorization is otherwise identical to an API key: an access token carries the End Customer's full administrative reach, so nothing is refused because of the role of the user who signed in. /core/v1/organization/users and /core/v1/organization/groups answer 200 for every access token.
Quick decision table
| You got | Do |
|---|---|
error=access_denied on the callback | Tell the user and offer to retry. |
state mismatch | Discard the callback. |
invalid_grant on the code exchange | Start a new authorization request. |
invalid_grant on refresh | Delete the stored tokens; the user connects again. |
invalid_client | Fix the credentials; rotate them if leaked. |
401 from the API or userinfo | Refresh once, then re-authorize. |
403 insufficient_scope from userinfo | The authorization did not request openid; add the scope and authorize again. |
429 anywhere | Back off with jitter; do not refresh ahead of need. |
