# Authentication

> Bearer API keys, how they reach every profile in your organization, and how to keep them safe.

Canonical: <https://www.tryadeli.com/docs/authentication>

Every request to `/api/v1` carries a bearer API key. There is no `X-API-Key`
header, no OAuth client-credentials flow, and no unauthenticated endpoint.

```http
Authorization: Bearer rk_live_...
Accept: application/json
```

## Creating a key

Keys are issued from the dashboard, not through the API. Owners and admins of
an organization can create and delete them; every member can see the list,
which shows who created each key.

1. Open **Settings → API keys**, or go to [`/settings/api-keys`](https://app.tryadeli.com/settings/api-keys).
2. Add an optional label and select **Create key**.
3. Copy the secret.

> **Warning: The secret is shown exactly once**
>
> Adeli stores a SHA-256 hash of your key and the first few characters for
> display. Nothing in the product can recover the plaintext, so if you lose it,
> delete the key and create another.

Keys look like `rk_live_` followed by 43 URL-safe characters. The dashboard
lists them by their prefix, such as `rk_live_Ab3dEf9…`, so you can tell which
key is which without ever revealing one.

## Keys belong to your organization

A key belongs to your organization, not to a profile, and can act on every
profile in it. Because of that, every request names the profile it acts on:

- With a `profileId` in the path, as in `/api/v1/profiles/{profileId}/accounts`,
  or in the query string or body.
- Or with an `accountId`. Each connected account belongs to exactly one profile,
  so the `accountId` names it, and you can leave `profileId` out.

Nothing defaults to a profile:

- A request with neither returns `400 profile_required`.
- A `profileId` in another organization returns `404 profile_not_found`, and an
  `accountId` in another organization returns `404 account_not_found`, not
  `403`. The API does not confirm whether something you cannot reach exists.
- A `profileId` and `accountId` that disagree return `404 account_not_found`.

Three endpoints act on the whole organization and take no profile:
`GET /api/v1/profiles`, `POST /api/v1/profiles` and `GET /api/v1/usage/x`.

There are no scopes or per-endpoint permissions. A key can do everything the API
offers, for every profile in your organization.

> **Note: Keep customers apart with profiles**
>
> If you serve multiple end customers, give each one its own profile and always
> pass its `profileId` or one of its `accountId`s. Treat a key as access to all
> of them: a leaked key exposes your whole organization, so rotate it.

## Breaking change (2026-10-08)

Until 2026-10-08, a key was bound to one profile, and a request without a
`profileId` used that profile. Every existing key was then promoted to its
organization and can now reach every profile in it. If you shared a key with
anyone outside your organization, such as one customer, rotate it.

Requests must now name a profile with `profileId` or an `accountId`, or they
return `400 profile_required`. `POST /api/v1/profiles` now creates a profile
instead of returning `403 profile_scope_violation`.

## Revoking a key

Delete the key from **Settings → API keys**. Revocation is immediate — there is no cache
and no grace period, and the next request with that key returns `401`. Deleting a
profile does not delete any key.

## Keep keys on your server

An Adeli key can publish posts and send messages on your customer's behalf.
Treat it like a database password.

- Never ship a key in browser JavaScript, a mobile app bundle, or a public repo.
- Never put one in a URL — they end up in logs, proxies, and analytics.
- Call Adeli from your backend and expose only the results to your frontend.

The `@adeli/sandbox` workspace in the Adeli repository is a working example of
this shape: the browser posts to a small server route, and that route is the only
place the key exists.

## Failed authentication

A missing, malformed, or revoked key returns `401` with the standard error
envelope:

```json
{
  "error": {
    "code": "unauthorized",
    "message": "A valid Bearer API key is required"
  }
}
```

Check a key quickly with a request that has no side effects:

**cURL**

```bash
curl -i "$ADELI_URL/api/v1/profiles" \
  -H "Authorization: Bearer $ADELI_API_KEY"
```

**JavaScript**

```javascript
const response = await fetch(`${process.env.ADELI_URL}/api/v1/profiles`, {
  headers: { Authorization: `Bearer ${process.env.ADELI_API_KEY}` },
});

console.log(response.status); // 200 when the key is valid, 401 when it is not
```

**Python**

```python
import os, requests

response = requests.get(
    f"{os.environ['ADELI_URL']}/api/v1/profiles",
    headers={"Authorization": f"Bearer {os.environ['ADELI_API_KEY']}"},
)

print(response.status_code)  # 200 when the key is valid, 401 when it is not
```

See [Errors](https://www.tryadeli.com/docs/errors) for everything else the API can return.
