# Authentication

> Authorize with the OAuth device flow, or with a long-lived Public API Access Token.

Canonical page: https://www.rundiffusion.com/docs/api/authentication
Endpoints: POST /api/v2/auth/device/start, GET /api/v2/auth/device/poll, POST /api/v2/auth/token/refresh, POST /api/v2/auth/device/revoke, POST /api/v2/auth/sign-out-everywhere
Authorization: OAuth device flow or Personal API Access Token or Company API Access Token

---

Every access token belongs to a person. Which one you use follows your plan.

| Your plan | Use |
| --- | --- |
| Personal | **Personal API Access Token**, which you create yourself |
| Team or Enterprise | **Company API Access Token**, which an account administrator creates |
| Either | **OAuth device flow** |

A company token still acts as one person. The difference from a personal token
is which account it reaches, and who creates it: a personal token reaches your
personal account and you make it yourself, while a company token reaches your
company's teams and is created for you by an admin holding the API tokens
permission.

OAuth is not a third tier. It is the alternative to a token, and it works for
personal calls and team calls alike. Reach for a **token** when a script or
service runs unattended, and for **OAuth** when a real person signs in to an app
you distribute: it expires on its own, it is revocable per device, and your app
never handles anyone's password.

Every request carries whichever one as a bearer credential:

```http
Authorization: Bearer <your-token>
```

## Reporting is the exception

Reporting returns usage for a whole company, so it takes the one credential that
names a company: a **company token**, on an Enterprise plan.

| Credential | Reporting |
| --- | --- |
| OAuth device flow | Not allowed (Names a person, not a company) |
| Personal API Access Token | Not allowed (Covers your data, not your company's) |
| Company API Access Token | Allowed (Enterprise plans, and the owner needs the reports permission) |

A personal token and OAuth both name a *person*, so neither is a narrower way to
ask for a company's numbers. Both are refused rather than scoped down.

The user behind the token must also hold the **view company reports** permission,
checked on every call rather than when the token was made. Losing the permission
stops reporting straight away, while leaving the token working everywhere else.

## OAuth device flow

The device flow is the RFC 8628 flow, so it works from environments that cannot
host a redirect listener or open an embedded webview, including desktop plugins,
command line tools, and backend jobs. Your application never handles the user's
password. The whole flow lives under `/api/v2/`, so you never leave the version
you are integrating against.

> **On a team, OAuth has to be granted**
>
> A team role decides whether its members may reach the API this way. If yours
> has not enabled it, calls return `PLUGIN_NOT_ALLOWED` even though the flow
> itself completed and your token is valid. A team admin turns it on under the
> **Allow plugin and API access** permission on the Team Roles page.
>
> Personal accounts need no grant. Neither does an access token, which is
> authorized by the person who created it rather than by a role.

### 1. Start the flow

Ask for a device code.

cURL:

```bash
curl -X POST https://api2.rundiffusion.com/api/v2/auth/device/start \
  -H "Content-Type: application/json" \
  -d '{}'
```

JavaScript:

```javascript
const start = await fetch(
  'https://api2.rundiffusion.com/api/v2/auth/device/start',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({}),
  },
).then(r => r.json());
```

Python:

```python
import requests

start = requests.post(
    "https://api2.rundiffusion.com/api/v2/auth/device/start",
    json={},
).json()
```

No authorization is needed to start a flow.

```json
{
  "device_code": "Qk9xZ2h0…",
  "user_code": "H4KM-92TQ",
  "verification_url": "https://app.rundiffusion.com/auth?code=H4KM-92TQ",
  "expires_in": 900,
  "interval": 5
}
```

Keep the `device_code` secret and never show it to the user: it is what you poll
with. Display the `user_code` instead, which is what the user confirms in the
browser. The codes are good for `expires_in` seconds and `interval` is the minimum
number of seconds between polls.

### 2. Send the user to verify

Direct them to `verification_url` and show them the user code. They sign in and
approve the request in their browser.

### 3. Poll until approved

Poll the device code, as a `GET` with a query parameter, at the interval the
start response gave you.

cURL:

```bash
curl -G https://api2.rundiffusion.com/api/v2/auth/device/poll \
  --data-urlencode "device_code=$DEVICE_CODE"
```

JavaScript:

```javascript
const poll = await fetch(
  `https://api2.rundiffusion.com/api/v2/auth/device/poll?device_code=${deviceCode}`,
).then(r => r.json());
```

Python:

```python
poll = requests.get(
    "https://api2.rundiffusion.com/api/v2/auth/device/poll",
    params={"device_code": device_code},
).json()
```

While the user has not finished, polling returns `428` with
`AUTHORIZATION_PENDING`. Keep waiting. A `400` `SLOW_DOWN` means double your
interval before trying again. Once approved, the poll returns the credentials
together with the same identity envelope `/me` would give you, so a client can
finish sign-in in one round trip:

```json
{
  "tokens": {
    "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6…",
    "refresh_token": "AMf-vBz…",
    "expires_at": 1785178015
  },
  "device_id": "b1f7c3d2-…",
  "user": {
    "uid": "k3PqV9…",
    "email": "sam@example.com",
    "email_verified": true,
    "sign_in_provider": "google.com",
    "created_at": "2025-11-03T16:42:08+00:00",
    "top_up": { "available": true, "tokens": 500 }
  },
  "accounts": [],
  "newly_joined_teams": []
}
```

`tokens.expires_at` is Unix epoch **seconds**, not an ISO timestamp. `device_id`
is the handle you send back as `X-Device-Id`, and the one you revoke later.
`user`, `accounts`, and `newly_joined_teams` are identical in shape to
[Identity](/docs/api/me); `accounts` is abbreviated here.

The `tokens` object holds two credentials:

- **`id_token`** is the bearer you send on every user-scoped call. It is
  short-lived, roughly an hour, and its `expires_at` is included. When it
  expires, exchange the refresh token for a new one (below).
- **`refresh_token`** is long-lived. It exists only to obtain new ID tokens, and
  it is the more sensitive of the two.

### Refreshing an expired ID token

`POST /api/v2/auth/token/refresh` with the refresh token. No `Authorization`
header: the refresh token is the credential, which is what makes this callable
once the ID token has already expired.

```http
POST /api/v2/auth/token/refresh
Content-Type: application/json

