> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tuneplane.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Get a bearer token, keep it fresh, and choose the right kind of credential.

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
curl -X POST https://tuneplane.your-company.com/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username": "alice", "password": "..."}'
```

```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "user": { "username": "alice", "role": "user" }
}
```

Send that token as `Authorization: Bearer <token>` on every other request.

## Four kinds of credential

They are not interchangeable, and using the wrong one is the most common integration mistake.

**Access token** `JWT, 8 hours`

What `POST /api/auth/login` and the CLI device flow return. Identifies **a person**. Carries their
role, so an admin's token opens admin endpoints. This is the one you want for scripts that act on
your own behalf.

**Refresh token** `30 days`

Issued alongside the access token, and only ever as an `httpOnly` cookie — never in a response
body, so browser JavaScript cannot read it. `POST /api/auth/refresh` exchanges it for a new access
token. A command-line client re-runs the device flow instead.

**Ingest token** `30 days, one run`

Injected into a training container as `TUNEPLANE_JOB_TOKEN`. Scoped to a single run and only accepted
by `/api/ingest/*`. A job holds this and nothing else — it never sees your account credentials.
See [Reporting from training code](https://docs.tuneplane.ai/en/api-reference/ingest.md).

**Deployment token** `revocable, no expiry`

Authorizes application traffic to one [model deployment](https://docs.tuneplane.ai/en/api-reference/inference.md) and nothing
else. Independent of any login session, so a service keeps working after the person who created it
leaves. Revoke it from the console.

> [!NOTE]
>
> Roles are `admin` and `operator` (the console labels the latter "Member"). Admin-only endpoints
> return `403` for an `operator` token — not `401`, which
> would wrongly suggest the token is bad.

## Log in from a browser or a script

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
TUNEPLANE_CLIENT_TOKEN=$(curl -sX POST https://tuneplane.your-company.com/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username": "alice", "password": "'"$PASSWORD"'"}' | jq -r .token)
```

`$PASSWORD` there is your own shell variable. `TUNEPLANE_CLIENT_TOKEN` is the platform's, and
the CLI reads the same one.

Ten attempts per IP per minute. A deployment using OIDC disables this route, so check
`GET /api/auth/config` for the login methods it offers before assuming.

## Log in from a machine with no browser

The device flow, RFC 8628. This is what `tp login --device-flow` uses, and what you want on an SSH
session or a build agent.

**1. Ask for a code**

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
curl -sX POST https://tuneplane.your-company.com/api/cli/device/code \
  -H "Content-Type: application/json" -d '{}'
```

```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
{
  "device_code": "5f3a...",
  "user_code": "WDJB-MJHT",
  "verification_uri": "https://tuneplane.your-company.com/cli/device",
  "verification_uri_complete": "https://tuneplane.your-company.com/cli/device?user_code=WDJB-MJHT"
}
```

**2. Approve it in a browser**

Open `verification_uri_complete` anywhere you are already signed in, on any device.

**3. Poll until it is approved**

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
curl -sX POST https://tuneplane.your-company.com/api/cli/device/token \
  -H "Content-Type: application/json" \
  -d '{"device_code": "5f3a..."}'
```

Returns `authorization_pending` until somebody approves it, then the access token and
`expires_in`. Poll no faster than every five seconds; twenty polls per minute is the ceiling.

## Check it worked

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
curl -H "Authorization: Bearer $TUNEPLANE_CLIENT_TOKEN" \
  https://tuneplane.your-company.com/api/auth/me
```

Returns your username, role, and quota. A `401` here means the token is absent, malformed, or
expired — try logging in again before looking anywhere else.

## Handling expiry

An access token lasts eight hours by default (`TUNEPLANE_JWT_HOURS` on the deployment). When one
expires, endpoints return `401`. Two ways to recover, depending on what you are:

* **A browser session**: `POST /api/auth/refresh` with the refresh cookie.
* **A script or CI job**: log in again, or re-run the device flow. Do not cache a token across runs
  and hope; check `GET /api/auth/me` and re-authenticate on `401`.

`POST /api/auth/logout` revokes the refresh token and, on deployments with Redis configured,
invalidates the access token immediately rather than at expiry.

> **Why the refresh token never appears in a response body**
>
> A token that JavaScript can read is a token an injected script can steal, and a 30-day refresh
> token is worth far more than an 8-hour access token. Keeping it in an `httpOnly` cookie means an
> XSS bug costs a session rather than a month of access. It also means a command-line client cannot
> use it at all — which is why the device flow exists as a separate path rather than an afterthought.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.