# Tokens and keys

> API keys with scopes and expiry, agent tokens with a class, the audit of agent actions, SSH keys and the device login.

An API key belongs to an organization and is sent as a bearer token. Without `scopes` it is the whole organization; with scopes it can only do less (`read`, `deploy`, `deploy:<app>`, `mail:send`, `mail:send:<domain>`). An `agent` block makes it an agent token with a class (`sandbox`, `twin` or `production`) and an optional list of apps; the platform enforces both. See the guide [Agents and tokens](https://docs.rytiv.com/guides/agents-and-tokens.md).

A key is shown exactly once, in the answer that creates it. Rytiv stores only a hash.

## Endpoints

- [`POST /v1/tokens`](#post-v1tokens): Creates an API key. The value is in this answer only
- [`GET /v1/tokens`](#get-v1tokens): The organization's keys, without their values
- [`DELETE /v1/tokens/:id`](#delete-v1tokensid): Revokes a key. Requests already using it are rejected right away
- [`GET /v1/agent-actions`](#get-v1agent-actions): The audit of what agent tokens did: every judged request with its verdict. `?token=` narrows to one key, `?limit=` bounds the page
- [`GET /v1/ssh`](#get-v1ssh): The SSH gateway's address and host key
- [`GET /v1/ssh-keys`](#get-v1ssh-keys): The public keys that may open an SSH session
- [`POST /v1/ssh-keys`](#post-v1ssh-keys): Adds a public key
- [`DELETE /v1/ssh-keys/:id`](#delete-v1ssh-keysid): Removes a public key
- [`GET /v1/ssh/tokens`](#get-v1sshtokens): The short-lived SSH tokens of the organization
- [`POST /v1/ssh/tokens`](#post-v1sshtokens): Creates a short-lived SSH token
- [`DELETE /v1/ssh/tokens/:id`](#delete-v1sshtokensid): Revokes an SSH token
- [`POST /v1/device/lookup`](#post-v1devicelookup): Looks up a device login by its user code, for the console's confirmation page
- [`POST /v1/device/approve`](#post-v1deviceapprove): Approves a device login; the waiting CLI then receives a key for this organization

## POST /v1/tokens

Creates an API key. The value is in this answer only.

**Auth:** API key with full access (a key with scopes cannot call this route). Refused for every agent token (`agent-token-credential`).

### Request body

JSON, the shape `CreateTokenRequest` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | required | A name for the key, shown in lists. 1 to 120 characters. |
| `scopes` | array of string | optional | What the key may do: `read`, `deploy`, `deploy:<app>`, `mail:send`, `mail:send:<domain>`. Omitted: full access. 0 to 20 items. |
| `expiresAt` | string (ISO 8601) | optional | When the key stops working (a moment). Not together with `ttlSeconds`. |
| `ttlSeconds` | integer | optional | When the key stops working, as seconds from now. Never later than the key that creates it. 60 to 31536000. |
| `agent` | object | optional | Makes it an agent token. |
| `agent.class` | `sandbox` \| `twin` \| `production` | required | `sandbox` and `twin` may do what the key may do; `production` may only read. |
| `agent.apps` | array of string | optional | The apps the token may touch. Omitted: the whole organization. 1 to 50 items. |
| `agent.prodDataRead` | boolean | optional | Class `production` only: may the agent read what production holds (files, a shell, query results), not only what it says about itself. |

### Example request

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

### Response

`201` with a JSON object of these fields:

| Field | Type | Description |
| --- | --- | --- |
| `id` | string |  |
| `name` | string |  |
| `prefix` | string | the first characters, shown in lists |
| `token` | string | the key itself; shown here only |
| `scopes` | array of string | if the key has scopes |
| `expiresAt` | string (ISO 8601) | if the key expires |
| `agent` | object | if it is an agent token: `class`, `apps`, `prodDataRead` |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `bad-request` | The request is malformed or a field is out of range. `error` names every field; `issues` lists them. |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |

## GET /v1/tokens

The organization's keys, without their values.

**Auth:** API key with full access (a key with scopes cannot call this route). Every class of agent token may call it.

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/tokens" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with a list of keys, each:

```json
[
  {
    "id": "<id>",
    "orgId": "<orgId>",
    "name": "my-app",
    "prefix": "UPLOADS",
    "createdAt": "2026-10-21T09:00:00Z"
  }
]
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `id` | string | always | The key's id. |
| `orgId` | string | always | The organization that owns it. |
| `userId` | string | if set | The person who created it, if a person did. |
| `name` | string | always | Its name. |
| `prefix` | string | always | The first characters, to recognise it. |
| `createdAt` | string (ISO 8601) | always | When it was created. |
| `lastUsedAt` | string (ISO 8601) | if set | When it was last used. |
| `agent` | object | if set | Agent tokens: class, apps and `prodDataRead`. |
| `agent.class` | `sandbox` \| `twin` \| `production` | always | `sandbox` and `twin` may do what the key may do; `production` may only read. |
| `agent.apps` | array of string | if set | The apps the token may touch. Omitted: the whole organization. |
| `agent.prodDataRead` | boolean | always | Class `production` only: may the agent read what production holds (files, a shell, query results), not only what it says about itself. |
| `scopes` | array of string | if set | The key's scopes; absent means full access. |
| `expiresAt` | string (ISO 8601) | if set | When it stops working; absent means never. |
| `expired` | boolean | if set | Whether it has, as of this answer. |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |

## DELETE /v1/tokens/:id

Revokes a key. Requests already using it are rejected right away.

**Auth:** API key with full access (a key with scopes cannot call this route). Refused for every agent token (`agent-token-credential`).

### Path parameters

| Name | Description |
| --- | --- |
| `id` | The key's id (`tok-...`). |

### Example request

```bash
curl -X DELETE "https://api.rytiv.app/v1/tokens/<id>" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`204` with no body.

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## GET /v1/agent-actions

The audit of what agent tokens did: every judged request with its verdict. `?token=` narrows to one key, `?limit=` bounds the page.

**Auth:** API key with full access (a key with scopes cannot call this route). Every class of agent token may call it.

### Query parameters

| Name | Description |
| --- | --- |
| `limit` | How many entries, at most. |
| `token` | Only the actions of this key. |

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/agent-actions" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with a JSON body.

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |

## GET /v1/ssh

The SSH gateway's address and host key.

**Auth:** API key with full access or the scope `read`. Every class of agent token may call it.

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/ssh" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with a JSON body.

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |

## GET /v1/ssh-keys

The public keys that may open an SSH session.

**Auth:** API key with full access (a key with scopes cannot call this route). Every class of agent token may call it.

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/ssh-keys" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with a JSON body.

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |

## POST /v1/ssh-keys

Adds a public key.

**Auth:** API key with full access (a key with scopes cannot call this route). Refused for every agent token (`agent-token-credential`).

### Request body

JSON, the shape `SshKeyCreate` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `publicKey` | string | required | 1 to 16384 characters. |
| `name` | string | optional | 1 to 100 characters. |

### Example request

```bash
curl -X POST "https://api.rytiv.app/v1/ssh-keys" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"publicKey":"<publicKey>"}'
```

### Response

`201` with a JSON body.

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid-key` |  |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 409 | `duplicate` |  |
| 409 | `limit` |  |

## DELETE /v1/ssh-keys/:id

Removes a public key.

**Auth:** API key with full access (a key with scopes cannot call this route). Refused for every agent token (`agent-token-credential`).

### Path parameters

| Name | Description |
| --- | --- |
| `id` | The object's id. |

### Example request

```bash
curl -X DELETE "https://api.rytiv.app/v1/ssh-keys/<id>" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`204` with no body.

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## GET /v1/ssh/tokens

The short-lived SSH tokens of the organization.

**Auth:** API key with full access (a key with scopes cannot call this route). Every class of agent token may call it.

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/ssh/tokens" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with a JSON body.

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |

## POST /v1/ssh/tokens

Creates a short-lived SSH token.

**Auth:** API key with full access (a key with scopes cannot call this route). Refused for every agent token (`agent-token-credential`).

### Request body

JSON, the shape `SshTokenCreate` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `app` | string | required | 1 to 63 characters. |
| `machine` | string | optional | 1 to 64 characters. |
| `ttlS` | integer | optional | How long the link opens a connection, in seconds. 60 to 259200. |

### Example request

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

### Response

`201` with a JSON body.

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `bad-request` | The request is malformed or a field is out of range. `error` names every field; `issues` lists them. |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |

## DELETE /v1/ssh/tokens/:id

Revokes an SSH token.

**Auth:** API key with full access (a key with scopes cannot call this route). Refused for every agent token (`agent-token-credential`).

### Path parameters

| Name | Description |
| --- | --- |
| `id` | The object's id. |

### Example request

```bash
curl -X DELETE "https://api.rytiv.app/v1/ssh/tokens/<id>" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`204` with no body.

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## POST /v1/device/lookup

Looks up a device login by its user code, for the console's confirmation page.

**Auth:** API key with full access (a key with scopes cannot call this route). Refused for every agent token (`agent-token-credential`).

### Example request

```bash
curl -X POST "https://api.rytiv.app/v1/device/lookup" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"userCode":"BCDF-GHJK"}'
```

### Response

`200` with a JSON object carrying `client`, `version`, `requestedFrom`, `requestedAt`, `expiresAt`.

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `bad-request` | The request is malformed or a field is out of range. `error` names every field; `issues` lists them. |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |

## POST /v1/device/approve

Approves a device login; the waiting CLI then receives a key for this organization.

**Auth:** API key with full access (a key with scopes cannot call this route). Refused for every agent token (`agent-token-credential`).

### Request body

JSON, the shape `DeviceApproveRequest` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `userCode` | string | required | 1 to 40 characters. |
| `approve` | boolean | optional | default `true`. |

### Example request

```bash
curl -X POST "https://api.rytiv.app/v1/device/approve" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"userCode":"<userCode>"}'
```

### Response

`200` with a JSON object carrying `ok`, `approved`.

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `bad-request` | The request is malformed or a field is out of range. `error` names every field; `issues` lists them. |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