{ "refresh_token": "AMf-vBz…" }
```

The response is the same three keys the poll returns:

```json
{
  "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6…",
  "refresh_token": "AMf-vBz…",
  "expires_at": 1785181615
}
```

Store the `refresh_token` you get back, replacing what you had. It is usually the
one you sent, unchanged, but it may be rotated, and echoing it either way means you
never have to work out which happened.

`expires_at` is absolute Unix **seconds**, not a lifetime, so it needs no
arithmetic.

Two failures worth telling apart:

- **`401 TOKEN_INVALID`** means the refresh token is finished. Discard it and start
  a new device flow. The message is deliberately identical whether it expired, was
  revoked, or never existed.
- **`503 UPSTREAM_UNAVAILABLE`** is transient. Retry with backoff and keep the
  refresh token: signing the user out here would be throwing away a working
  credential over a passing fault.

> `AUTHORIZATION_PENDING` and `SLOW_DOWN` are expected parts of the flow, not
> failures. `DEVICE_CODE_EXPIRED` (`410`) means the user took too long: start a
> new flow. Treat any other error code as terminal.

### Keeping the tokens safe

Both tokens are secret and neither is public-facing. Treat them like passwords:
keep them server-side, in an environment variable or a secret manager, never in
a browser bundle, a mobile or desktop app, client-side code, or a committed
file.

Guard the refresh token most closely. The ID token expires on its own within the
hour, so an exposed one has a limited window. The refresh token is long-lived
and can mint new ID tokens until it is revoked, so a leaked refresh token is
persistent access to the user's account until you act.

### Revoking

Revocation is also your response to a leak. There are two levers:

- **`POST /api/v2/auth/device/revoke`** (with the `X-Device-Id` header)
  invalidates a single device immediately, even before its current ID token
  expires. Other devices the user is signed in on keep working.
- **`POST /api/v2/auth/sign-out-everywhere`** revokes every refresh token for
  the user, signing them out of all devices at once.

If a token is exposed, revoke, then run the device flow again to re-establish a
clean session.

## Public API Access Token

A long-lived key you paste into a script. There are two kinds, and the one you
get depends on your plan.

| | Personal token | Company token |
| --- | --- | --- |
| For | Personal plans | Team and Enterprise plans |
| Acts as | You | You |
| Covers | Your own library, boards, uploads, generations | Your company's teams, plus company reporting |
| Account selection | no `team_id` | `team_id=<teamId>` |
| Reporting | No | Yes, on Enterprise, with the reports permission |
| Who can create it | You | A company admin with the API tokens permission |

Both live on the **Plugins & APIs** page in RunDiffusion. A personal token is
yours to create; a company token is minted by an admin, who picks the member it
acts as. You can hold as many as you like, so give each one a name and a
separate token rather than sharing a single value between integrations: revoking
then affects one of them.

If you are on a team and need a token, ask an account administrator. They create
it against your name, so everything it does is still attributed to you.

A personal token authenticates the same requests an OAuth token does, including
[account selection](#selecting-an-account):

cURL:

```bash
curl https://api2.rundiffusion.com/api/v2/library?limit=5 \
  -H "Authorization: Bearer $RUNDIFFUSION_API_TOKEN"
