> For the complete documentation index, see [llms.txt](https://docs.xibosignage.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.xibosignage.com/developer/integrate/authentication.md).

# Authentication

Every request must carry a valid `access_token` in an Authorization header:

{% code title="HTTP Header" %}

```http
Authorization: Bearer <<access token>>
```

{% endcode %}

This page covers how to register an application, obtain a token, refresh it, and scope it.

{% hint style="info" %}
Throughout this page, replace `https://cms.example.org` with the URL of your CMS. If your CMS is installed in a sub-directory, include it — for example `https://example.org/xibo/api/authorize/access_token`.
{% endhint %}

### Quick start

If you're building a server-side integration and just want a token, this is the whole flow:

```bash
curl -X POST https://cms.example.org/api/authorize/access_token \
  -d "grant_type=client_credentials" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"
```

Response:

```json
{
  "token_type": "Bearer",
  "expires_in": 3600,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."
}
```

Use it:

```bash
curl https://cms.example.org/api/display \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."
```

The rest of this page explains where the credentials come from and what to do when the token expires.

### Step 1: Register an Application

Before you can request a token, an Application must exist in the CMS.

1. Sign in to the CMS and go to **Administration** → **Applications**.
2. Select **Add Application** and give it a name. This name is shown to users on the authorisation screen, so make it recognisable.
3. Save, then edit the new Application to configure it.

The Application edit form controls everything about how your integration authenticates:

<table><thead><tr><th width="216">Setting</th><th>What it does</th></tr></thead><tbody><tr><td><strong>Client ID</strong></td><td>Public identifier for your application. Safe to include in configuration files.</td></tr><tr><td><strong>Client Secret</strong></td><td>Password for your application. Treat it like a password — see Keeping credentials safe.</td></tr><tr><td><strong>Confidential</strong></td><td>Whether the application can keep a secret. Leave enabled for server-side integrations.</td></tr><tr><td><strong>Authorisation Code / Client Credentials</strong></td><td>Which grant types this application may use. Enable only what you need.</td></tr><tr><td><strong>Redirect URIs</strong></td><td>Required for the authorisation code grant. The CMS will only redirect to a URI listed here.</td></tr><tr><td><strong>Scopes</strong></td><td>Which parts of the API this application may reach. Defaults to <code>all</code>.</td></tr></tbody></table>

{% hint style="warning" %}
The Client Secret is generated when the Application is created and cannot be retrieved later in plain text. If you lose it, reset it from the Application edit form — this immediately invalidates any integration using the old value.
{% endhint %}

### Step 2: Choose a grant type

Two grant types are supported. Pick based on *whose* data you're acting on.

|                          | Client Credentials                                        | Authorisation Code                                                                   |
| ------------------------ | --------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| **`grant_type` value**   | `client_credentials`                                      | `authorization_code`                                                                 |
| **Acts as**              | The user who owns the Application                         | The user who signs in and approves                                                   |
| **Use for**              | Server-to-server integrations, scheduled jobs, middleware | Applications used by multiple CMS users, or where you must not hold user credentials |
| **User interaction**     | None                                                      | Sign-in and consent in a browser                                                     |
| **Refresh tokens**       | Not issued                                                | Issued                                                                               |
| **Needs a redirect URI** | No                                                        | Yes                                                                                  |

{% hint style="danger" %}
The Applications page labels the authorisation code grant **Authorisation Code**, and older documentation referred to it as `access_code`. Neither is the value you send. The `grant_type` parameter must be exactly `authorization_code`, per the OAuth 2.0 specification.
{% endhint %}

### Step 3: Obtain an access token

All token requests are `POST` to `/api/authorize/access_token`, with parameters in a form-encoded request body (`application/x-www-form-urlencoded`). They are **not** query string parameters.

#### Client credentials grant

A single request. There is no user to redirect and no consent screen.

{% tabs %}
{% tab title="cURL" %}

```bash
curl -X POST https://cms.example.org/api/authorize/access_token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"
```

{% endtab %}

{% tab title="PHP" %}

```php
$client = new \GuzzleHttp\Client();

$response = $client->post('https://cms.example.org/api/authorize/access_token', [
    'form_params' => [
        'grant_type' => 'client_credentials',
        'client_id' => getenv('XIBO_CLIENT_ID'),
        'client_secret' => getenv('XIBO_CLIENT_SECRET'),
    ],
]);

$token = json_decode($response->getBody(), true)['access_token'];
```

{% endtab %}

{% tab title="Python" %}

```python
import os, requests

response = requests.post(
    "https://cms.example.org/api/authorize/access_token",
    data={
        "grant_type": "client_credentials",
        "client_id": os.environ["XIBO_CLIENT_ID"],
        "client_secret": os.environ["XIBO_CLIENT_SECRET"],
    },
    timeout=30,
)
response.raise_for_status()
token = response.json()["access_token"]
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const body = new URLSearchParams({
  grant_type: 'client_credentials',
  client_id: process.env.XIBO_CLIENT_ID,
  client_secret: process.env.XIBO_CLIENT_SECRET,
});

const response = await fetch('https://cms.example.org/api/authorize/access_token', {
  method: 'POST',
  body,
});

const { access_token: token } = await response.json();
```

{% endtab %}
{% endtabs %}

The token acts as the CMS user who owns the Application. It can do everything that user can do, and nothing they cannot — API permissions are still subject to normal CMS user permissions and features.

#### Authorisation code grant

Three steps: send the user to the CMS, receive a code, exchange the code for a token.

**1. Redirect the user to the authorisation endpoint**

```
https://cms.example.org/api/authorize/?client_id=YOUR_CLIENT_ID
  &response_type=code
  &redirect_uri=https://yourapp.example.com/callback
  &scope=all
  &state=RANDOM_UNGUESSABLE_STRING
```

<table><thead><tr><th width="159.666748046875">Parameter</th><th width="160.666748046875">Required</th><th>Notes</th></tr></thead><tbody><tr><td><code>client_id</code></td><td>Yes</td><td>From the Application page.</td></tr><tr><td><code>response_type</code></td><td>Yes</td><td>Always <code>code</code>.</td></tr><tr><td><code>redirect_uri</code></td><td>Yes</td><td>Must exactly match one of the URIs registered against the Application.</td></tr><tr><td><code>scope</code></td><td>No</td><td>Space-delimited. Omit to receive all scopes configured for the Application.</td></tr><tr><td><code>state</code></td><td>Strongly recommended</td><td>Random per-request value. Verify it on return to protect against CSRF.</td></tr></tbody></table>

The user signs in if they aren't already, then sees an authorisation screen naming your application. If they approve, the CMS redirects to your `redirect_uri`.

**2. Receive the authorisation code**

```
https://yourapp.example.com/callback?code=AUTH_CODE&state=RANDOM_UNGUESSABLE_STRING
```

Check that `state` matches the value you sent before going any further. If the user declines, you receive an `error` parameter instead of `code`.

**3. Exchange the code for a token**

Authorisation codes are valid for **10 minutes** and may be used once.

```bash
curl -X POST https://cms.example.org/api/authorize/access_token \
  -d "grant_type=authorization_code" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "redirect_uri=https://yourapp.example.com/callback" \
  -d "code=AUTH_CODE"
```

Response:

```json
{
  "token_type": "Bearer",
  "expires_in": 3600,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
  "refresh_token": "def50200a1b2c3..."
}
```

Store the `refresh_token` securely alongside the user's record.

#### Refreshing a token

Access tokens last one hour. Rather than sending the user through the authorisation screen again, exchange the refresh token:

```bash
curl -X POST https://cms.example.org/api/authorize/access_token \
  -d "grant_type=refresh_token" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "refresh_token=YOUR_REFRESH_TOKEN"
```

The response contains a new access token **and a new refresh token**. Replace the stored refresh token each time — the previous one is invalidated.

Client credentials integrations do not receive refresh tokens. Simply request a new token when the old one expires.

### Step 4: Call the API

The API is served from `/api` on your CMS. Send the token as a Bearer credential on every request:

```bash
curl "https://cms.example.org/api/display?start=0&length=10" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

See OpenAPI for the full route reference, or Getting Started with Postman to explore interactively.

### Token lifetimes

<table><thead><tr><th width="177.6666259765625">Token</th><th width="196.333251953125">Lifetime</th><th>Renewal</th></tr></thead><tbody><tr><td>Access token</td><td>1 hour</td><td>Request a new one, or use a refresh token</td></tr><tr><td>Authorisation code</td><td>10 minutes, single use</td><td>Restart the authorisation flow</td></tr><tr><td>Refresh token</td><td>1 month</td><td>Rotated on every use</td></tr></tbody></table>

Cache your access token for its lifetime rather than requesting a new one per API call. Requesting a token on every call is wasteful and will trip rate limiting on busy systems.

{% hint style="info" %}
Treat `expires_in` as advisory rather than a guarantee. Tokens can be invalidated early — for example if the Application's secret is reset or its scopes are changed. Handle a `401` at any time by re-authenticating and retrying once.
{% endhint %}

### Scopes

Scopes limit which API routes a token can reach. Each scope defines a set of route and method combinations; a request is allowed if any scope on the token permits that route and method.

* The `all` scope grants access to every route and is the default.
* Request specific scopes with a space-delimited `scope` parameter on the token request.
* If you request no scopes, the token receives every scope configured against the Application.
* A token that reaches a route none of its scopes permit receives `403 Access to this route is denied for this scope`.

Scopes are configured per Application under **Administration** → **Applications**. Grant the narrowest set your integration needs — a data-feed integration that only writes to a DataSet has no reason to be able to delete Displays.

{% hint style="info" %}
Scopes constrain a token; they do not expand it. The effective permission is the intersection of the token's scopes and the CMS permissions of the user the token acts as.
{% endhint %}

### Error responses

Token endpoint errors follow the OAuth 2.0 error format:

```json
{
  "error": "invalid_client",
  "error_description": "Client authentication failed",
  "message": "Client authentication failed"
}
```

<table data-header-hidden><thead><tr><th width="107.6666259765625">Status</th><th>Error</th><th>Usual cause</th></tr></thead><tbody><tr><td><code>400</code></td><td><code>unsupported_grant_type</code></td><td><code>grant_type</code> missing, misspelled, or sent as a query parameter instead of a form field</td></tr><tr><td><code>400</code></td><td><code>invalid_request</code></td><td>A required parameter is missing</td></tr><tr><td><code>400</code></td><td><code>invalid_grant</code></td><td>Authorisation code expired, already used, or <code>redirect_uri</code> doesn't match the one used to obtain it</td></tr><tr><td><code>400</code></td><td><code>invalid_scope</code></td><td>A requested scope isn't configured against the Application</td></tr><tr><td><code>401</code></td><td><code>invalid_client</code></td><td>Wrong <code>client_id</code>/<code>client_secret</code>, or the grant type isn't enabled for this Application</td></tr><tr><td><code>401</code></td><td><code>access_denied</code></td><td>Access token expired or malformed</td></tr><tr><td><code>403</code></td><td>—</td><td>Token is valid but its scopes don't permit this route</td></tr></tbody></table>

### Keeping credentials safe

* **Always use HTTPS.** A Bearer token in a request over plain HTTP is a credential in the clear. The CMS should not be exposed over HTTP in production.
* **Never embed a client secret in a browser or mobile application.** Anything shipped to a device is public. Client credentials belong on a server you control.
* **A client credentials token inherits the Application owner's permissions.** Create a dedicated CMS user for each integration, with only the permissions and features that integration needs, and own the Application from that user. Don't run integrations as a Super Admin out of convenience.
* **Store secrets outside your codebase** — environment variables or a secrets manager, never source control.
* **Reset the secret if it may have leaked.** Resetting takes effect immediately.
* **Review Applications periodically** and delete ones no longer in use.

### Troubleshooting

<details>

<summary>I get <code>unsupported_grant_type</code> even though I sent a grant type</summary>

The parameters must be in the POST body as `application/x-www-form-urlencoded`, not in the query string and not as JSON. In cURL, use `-d`; in most HTTP libraries, this is the "form" or "form\_params" option rather than the "json" option.

Also check the value itself: the authorisation code grant is `authorization_code`, not `access_code` or `auth_code`.

</details>

<details>

<summary>I get <code>invalid_client</code> with credentials I'm sure are correct</summary>

Check that the grant type you're requesting is enabled against that Application. An Application with only Client Credentials enabled will reject an `authorization_code` request with `invalid_client` rather than a more specific error.

Also confirm you're using the Client ID and Secret, not a CMS username and password.

</details>

<details>

<summary>The authorisation redirect fails or returns to the wrong place</summary>

The `redirect_uri` must match a URI registered against the Application exactly, including scheme, host, port, path, and trailing slash. The same value must then be sent again when exchanging the code.

</details>

<details>

<summary>Everything worked, then started returning 401 after an hour</summary>

Working as intended — access tokens last one hour. Cache the token with its expiry and renew before or on expiry, rather than assuming a token is permanent.

</details>

<details>

<summary>API configuration problem, consult your administrator</summary>

The CMS could not load its API signing keys. On a self-hosted install, check that the private key path and encryption key are configured and readable by the web server. Contact your CMS administrator, or Xibo Support if you are Cloud hosted.

</details>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.xibosignage.com/developer/integrate/authentication.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
