---
title: Authenticate to the API
description: Send an API key, exchange it for an access token, use OAuth client
  credentials, and refresh tokens for people.
url: https://hexelstudio.com/docs/identity/authenticate
---

Every call to `https://api.hexelstudio.com/v1/...` must say who is calling. Scripts, servers and CI jobs authenticate as a [service user](https://hexelstudio.com/docs/identity/service-users), with an API key or an OAuth client. People sign in to Hexel Studio with their Hexel account, and the console gets tokens for them.

| Method | Best for | What you send to the API |
| --- | --- | --- |
| [API key in a header](#send-an-api-key-directly) | Scripts, quick tests, low request rates | `X-API-Key: $API_KEY` |
| [API key exchanged for a token](#exchange-an-api-key-for-an-access-token) | Services that make many calls | `Authorization: Bearer $ACCESS_TOKEN` |
| [OAuth client credentials](#use-oauth-client-credentials) | Services and tools that speak OAuth 2.0 | `Authorization: Bearer $ACCESS_TOKEN` |
| [Role session](https://hexelstudio.com/docs/identity/roles#assume-a-role) | Temporary access to a role's permissions | `Authorization: Bearer $ACCESS_TOKEN` |

An access token is a short-lived signed token for one identity in one organization. Tokens are issued by the token service at `https://sts.hexelstudio.com`. See [Tokens](https://hexelstudio.com/docs/identity/tokens) for lifetimes and claims.

## Send an API key directly

Send the key in the `X-API-Key` header. Hexel Studio checks the key at the edge, exchanges it for an access token of the key's service user, and passes the request on with that token.

```bash
curl https://api.hexelstudio.com/v1/workspaces \
  -H "X-API-Key: $API_KEY"
```

- The organization comes from the key. You don't need `X-Organization-Id`. If you send it, it must be the key's organization.
- If a request carries both `Authorization: Bearer ...` and `X-API-Key`, the bearer token is used and the key is ignored.
- Hexel Studio removes `X-API-Key` from the request before it reaches the service that handles it.
- Some product APIs accept only a bearer token. If a key gets `401` on a route where a token works, [exchange the key](#exchange-an-api-key-for-an-access-token) first.

Errors from the API key check have this shape:

```json
{"error": {"code": "403", "message": "X-Organization-Id does not match the API key's organization."}}
```

| Status | Cause |
| --- | --- |
| `401` | The key is unknown, expired or revoked, or its service user is disabled or deleted. |
| `403` | `X-Organization-Id` names a different organization than the key's. |
| `429` | Too many invalid API keys from your IP address (20 a minute), or a [rate limit](https://hexelstudio.com/docs/identity/limits) was exceeded. Wait for the `Retry-After` seconds. |
| `503` | API key checking is temporarily unavailable. Retry with backoff. |

<Note>
Each request with an API key is checked again. For many requests per minute, exchange the key once and reuse the access token until it expires.
</Note>

## Exchange an API key for an access token

`POST https://sts.hexelstudio.com/token` with the key in `X-API-Key`. The body is optional for this grant. If you send one, set `grant_type` to `api_key`.

```bash
curl -X POST https://sts.hexelstudio.com/token \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"grant_type": "api_key"}'
```

```json
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "refresh_token": "",
  "token_type": "Bearer",
  "expires_in": 900
}
```

<ResponseField name="access_token" type="string">
  The access token. Send it as `Authorization: Bearer <access_token>`.
</ResponseField>
<ResponseField name="refresh_token" type="string">
  Always empty for this grant. Exchange the key again when the token expires.
</ResponseField>
<ResponseField name="token_type" type="string">
  Always `Bearer`.
</ResponseField>
<ResponseField name="expires_in" type="integer">
  Seconds until the token expires: `900` (15 minutes).
</ResponseField>

Then call the API with the token:

```bash
curl https://api.hexelstudio.com/v1/workspaces \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

An unknown, expired or revoked key, or a key whose service user is disabled, gets `401` with `{"error": "invalid api key"}`.

## Use OAuth client credentials

An [OAuth client](https://hexelstudio.com/docs/identity/service-users#oauth-clients) is a `client_id` and `client_secret` pair that belongs to a service user. Exchange it for an access token with the OAuth 2.0 `client_credentials` grant.

<CodeGroup>

```bash curl
curl -X POST https://sts.hexelstudio.com/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "'"$CLIENT_ID"'",
    "client_secret": "'"$CLIENT_SECRET"'"
  }'
```

```typescript TypeScript
const res = await fetch("https://sts.hexelstudio.com/token", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    grant_type: "client_credentials",
    client_id: process.env.CLIENT_ID,
    client_secret: process.env.CLIENT_SECRET,
  }),
});
if (!res.ok) {
  throw new Error(`Token request failed: ${res.status} ${await res.text()}`);
}
const { access_token, expires_in } = await res.json();
```

```python Python
import os
import requests

resp = requests.post(
    "https://sts.hexelstudio.com/token",
    json={
        "grant_type": "client_credentials",
        "client_id": os.environ["CLIENT_ID"],
        "client_secret": os.environ["CLIENT_SECRET"],
    },
    timeout=10,
)
resp.raise_for_status()
access_token = resp.json()["access_token"]
```

</CodeGroup>

<ParamField body="grant_type" type="string" required>
  `client_credentials`.
</ParamField>
<ParamField body="client_id" type="string" required>
  The OAuth client's ID, for example `client_...`.
</ParamField>
<ParamField body="client_secret" type="string" required>
  The client secret, shown once when you create or rotate the client.
</ParamField>

The token endpoint accepts a JSON body (`Content-Type: application/json`) or a form body (`Content-Type: application/x-www-form-urlencoded`). Instead of the body, you can send the client ID and secret with HTTP Basic authentication (`Authorization: Basic ...`, the ID and secret form-URL-encoded first, as RFC 6749 describes). Use one method: credentials in both places are refused with `400`.

The response has the same fields as the [API key exchange](#exchange-an-api-key-for-an-access-token): a 15-minute access token, `token_type` `Bearer`, `expires_in` `900` and an empty `refresh_token`. Request a new token when it expires.

| Status | Body | Cause |
| --- | --- | --- |
| `400` | `{"error": "client_id and client_secret required"}` | A field is missing. |
| `400` | `{"error": "use one client authentication method: HTTP Basic or the request body"}` | Credentials sent both ways. |
| `401` | `{"error": "invalid client credentials"}` | Unknown, revoked or expired client, wrong secret, or the service user is disabled. |

## Get a role session directly

With an API key or client credentials, you can ask for a session of a [role](https://hexelstudio.com/docs/identity/roles) in the same call. Add `resource` with the role ID. The role's trust policy must allow your service user.

```bash
curl -X POST https://sts.hexelstudio.com/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "'"$CLIENT_ID"'",
    "client_secret": "'"$CLIENT_SECRET"'",
    "resource": "<role-id>",
    "session_name": "nightly-export"
  }'
```

The response is a role session, described in [Assume a role](https://hexelstudio.com/docs/identity/roles#assume-a-role). People and service users that already hold an access token use token exchange instead, on the same page.

## People: sign in with a Hexel account

People sign in to the console with their [Hexel account](https://hexelstudio.com/docs/identity/accounts). The console exchanges the sign-in for a Hexel Studio access token and a refresh token for the organization you select, and refreshes it for you.

Exchanging a Hexel account sign-in for a token from your own application is not open for self-service. Contact Hexel Studio if you need it. For automation, use a service user.

## Refresh tokens

Only a sign-in with a Hexel account returns a refresh token. API keys and OAuth clients never do: exchange the credential again instead. [Role sessions](https://hexelstudio.com/docs/identity/roles) can't be refreshed: assume the role again.

To refresh, `POST https://sts.hexelstudio.com/refresh` with the refresh token:

```bash
curl -X POST https://sts.hexelstudio.com/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh_token": "'"$REFRESH_TOKEN"'"}'
```

```json
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "refresh_token": "<new-refresh-token>",
  "token_type": "Bearer",
  "expires_in": 900
}
```

- Each refresh token works once. The response carries a new one; store it and discard the old one.
- A refresh keeps the organization. If you send `X-Organization-Id` with a different organization, the request fails with `400`. Sign in again to switch.
- A refresh fails with `401` after the refresh token's 30-day lifetime, after the sign-in session's deadline, or once the account is suspended. It fails with `403` if the person is no longer an active member of the organization.
- `expires_in` can be under 900 when the sign-in session ends sooner.

To sign out, `POST https://sts.hexelstudio.com/revoke` with `{"refresh_token": "..."}`. This ends every refresh token of that person's account, in every organization, and answers `{"revoked": true}`. Access tokens already issued stay valid until they expire. See [Tokens](https://hexelstudio.com/docs/identity/tokens) for lifetimes and the sign-in deadline.

## Token endpoint limits

`POST /token` issues at most **60 tokens a minute for each credential**: each API key, each OAuth client, each person signing in, and each user, service user or role that exchanges an access token for a role session. The count resets at the start of each minute. Over the limit, you get:

```json
{"error": "rate_limited", "error_description": "too many tokens issued for this credential; retry later"}
```

with status `429` and a `Retry-After` header in seconds. Cache tokens and reuse them until they are close to expiry. See [Limits](https://hexelstudio.com/docs/identity/limits) for the API's other rate limits.

## Next steps

<CardGroup cols={2}>
  <Card title="Service users and API keys" icon="bot" href="https://hexelstudio.com/docs/identity/service-users">
    Create a service user, its API keys and OAuth clients.
  </Card>
  <Card title="Roles" icon="user-cog" href="https://hexelstudio.com/docs/identity/roles">
    Assume a role for temporary, scoped access.
  </Card>
  <Card title="Request and response headers" icon="list" href="https://hexelstudio.com/docs/identity/headers">
    Every header the API reads or returns.
  </Card>
  <Card title="Tokens" icon="key-round" href="https://hexelstudio.com/docs/identity/tokens">
    Lifetimes, claims and the sign-in deadline.
  </Card>
</CardGroup>