```

JavaScript:

```javascript
const page = await fetch(
  'https://api2.rundiffusion.com/api/v2/library?limit=5',
  {
    headers: {
      Authorization: `Bearer ${process.env.RUNDIFFUSION_API_TOKEN}`,
    },
  },
).then(r => r.json());
```

Python:

```python
import os
import requests

page = requests.get(
    "https://api2.rundiffusion.com/api/v2/library",
    headers={
        "Authorization": f"Bearer {os.environ['RUNDIFFUSION_API_TOKEN']}",
    },
    params={"limit": 5},
).json()
```

A company token reaches your company's teams. Name the team in `team_id`, and
it works exactly like the call above:

cURL:

```bash
curl "https://api2.rundiffusion.com/api/v2/library?limit=5&team_id=Tq8vNc…" \
  -H "Authorization: Bearer $RUNDIFFUSION_API_TOKEN"
```

JavaScript:

```javascript
const page = await fetch(
  'https://api2.rundiffusion.com/api/v2/library?limit=5&team_id=Tq8vNc…',
  {
    headers: {
      Authorization: `Bearer ${process.env.RUNDIFFUSION_API_TOKEN}`,
    },
  },
).then(r => r.json());
```

Python:

```python
import os
import requests

page = requests.get(
    "https://api2.rundiffusion.com/api/v2/library",
    headers={
        "Authorization": f"Bearer {os.environ['RUNDIFFUSION_API_TOKEN']}",
    },
    params={"limit": 5, "team_id": "Tq8vNc…"},
).json()
```

Omitting `team_id` with a company token is refused: the token acts on a team,
so the team must be named.

> **Keep access tokens server-side**
>
> Both kinds are long-lived secrets, and neither is public-facing despite the
> name: never ship one in a browser bundle, a mobile app, or anything a user can
> read.
>
> If an API access token leaks, regenerate or delete it, which invalidates the old
> value straight away. There is no session behind a token to revoke, just the key,
> and it is not tied to a device, so signing out of a device does not stop it.

## Selecting an account

A user can belong to more than one account: their personal account and any teams
they are on. The `team_id` query parameter is the whole selection: name a team
to act on it, omit the parameter to act on your personal account.

```text
?team_id=Tq8vNc…
```

There is nothing else to send and nothing to keep consistent with it. A company
token does add one rule of its own: it acts on a team by construction, so
omitting `team_id` with one is refused rather than read as personal.

Endpoints act on one team per call, so `team_id` takes exactly one ID.
[Library](/docs/api/library/list) also accepts a comma-separated list, e.g.
`team_id=Tq8vNc…,Wm2xLp…`, to read across teams in one page. A malformed list, or
several IDs where one is expected, answers `400 ACCOUNT_SELECTION_INVALID`. Team
IDs come from [Identity](/docs/api/me).

## Storing credentials

Read tokens from the environment or a secret manager, never from source. A
practical baseline:

- Keep credentials out of version control and out of client bundles.
- Use separate credentials per integration so one can be rolled without
  disrupting the others.
- Roll immediately on any suspected exposure.
