# Deploys and releases

> Detect a project, build it, deploy an image, follow a deploy, list releases and roll back; previews and Git-connected deploys.

A deploy puts an image behind an app: an OCI reference such as `nginx:alpine`, or `build:<id>` for a build Rytiv made from your source. Every deploy that goes live is a release, and a rollback deploys an earlier release's image again without a rebuild.

A deploy rolls one machine at a time: the new machine must boot and answer on its port (and pass the health check, if you set one) before an old one is taken out of traffic. If the new machine fails, the deploy stops and the old version keeps serving. After the switch, the deploy keeps watching the app's error rate for a while and rolls back on its own if it gets worse than before; the deploy's `watch` field says what it found.

The CLI does the whole path for you: `ry deploy <app> --source .` detects the project, uploads it, builds it on Rytiv's machines and deploys the result. See the guide [Deploy a Next.js app](https://docs.rytiv.com/guides/deploy-nextjs.md).

## Endpoints

- [`POST /v1/detect`](#post-v1detect): Reads a project's manifest and answers what Rytiv would build and start, before any build
- [`POST /v1/apps/:name/builds`](#post-v1appsnamebuilds): Uploads a source archive and starts a build on Rytiv's build machines
- [`GET /v1/apps/:name/builds/:id`](#get-v1appsnamebuildsid): One build with its state and log
- [`POST /v1/apps/:name/deploy`](#post-v1appsnamedeploy): Deploys an image to the app and answers the deploy right away. Poll `GET /v1/deploys/:id` until `status` is `done` or `error`
- [`GET /v1/apps/:name/deploys`](#get-v1appsnamedeploys): The app's deploys, newest first
- [`GET /v1/deploys/:id`](#get-v1deploysid): One deploy: its status, the machines it created and retired, and after the switch the watch verdict
- [`GET /v1/apps/:name/releases`](#get-v1appsnamereleases): The app's releases, which one runs, and which one a rollback would bring back
- [`POST /v1/apps/:name/rollback`](#post-v1appsnamerollback): Deploys an earlier release's image again, without a rebuild, with the app's current settings. Empty body: the previous release that went live
- [`GET /v1/apps/:name/previews`](#get-v1appsnamepreviews): The preview apps of a Git-connected app, one per branch
- [`POST /v1/apps/:name/previews`](#post-v1appsnamepreviews): Creates a preview app for a branch
- [`DELETE /v1/apps/:name/previews/:branch`](#delete-v1appsnamepreviewsbranch): Removes the preview app of a branch
- [`PUT /v1/apps/:name/git`](#put-v1appsnamegit): Connects the app to a repository of an installed GitHub app: a push then deploys
- [`GET /v1/apps/:name/git`](#get-v1appsnamegit): The app's repository connection
- [`DELETE /v1/apps/:name/git`](#delete-v1appsnamegit): Disconnects the repository
- [`GET /v1/apps/:name/git/deploys`](#get-v1appsnamegitdeploys): The deploys a push triggered
- [`POST /v1/git/connect`](#post-v1gitconnect): Starts the installation of the GitHub app and answers the link to open
- [`GET /v1/git/connect/:id`](#get-v1gitconnectid): Whether that installation has completed
- [`POST /v1/git/installations`](#post-v1gitinstallations): Records an installation of the GitHub app
- [`GET /v1/git/installations`](#get-v1gitinstallations): The organization's installations
- [`DELETE /v1/git/installations/:id`](#delete-v1gitinstallationsid): Forgets an installation

## POST /v1/detect

Reads a project's manifest and answers what Rytiv would build and start, before any build.

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

### Request body

JSON, the shape `DetectRequest` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `app` | string | optional |  |
| `inventory` | object | required |  |
| `inventory.packageJson` | object of any or null | optional |  |
| `inventory.files` | array of string | optional | 0 to 4000 items. |
| `inventory.envExample` | string or null | optional |  |
| `inventory.envRefs` | array of string | optional | 0 to 500 items. |
| `inventory.configs` | object of string | optional |  |

### Example request

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

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

## POST /v1/apps/:name/builds

Uploads a source archive and starts a build on Rytiv's build machines.

**Auth:** API key with full access or the scope `deploy` or `deploy:<app>` for this app. 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. |

### Query parameters

| Name | Description |
| --- | --- |
| `buildMemoryMb` |  |
| `commitSha` |  |

### Example request

```bash
curl -X POST "https://api.rytiv.app/v1/apps/<name>/builds" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @source.tar.gz
```

### Response

`202` with a JSON object carrying `id`, `buildId`, `status`.

### 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/apps/:name/builds/:id

One build with its state and log.

**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. |
| `id` | The object's id. |

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/apps/<name>/builds/<id>" \
  -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. |

## POST /v1/apps/:name/deploy

Deploys an image to the app and answers the deploy right away. Poll `GET /v1/deploys/:id` until `status` is `done` or `error`.

**Auth:** API key with full access or the scope `deploy` or `deploy:<app>` for this app. 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 `DeployRequest` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `image` | string | required | What to deploy: an OCI reference, or `build:<id>` for a build of yours. 1 to 512 characters. |
| `class` | `shared-cpu-1x` \| `shared-cpu-2x` \| `shared-cpu-4x` \| `shared-cpu-8x` \| `performance-1x` \| `performance-2x` \| `performance-4x` \| `performance-8x` | optional | A named size instead of `vcpus`; the two must not contradict each other. |
| `vcpus` | integer | optional | Virtual CPUs. 1 to 16, default `1`. |
| `memoryMb` | integer | optional | Memory in MB. 128 to 16384, default `512`. |
| `env` | object of string | optional | Plain environment variables. Use secrets for credentials. |
| `diskMb` | integer | optional | The machine's disk in MB. Without it, the image's size plus headroom. 512 to 131072. |
| `volumeGb` | integer | optional | A volume of this size, attached to the machine. 1 to 500. |
| `start` | string | optional | The start command, run through `/bin/sh -c`. It replaces what detection chose or the image's `CMD`. 1 to 2000 characters. |
| `internalPort` | integer | optional | The port the app listens on. 1 to 65535, default `3000`. |
| `suspendPolicy` | object | optional | When the machine sleeps. Without it, a new app never sleeps; a deploy without it keeps the app's rule. |
| `suspendPolicy.mode` | `suspend` \| `stop` \| `off` | optional | `suspend` keeps the memory and resumes fast (machines up to 2048 MB; larger ones stop instead), `stop` shuts down, `off` never sleeps. default `"suspend"`. |
| `suspendPolicy.idleSeconds` | integer | optional | Seconds without requests and without CPU load before the machine sleeps. 10 to 86400, default `300`. |
| `suspendPolicy.stopAfterSeconds` | integer | optional | Second stage: after this many seconds since the last activity a suspended machine is stopped (its memory snapshot dropped, its disk kept). Only with `mode: "suspend"`, and greater than `idleSeconds`. 10 to 604800. |
| `healthCheck` | object | optional | An HTTP check a new machine must pass before it gets traffic. |
| `healthCheck.path` | string | optional | The path to call; empty means the port alone. default `""`. |
| `healthCheck.timeoutMs` | integer | optional | How long one probe may take. 100 to 30000, default `2000`. |
| `healthCheck.intervalMs` | integer | optional | Time between probes. 100 to 10000, default `500`. |
| `healthCheck.healthyThreshold` | integer | optional | Probes that must pass in a row. 1 to 10, default `1`. |
| `healthCheck.maxWaitMs` | integer | optional | How long a new machine may take in total before the deploy gives up on it. 1000 to 300000. |
| `restart` | object | optional | What happens when the process ends. Without it, a crashed machine is restarted. |
| `restart.policy` | `never` \| `on-failure` | optional | `never` or `on-failure`. default `"never"`. |
| `restart.retries` | integer | optional | Restarts before the machine stays down. 1 to 10, default `3`. |
| `killSignal` | `SIGTERM` \| `SIGINT` \| `SIGQUIT` \| `SIGHUP` \| `SIGUSR1` \| `SIGUSR2` \| `SIGWINCH` \| `SIGKILL` | optional | The signal sent before a machine is stopped or replaced. Default: the image's `STOPSIGNAL`, else `SIGTERM`. |
| `killTimeoutS` | integer | optional | Seconds between that signal and `SIGKILL`. 1 to 300. |
| `durableCheckpoint` | boolean | optional | Agent machine: the whole state (memory and disk) is checkpointed and survives a host failure. default `false`. |
| `checkpointIntervalS` | integer | optional | 5 to 3600. |
| `console` | boolean | optional | default `false`. |
| `command` | array of string | optional | Runs on a machine of its own, as an argument list. 1 to 64 items. |
| `diskIsCache` | boolean | optional |  |
| `readiness` | `port` \| `boot` | optional | `boot`: ready when the machine boots. `port`: ready when `internalPort` answers. |
| `exposePorts` | array of integer | optional | 0 to 8 items. |
| `egress` | object | optional | Outbound network: `allow-all`, `deny-all`, or `allowlist` with `domains` and `cidrs`. |
| `egress.mode` | `deny-all` \| `allowlist` | required | `view` to watch, `control` to take keyboard and mouse. |
| `egress.domains` | array of string | required | 0 to 100 items. |
| `egress.cidrs` | array of string | required | 0 to 10 items. |
| `count` | integer | optional | How many machines the app runs. A deploy without it keeps the current count. 1 to 20. |

### Example request

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

### Response

`202` with the deploy (`deployId` repeats `id` for older readers):

```json
{
  "id": "<id>",
  "orgId": "<orgId>",
  "app": "my-app",
  "image": "nginx:alpine",
  "status": "running",
  "created": [
    "<created>"
  ],
  "retired": [
    "<retired>"
  ],
  "at": "2026-10-21T09:00:00Z",
  "deployId": "<deployId>"
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `id` | string | always |  |
| `orgId` | string | always | The organization that owns it. |
| `app` | string | always |  |
| `image` | string | always | An OCI reference such as `nginx:alpine`, or `build:<id>` for a build of your organization. |
| `status` | `running` \| `done` \| `error` | always |  |
| `created` | array of string | always |  |
| `retired` | array of string | always |  |
| `at` | string (ISO 8601) | always |  |
| `error` | string | if set |  |
| `bootLog` | string | if set |  |
| `watch` | object | if set |  |
| `watch.forDeployId` | string | always |  |
| `watch.from` | string (ISO 8601) | always |  |
| `watch.until` | string (ISO 8601) | always |  |
| `watch.baseline` | object | always |  |
| `watch.rounds` | integer | always | 0 to 9007199254740991. |
| `watch.lastAt` | string (ISO 8601) | if set |  |
| `watch.lastText` | string | if set |  |
| `watch.outcome` | `healthy` \| `unproven` \| `rolled-back` \| `not-rolled-back` \| `superseded` \| `expired` | if set |  |
| `watch.outcomeText` | string | if set |  |
| `watch.outcomeAt` | string (ISO 8601) | if set |  |
| `watch.rollback` | object | if set |  |
| `watch.database` | object | if set |  |
| `kind` | `"static"` | if set | `static` for a site deploy. |
| `deployId` | string | always | The same value as `id`, for readers from before. |

### 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 | `build-not-found` | `build:<id>` is not a build of your organization. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |
| 409 | `deploy-in-flight` | Another deploy of this app is running. |
| 409 | `volume-needs-single-machine` | An app with a volume runs exactly one machine. |

## GET /v1/apps/:name/deploys

The app's deploys, newest first.

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

### Query parameters

| Name | Description |
| --- | --- |
| `limit` | How many entries, at most. |

### Example request

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

### Response

`200` with a list of deploys, each:

```json
[
  {
    "id": "<id>",
    "orgId": "<orgId>",
    "app": "my-app",
    "image": "nginx:alpine",
    "status": "running",
    "created": [
      "<created>"
    ],
    "retired": [
      "<retired>"
    ],
    "at": "2026-10-21T09:00:00Z"
  }
]
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `id` | string | always | The deploy's id. |
| `orgId` | string | always | The organization that owns it. |
| `app` | string | always | The app. |
| `image` | string | always | What was deployed. |
| `status` | `running` \| `done` \| `error` | always | `running` while it rolls, then `done` or `error`. |
| `created` | array of string | always | The machines this deploy created. |
| `retired` | array of string | always | The machines it took out of traffic. |
| `at` | string (ISO 8601) | always | When it started. |
| `error` | string | if set | On `error`: why. |
| `bootLog` | string | if set | On `error`: the last console lines of the machine that failed. |
| `watch` | object | if set | After the switch: what the watch over the app's error rate found (`healthy`, `unproven`, `rolled-back` and more). Absent while the deploy rolls, and for a rollback. |
| `watch.forDeployId` | string | always |  |
| `watch.from` | string (ISO 8601) | always |  |
| `watch.until` | string (ISO 8601) | always |  |
| `watch.baseline` | object | always |  |
| `watch.rounds` | integer | always | 0 to 9007199254740991. |
| `watch.lastAt` | string (ISO 8601) | if set |  |
| `watch.lastText` | string | if set |  |
| `watch.outcome` | `healthy` \| `unproven` \| `rolled-back` \| `not-rolled-back` \| `superseded` \| `expired` | if set |  |
| `watch.outcomeText` | string | if set |  |
| `watch.outcomeAt` | string (ISO 8601) | if set |  |
| `watch.rollback` | object | if set |  |
| `watch.database` | object | if set |  |

### 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/deploys/:id

One deploy: its status, the machines it created and retired, and after the switch the watch verdict.

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

### Path parameters

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

### Example request

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

### Response

`200` with the deploy:

```json
{
  "id": "<id>",
  "orgId": "<orgId>",
  "app": "my-app",
  "image": "nginx:alpine",
  "status": "running",
  "created": [
    "<created>"
  ],
  "retired": [
    "<retired>"
  ],
  "at": "2026-10-21T09:00:00Z"
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `id` | string | always | The deploy's id. |
| `orgId` | string | always | The organization that owns it. |
| `app` | string | always | The app. |
| `image` | string | always | What was deployed. |
| `status` | `running` \| `done` \| `error` | always | `running` while it rolls, then `done` or `error`. |
| `created` | array of string | always | The machines this deploy created. |
| `retired` | array of string | always | The machines it took out of traffic. |
| `at` | string (ISO 8601) | always | When it started. |
| `error` | string | if set | On `error`: why. |
| `bootLog` | string | if set | On `error`: the last console lines of the machine that failed. |
| `watch` | object | if set | After the switch: what the watch over the app's error rate found (`healthy`, `unproven`, `rolled-back` and more). Absent while the deploy rolls, and for a rollback. |
| `watch.forDeployId` | string | always |  |
| `watch.from` | string (ISO 8601) | always |  |
| `watch.until` | string (ISO 8601) | always |  |
| `watch.baseline` | object | always |  |
| `watch.rounds` | integer | always | 0 to 9007199254740991. |
| `watch.lastAt` | string (ISO 8601) | if set |  |
| `watch.lastText` | string | if set |  |
| `watch.outcome` | `healthy` \| `unproven` \| `rolled-back` \| `not-rolled-back` \| `superseded` \| `expired` | if set |  |
| `watch.outcomeText` | string | if set |  |
| `watch.outcomeAt` | string (ISO 8601) | if set |  |
| `watch.rollback` | object | if set |  |
| `watch.database` | object | if set |  |

### 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/apps/:name/releases

The app's releases, which one runs, and which one a rollback would bring back.

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

### Query parameters

| Name | Description |
| --- | --- |
| `limit` | How many entries, at most. |

### Example request

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

### Response

`200` with the release list:

```json
{
  "app": "<app>",
  "releases": [
    {
      "id": "<id>",
      "version": 1,
      "status": "running",
      "at": "2026-10-21T09:00:00Z",
      "startedAt": "2026-10-21T09:00:00Z",
      "image": "nginx:alpine",
      "source": {
        "kind": "git",
        "repo": "<repo>",
        "branch": "<branch>",
        "commit": "<commit>"
      },
      "current": true,
      "rolledBack": true,
      "artifact": "retained",
      "rollback": {
        "possible": true
      }
    }
  ],
  "retention": {
    "keepBuilds": 1
  }
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `app` | string | always |  |
| `currentId` | string | if set |  |
| `releases` | array of object | always | The releases, newest first. |
| `rollbackTarget` | object | if set |  |
| `rollbackTarget.id` | string | always |  |
| `rollbackTarget.version` | integer | always | 1 to 9007199254740991. |
| `retention` | object | always |  |
| `retention.keepBuilds` | integer | always | 1 to 9007199254740991. |

### 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/apps/:name/rollback

Deploys an earlier release's image again, without a rebuild, with the app's current settings. Empty body: the previous release that went live.

**Auth:** API key with full access or the scope `deploy` or `deploy:<app>` for this app. 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 `RollbackRequest` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `version` | integer | optional | 1 to 9007199254740991. |
| `releaseId` | string | optional | 1 to 64 characters. |

### Example request

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

### Response

`202` with the deploy that brings the release back:

```json
{
  "id": "<id>",
  "orgId": "<orgId>",
  "app": "my-app",
  "image": "nginx:alpine",
  "status": "running",
  "created": [
    "<created>"
  ],
  "retired": [
    "<retired>"
  ],
  "at": "2026-10-21T09:00:00Z",
  "deployId": "<deployId>"
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `id` | string | always |  |
| `orgId` | string | always | The organization that owns it. |
| `app` | string | always |  |
| `image` | string | always | An OCI reference such as `nginx:alpine`, or `build:<id>` for a build of your organization. |
| `status` | `running` \| `done` \| `error` | always |  |
| `created` | array of string | always |  |
| `retired` | array of string | always |  |
| `at` | string (ISO 8601) | always |  |
| `error` | string | if set |  |
| `bootLog` | string | if set |  |
| `watch` | object | if set |  |
| `watch.forDeployId` | string | always |  |
| `watch.from` | string (ISO 8601) | always |  |
| `watch.until` | string (ISO 8601) | always |  |
| `watch.baseline` | object | always |  |
| `watch.rounds` | integer | always | 0 to 9007199254740991. |
| `watch.lastAt` | string (ISO 8601) | if set |  |
| `watch.lastText` | string | if set |  |
| `watch.outcome` | `healthy` \| `unproven` \| `rolled-back` \| `not-rolled-back` \| `superseded` \| `expired` | if set |  |
| `watch.outcomeText` | string | if set |  |
| `watch.outcomeAt` | string (ISO 8601) | if set |  |
| `watch.rollback` | object | if set |  |
| `watch.database` | object | if set |  |
| `kind` | `"static"` | if set | `static` for a site deploy. |
| `deployId` | string | always | The same value as `id`, for readers from before. |

### 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. |
| 404 | `release-not-found` | No release with that version or id. |
| 409 | `deploy-in-flight` | Another deploy of this app is running. |
| 409 | `no-previous-release` | There is no earlier release that went live. |
| 409 | `release-artifact-gone` | The release's image is no longer available. |
| 409 | `rollback-needs-machine-config` | The app has no machine whose settings a rollback could reuse. |
| 409 | `rollback-not-for-resources` | The app backs a database; databases are restored, not rolled back. |
| 409 | `rollback-same-build` | That release runs the same build as the current one. |
| 409 | `rollback-target-current` | That release is the one that runs. |
| 409 | `rollback-target-never-live` | That release never went live, so there is nothing to bring back. |

## GET /v1/apps/:name/previews

The preview apps of a Git-connected app, one per branch.

**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>/previews" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with a JSON object carrying `previews`, `ttlDays`, `maxPerApp`.

### 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/apps/:name/previews

Creates a preview app for a branch.

**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>/previews" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"branch":"feature/login"}'
```

### Response

A JSON body on success.

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `branch` |  |
| 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 | `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 | `already-a-preview` |  |
| 409 | `base-branch` |  |
| 409 | `not-connected` |  |
| 409 | `preview-name-taken` |  |

## DELETE /v1/apps/:name/previews/:branch

Removes the preview app of a branch.

**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. |
| `branch` | The branch name of the preview. |

### Example request

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

## PUT /v1/apps/:name/git

Connects the app to a repository of an installed GitHub app: a push then deploys.

**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 `ConnectRepoRequest` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `installationId` | string | required | 1 to 64 characters. |
| `repo` | string | required | 1 to 300 characters. |
| `branch` | string | optional | 1 to 255 characters. |
| `repoExternalId` | string | optional | 1 to 64 characters. |
| `autoDeploy` | boolean | optional |  |
| `subdirectory` | string | optional | at most 200 characters. |
| `previewDeploys` | boolean | optional |  |

### Example request

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

### 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. |
| 400 | `branch` |  |
| 400 | `subdirectory` |  |
| 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/git

The app's repository connection.

**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>/git" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with a JSON object carrying `connected`.

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

## DELETE /v1/apps/:name/git

Disconnects the repository.

**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>/git" \
  -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/apps/:name/git/deploys

The deploys a push triggered.

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

### Query parameters

| Name | Description |
| --- | --- |
| `limit` | How many entries, at most. |

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/apps/<name>/git/deploys" \
  -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. |

## POST /v1/git/connect

Starts the installation of the GitHub app and answers the link to open.

**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/git/connect" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`201` 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/git/connect/:id

Whether that installation has completed.

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

### Path parameters

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

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/git/connect/<id>" \
  -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. |

## POST /v1/git/installations

Records an installation of the GitHub app.

**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 `ConnectInstallationRequest` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `provider` | `github` | optional |  |
| `externalId` | string | required |  |
| `state` | string | required |  |
| `code` | string | required | 1 to 256 characters. |

### Example request

```bash
curl -X POST "https://api.rytiv.app/v1/git/installations" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"externalId":"<externalId>","state":"<state>","code":"<code>"}'
```

### Response

`200` or `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 | `state` |  |
| 403 | `token-scope` | The key's scopes do not cover this route. |

## GET /v1/git/installations

The organization's installations.

**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/git/installations" \
  -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. |

## DELETE /v1/git/installations/:id

Forgets an installation.

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

### Query parameters

| Name | Description |
| --- | --- |
| `force` |  |

### Example request

```bash
curl -X DELETE "https://api.rytiv.app/v1/git/installations/<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. |
