# Apps

> Create, list, label and delete apps; attach databases, buckets and volumes; link apps to each other.

An app is a name with an address: it answers on `https://<app>.rytiv.app` and owns its machines, deploys, secrets, logs and releases. Names are unique across all of Rytiv: 2 to 63 characters, lowercase letters, digits and hyphens, starting with a letter. A computer is an app too, created with `"computer": true` (see [Computers](https://docs.rytiv.com/reference/api/computers.md)).

Deploying is on its own page: [Deploys and releases](https://docs.rytiv.com/reference/api/deploys.md).

## Endpoints

- [`POST /v1/apps`](#post-v1apps): Creates an app. With `computer: true` it is created as a computer and counts against the organization's computer limit
- [`GET /v1/apps`](#get-v1apps): Lists the organization's apps. Repeat `label=<key>=<value>` to filter by label. A key with `deploy:<app>` scopes sees only its apps
- [`GET /v1/apps/:name`](#get-v1appsname): One app
- [`PATCH /v1/apps/:name`](#patch-v1appsname): Changes an app's labels
- [`DELETE /v1/apps/:name`](#delete-v1appsname): Deletes the app and all its machines. It cannot be undone. An app that backs a database is removed with the database, not on its own
- [`PUT /v1/apps/:name/labels`](#put-v1appsnamelabels): Replaces the app's labels
- [`GET /v1/apps/:name/maintenance`](#get-v1appsnamemaintenance): Whether the app is in maintenance mode
- [`PUT /v1/apps/:name/maintenance`](#put-v1appsnamemaintenance): Switches maintenance mode on or off
- [`POST /v1/apps/:name/attach`](#post-v1appsnameattach): Attaches a database (`resource`), a bucket (`bucket`, optional `as` prefix) or a free volume (`volume`) to the app. The app sees the new environment from its next start
- [`POST /v1/apps/:name/detach`](#post-v1appsnamedetach): Detaches a bucket from the app and removes the environment it injected
- [`GET /v1/attachments`](#get-v1attachments): Every database attachment of the organization
- [`POST /v1/apps/:name/link`](#post-v1appsnamelink): Lets this app reach another app of the organization over the private network
- [`DELETE /v1/apps/:name/link/:target`](#delete-v1appsnamelinktarget): Removes that link
- [`GET /v1/app-links`](#get-v1app-links): Every app link of the organization

## POST /v1/apps

Creates an app. With `computer: true` it is created as a computer and counts against the organization's computer limit.

**Auth:** API key with full access (a key with scopes cannot call this route). An agent token of class `sandbox` or `twin` may call it; class `production` is refused (`agent-token-production-readonly`).

### Request body

JSON, the shape `CreateAppRequest` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | required | The app's name: 2 to 63 characters, lowercase letters, digits and hyphens, starting with a letter. Unique across Rytiv. |
| `labels` | object of string | optional | Free key/value pairs to find the app again (`?label=key=value`). Never a secret, never part of the environment. |
| `computer` | boolean | optional | `true` creates the app as a computer: it counts against the organization's computer limit. |

### Example request

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

### Response

`201` with the app:

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

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `name` | string | always | The app's name. |
| `orgId` | string | always | The organization that owns it. |
| `createdAt` | string (ISO 8601) | always | When it was created. |
| `kind` | `machine` \| `static` | if set | `machine`: served by machines. `static`: files served at the edge, no machine. |
| `protection` | `password` \| `link` \| `signed` | if set | How the app is protected, if it is; absent means public. |
| `slug` | string | if set | The app's fixed identifier; the CNAME target of a custom domain is built from it. |
| `labels` | object of string | if set | The app's labels. |

### 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. |
| 400 | `name-reserved` | The platform keeps this name (for example `api`, `console`, `docs`, `www`, and every name that is `rytiv` or starts with `rytiv-`). |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `org-limit-apps` | The organization has as many apps as it may have (computers included). The body carries `limit` and `current`. |
| 403 | `org-limit-computers` | The organization has as many computers as it may have. The body carries `limit` and `current`. Delete one, or contact us for more. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 409 | `name-taken` | The name is used somewhere on Rytiv (an app, a database, a volume or a bucket). |

## GET /v1/apps

Lists the organization's apps. Repeat `label=<key>=<value>` to filter by label. A key with `deploy:<app>` scopes sees only its apps.

**Auth:** API key with full access or a deploy scope (`deploy` or `deploy:<app>`), or the scope `read`. Every class of agent token may call it.

### Query parameters

| Name | Description |
| --- | --- |
| `label` | Filter by label, `key=value`; repeat for several. Apps match when every term matches. |

### Example request

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

### Response

`200` with a list of apps, each:

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

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `name` | string | always | The app's name. |
| `orgId` | string | always | The organization that owns it. |
| `createdAt` | string (ISO 8601) | always | When it was created. |
| `kind` | `machine` \| `static` | if set | `machine`: served by machines. `static`: files served at the edge, no machine. |
| `protection` | `password` \| `link` \| `signed` | if set | How the app is protected, if it is; absent means public. |
| `slug` | string | if set | The app's fixed identifier; the CNAME target of a custom domain is built from it. |
| `labels` | object of string | if set | The app's labels. |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid-label-filter` | A `label` term is not `key=value` with a valid key. |
| 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/apps/:name

One app.

**Auth:** API key with full access or the scope `deploy` or `deploy:<app>` for this app, or the scope `read`. Every class of agent token may call it.

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The app's name. |

### Example request

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

### Response

`200` with the app:

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

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `name` | string | always | The app's name. |
| `orgId` | string | always | The organization that owns it. |
| `createdAt` | string (ISO 8601) | always | When it was created. |
| `kind` | `machine` \| `static` | if set | `machine`: served by machines. `static`: files served at the edge, no machine. |
| `protection` | `password` \| `link` \| `signed` | if set | How the app is protected, if it is; absent means public. |
| `slug` | string | if set | The app's fixed identifier; the CNAME target of a custom domain is built from it. |
| `labels` | object of string | if set | The app's labels. |

### 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. |

## PATCH /v1/apps/:name

Changes an app's labels.

**Auth:** API key with full access (a key with scopes cannot call this route). An agent token of class `sandbox` or `twin` may call it; class `production` is refused (`agent-token-production-readonly`).

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The app's name. |

### Request body

JSON, the shape `PatchAppRequest` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `labels` | object of string or null | required |  |

### Example request

```bash
curl -X PATCH "https://api.rytiv.app/v1/apps/<name>" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"labels":{"<key>":"<value>"}}'
```

### Response

A JSON body on success.

### 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. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## DELETE /v1/apps/:name

Deletes the app and all its machines. It cannot be undone. An app that backs a database is removed with the database, not on its own.

**Auth:** API key with full access (a key with scopes cannot call this route). An agent token of class `sandbox` or `twin` may call it; class `production` is refused (`agent-token-production-readonly`).

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The app's name. |

### Example request

```bash
curl -X DELETE "https://api.rytiv.app/v1/apps/<name>" \
  -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. |
| 409 | `sandbox-carrier` |  |

## PUT /v1/apps/:name/labels

Replaces the app's labels.

**Auth:** API key with full access (a key with scopes cannot call this route). An agent token of class `sandbox` or `twin` may call it; class `production` is refused (`agent-token-production-readonly`).

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The app's name. |

### Request body

JSON, the shape `PutAppLabelsRequest` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `labels` | object of string | required |  |

### Example request

```bash
curl -X PUT "https://api.rytiv.app/v1/apps/<name>/labels" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"labels":{"<key>":"<value>"}}'
```

### Response

A JSON body on success.

### 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. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## GET /v1/apps/:name/maintenance

Whether the app is in maintenance mode.

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

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The app's name. |

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/apps/<name>/maintenance" \
  -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. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## PUT /v1/apps/:name/maintenance

Switches maintenance mode on or off.

**Auth:** API key with full access (a key with scopes cannot call this route). An agent token of class `sandbox` or `twin` may call it; class `production` is refused (`agent-token-production-readonly`).

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The app's name. |

### Request body

JSON, the shape `SetMaintenanceRequest` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `enabled` | boolean | required | `false` creates the schedule paused. |

### Example request

```bash
curl -X PUT "https://api.rytiv.app/v1/apps/<name>/maintenance" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled":true}'
```

### Response

`200` 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. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## POST /v1/apps/:name/attach

Attaches a database (`resource`), a bucket (`bucket`, optional `as` prefix) or a free volume (`volume`) to the app. The app sees the new environment from its next start.

**Auth:** API key with full access (a key with scopes cannot call this route). An agent token of class `sandbox` or `twin` may call it; class `production` is refused (`agent-token-production-readonly`).

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The app's name. |

### Request body

JSON, the shape `AttachRequest` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `resource` | string | required | The name of the database to attach. The app gets its connection URL as the secret `DATABASE_URL`. |

### Example request

```bash
curl -X POST "https://api.rytiv.app/v1/apps/<name>/attach" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"resource":"my-db"}'
```

### Response

`200` with a JSON object carrying `app`, `resource`, `injected`, `url`.

### 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. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## POST /v1/apps/:name/detach

Detaches a bucket from the app and removes the environment it injected.

**Auth:** API key with full access (a key with scopes cannot call this route). An agent token of class `sandbox` or `twin` may call it; class `production` is refused (`agent-token-production-readonly`).

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The app's name. |

### Example request

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

### Response

A JSON body on success.

### 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/attachments

Every database attachment of the organization.

**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/attachments" \
  -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/apps/:name/link

Lets this app reach another app of the organization over the private network.

**Auth:** API key with full access (a key with scopes cannot call this route). An agent token of class `sandbox` or `twin` may call it; class `production` is refused (`agent-token-production-readonly`).

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The app's name. |

### Request body

JSON, the shape `AppLinkRequest` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `target` | string | required |  |

### Example request

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

### Response

`201` with a JSON object carrying `app`, `target`, `name`, `activeInS`.

### 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. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## DELETE /v1/apps/:name/link/:target

Removes that link.

**Auth:** API key with full access (a key with scopes cannot call this route). An agent token of class `sandbox` or `twin` may call it; class `production` is refused (`agent-token-production-readonly`).

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The app's name. |
| `target` | The name of the linked app. |

### Example request

```bash
curl -X DELETE "https://api.rytiv.app/v1/apps/<name>/link/<target>" \
  -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/app-links

Every app link of the organization.

**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/app-links" \
  -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. |
