# Authentication

> Every request to Rytiv carries an API key as a bearer token. Create keys in the console, with the CLI or over the API, narrow them with scopes or an agent class, and revoke them at any time.

Rytiv has one kind of credential for programs: an **API key**. It belongs to your organization, it is sent as a bearer token, and the same key works for the REST API, the MCP server and the CLI.

```http
Authorization: Bearer ryt_...
```

Keys start with `ryt_`. Rytiv stores only a hash of a key, so a key is shown exactly once, when it is created. After that, only its first characters (the `prefix`) are visible.

## Create a key

**CLI**

```bash
ry tokens create ci-deploy
```

**curl**

```bash
curl -X POST https://api.rytiv.app/v1/tokens \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "ci-deploy"}'
```

**TypeScript**

```ts
const res = await fetch("https://api.rytiv.app/v1/tokens", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RYTIV_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ name: "ci-deploy" }),
});
const { id, prefix, token } = await res.json(); // token is shown only in this answer
```

`POST /v1/tokens` takes a `name` (1 to 120 characters) and answers `201` with `id`, `name`, `prefix` and `token`. Store `token` right away: neither the API nor the CLI can show it again.

You can also create a key in the Rytiv console. Every way creates the same kind of key. Creating a key needs a full key: a key narrowed by [scopes](#narrow-a-key-scopes) or an [agent class](#agent-keys) cannot create, reveal or revoke keys.

### Request fields

| Field | Type | Notes |
| --- | --- | --- |
| `name` | string | 1 to 120 characters |
| `scopes` | string[] | Up to 20 [scopes](#narrow-a-key-scopes). Without it, the key is the whole organization. |
| `ttlSeconds` | integer | The key stops working after this long: 60 seconds to 365 days |
| `expiresAt` | string | Or: the moment it stops working (ISO 8601). Not together with `ttlSeconds`. |
| `agent` | object | Makes it an [agent key](#agent-keys): `{class, apps, prodDataRead}` |

## Narrow a key: scopes

A key without scopes may do everything your organization may do. A scope list makes it do less, never more:

| Scope | What it allows |
| --- | --- |
| `read` | Every read of the product API, except credentials and billing |
| `deploy` | The deploy routes of every app: build, deploy, rollback, releases, logs, secrets. Never create or delete an app, never a key. |
| `deploy:<app>` | The same for exactly that app |
| `mail:send` | Sending mail from any of your sending domains |
| `mail:send:<domain>` | Sending mail from exactly that domain |

```bash
ry tokens create ci --deploy web --expires-in 30d
```

A scope the platform does not know is refused with `400` when the key is created, never stored as a key that silently does less than its name says.

## Agent keys

A coding agent works with a key a person handed over without watching every request. So the platform, not the agent's prompt, holds the key to what it is meant for. `agent` on `POST /v1/tokens` takes:

| Field | Type | Notes |
| --- | --- | --- |
| `class` | string | `sandbox`, `twin` or `production` |
| `apps` | string[] | Optional, 1 to 50 app names. Without it, the whole organization. |
| `prodDataRead` | boolean | `production` only: may the agent read what production holds (the database, files, a shell), not only what it says about itself |

```bash
ry tokens create claude-code --agent sandbox --app web --app worker
ry tokens create claude-code-prod --agent production --read-prod-data
```

What every request with an agent key is judged by, in this order:

1. **Credentials are never reachable.** Creating, revealing or revoking keys, SSH keys, webhooks and Git installations, approving a device login, administering the organization: refused for every class with `403` and the code `agent-token-credential`.
2. **The app list, when there is one.** A request that names an app outside the list, or a change that names no app at all, is refused with `403` and `agent-token-app-scope`.
3. **The class.** `sandbox` and `twin` may do what a key of your organization may do. `production` may read what the platform says about a resource (status, releases, logs); a change answers `403` with `agent-token-production-readonly`, and reading what production holds answers `403` with `agent-token-production-data` unless the key has `prodDataRead`.
4. **Its own organization only.** The header `X-Rytiv-Org` is refused with `agent-token-delegation`.

`scopes` and `agent` combine: the platform holds both, and the narrower one wins.

Every action of an agent key is written to an audit before it runs; if the audit cannot be written, the action does not run (`503`, `agent-audit-unavailable`). `GET /v1/agent-actions` lists the audit of your organization, newest first, with `?token=<id>` for one key and `?limit=` up to 500.

A key's class is what the person who created it says it is for. The platform does not know yet which app is production; the app list is what keeps a sandbox key away from production apps today. With [projects](https://docs.rytiv.com/concepts/projects.md) the class picks the environment of the project, and the list goes.

## List and revoke keys

| Method | Path | What it does |
| --- | --- | --- |
| `GET` | `/v1/tokens` | Your keys, without their values: `id`, `name`, `prefix`, `createdAt`, `lastUsedAt`, and `scopes`, `agent`, `expiresAt` where set |
| `DELETE` | `/v1/tokens/:id` | Revokes a key. Requests already using it are rejected right away. Answers `204`. |

```bash
ry tokens list
ry tokens revoke tok-1a2b3c4d
```

## Connect without a key

A hosted connector has no place to paste a key, so `/v1/mcp` also accepts OAuth 2.1. A client that supports it needs nothing but the address `https://api.rytiv.app/v1/mcp`:

1. A request without a token answers `401` with `WWW-Authenticate: Bearer resource_metadata=...`. The client reads `/.well-known/oauth-protected-resource/v1/mcp` and `/.well-known/oauth-authorization-server` from there.
2. It registers itself at `POST /v1/oauth/register` (RFC 7591) and gets a client id. There are no client secrets: every client is a public client, and the exchange is bound to PKCE with `S256`.
3. `GET /v1/oauth/authorize` sends you to the Rytiv console, where you see which client wants to connect to which organization and approve or deny it once.
4. The client exchanges the code at `POST /v1/oauth/token` for an access token, valid for one hour, and a refresh token, valid for 30 days and rotated on every use.

The scopes are `mcp` and `offline_access`, nothing finer. The access token is an API key of the organization that approved, named after the client; it appears in `GET /v1/tokens` and is revoked like any other key, which ends the connection. Redirect addresses must be `https`, a loopback `http` address, or a native app's own scheme.

## Sign in with the CLI

On your own machine, `ry login` is the easier way. It shows a code, you confirm it in the console, and the CLI stores a key for itself.

```bash
ry login
```

Where nobody can open a browser (CI, a container, a coding agent), give the CLI a key instead:

```bash
ry login --token <token>
```

Or set it in the environment, which the CLI never writes to disk:

| Variable | Meaning |
| --- | --- |
| `RYTIV_TOKEN` | The API key for this shell. Takes precedence over a saved login. |
| `RYTIV_ENDPOINT` | The API endpoint to talk to. Without it, the CLI uses `https://api.rytiv.app`. |
| `RYTIV_PROFILE` | Which saved login to use, if you keep several (`ry profile`). |

The CLI keeps saved logins in `~/.rytiv/config.json`, readable only by you.

## Errors

A request without a key answers `401` with `missing api token (Authorization: Bearer <token>)`, a request with an unknown, expired or revoked key answers `401` with `invalid api token`. A key that may not do what the request asks answers `403` with one of the codes above. All of them follow the [error format](https://docs.rytiv.com/reference/api-basics.md#errors).

## What a key can do

A key acts as your organization: it reaches every app, database, computer and sandbox the organization has, and nothing of any other organization. A name that belongs to another organization is answered exactly like a name that does not exist. Scopes and an agent class narrow that, and the platform enforces both. The guide [Agents and tokens](https://docs.rytiv.com/guides/agents-and-tokens.md) walks through a key per agent, OAuth for hosted clients and the audit of what an agent did.

> **Not yet available:** **Passkey confirmations** for irreversible steps, so that a `production` key can change what can be undone and a person confirms the rest. See [Confirmations](https://docs.rytiv.com/concepts/confirmations.md).
