# Authentication

> Two ways in. For your own scripts, create a personal access token under Settings then API and send it as a bearer token; for an application other people connect, use OAuth instead. Tokens start with sk_live_ and are stored only as a SHA-256 hash, so the value is shown exactly once. Three scopes exist — read, write and delete — and deleting is deliberately separate from writing. A token acts as you and revoking one takes effect immediately.

_Source: https://clusters.soleilpictures.com/docs/api/authentication · Updated 2026-10-07_

## Which one do you want

| | |
|---|---|
| **A personal access token** | Your own scripts, your own pipeline, a cron job. You make it, you hold it. |
| **[OAuth](/docs/api/oauth)** | An application that *other people* connect — including any MCP client. Nobody pastes a credential. |

Both end at the same place: a bearer token that resolves to one person's own
session. Everything on this page about scopes, what a credential can reach, and
the audit trail applies to both.

## Minting a token

**Settings → Connections → New token.** Give it a name that says where it will be used,
so you know which one to revoke later.

An account can mint tokens once it is 72 hours old — an
account's first hours are when throwaway sign-ups do their damage.

Then choose what it may do — **Read only**, **Read & write**, or **Full
access**. Read only is the right choice unless you know you need more; full
access is the only one that can delete, and is worth withholding from an AI
assistant unless you specifically want it able to throw things away. Deletes
are recoverable, but you would have to notice first.

The token is displayed **once**, at creation. It is the prefix
`undefined` followed by 40 hexadecimal characters — 160 bits of
randomness:

```
undefined<40 hex characters>
```

Only a SHA-256 hash is stored. There is no "show token" and no recovery — this
is deliberate, and it means a database compromise does not yield working
credentials.

> **Warning:** This format is identical in shape to a Stripe live secret key.
> Secret scanners — GitHub push protection among them — will flag a Soleil
> token as a Stripe key and block the commit. That is useful when a real token
> leaks, and a nuisance when you paste an example into a repo. Write examples
> as `undefined…` rather than spelling out 40 plausible characters.

## Using it

```sh
curl https://clusters.soleilpictures.com/api/v1/me \
  -H "Authorization: Bearer undefined…"
```

The scheme must be `Bearer`. A missing or malformed header gets `401` with a
`WWW-Authenticate` header.

## Scopes

Three: `delete` · `read` · `write`.

| Scope | Allows |
|---|---|
| `read` | Every `GET` |
| `write` | Creating and changing — `POST`, `PATCH` |
| `delete` | Removing — `DELETE` |

**Deleting is deliberately separate from writing.** "Can add cards to my
moodboard" and "can destroy my moodboard" are different levels of trust to hand
out, and the distinction matters most when the caller is a language model
driving [MCP tools](/docs/mcp).

A token missing the scope it needs gets `403` with
`code: "insufficient_scope"` and a `required_scope` field naming what was
missing, rather than a generic refusal.

`GET /me` reports the scopes a token actually has — check it before assuming.

## A token acts as you

The design point worth understanding:

The token resolves to your user, and then every request runs **as that user**,
under the same row-level security the app runs under.

- Boards you own — full access
- Boards you were invited to as an editor — writable
- Boards you can only view — reads succeed, writes get `403`
- Boards you cannot see — `404`, never a confirmation they exist

There is no per-resource permission list on a token, because there does not need
to be. This is also why a token cannot be used to escalate: it has no authority
of its own.

## Revoking

**Settings → Connections → Revoke**, effective immediately. The next request with that
token gets `401`.

Unknown, revoked and expired tokens all return the same `401 invalid token` —
the API does not distinguish, so a probe cannot learn which tokens once existed.

Up to **20 active tokens** per account. Revoked ones do not count against it.

## Expiry

Tokens do not expire by default. An optional lifetime can be set at creation.

## Keeping a token safe

> **Warning:** A `undefined` token is equivalent to your account for
> everything the API can reach. Treat it like a password.

- **Never put one in front-end code.** CORS is open so it will work, and anyone who opens dev tools will have it.
- **Never commit one.** Use an environment variable.
- **One token per integration**, so you can revoke one without breaking the others.
- **Read-only unless you need writes.**
- **Rotate** by creating the new token, deploying it, then revoking the old one — both work simultaneously, so there is no downtime.

## When a token should not be a person

A personal token acts as **you**, which means an integration built on one stops
working the day you leave the workspace. For anything a team depends on, create
a [service account](/docs/api/service-accounts) instead: a credential owned by
the workspace, scoped to it, and unaffected by who comes and goes.

## Rate limiting

**1000 requests per hour, per token.**
([Service tokens](/docs/api/service-accounts) default to
10000.)

Every response carries the current state, not just refusals:

| Header | Meaning |
|---|---|
| `x-ratelimit-limit` | Your ceiling |
| `x-ratelimit-remaining` | What is left in this window |
| `x-ratelimit-reset` | Unix seconds when the window resets |
| `retry-after` | Seconds to wait — only on `429` |

A client that can only learn its budget by being rejected has to hit the wall to
find it, so the numbers ride along on every call. `GET /me` reports the same
figures in its body.

Separate tokens have separate budgets, which is another reason to give each
integration its own.

## Tokens and MCP

[The MCP server](/docs/mcp) uses the same token — it holds no credentials of its
own and simply forwards yours. Everything on this page applies to it.
