# Authentication

> SyncWords API keys, bearer tokens, and what a 401 or 403 means.

VOD Express uses the same credentials as the SyncWords Live API. If your account already calls the
Live API, its keys, and its tokens, work here unchanged.

Every request carries a bearer token:

```http
Authorization: Bearer 482|kJq8vN3mX2pL7rT5wY9zB4cF6hD1sA0gE8uI2oP
```

## Get a token

Exchange your organization's **public key** and **secret key** for a token with
`POST /token/create`, which needs no token itself. You find the pair in the SyncWords dashboard, or
receive it from your SyncWords contact ([Get access](/vod-express/quickstart#get-access)). The URLs
are those of [Environments](/vod-express/environments).

```bash cURL
curl -s https://api-vod-express.syncwords.com/v1/token/create \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-d '{"username": "YOUR_PUBLIC_KEY", "password": "YOUR_SECRET_KEY"}'
```

```python Python
import requests

response = requests.post(
  "https://api-vod-express.syncwords.com/v1/token/create",
  json={"username": PUBLIC_KEY, "password": SECRET_KEY},
  headers={"Accept": "application/json"},
)
response.raise_for_status()
token = response.json()["access_token"]
```

```javascript Node.js
const response = await fetch('https://api-vod-express.syncwords.com/v1/token/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ username: PUBLIC_KEY, password: SECRET_KEY }),
});
const { access_token: token } = await response.json();
```

```json 200
{
  "access_token": "482|kJq8vN3mX2pL7rT5wY9zB4cF6hD1sA0gE8uI2oP",
  "token_type": "Bearer",
  "expires_in": 1800
}
```

A wrong pair answers `401 {"message": "Invalid login details", "code": "invalid_credentials"}`.
The answer has the Live API's field names, so a client that creates its token with the Live API's
`POST /v2/token/create` can switch to this URL and change nothing else; a token from either works
on both APIs.

Creating a token is not counted against your [rate limit](/vod-express/guides/limits#rate-limit).
One key pair refused 60 times in a minute is answered `429` until the minute is over.

The token belongs to the **organization**, not to a person, so projects created with it are the
organization's. Keep the key pair on your servers: anyone holding it can create tokens.

## Token lifetime

`expires_in` is the token's lifetime in seconds. An answer without it is a token that does not
expire by age.

Do not build on the number either way. Handle expiry the simple way: when any call answers `401`,
create a new token and repeat the call. The
[monitoring loop](/vod-express/guides/monitoring#a-good-polling-loop) and the
[automation example](/vod-express/examples/automation) do exactly that.

> **Warning:**
> There is no refresh here: create a new token. Do not call the Live API's
> `POST /v2/token/refresh` instead. A refresh **revokes every token your organization holds**, so
> it signs out every other integration using the same keys, on both APIs.

Creating a token does not revoke earlier ones; each stays valid until it expires.

## Check what a token can do

`GET /whoami` names the account behind the token and says whether VOD Express is enabled for it:

```bash
curl -s -H "Authorization: Bearer $TOKEN" https://api-vod-express.syncwords.com/v1/whoami
```

```json
{
  "data": {
    "organization": { "id": 141, "name": "Acme Media" },
    "user": { "id": null, "email": null, "apiUser": true },
    "vodExpress": {
      "enabled": true,
      "defaultBuildVersion": "0.9.0-alpha1-2026-10-05T14-57Z-bf4a93",
      "hasAwsAccess": false,
      "canManageAccount": true
    }
  }
}
```

| Field | Meaning |
|---|---|
| `vodExpress.enabled` | VOD Express is available to the account: every account with access to SyncWords Live has it, unless its trial expired or ran out. While it is `false`, every other endpoint answers `403`. |
| `vodExpress.defaultBuildVersion` | The processing build a new project gets unless it names one. See [Builds](/vod-express/configuration/overview#builds-and-the-schema). |
| `vodExpress.hasAwsAccess` | An upload role has been vended for the account (`PUT /account/aws-access`). See [Upload with the AWS CLI](/vod-express/uploads/aws-role). |
| `vodExpress.canManageAccount` | Whether this token may change the account: its upload role, [storage connections](/vod-express/uploads/own-bucket) and settings. `true` for an organization key and an administrator's user token; `false` for another user's. |

## When authentication fails

| Status | `code` | What to do |
|---|---|---|
| `401` | `unauthenticated` | No token, an expired one, or one SyncWords does not know. Create a new token and repeat. |
| `401` | `invalid_credentials` | `POST /token/create` with a key pair that matches no account. Check the pair. |
| `403` | `vod_express_not_enabled` | The token is valid but VOD Express was switched off for the account. Ask [support](/vod-express/support). |
| `403` | `account_restricted` | The token is valid but the account is restricted: its access to SyncWords was revoked, or its trial expired or ran out of minutes. `message` says which. Ask [support](/vod-express/support). |
| `429` | `too_many_requests` | More than 1000 requests in a minute from your account. Wait the seconds `Retry-After` says. See [Limits](/vod-express/guides/limits#rate-limit). |

## Signing in from a SyncWords app

SyncWords' own web applications sign their users in another way, in the same `Authorization`
header. Integrations always use API tokens.
