Authentication
Authorize with the OAuth device flow, or with a long-lived Public API Access Token.
On this page
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:
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 flowAn ID token from the RFC 8628 device code flow. Works for personal and team calls alike, so it is the alternative to either token. | Not allowedNames a person, not a company |
| Personal API Access TokenFor personal plans. A long-lived key that acts as you on your personal account: your library, boards, uploads, and generations. | Not allowedCovers your data, not your company's |
| Company API Access TokenFor team and enterprise plans. A long-lived key that acts as you on your company's teams, plus company usage reporting. | AllowedEnterprise 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.
1. Start the flow
Ask for a device code.
curl -X POST https://api2.rundiffusion.com/api/v2/auth/device/start \
-H "Content-Type: application/json" \
-d '{}'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());import requests
start = requests.post(
"https://api2.rundiffusion.com/api/v2/auth/device/start",
json={},
).json()No authorization is needed to start a flow.
{
"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 -G https://api2.rundiffusion.com/api/v2/auth/device/poll \
--data-urlencode "device_code=$DEVICE_CODE"const poll = await fetch(
`https://api2.rundiffusion.com/api/v2/auth/device/poll?device_code=${deviceCode}`,
).then(r => r.json());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:
{
"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; accounts is abbreviated here.
The tokens object holds two credentials:
id_tokenis the bearer you send on every user-scoped call. It is short-lived, roughly an hour, and itsexpires_atis included. When it expires, exchange the refresh token for a new one (below).refresh_tokenis 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.
POST /api/v2/auth/token/refresh
Content-Type: application/json
{ "refresh_token": "AMf-vBz…" }The response is the same three keys the poll returns:
{
"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_INVALIDmeans 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_UNAVAILABLEis 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.
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 theX-Device-Idheader) 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-everywhererevokes 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:
curl https://api2.rundiffusion.com/api/v2/library?limit=5 \
-H "Authorization: Bearer $RUNDIFFUSION_API_TOKEN"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());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 "https://api2.rundiffusion.com/api/v2/library?limit=5&team_id=Tq8vNc…" \
-H "Authorization: Bearer $RUNDIFFUSION_API_TOKEN"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());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.
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.
?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 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.
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.
