# Rytiv documentation > Rytiv is where your coding agent builds and ships. Connect it once, and it gets computers and sandboxes to work in, and apps and Postgres to ship to, in the EU. Rytiv gives a coding agent such as Claude Code a place of its own: computers it can work in, databases it can branch, and apps it can deploy. Your agent talks to Rytiv over MCP; you use the same platform from the `ry` CLI, the REST API or the console. Your apps, databases and files stay in the EU. - [Connect your coding agent](https://docs.rytiv.com/quickstart.md): One command adds Rytiv to Claude Code. Then ask it to build. - [Deploy a Next.js app](https://docs.rytiv.com/guides/deploy-nextjs.md): From a directory to a URL with ry deploy, in a few minutes. - [Create a computer](https://docs.rytiv.com/computers.md): A machine of its own for an agent: files that stay, a desktop you can watch. - [Branch a database](https://docs.rytiv.com/postgres.md): A Postgres copy in seconds, where your agent tries the migration first. ## Find your way - [Get started](https://docs.rytiv.com/quickstart.md): Quickstart, API keys and the CLI login, and how these docs serve agents. - [Guides](https://docs.rytiv.com/guides/deploy-nextjs.md): Task by task: deploy, Postgres, computers, agents and tokens, credit, domains, buckets, cron and jobs. - [Concepts](https://docs.rytiv.com/concepts/how-it-works.md): How Rytiv works, limits and quotas, the error catalog, regions, security and data. - [API reference](https://docs.rytiv.com/reference/api.md): Every endpoint with its fields, an example request, the shape of the answer and its error codes. Generated from the code. - [CLI reference](https://docs.rytiv.com/reference/cli.md): Every ry command with its flags and an example, generated from the CLI. - [SDK & MCP](https://docs.rytiv.com/reference/mcp-tools.md): The tools your agent sees over MCP, and the TypeScript SDK. ## Three ways in | Way | For | Where | | --- | --- | --- | | MCP server | Your coding agent | `https://api.rytiv.app/v1/mcp`, see the [quickstart](https://docs.rytiv.com/quickstart.md) | | CLI `ry` | You, in a terminal or in CI | [CLI reference](https://docs.rytiv.com/reference/cli.md) | | REST API | Your own code | [API reference](https://docs.rytiv.com/reference/api.md) | All three use the same API and the same keys, and they see the same apps, databases and machines. The console at `https://console.rytiv.com` is where you sign in, create keys, watch a computer's desktop and top up credit. ## What you get - **Computers**: a machine for an agent with a home disk that stays, in three sizes, with a desktop you can watch or take over. See [Computers](https://docs.rytiv.com/computers.md). - **Apps**: deploy from source or from an image; every app answers on `https://.rytiv.app` with TLS, rolls one machine at a time and can be rolled back. See [Apps](https://docs.rytiv.com/apps.md). - **Postgres**: databases with copy-on-write branches and point-in-time forks, reachable only from the apps you attach. See [Postgres](https://docs.rytiv.com/postgres.md). - **Sandboxes**: short-lived machines for one task each, removed at their deadline. See [Sandboxes](https://docs.rytiv.com/sandboxes.md). - **Buckets, cron, jobs, custom domains, mail**: the rest of what an app needs, in the [guides](https://docs.rytiv.com/guides/buckets.md). --- # Quickstart: connect your coding agent > Add Rytiv to Claude Code with one command. Your agent can then deploy apps, create databases and work in computers on Rytiv. Rytiv runs an MCP server at `https://api.rytiv.app/v1/mcp`. Connect your agent to it once, and the agent sees Rytiv's [tools](https://docs.rytiv.com/reference/mcp-tools.md): it can list and deploy apps, read their logs, set secrets, create Postgres databases and branches, run read-only SQL, and create, use and snapshot computers. ## Before you start You need a Rytiv account and an API key. Accounts come from the waitlist on rytiv.com, and every admitted account starts with 100 EUR of [credit](https://docs.rytiv.com/billing.md). A key acts for your whole organization unless you narrow it, so treat it like a password. ## 1. Create an API key Create a key in the Rytiv console (Team, then Tokens), or with the CLI if you have it installed. The value is shown exactly once. **CLI** ```bash ry tokens create claude-code --agent sandbox ``` **curl** ```bash curl -X POST https://api.rytiv.app/v1/tokens \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "claude-code", "agent": {"class": "sandbox"}}' ``` `--agent sandbox` makes it an [agent key](https://docs.rytiv.com/authentication.md#agent-keys): it can do what a key of your organization can do, except create, reveal or revoke credentials. Add `--app ` to limit it to the apps you name. Creating a key over the API needs a key already, so your first one comes from the console or from `ry login`. See [Authentication](https://docs.rytiv.com/authentication.md). ## 2. Add Rytiv to Claude Code Run this in a terminal, with your key in place of ``: ```bash claude mcp add --transport http rytiv https://api.rytiv.app/v1/mcp --header "Authorization: Bearer " ``` This adds a server named `rytiv` in the default scope, for you in the current project. Add `--scope user` to use it in all your projects. > **Keep the key out of shared files:** To share the setup with your team through a project's `.mcp.json`, reference an environment variable instead of the key. Claude Code expands `${VAR}` in the `url` and `headers` of an HTTP server: > > ```json > { > "mcpServers": { > "rytiv": { > "type": "http", > "url": "https://api.rytiv.app/v1/mcp", > "headers": { "Authorization": "Bearer ${RYTIV_TOKEN}" } > } > } > } > ``` > > Each person then sets `RYTIV_TOKEN` in their own environment. ## 3. Check the connection ```bash claude mcp list ``` The list shows `rytiv` with its status. Inside Claude Code, `/mcp` shows the same. ## 4. Ask your agent to build Start Claude Code in your project and ask in plain words. A few first prompts: - "List my apps on Rytiv." - "Deploy `nginx:alpine` on Rytiv as an app called `hello` (it listens on port 80) and give me its URL." - "Show me the logs of `hello`." - "Create a Postgres database called `appdb` on Rytiv, branch it, and try the migration on the branch." - "Create a computer on Rytiv, clone my repository into it and run the tests there." The agent picks the tools itself. Every tool carries annotations that your client can use to decide which calls to confirm with you: `readOnlyHint` on all of them, and `destructiveHint` and `idempotentHint` on the tools that write. `execute_sql` only reads unless you name a branch, so a write goes to a copy and never to the database itself. ## Let read-only tools run without asking In Claude Code, the tools of a server named `rytiv` are called `mcp__rytiv__`. To stop Claude Code from asking before the tools that only read, allow them in your project's `.claude/settings.json`: ```json { "permissions": { "allow": ["mcp__rytiv__search_docs", "mcp__rytiv__list_apps", "mcp__rytiv__get_logs"] } } ``` Leave the tools that change something on asking until you trust the setup. ## Other agents and clients The Codex plugin and the Cursor install link follow; both connect to the same endpoint with the same key. Until then, any MCP client that speaks Streamable HTTP with a bearer header can use the address above. For Codex that is `codex mcp add rytiv --url https://api.rytiv.app/v1/mcp --bearer-token-env-var RYTIV_TOKEN`, with `RYTIV_TOKEN` set in the environment Codex starts from. **Hosted connectors** that sign in with OAuth, for example a custom connector in claude.ai, need nothing but the address `https://api.rytiv.app/v1/mcp`: they register themselves, you approve the connection once in the Rytiv console, and no key is pasted anywhere. See [Connect without a key](https://docs.rytiv.com/authentication.md#connect-without-a-key). Passkey confirmations for irreversible steps are not yet available, see [Confirmations](https://docs.rytiv.com/concepts/confirmations.md). ## What the key can do A key without an agent class acts as your organization: everything the tools allow, in every app of your organization, and nothing outside it. An agent key is held to its class by the platform, not by the prompt: `sandbox` and `twin` may do what your organization's keys do, except create, reveal or revoke credentials; `production` may only read. With `--app`, the key reaches the named apps and nothing else. The details and the error codes are under [Agent keys](https://docs.rytiv.com/authentication.md#agent-keys). ## Next steps - [MCP tools](https://docs.rytiv.com/reference/mcp-tools.md): every tool with its parameters. - [How Rytiv works](https://docs.rytiv.com/concepts/how-it-works.md): what your agent is creating. - [Computers](https://docs.rytiv.com/computers.md): a machine of its own for your agent, with Codex, OpenCode, Node.js, git and gh preinstalled, and how to run Claude Code inside it. - [Agents and tokens](https://docs.rytiv.com/guides/agents-and-tokens.md): a key per agent, narrowed by class, apps and scopes, and the audit of what it did. - [CLI reference](https://docs.rytiv.com/reference/cli.md) and [API reference](https://docs.rytiv.com/reference/api.md): the same platform from your terminal and your code. --- # 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:` | The same for exactly that app | | `mail:send` | Sending mail from any of your sending domains | | `mail:send:` | 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=` 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 ``` 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 )`, 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). --- # Docs for agents > These docs are written for people and for coding agents. Every page is available as Markdown, and the whole set as one file. Your agent does not need to scrape this site. Everything here is also served as plain Markdown, generated from the same source as the pages you read. ## llms.txt [`/llms.txt`](https://docs.rytiv.com/llms.txt) lists every page with a one-line description and a link to its Markdown form, grouped like the sidebar. It follows the format proposed at llmstxt.org. [`/llms-full.txt`](https://docs.rytiv.com/llms-full.txt) is the whole documentation in one file, in sidebar order, each page introduced by its address. ## Every page as Markdown Add `.md` to the path of any page: ```bash curl https://docs.rytiv.com/quickstart.md curl https://docs.rytiv.com/reference/mcp-tools.md ``` The start page is at `/index.md`. Links inside the Markdown point at the Markdown form of the linked page. ## Copy page Every page has a **Copy page** button next to its title. It copies the page's Markdown, so you can paste it into a conversation with your agent. ## Inside the agent Once your agent is [connected](https://docs.rytiv.com/quickstart.md), it has the tool `search_docs`. It answers questions about how Rytiv works from a knowledge base inside the platform, without a request to this site. ## Generated, not written twice The [MCP tools](https://docs.rytiv.com/reference/mcp-tools.md) page is generated from the table the MCP server answers `tools/list` with, and the [CLI reference](https://docs.rytiv.com/reference/cli.md) from the CLI's own command definitions. If a parameter is listed there, the server or the CLI accepts it. --- # Deploy a Next.js app > From a Next.js directory to a URL with one command, then secrets, logs, releases and rollbacks. The same path works for every framework Rytiv detects. `ry deploy` reads your project, uploads it, builds it on Rytiv's build machines and deploys the result behind `https://.rytiv.app`. Next.js is detected from `next` in your dependencies; nothing in the project has to change. ## Before you start - A Rytiv account and the CLI: `curl -fsSL https://get.rytiv.com/install.sh | sh`, then `ry login`. See [Authentication](https://docs.rytiv.com/authentication.md). - A Next.js project with a `package-lock.json` (the builder runs `npm install --include=dev`; a pnpm, yarn or bun lockfile without a `package-lock.json` is ignored, with a note). ## 1. Deploy 1. From the project directory, deploy to an app named `web`: ```bash ry deploy web --source . ``` The app is created if it does not exist. The CLI prints what it detected (framework, build and start command, port), uploads the directory, shows the build's place in line and its progress, then rolls the deploy. 2. Open the URL the last line prints: `https://web.rytiv.app`. 3. Save the settings so the next deploy needs no flags: ```bash ry config init web ``` This writes a `rytiv.json` next to your code. `ry deploy` reads it from the current directory or any directory above it; a flag on the command line always wins over the file. Or run `ry` on its own in the directory: it signs you in if needed, shows what it detected, asks for an app name and deploys. `ry new next my-site` scaffolds a new Next.js project with a `rytiv.json`. ## What is detected | What | Value for Next.js | | --- | --- | | Build command | `npm run build` | | Start command | `npx next start -H 0.0.0.0 -p $PORT` | | Port | `3000`, unless `.env.example` sets `PORT` or the start script passes `-p` | | Build machine | 8192 MB, 4 vCPU (the larger of the two build sizes; Next.js and Dockerfile builds get it) | | Suggested machine | 1024 MB, 1 vCPU for a new app | | Served at the edge | `.next/static` at `/_next/static`, and `public/` at `/` | A `Dockerfile` or `Containerfile` at the root of the directory wins over the detection. `next.config.js` (also `.mjs`, `.cjs`, `.ts`) is read as text, never executed; `output: "export"` makes the app a static site served from `out/` without a machine. What is uploaded: the directory without `node_modules`, `.git`, `.next`, `.svelte-kit`, `.nuxt`, `.output`, `.astro`, `.env.local` and `.env.*.local`. Your `.gitignore` is not applied; a `.env` file is uploaded, and the CLI warns about `.env*` files that git ignores. > **The port:** The platform exports `PORT=3000` (with `HOST=0.0.0.0` and `NODE_ENV=production`) into the machine; `--port` tells Rytiv where to route and probe, it does not change `PORT`. If your app must listen on another port, set `PORT` yourself with `ry deploy -e PORT=8080 --port 8080`, or keep 3000. ## 2. Environment and secrets **CLI** ```bash ry secrets set web DATABASE_PASSWORD=example-value ry secrets set web NEXT_PUBLIC_SITE_URL=https://www.example.com --build ry deploy web -e LOG_LEVEL=info ``` **curl** ```bash curl -X PUT "https://api.rytiv.app/v1/apps/web/secrets" \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{"secrets": {"DATABASE_PASSWORD": "example-value"}, "build": {"DATABASE_PASSWORD": false}}' ``` **TypeScript** ```ts await fetch("https://api.rytiv.app/v1/apps/web/secrets", { method: "PUT", headers: { Authorization: `Bearer ${process.env.RYTIV_TOKEN}`, "Content-Type": "application/json" }, body: JSON.stringify({ secrets: { DATABASE_PASSWORD: "example-value" } }), }); ``` **Python** ```python import os, requests requests.put( "https://api.rytiv.app/v1/apps/web/secrets", headers={"Authorization": f"Bearer {os.environ['RYTIV_TOKEN']}"}, json={"secrets": {"DATABASE_PASSWORD": "example-value"}}, ) ``` - **Secrets** are stored encrypted and never readable back. They reach the app at its next deploy; `ry secrets set --now` rolls the app right away. - **Runtime or build.** Every secret is available to the running app. Only secrets marked for the build are visible to `next build`: a new secret whose name starts with `NEXT_PUBLIC_` (or `VITE_`, `PUBLIC_`, `NUXT_PUBLIC_`, `GATSBY_`, `REACT_APP_`) is marked by default, anything else needs `--build`. `--no-build` takes the mark away. In the API this is the `build` map of `PUT /v1/apps/:name/secrets`. - **Plain variables** (`-e KEY=VALUE`) are stored in the clear and shown in `ry machines`; use them for settings, not credentials. A [Postgres database](https://docs.rytiv.com/postgres.md) attached with `ry attach appdb --to web` gives the app its `DATABASE_URL` as a managed secret. ## 3. Watch it run ```bash ry logs web -f ry status ry machines web ``` `ry logs` shows what the app's machines write to their console; `-f` follows. A machine is `running` when it answers on its port. ## 4. Health checks and sizes ```bash ry deploy web --health-path /api/health --health-timeout 90 ry deploy web --memory 2048 --vcpus 2 ``` With `--health-path`, a new machine gets traffic only after that path answered `2xx` or `3xx`; without it, the port alone decides. A size you do not name keeps the app's current one. See [`ry deploy`](https://docs.rytiv.com/reference/cli/deploy.md) for every flag. ## 5. Releases and rollback ```bash ry deploys web ry releases web ry rollback web ``` Every deploy that goes live is a release. `ry rollback web` deploys the previous release's image again, without a rebuild and with the app's current settings; `ry rollback web 3` names a version. Secrets, volumes and databases stay as they are. After a deploy switches traffic, Rytiv watches the app's error rate for five minutes against the rate before the switch. If at least 20 requests came in and the share of `5xx` answers is clearly worse than before (at least 5 percent, and more than three times the old rate plus two points), the deploy is rolled back on its own. The verdict is in the deploy's `watch` field (`GET /v1/deploys/:id`): `healthy`, `unproven` (not enough traffic to judge), `rolled-back`, and a few more. Databases are never rolled back automatically. ## 6. Deploy on push ```bash ry git connect web --repo acme/web --branch main ``` This installs Rytiv's GitHub app through the browser and links the app to the repository. Every push to the branch builds and deploys through the same path as `ry deploy`; `ry git status web` shows the last push. Previews (`ry preview create web --branch feature/login`) are apps of their own per branch, protected by a link, closed when the branch or pull request closes or after 14 idle days, at most 10 per app. ## Next - [Custom domains and TLS](https://docs.rytiv.com/guides/custom-domains.md): put `www.example.com` on the app. - [Cron and jobs](https://docs.rytiv.com/guides/cron-and-jobs.md): call a path on a schedule, run a migration as a job. - [Apps](https://docs.rytiv.com/apps.md): everything an app can do, and the deploy request field by field. --- # Apps > Deploy from a source directory or a prebuilt image. Rolling deploys, secrets, logs, releases and rollbacks. An app is a name with an address. It runs one or more machines from an image you deploy, or, for a static site, no machine at all. Machines, deploys, secrets, logs and releases all belong to an app. ## Names and addresses App names are unique across all of Rytiv: 2 to 63 characters, lowercase letters, digits and hyphens, starting with a letter. A few names are reserved for the platform (for example `api`, `console`, `docs` and `www`, and every name that is `rytiv` or starts with `rytiv-`). An app answers on `https://.rytiv.app`. Extra ports of a machine answer on `https://-.rytiv.app`. ## Deploy from source Point the CLI at a directory. Rytiv reads the project, builds it on its own machines and deploys the result: ```bash ry deploy web --source . ``` Or run `ry` alone in the directory: it signs you in if needed, shows what it detected, asks for an app name and deploys. ```bash ry ``` **What is detected.** Node.js projects with these frameworks: Next.js, SvelteKit, Nuxt, Remix, TanStack Start, SolidStart, Analog, Astro, Vite, Gatsby, Docusaurus, Create React App, VitePress, Eleventy and Angular, plus plain Node.js. A `Dockerfile` or `Containerfile` at the root of the directory wins over the detection; anything that is not Node.js needs one. `ry detect` shows the finding without changing anything. **Static sites** (for example Astro without an adapter, Next.js with `output: "export"`, Vite, or a folder with an `index.html`) need no machine: the built files are served directly. ### rytiv.json `ry deploy` reads a `rytiv.json` in the current directory or above it, so a deploy needs no flags. `ry config init ` writes one. A flag on the command line always wins over the file. ```json { "app": "web", "source": ".", "port": 3000, "memoryMb": 512, "vcpus": 1 } ``` Other keys: `image` (instead of `source`), `diskMb`, `buildMemoryMb`, `count`, `start`, `healthPath`, `healthTimeoutS`, `restart`, `killSignal`, `killTimeoutS`, `sleep` and `env`. An unknown key is an error. ## Deploy an image **CLI** ```bash ry deploy web --image nginx:alpine --port 80 ``` **curl** ```bash curl -X POST https://api.rytiv.app/v1/apps \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "web"}' curl -X POST https://api.rytiv.app/v1/apps/web/deploy \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{"image": "nginx:alpine", "internalPort": 80}' ``` **TypeScript** ```ts const api = "https://api.rytiv.app/v1"; const headers = { Authorization: `Bearer ${process.env.RYTIV_TOKEN}`, "Content-Type": "application/json", }; await fetch(`${api}/apps`, { method: "POST", headers, body: JSON.stringify({ name: "web" }) }); const res = await fetch(`${api}/apps/web/deploy`, { method: "POST", headers, body: JSON.stringify({ image: "nginx:alpine", internalPort: 80 }), }); const deploy = await res.json(); // { id, status: "running", ... } ``` `POST /v1/apps` answers `201`, or `409` with `name-taken` if the name is used anywhere on Rytiv. `POST /v1/apps/:name/deploy` answers `202` with the deploy. Poll `GET /v1/deploys/:id` until `status` is `done` or `error`. On `error`, the answer names the reason in `error` and carries the last lines of the machine's console in `bootLog`. ### Deploy request | Field | Type | Default | Notes | | --- | --- | --- | --- | | `image` | string | required | An OCI reference such as `nginx:alpine`, or `build:` for a build of yours | | `internalPort` | integer | `3000` | The port your app listens on | | `memoryMb` | integer | kept, `512` for a new app | 128 to 16384 | | `vcpus` | integer | kept, `1` for a new app | 1 to 16 | | `diskMb` | integer | kept | 512 to 131072 | | `count` | integer | kept, `1` for a new app | Machines, 1 to 20 | | `env` | object | `{}` | Plain environment variables. Use [secrets](#secrets) for credentials. | | `start` | string | | Start command, run through `/bin/sh -c` | | `healthCheck` | object | | `path` for an HTTP check (empty: the port only), `timeoutMs`, `intervalMs`, `healthyThreshold`, `maxWaitMs` | | `restart` | object | | `policy`: `never` or `on-failure`, with `retries` 1 to 10. Without it, a crashed machine is restarted. | | `killSignal` | string | the image's, else `SIGTERM` | Sent before a machine is stopped or replaced | | `killTimeoutS` | integer | `30` | Seconds between that signal and `SIGKILL`, 1 to 300 | "Kept" means: a deploy that does not name the field keeps what the app's machines have. Every other field comes from the request or its default, so send `internalPort`, `env` and `start` with every deploy. ### How a deploy rolls A deploy starts a new machine, waits until it runs and answers on its port (and passes the health check, if you set one), then takes one old machine out of traffic and removes it, one at a time. If a new machine fails, the deploy stops, the new machine is discarded and the old version keeps serving. An app with a volume works differently: its single machine stops before the new one starts, so there is a short gap. ### Sleep when idle An app sleeps only if you give it a sleep rule. Without one, its machines run until you stop them. Name the idle time on a deploy: ```bash ry deploy web --sleep 5m ``` A machine that has had no requests and no CPU load for that long goes to sleep, and the next request wakes it. Up to 2 GB of memory it sleeps with its memory and resumes in about a quarter of a second; a larger machine stops instead and boots cold, and its files stay. `--sleep off` takes the rule away again, for a worker or a cron app that has to stay awake. In a [deploy request](#deploy-request) the same rule is `suspendPolicy`, for example `{"mode": "suspend", "idleSeconds": 300}`. ## Secrets Secrets are environment variables stored encrypted. Nobody can read them back, not even you: the API lists names, never values. **CLI** ```bash ry secrets set web API_KEY=example-value OTHER_KEY=other-value ``` **curl** ```bash curl -X PUT https://api.rytiv.app/v1/apps/web/secrets \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{"secrets": {"API_KEY": "example-value"}, "staged": true}' ``` | Method | Path | What it does | | --- | --- | --- | | `PUT` | `/v1/apps/:name/secrets` | `{secrets, staged}`. With `staged: true` (the default) the values apply at the next deploy. With `staged: false` Rytiv rolls the app now if a machine lacks the new value. | | `GET` | `/v1/apps/:name/secrets` | `name`, `updatedAt` and `managed` for every secret. Never a value. | | `DELETE` | `/v1/apps/:name/secrets/:key` | Removes one secret. `204`. | Secrets that Rytiv manages for you, such as the `DATABASE_URL` of an [attached database](https://docs.rytiv.com/postgres.md#2-connect-from-an-app), cannot be changed or removed this way. Each secret carries a `build` choice: whether the next source build sees it. A new secret is marked for the build only when its name starts with a public prefix such as `NEXT_PUBLIC_` or `VITE_`; `ry secrets set KEY --build` marks one, `--no-build` takes the mark away. ## Logs ```bash ry logs web -f ``` `GET /v1/apps/:name/logs` answers `{app, machines}`, one entry per machine with the console output of that machine in `text` and a `cursor`. Pass `cursor.=` on the next call to get only what is new, and `machine=` for one machine. Logs are what your app's machines write to their console. ## Machines | Method | Path | What it does | | --- | --- | --- | | `GET` | `/v1/apps/:name/machines` | The app's machines | | `GET` | `/v1/machines/:id` | One machine | | `POST` | `/v1/machines/:id/start` | Start a stopped or suspended machine | | `POST` | `/v1/machines/:id/stop` | Stop it | | `POST` | `/v1/machines/:id/suspend` | Suspend it | | `POST` | `/v1/machines/:id/destroy` | Remove it | A machine is `requested`, `scheduled`, `starting`, `running`, `suspending`, `suspended`, `stopping`, `stopped`, `deleted` or `failed`. ```bash ry machines web ry exec web -- ls -la /app ``` ## Releases and rollback Every deploy is a release. A rollback deploys an earlier release's image again, without a rebuild, with the app's current settings. Secrets, volumes and databases stay as they are. ```bash ry releases web ry rollback web ``` | Method | Path | What it does | | --- | --- | --- | | `GET` | `/v1/apps/:name/releases` | The releases, which one runs, and which one a rollback would bring back | | `POST` | `/v1/apps/:name/rollback` | `{}` for the previous release that went live, or `{version}` or `{releaseId}`. Answers `202` with the deploy. | ## Delete an app ```bash ry apps destroy web ``` `DELETE /v1/apps/:name` removes the app and all its machines and answers `204`. It cannot be undone. The CLI asks before it does this; pass `--yes` in scripts. An app that runs a [database](https://docs.rytiv.com/postgres.md) is removed with the database, not on its own. ## Errors you will meet | Status | Code | When | | --- | --- | --- | | `400` | `name-reserved` | The name is reserved for the platform | | `409` | `name-taken` | The name is used somewhere on Rytiv | | `403` | `org-limit-apps` | An account on the starting credit can have 10 apps, computers included. See [Credit and pricing](https://docs.rytiv.com/billing.md#limits-on-the-starting-credit). | | `402` | `credit-blocked` | The credit is used up: nothing new starts until a top-up. See [Credit and pricing](https://docs.rytiv.com/billing.md#when-the-credit-is-used-up). | | `404` | `build-not-found` | `build:` is not a build of your organization | | `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 | See the [CLI reference](https://docs.rytiv.com/reference/cli/deploy.md) for every flag of `ry deploy`, and [Apps](https://docs.rytiv.com/reference/api/apps.md) and [Deploys and releases](https://docs.rytiv.com/reference/api/deploys.md) in the API reference for every route with its fields. The guide [Deploy a Next.js app](https://docs.rytiv.com/guides/deploy-nextjs.md) walks the path from a directory to a URL. --- # Computers > A computer is a machine of its own for a coding agent. Create one, connect Claude Code to it, work in it over a shell or its desktop, and let it shut down when idle; the home disk stays. Some work needs a place whose files are still there tomorrow: a checkout, a half-finished migration, the notes of a long task. A computer is that place. It is an app with one machine that runs the computer image, a home disk mounted at `/data` that outlives the machine, and a desktop you can watch or take over from the console. ## Sizes | Size | vCPU | Memory | Home disk | | --- | --- | --- | --- | | `small` | 1 | 2304 MB | 50 GB | | `base` (default) | 2 | 4096 MB | 100 GB | | `large` | 4 | 8192 MB | 128 GB | An organization on starting credit may run 2 computers at a time; see [Limits and quotas](https://docs.rytiv.com/concepts/limits.md). ## 1. Create a computer **Console** Open **Computers**, choose **New computer**, give it a name and a size. The console creates the app, the home disk and the key, and deploys the computer image. **CLI** ```bash ry computer create dev-1 --image rytiv/computer:0.1.0 ry computer create dev-2 --size large --image rytiv/computer:0.1.0 ``` `--image` names the computer image: Rytiv's released image `rytiv/computer:`, or a `build:` of your own. Without `--image`, the CLI reads `RYTIV_COMPUTER_IMAGE` from the environment and refuses when neither is set. **curl** A computer is an app created with `computer: true`, a home volume attached to it, a secret `COMPUTER_VIEW_KEY` the desktop needs, and a deploy of the computer image with the size and a sleep rule. The CLI runs these calls in that order: ```bash curl -X POST "https://api.rytiv.app/v1/apps" \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "dev-1", "computer": true}' ``` Then `POST /v1/volumes` (`dev-1-home`), `POST /v1/apps/dev-1/attach` with the volume, `PUT /v1/apps/dev-1/secrets` with `COMPUTER_VIEW_KEY`, and `POST /v1/apps/dev-1/deploy` with `{"image": "rytiv/computer:0.1.0", "memoryMb": 4096, "vcpus": 2, "internalPort": 8080, "suspendPolicy": {"mode": "suspend", "idleSeconds": 600, "stopAfterSeconds": 86400}}`. See the [API reference](https://docs.rytiv.com/reference/api/computers.md). Names are 2 to 31 characters of lowercase letters, digits and hyphens. The home disk is a volume named `-home`; it is kept while the computer is shut down and when its machine is replaced. What the image brings: Node.js with npm, Codex, OpenCode, git, the GitHub CLI, Python 3 with pip, a browser and a desktop. Claude Code is not pre-installed; inside the computer, `rytiv-setup claude` installs it with its own official installer under its own terms (`--stable`, `--version ` and `--print` are accepted), and `rytiv-setup` alone shows what the image has and has not. ## 2. Connect Claude Code Two ways, for two kinds of work. 1. **Claude Code on your laptop, Rytiv as its tool.** Add the MCP server once; the agent then creates computers, runs commands in them and snapshots them with the tools `computer_create`, `computer_exec` and `computer_snapshot`: ```bash claude mcp add --transport http rytiv https://api.rytiv.app/v1/mcp --header "Authorization: Bearer " ``` Then ask in plain words: "Create a computer on Rytiv, clone the repository into it and run the tests." 2. **Claude Code inside the computer.** Open a shell in it and install Claude Code there; the session keeps running when your laptop is closed: ```bash ry ssh dev-1 ``` Inside: `rytiv-setup claude`, then `claude`. Sign in with your own Claude account; nothing of it touches Rytiv. A computer the MCP tool `computer_create` makes is an agent machine: it does not sleep on its own, and it can take [snapshots](#snapshots). A computer from the console or `ry computer create` shuts down when idle and keeps its home; it takes no snapshots. ## 3. Work in it ```bash ry exec dev-1 -- git status ry exec dev-1 --dir /data -- npm test ry ssh dev-1 ry forward dev-1 3000:3000 ``` `ry exec` runs one command and exits with its exit code; `ry ssh` opens an interactive shell over the Rytiv API, no SSH keys needed; `ry forward` brings a port of the computer to your machine without a public port. Each of them wakes a shut-down computer. Through MCP, `computer_exec` does what `ry exec` does, as an argument list without a shell. Files you want to keep go under `/data`, the home. The root disk is the image's and starts fresh at every boot. ## 4. Watch or take over the desktop ```bash ry computer view dev-1 ry computer control dev-1 ry computer release dev-1 ry computer audit dev-1 ``` `view` gives a link to watch, for any number of viewers, valid 60 to 3600 seconds (default 900). `control` gives a link to take keyboard and mouse: there is one holder per machine, the agent inside pauses while you have control, and `--force` takes control from the current holder. `audit` shows who got which link and how control ended. The console shows the same desktop on the computer's page. ## 5. Idle shutdown After 10 minutes without activity the computer shuts down; your files stay. Every size is above the 2048 MB up to which a machine sleeps with its memory, so a computer stops instead: open programs end, and the next `ry exec`, `ry ssh`, request or console visit boots it fresh with the home disk as it was. CPU load, a held control lease and an open viewer with frames flowing count as activity. While it is shut down it costs nothing for memory and CPU; the home disk is billed as a volume. See [Credit and prices](https://docs.rytiv.com/billing.md). ## Snapshots A machine created with `durableCheckpoint: true` (what the MCP tool `computer_create` does, and `POST /v1/apps/:name/machines` with that field) can be snapshotted and restored: | Method | Path | What it does | | --- | --- | --- | | `POST` | `/v1/machines/:id/checkpoints` | `{label, memory, comment}`. Takes a snapshot now. | | `GET` | `/v1/machines/:id/checkpoints` | The machine's named snapshots | | `POST` | `/v1/machines/:id/checkpoints/:label/restore` | Brings the machine back to that snapshot. With `keepCurrent: true` (the default) the current state is saved first. | | `DELETE` | `/v1/machines/:id/checkpoints/:label` | Forgets a snapshot | Disk only (`memory: false`, the default) is a short pause, and a restore boots fresh with the files as they were. With memory (`memory: true`) a restore resumes the very processes; the pause grows with the machine's memory. Labels use letters, digits, `.`, `-` and `_`, start with a letter or digit, and are at most 63 characters; `auto-` is reserved. Through MCP, `computer_snapshot` takes one. Such a machine also survives a host failure with its last automatic checkpoint. ## Delete ```bash ry apps destroy dev-1 ry volume destroy vol-1a2b3c4d ``` Deleting the app removes the machine; the home volume stays until you delete it too (`ry volume list` shows it). Both cannot be undone; the CLI asks you to type the name, automation passes `--yes`. --- # Postgres > Create a Postgres database, connect an app or your laptop to it, branch it for an experiment, fork it from a point in time, restore it in place, and set when it sleeps. A Rytiv database is PostgreSQL 16 on a machine of its own, with its data on a volume and a continuous archive of its write-ahead log. Only apps you attach can reach it, always over TLS. Every database can be **branched** (a copy-on-write copy of its disk, ready in seconds) and **forked** (a full copy from its archive, from now or from a moment in the past), so an agent can change a schema without touching the original. ## 1. Create a database **CLI** ```bash ry postgres create appdb ry postgres create appdb --size 20 --locale en-US ``` **curl** ```bash curl -X POST "https://api.rytiv.app/v1/resources" \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "appdb", "kind": "postgres"}' ``` **TypeScript** ```ts const res = await fetch("https://api.rytiv.app/v1/resources", { method: "POST", headers: { Authorization: `Bearer ${process.env.RYTIV_TOKEN}`, "Content-Type": "application/json" }, body: JSON.stringify({ name: "appdb", kind: "postgres" }), }); const db = await res.json(); // db.url is shown in this answer only ``` **Python** ```python import os, requests db = requests.post( "https://api.rytiv.app/v1/resources", headers={"Authorization": f"Bearer {os.environ['RYTIV_TOKEN']}"}, json={"name": "appdb", "kind": "postgres"}, ).json() # db["url"] is shown in this answer only ``` Your agent uses the tool [`create_postgres`](https://docs.rytiv.com/reference/mcp-tools.md#create_postgres), which never returns the password. | Field | Type | Default | Notes | | --- | --- | --- | --- | | `name` | string | required | Like an [app name](https://docs.rytiv.com/apps.md#names-and-addresses), unique across Rytiv | | `kind` | string | required | `postgres` | | `memoryMb` | integer | `1024` | 128 to 16384 | | `sizeGb` | integer | `10` | Size of the data volume, 4 to 500 | | `locale` | string | `de-DE` | ICU locale for sorting and comparing text, such as `en-US`. Fixed once the database exists. | | `durability` | string | `standard` | `standard`: one machine. `host-redundant`: a standby on a second host (API only; a redundant database cannot be branched or forked). | | `suspendPolicy` | object | suspend after 300 s | When the database sleeps, see [Sleep](#5-sleep). `{"mode": "off"}` keeps it always on. | `POST /v1/resources` answers `201` with `name`, `kind`, `port`, `secretKey` (`DATABASE_URL`) and `url`. **The `url` contains the password and is returned only here**; the CLI shows it only with `--json`. You do not need to store it if you attach the database to an app: the app gets it as a secret. ## 2. Connect from an app ```bash ry attach appdb --to web ry deploy web ``` `POST /v1/apps/:name/attach` with `{"resource": "appdb"}` gives the app a managed secret `DATABASE_URL`. A running machine keeps the environment it started with, so deploy the app again (or stop and start its machine) after attaching. The URL looks like this: ```text postgres://postgres:@appdb.internal:5432/appdb?sslmode=verify-full&sslrootcert=/etc/rytiv/ca/postgres.crt ``` - `appdb.internal` resolves only inside apps that are attached to the database. Everyone else cannot reach it. - The database accepts TLS connections only. The certificate authority is at `/etc/rytiv/ca/postgres.crt` in every machine, so `verify-full` works without setup. - The database name is the resource name with `-` replaced by `_`. ## 3. Connect from your computer A database has no public address. Open a tunnel with the CLI and connect to the local end: ```bash ry proxy appdb 15432:5432 psql -h 127.0.0.1 -p 15432 -U postgres appdb ``` The first connection wakes a sleeping database. ## 4. Branch, fork, restore ### Branch a database A branch is a copy-on-write copy of the database **as it is now**, taken on its host. It is ready in seconds, takes almost no space at first, and is a database of its own with its own password: writes to the branch never reach the source. **CLI** ```bash ry postgres branch appdb appdb-try --expires-in 1d ``` **curl** ```bash curl -X POST "https://api.rytiv.app/v1/resources/appdb/branch" \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "appdb-try", "ttlSeconds": 86400}' ``` **TypeScript** ```ts await fetch("https://api.rytiv.app/v1/resources/appdb/branch", { method: "POST", headers: { Authorization: `Bearer ${process.env.RYTIV_TOKEN}`, "Content-Type": "application/json" }, body: JSON.stringify({ name: "appdb-try", ttlSeconds: 86400 }), }); ``` **Python** ```python import os, requests requests.post( "https://api.rytiv.app/v1/resources/appdb/branch", headers={"Authorization": f"Bearer {os.environ['RYTIV_TOKEN']}"}, json={"name": "appdb-try", "ttlSeconds": 86400}, ) ``` | Field | Type | Notes | | --- | --- | --- | | `name` | string | Name of the new database | | `ttlSeconds` | integer | Delete the branch after this long, 60 to 2592000 (30 days) | | `expiresAt` | string | Or: delete it at this moment (ISO 8601). Not together with `ttlSeconds`. | | `ephemeral` | boolean | Ready faster, without an archive of its own, and lost if its host fails | The answer is `202` with `state: "restoring"`. Poll `GET /v1/resources/:name` until `state` is `ready`; the CLI waits for you. A database can have up to 15 branches. This is where an agent changes a schema or tries a migration. Through MCP, [`create_branch`](https://docs.rytiv.com/reference/mcp-tools.md#create_branch) makes one, and [`execute_sql`](https://docs.rytiv.com/reference/mcp-tools.md#execute_sql) only writes when you name a branch of the database; on the database itself it runs read-only. ### Fork from a point in time A fork is a full copy made from the database's archive, on any host. It takes longer than a branch, minutes for a large database, and can start from any moment the archive covers (7 days): ```bash ry postgres fork appdb appdb-yesterday --at 2026-10-19T09:00:00Z ``` `POST /v1/resources/:name/fork` takes `name`, an optional `asOf` (ISO 8601) and the same `ttlSeconds` or `expiresAt` as a branch. ### Connection URL of a copy A branch or fork has its own password. Attach it to an app like any database, or fetch its URL **once**: ```bash curl -X POST "https://api.rytiv.app/v1/resources/appdb-try/url" \ -H "Authorization: Bearer $RYTIV_TOKEN" ``` The answer is `{name, url}`. A second call answers `409` with `url-handed-out`: the URL is handed out exactly once. For the original database, the URL came with the create answer and this route answers `409` with `not-a-copy`. The CLI asks for it right after a branch or fork is ready and prints it with `--json`. ### Restore in place ```bash ry postgres restore appdb --at 2026-10-19T09:00:00Z ``` `POST /v1/resources/:name/restore` with `{asOf}` puts a database back to that moment. Name, URL and password stay the same; open connections end once, at the switch. The state before the restore is kept as a branch named `-old-`, or the name you pass as `backupName`. The CLI asks before it does this; pass `--yes` in scripts. ### Expiry `PATCH /v1/resources/:name` with `ttlSeconds` or `expiresAt` sets when a branch or fork is deleted; `"expiresAt": null` keeps it. Only copies can expire. ```bash ry postgres expire appdb-try --in 12h ry postgres expire appdb-try --never ``` ## 5. Sleep A standalone database carries a sleep policy: by default it is suspended after 300 seconds without a running query, an open transaction or traffic, and stopped a day after that; the next connection wakes it. A redundant database never sleeps. ```bash ry postgres sleep appdb --after 10m ry postgres sleep appdb --off ry postgres create reports --always-on ``` `PATCH /v1/resources/:name` with `suspendPolicy` changes the rule; `{"mode": "off"}` keeps the database always on. The policy is stored with the database and shown by `ry postgres list`; whether a region's hosts act on it is part of the region's configuration, so do not build on a database being asleep. ## List and delete | Method | Path | What it does | | --- | --- | --- | | `GET` | `/v1/resources` | Your databases, with `state` (`restoring`, `ready`, `failed`), `sourceName` and `expiresAt` for copies. Never a URL. | | `GET` | `/v1/resources/:name` | One database | | `DELETE` | `/v1/resources/:name` | Deletes the database, its machine and its volume, and detaches it from every app. It cannot be undone. | ```bash ry postgres list ry postgres destroy appdb-try ``` ## Errors you will meet | Status | Code | When | | --- | --- | --- | | `409` | `name-taken` | The name is used somewhere on Rytiv | | `400` | `name-reserved` | The name is reserved for the platform | | `503` | `no-capacity` | No room right now, try again | | `409` | `url-handed-out` | The URL of this copy was handed out already | | `409` | `not-ready` | The copy is still restoring, or failed | | `409` | `conflict` | The database already has 15 branches: delete one, or fork | See [Postgres](https://docs.rytiv.com/reference/api/postgres.md) in the API reference for every field, and the [CLI reference](https://docs.rytiv.com/reference/cli/postgres.md) for every flag of `ry postgres`. --- # Sandboxes > Short-lived machines for one task each. A sandbox starts from a template, runs commands and holds files, and is removed at its deadline. A sandbox is a machine with a deadline. Create it from a template, run commands in it, read and write its files, and let it go: at its deadline Rytiv removes the machine and everything on its disk. Each sandbox has an id like `sbx-1a2b3c4d5e6f` and its own address. ## Create a sandbox **CLI** ```bash ry sandbox create app:web --ttl 30m ``` **curl** ```bash curl -X POST https://api.rytiv.app/v1/sandboxes \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{"template": "app:web", "ttlSeconds": 1800}' ``` **TypeScript** ```ts const res = await fetch("https://api.rytiv.app/v1/sandboxes", { method: "POST", headers: { Authorization: `Bearer ${process.env.RYTIV_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ template: "app:web", ttlSeconds: 1800 }), }); const sandbox = await res.json(); console.log(sandbox.id, sandbox.state, sandbox.url); ``` `POST /v1/sandboxes` answers when the machine runs, with `201` and the sandbox. The answer also carries `createMs`, the time from your request to the running machine, and, for a sandbox with signed access, an `accessToken`. ### Templates A sandbox boots the image of a template you already have: | Template | Boots | | --- | --- | | `app:` | The image an app of your organization runs right now. The app must run a machine. | | `build:` | A build of your organization that produced a machine image (not a static site). | Nothing else is inherited from the template: no environment variables and no secrets of the app. Pass what the sandbox needs in `env`. ### Request | Field | Type | Default | Notes | | --- | --- | --- | --- | | `template` | string | required | `app:` or `build:` | | `ttlSeconds` | integer | `300` | Lifetime from now, 10 to 86400 | | `vcpus` | integer | `1` | 1 to 16 | | `memoryMb` | integer | `512` | 128 to 16384 | | `diskMb` | integer | | 512 to 131072 | | `env` | object | `{}` | Up to 128 variables, names like `MY_VAR`. A few `RYTIV_` names are reserved. | | `ports` | integer[] | `[]` | Up to 8 more ports to reach from outside | | `internalPort` | integer | the template app's port, else `3000` | The port `url` points at | | `readiness` | `boot` or `port` | `boot` | Ready when the machine boots, or when `internalPort` answers | | `access` | `signed` or `public` | `signed` | Who can open the sandbox's URLs, see [Access](#access) | | `idleSeconds` | integer | `300` | 10 to 86400, see the note below | | `metadata` | object | `{}` | Up to 16 key/value pairs for your own bookkeeping | | `egress` | object | allow all | Outbound network, see [Network](#network) | Unknown fields are refused with `400`. > **Idle sleep:** `idleSeconds` is accepted and stored with the sandbox. Whether a region's hosts put an idle sandbox to sleep is part of the region's configuration; do not build on a sandbox being asleep. Its deadline (`ttlSeconds`) is what ends it. ### The sandbox object | Field | Meaning | | --- | --- | | `id` | `sbx-` and 12 hex digits | | `template` | The template it was created from | | `state` | `starting`, `running`, `sleeping` or `ended` | | `createdAt`, `expiresAt` | When it was created, and its deadline | | `endedAt`, `endReason` | When and why it ended: `expired`, `killed`, `failed` or `app-deleted` | | `machineId` | The machine behind the sandbox | | `vcpus`, `memoryMb`, `readiness`, `access`, `metadata`, `egress` | As requested | | `url` | `https://.rytiv.app` | | `ports` | One `{port, url}` per extra port, with `url` like `https://-.rytiv.app` | ## Access With `access: "signed"` (the default), the sandbox's URLs only answer requests that carry a token. The create answer contains one: ```json "accessToken": { "token": "...", "expiresAt": "...", "header": "x-rytiv-access" } ``` Send it in the header named by `header`. It is valid for the lifetime the sandbox was created with, at most 24 hours. With `access: "public"`, anyone with the URL can open it. ## Run a command **CLI** ```bash ry sandbox exec sbx-1a2b3c4d5e6f -- uname -a ``` **curl** ```bash curl -X POST https://api.rytiv.app/v1/sandboxes/sbx-1a2b3c4d5e6f/exec \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{"cmd": ["uname", "-a"]}' ``` **TypeScript** ```ts const res = await fetch(`https://api.rytiv.app/v1/sandboxes/${id}/exec`, { method: "POST", headers: { Authorization: `Bearer ${process.env.RYTIV_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ cmd: ["uname", "-a"] }), }); const { exitCode, stdout, stderr } = await res.json(); ``` `POST /v1/sandboxes/:id/exec` waits for the command and answers with its result. | Field | Type | Default | Notes | | --- | --- | --- | --- | | `cmd` | string or string[] | required | A string runs through `/bin/sh -c`, an array is the argument list (no shell) | | `cwd` | string | | An absolute path | | `env` | object | | More environment for this command | | `timeoutMs` | integer | `60000` | 1000 to 600000, and never past the sandbox's deadline | | `stdin` | string | | Up to 1 MiB | The answer has `exitCode`, `signal`, `timedOut`, `durationMs`, `stdout` and `stderr`. Each output stream is cut at 1 MiB, and `stdoutTruncated` or `stderrTruncated` say so. `ry sandbox exec` exits with the command's own exit code, `124` on a timeout, `128 + n` when signal `n` ended it, and `255` for an error of its own. ## Files | Method | Path | Body and answer | | --- | --- | --- | | `GET` | `/v1/sandboxes/:id/files?path=/abs/path` | The file's bytes | | `PUT` | `/v1/sandboxes/:id/files?path=/abs/path` | Send the bytes. Optional `mode` (default `0644`) and `mkdirs=1`. Answers `{path, bytes, sha256}`. | | `GET` | `/v1/sandboxes/:id/files/list?path=/abs/dir` | `{path, entries}`, each entry `{name, type, size, mode, mtimeMs}` | Paths are absolute and may not contain `..`. One file is at most 64 MiB. ```bash curl -X PUT "https://api.rytiv.app/v1/sandboxes/sbx-1a2b3c4d5e6f/files?path=/tmp/hello.txt" \ -H "Authorization: Bearer $RYTIV_TOKEN" \ --data-binary "hello" ``` ## Network `egress` decides what the sandbox may reach: | `mode` | Meaning | | --- | --- | | `allow-all` | The default | | `deny-all` | Nothing, not even DNS | | `allowlist` | Only the listed `domains` (up to 100, a leading `*.` allowed) and IPv4 `cidrs` (up to 10), on ports 80 and 443 | The policy is fixed for the sandbox's lifetime. ```json { "template": "app:web", "egress": { "mode": "allowlist", "domains": ["api.github.com", "*.npmjs.org"] } } ``` ## Extend, list, end | Method | Path | What it does | | --- | --- | --- | | `POST` | `/v1/sandboxes/:id/timeout` | `{ttlSeconds}`: the new deadline is now plus `ttlSeconds` (10 to 86400). Calling it twice does not add up. | | `GET` | `/v1/sandboxes` | Running sandboxes, newest first. `?state=all` adds the ones that ended in the last 14 days. | | `GET` | `/v1/sandboxes/:id` | One sandbox | | `DELETE` | `/v1/sandboxes/:id` | Ends it now. `204`, or `202` if it is already ending. Its machine and disk are gone. | ```bash ry sandbox timeout sbx-1a2b3c4d5e6f 1h ry sandbox list --all ry sandbox kill sbx-1a2b3c4d5e6f ``` ## Limits and errors An organization can run 100 sandboxes at a time by default; the next create answers `429` with `too-many-sandboxes`. | Status | Code | When | | --- | --- | --- | | `404` | `template-not-found` | No such app or build in your organization | | `409` | `template-not-runnable` | The build is a static site, or the app runs no machine | | `404` | `sandbox-not-found` | No such sandbox in your organization | | `409` | `sandbox-ended`, `sandbox-starting` | The sandbox has ended, or is not running yet | | `429` | `too-many-sandboxes` | The organization's limit is reached | | `503` | `no-capacity` | No room right now, try again | See the [CLI reference](https://docs.rytiv.com/reference/cli/sandbox.md) for every flag of `ry sandbox`. --- # Agents and tokens > Give a coding agent a key of its own, narrow it with scopes or an agent class, connect a hosted client through OAuth, and read the audit of what the agent did. A coding agent works on Rytiv with an API key like any other client. What sets its key apart is that you hand it over without watching every request, so the platform, not the agent, has to know what the key is meant for. Rytiv gives you three tools for that: scopes, agent tokens with a class, and an audit of every judged request. ## One key per agent Create a key for each agent and name it after the agent. A key is shown once; store it where the agent reads it (`RYTIV_TOKEN`, a `.mcp.json` with `${RYTIV_TOKEN}`, your CI's secret store). **CLI** ```bash ry tokens create claude-code --agent sandbox --app web --app web-try ``` **curl** ```bash curl -X POST "https://api.rytiv.app/v1/tokens" \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "claude-code", "agent": {"class": "sandbox", "apps": ["web", "web-try"]}}' ``` **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: "claude-code", agent: { class: "sandbox", apps: ["web", "web-try"] } }), }); const { token } = await res.json(); // shown in this answer only ``` **Python** ```python import os, requests res = requests.post( "https://api.rytiv.app/v1/tokens", headers={"Authorization": f"Bearer {os.environ['RYTIV_TOKEN']}"}, json={"name": "claude-code", "agent": {"class": "sandbox", "apps": ["web", "web-try"]}}, ) token = res.json()["token"] # shown in this answer only ``` Creating a key needs a key with full access; your first one comes from the console or from `ry login`. ## Agent tokens: a class and a list of apps An `agent` block makes the key an agent token. The platform enforces two facts about it on every request: | Class | What the agent may do | | --- | --- | | `sandbox` | Everything an API key may do, inside the apps on its list. For trying things out. | | `twin` | The same, for a per-task copy of production: a preview app, a database branch, a computer. | | `production` | Read only. Status, releases and logs are open; what production holds (files, a shell, query results) needs `prodDataRead: true` on the token. Any change is refused with `agent-token-production-readonly`. | `apps` names the apps the token may touch; a request about another app, or about something that is not an app (a bucket, a volume), is refused with `agent-token-app-scope`. Without the list, the token reaches the whole organization. Whatever the class, an agent token can never mint, reveal or revoke a credential, administer the organization, or act for another organization (`agent-token-credential`, `agent-token-delegation`). An agent that is told "this is production, only read" by its prompt is one injected sentence away from forgetting it; the token does not forget. With `ry tokens create`: `--agent `, `--app ` (repeatable) and `--read-prod-data`. ## Scopes: less than full access Scopes narrow a key the way a CI job or a partner needs it. A key without scopes is the whole organization; a key with scopes can only do less. | Scope | Allows | | --- | --- | | `read` | Every read of the product API that is a status: apps, machines, deploys, databases, buckets, domains, schedules, webhooks. Not credentials, not billing, not the files of a machine. | | `deploy` | The deploy routes of every app: build, deploy, releases, rollback, logs, machines, secrets. Never creating or deleting an app, never a key. | | `deploy:` | The same for exactly that app. The app list shows only the apps the key may deploy. | | `mail:send` | Sending mail from any proven domain of the organization. | | `mail:send:` | Sending from exactly that domain. | ```bash ry tokens create ci-web --deploy web --expires-in 30d ry tokens create status-page --scope read ``` A scoped key cannot open a shell, a tunnel or the console of a machine; those need full access. A route the scope does not cover answers `403` with `token-scope` and names the scope needed. Scopes and an agent class combine; the narrower rule wins. ## Expiry A key may stop working at a moment (`expiresAt`) or after a duration (`ttlSeconds`, in the CLI `--expires-in 12h` or `--expires-at 2027-01-31T00:00:00Z`): at least 60 seconds and at most 365 days ahead. A key with an expiry cannot create a key that outlives it; without an explicit expiry, the new key inherits the creator's. ```bash ry tokens list ry tokens revoke tok-1a2b3c4d ``` Revoking a key rejects requests already using it right away. ## Connect a hosted client with OAuth A client that has no place for a pasted key (a hosted connector) connects to the MCP server through OAuth 2.1: 1. `POST https://api.rytiv.app/v1/mcp` without a key answers `401` with a `WWW-Authenticate` header that names the discovery document `/.well-known/oauth-protected-resource/v1/mcp`. 2. The client reads `/.well-known/oauth-authorization-server`, registers itself at `POST /v1/oauth/register` (no client secret; every client is a public client bound to PKCE), and sends you to `GET /v1/oauth/authorize`. 3. You land on `https://console.rytiv.com/connect`, signed in, and approve the client for the active team. 4. The client exchanges its code at `POST /v1/oauth/token` and receives an access token (valid 1 hour) and a refresh token (30 days, renewed on use). The access token is an ordinary key of the organization, named after the client, and appears under **Team, Tokens & keys** in the console, where you revoke it. Scopes of the OAuth server: `mcp` and `offline_access`. In Claude Code, Codex and Cursor you do not need OAuth: add the server with your key as a bearer header, see the [quickstart](https://docs.rytiv.com/quickstart.md). ## Read what the agent did Every request judged for an agent token is recorded: the route, the verdict, the token. ```bash curl "https://api.rytiv.app/v1/agent-actions?limit=100" \ -H "Authorization: Bearer $RYTIV_TOKEN" ``` `?token=` narrows the list to one key. Reading the audit needs a key with full access. ## Confirmations A change in production by an agent waits for a person. The console carries the page for it (`/confirm/`), and the platform refuses the change until the confirmation exists; in this release the refusal stands, and the person makes the change with their own key or in the console. See [Confirmations](https://docs.rytiv.com/concepts/confirmations.md). --- # Credit and pricing > Rytiv is paid from prepaid credit in euros. Every account admitted from the waitlist starts with 100 EUR; usage is drawn from the credit as it runs. Prices, top-ups, auto-recharge, and what happens at zero. Rytiv is pay as you go from **prepaid credit in euros**. Every account admitted from the waitlist starts with **100 EUR of credit**. There are no plans and no seats: a machine pays for memory while it is awake and for CPU only while it is computing, storage is priced per gigabyte and month. Nothing is invoiced later. ## Prices Introductory prices until 31 January 2027. Any change is announced at least 30 days in advance. All prices in EUR, excluding VAT. The pricing page on rytiv.com is the reference; this page repeats it. | What | Price | How it is counted | | --- | --- | --- | | Memory | 0.0076 EUR per GB-hour | Only while the machine is awake. A sleeping machine keeps its memory as a snapshot, billed at the snapshot rate. | | CPU | 0.035 EUR per vCPU-hour | Active CPU time only. Idle time is not charged. | | Disk (volumes) | 0.10 EUR per GB-month | The volumes your machines, computers and databases keep their data on | | Object storage | 0.022 EUR per GB-month | S3-compatible buckets | | Snapshots and templates | 0.08 EUR per GB-month | Saved machine snapshots, and the templates new machines and sandboxes start from | A Postgres database, a computer and a sandbox each run as a machine and are billed like one, by memory, CPU and disk. There is no separate price for any of them. **Included at no charge:** builds on Rytiv's builders, TLS certificates and custom domains, outbound data transfer, and team members. Fair use applies. ## Limits on the starting credit An account on the starting credit can have **2 computers** and **10 apps** at a time, computers included. The limit is checked when something is created, never when it wakes or starts: what you have keeps running. The next create answers `403` with `org-limit-computers` or `org-limit-apps`, the body carries `limit` and `current`, and the sentence says what to delete or that you can contact us for more. Every other bound is on [Limits and quotas](https://docs.rytiv.com/concepts/limits.md). The starting credit is granted once per person. An organization without it begins with a balance of zero and is blocked until its first top-up. ## When the credit is used up The balance reaching zero **stops** your machines; nothing is deleted, disks and volumes stay. Reads keep working, so logs, lists and the console still answer, and nothing new starts: a request that would start or create something answers `402` with the code `credit-blocked`. A top-up or an automatic recharge lifts the block, and the machines the block stopped are started again. Before that, the account carries `warned` once the balance falls to 20 percent of the reference top-up (the auto-recharge amount if you set one, otherwise your last top-up), and the console shows it. ## Top up Credit is topped up in the Rytiv console by an admin of the team, under **Credits & usage**: a one-time top-up by card payment (10 EUR to 1,000 EUR), or auto-recharge with a threshold, an amount (10 to 500 EUR) and a monthly ceiling (at least the amount, at most 2,000 EUR). Card details are entered on the payment provider's page and never reach Rytiv. A failed automatic charge is retried after 15 minutes. Top-ups cannot be made over the API or with the CLI: the routes behind the console page (`POST /v1/billing/topups`, `PUT /v1/billing/auto-recharge`, `POST /v1/billing/portal`) take only the console's signed proof of an admin, and a plain API key answers `403` with `billing-console-only`. ## Read your balance **curl** ```bash curl "https://api.rytiv.app/v1/billing/credits" \ -H "Authorization: Bearer $RYTIV_TOKEN" ``` **TypeScript** ```ts const res = await fetch("https://api.rytiv.app/v1/billing/credits", { headers: { Authorization: `Bearer ${process.env.RYTIV_TOKEN}` }, }); const { balanceCents, blocked, warned } = await res.json(); ``` **Python** ```python import os, requests account = requests.get( "https://api.rytiv.app/v1/billing/credits", headers={"Authorization": f"Bearer {os.environ['RYTIV_TOKEN']}"}, ).json() print(account["balanceCents"], account["blocked"]) ``` **CLI** ```bash ry billing preview ry usage --from 2026-10-01T00:00:00Z --to 2026-11-01T00:00:00Z ``` | Method | Path | What it does | | --- | --- | --- | | `GET` | `/v1/billing/credits` | `balanceCents` (and the exact `balanceMicros` as a string), `blocked`, `blockedAt`, `warned`, the auto-recharge settings and `prepaid` (whether the organization is on prepaid credit at all) | | `GET` | `/v1/billing/ledger` | The ledger, newest first: top-ups, usage bookings, grants, refunds and adjustments. `?limit=` up to 500. | | `GET` | `/v1/billing/preview` | What a period cost, line by line, from the metered usage; `ry billing preview` prints it | All three only read. A key narrowed by [scopes](https://docs.rytiv.com/authentication.md#narrow-a-key-scopes) cannot reach them (money stays with full keys); an [agent key](https://docs.rytiv.com/authentication.md#agent-keys) can read them. Every field is in the [API reference](https://docs.rytiv.com/reference/api/billing.md). ## Region At launch Rytiv has one region, in the EU. Your apps, databases and files stay there; see [Regions](https://docs.rytiv.com/concepts/regions.md). --- # Custom domains and TLS > Put your own domain on an app. Add it, set the DNS records the answer names, and Rytiv proves it, issues the certificate and renews it. Every app answers on `https://.rytiv.app` with a certificate from the start. A custom domain is added to an app, proven by a DNS record, pointed at Rytiv, and gets its own certificate. Wildcards are not accepted for a single domain; a whole zone can be proven once instead. ## 1. Add the domain **CLI** ```bash ry domains add web www.example.com ``` **curl** ```bash curl -X POST "https://api.rytiv.app/v1/apps/web/domains" \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{"domain": "www.example.com"}' ``` **TypeScript** ```ts const res = await fetch("https://api.rytiv.app/v1/apps/web/domains", { method: "POST", headers: { Authorization: `Bearer ${process.env.RYTIV_TOKEN}`, "Content-Type": "application/json" }, body: JSON.stringify({ domain: "www.example.com" }), }); const { status, dns } = await res.json(); // dns.txt and dns.point: the records to set ``` **Python** ```python import os, requests domain = requests.post( "https://api.rytiv.app/v1/apps/web/domains", headers={"Authorization": f"Bearer {os.environ['RYTIV_TOKEN']}"}, json={"domain": "www.example.com"}, ).json() print(domain["status"], domain["dns"]) ``` The answer is the domain in state `pending_dns` with the records to set. ## 2. Set the DNS records Two things have to be true: you own the domain, and it points at Rytiv. | Record | Name | Value | Proves | | --- | --- | --- | --- | | `TXT` | `_rytiv.www.example.com` | `rytiv-verify-` from the answer | ownership | | `CNAME` | `www.example.com` | `.rytivdns.com` (or `web.rytiv.app`) | ownership and traffic | | `A` | `example.com` | the ingress addresses from the answer | ownership and traffic, for an apex | A subdomain needs the CNAME (the TXT record is offered as well); an apex such as `example.com` cannot carry a CNAME and gets A records instead. The exact names and values are in the answer of `ry domains add` and of `GET /v1/apps/:name/domains`; copy them from there. ## 3. Wait, or check now Rytiv checks the records every 30 seconds at first, then with a growing gap up to an hour. `ry domains check web www.example.com` asks for a check right away. | Status | Meaning | | --- | --- | | `pending_dns` | Waiting for the records. | | `verified` | Ownership is proven; the certificate is being ordered. | | `cert_issued` | The certificate exists. | | `active` | The certificate exists and a machine of the app is ready to answer. | | `error` | The certificate could not be issued; `error` says why. A missing DNS record never leads here, it keeps the domain `pending_dns`. | ```bash ry domains list web ``` The list shows the status, the days left on the certificate and the records still missing. ## Certificates Rytiv orders the certificate over ACME with an HTTP challenge that the edge answers, as soon as ownership is proven. It is renewed when fewer than 30 days remain. The default address `https://.rytiv.app` is covered by the platform's own certificate. Port 80 answers a redirect to HTTPS. ## Redirect one domain to another ```bash ry domains redirect web example.com www.example.com ry domains redirect web example.com --off ``` The first domain then answers `308` to the second, keeping path and query. The target must be another domain of the same app or its default address; a target cannot itself redirect, and a domain others point at cannot start redirecting, so there are no chains and no loops. ## Prove a whole zone once ```bash ry domains zones add example.com ``` The answer names two CNAME records: `_acme-challenge.example.com` to a challenge name Rytiv gives you, and `*.example.com` to Rytiv's ingress. Once they resolve, every name directly below the zone (`app.example.com`, `api.example.com`) is yours without a record of its own and is covered by one wildcard certificate; `ry domains add web app.example.com` then needs no DNS change. Only one level below the zone is covered. ## Remove ```bash ry domains remove web www.example.com ``` The CLI asks you to type the name; pass `--yes` in scripts. The certificate is not revoked, the domain simply stops routing. ## Errors you will meet | Status | Code | When | | --- | --- | --- | | `400` | `bad-request` | The name is not a domain: a wildcard, an IP, a single label, a platform domain. The sentence says which. | | `409` | `shadows-default-address` | The domain is the default address of an app. | | `409` | `mail-domain` | The name is a sending domain of a zone and cannot become an app domain. | | `409` | `conflict` | The domain is already claimed by someone. | --- # Buckets > S3-compatible object storage. Create a bucket, upload and download with presigned URLs, attach it to an app for direct access, set CORS for browser uploads. A bucket holds objects and speaks the S3 protocol. Your app reaches it in two ways: ask the Rytiv API for a presigned URL per object, or attach the bucket to the app, which gives it a key scoped to that bucket and the `S3_*` environment variables to use any S3 client directly. ## 1. Create a bucket **CLI** ```bash ry bucket check my-uploads ry bucket create my-uploads ``` **curl** ```bash curl -X POST "https://api.rytiv.app/v1/buckets" \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "my-uploads"}' ``` **TypeScript** ```ts await fetch("https://api.rytiv.app/v1/buckets", { method: "POST", headers: { Authorization: `Bearer ${process.env.RYTIV_TOKEN}`, "Content-Type": "application/json" }, body: JSON.stringify({ name: "my-uploads" }), }); ``` **Python** ```python import os, requests requests.post( "https://api.rytiv.app/v1/buckets", headers={"Authorization": f"Bearer {os.environ['RYTIV_TOKEN']}"}, json={"name": "my-uploads"}, ) ``` Bucket names are 3 to 63 characters of lowercase letters, digits and hyphens, unique across Rytiv; names starting with `rytiv` are reserved. `GET /v1/bucket-names/:name` (`ry bucket check`) tells you whether a name is free and, if not, why. ## 2. Upload and download with presigned URLs A presigned URL is valid for one object and one method, for a limited time (default 15 minutes, at most 7 days). Nobody who holds it needs a key. **CLI** ```bash ry bucket presign my-uploads reports/2026-10.pdf --put --content-type application/pdf ry bucket presign my-uploads reports/2026-10.pdf ``` **curl** ```bash curl -X POST "https://api.rytiv.app/v1/buckets/my-uploads/presign" \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{"op": "put", "key": "reports/2026-10.pdf", "contentType": "application/pdf", "expiresInS": 900}' ``` **TypeScript** ```ts const res = await fetch("https://api.rytiv.app/v1/buckets/my-uploads/presign", { method: "POST", headers: { Authorization: `Bearer ${process.env.RYTIV_TOKEN}`, "Content-Type": "application/json" }, body: JSON.stringify({ op: "put", key: "reports/2026-10.pdf", contentType: "application/pdf" }), }); const { url, method, requiredHeaders } = await res.json(); await fetch(url, { method, headers: requiredHeaders, body: fileBytes }); ``` **Python** ```python import os, requests signed = requests.post( "https://api.rytiv.app/v1/buckets/my-uploads/presign", headers={"Authorization": f"Bearer {os.environ['RYTIV_TOKEN']}"}, json={"op": "put", "key": "reports/2026-10.pdf", "contentType": "application/pdf"}, ).json() with open("2026-10.pdf", "rb") as f: requests.put(signed["url"], headers=signed["requiredHeaders"], data=f) ``` The answer carries `url`, `method`, `expiresAt` and `requiredHeaders`: headers the call must send exactly as given, because they are part of the signature. For a `put` with `contentType`, that is the `Content-Type` the upload must use. Keys are at most 1024 bytes, with no leading or trailing `/`, no empty, `.` or `..` segments. ## 3. Attach the bucket to an app ```bash ry bucket attach my-uploads --to web ry bucket attach my-uploads --to web --as uploads ``` Attaching gives the app a key that opens exactly this bucket (read and write, no owner rights) and injects it as managed secrets. From its next start the app sees: | Variable | Meaning | | --- | --- | | `S3_BUCKET` | The bucket's name | | `S3_ENDPOINT` | The S3 endpoint to talk to, `https://s3..` | | `S3_REGION` | The region name to sign with | | `S3_ACCESS_MODE` | `key`: the app holds a key and speaks S3 directly | | `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY` | The key scoped to this bucket | With `--as uploads` every variable gets the prefix `UPLOADS_` (`UPLOADS_S3_BUCKET`, ...); from the second bucket on an app the prefix is required. The key is shown once, in the attach answer; attaching again rotates it. `ry bucket detach my-uploads --from web` removes the variables and revokes the key; the objects stay. The endpoint supports virtual-host addressing (`.s3..`). With the AWS SDK for JavaScript 3.729 or newer, set `requestChecksumCalculation: "WHEN_REQUIRED"`, or presigned uploads fail with `InvalidDigest`. ## 4. Allow uploads from the browser A browser that uploads straight to a presigned URL sends a preflight first; without a CORS rule the storage refuses it, and nothing of it reaches your server. ```bash ry bucket cors-set my-uploads --allow-origin https://www.example.com ry bucket cors my-uploads ``` `ry bucket cors-set` writes one rule with the given origins, methods `GET,PUT,HEAD`, headers `*,authorization`, and `ETag` exposed, so an upload client can read the answer of its own `PUT`. `PUT /v1/buckets/:name/cors` takes the whole list of rules and replaces what was there; an empty list removes them. CORS is not authentication: the presigned URL's signature is what protects the object. ## 5. List and delete objects ```bash ry bucket ls my-uploads --prefix reports/ ry bucket rm my-uploads reports/2026-10.pdf ``` `GET /v1/buckets/:name/objects?prefix=&limit=&cursor=` lists up to 1000 objects per page (default 100) with a `nextCursor` for the next page. `DELETE /v1/buckets/:name/objects?key=` deletes one object; the key goes in the query string. ## 6. Delete the bucket ```bash ry bucket destroy my-uploads ry bucket destroy my-uploads --force ``` A bucket that still holds objects is refused with `409`; `--force` (`?force=1` on `DELETE /v1/buckets/:name`) deletes the objects first, then the attachments' variables, then the bucket. The CLI asks you to type the name; pass `--yes` in scripts. --- # Cron and jobs > Run something of an app on a schedule, by calling a path or by running a command on a machine of its own, and run one-off commands as jobs with their exit code and output kept. A schedule runs on a cron expression. It either wakes the app and calls a path of it, signed so the app can verify the call, or it runs a command on a machine of its own, built from the app's image and environment. A job is that same machine without a schedule: one command, run once. ## A path on a schedule For frequent, light work the app does itself, down to every minute. **CLI** ```bash ry cron add web "*/15 * * * *" --path /api/tick ``` **curl** ```bash curl -X POST "https://api.rytiv.app/v1/apps/web/schedules" \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{"schedule": "*/15 * * * *", "path": "/api/tick", "method": "POST"}' ``` **TypeScript** ```ts await fetch("https://api.rytiv.app/v1/apps/web/schedules", { method: "POST", headers: { Authorization: `Bearer ${process.env.RYTIV_TOKEN}`, "Content-Type": "application/json" }, body: JSON.stringify({ schedule: "*/15 * * * *", path: "/api/tick", method: "POST" }), }); ``` **Python** ```python import os, requests requests.post( "https://api.rytiv.app/v1/apps/web/schedules", headers={"Authorization": f"Bearer {os.environ['RYTIV_TOKEN']}"}, json={"schedule": "*/15 * * * *", "path": "/api/tick", "method": "POST"}, ) ``` The expression has five fields (minute, hour, day of month, month, day of week) or is `@hourly`, `@daily` and the like, read in the `timezone` you name (default `UTC`, for example `Europe/Berlin`). `method` is `GET` (default) or `POST`. Each attempt has `timeoutSec` (default 60, at most 240). ### Verify the call A path is called through the app's address with these headers: | Header | Value | | --- | --- | | `Rytiv-Cron-Signature` | `v1=`: HMAC-SHA256 of `.` under the app's secret `RYTIV_CRON_SECRET` | | `Rytiv-Cron-Timestamp` | Unix seconds when the call was signed | | `Rytiv-Cron-Run` | The run's id, the same across retries | | `Rytiv-Cron-Schedule` | The schedule's id | | `Rytiv-Cron-Scheduled-For` | The slot the run is for | `` is the path with its query, exactly as sent. The secret `RYTIV_CRON_SECRET` is created as a managed secret with the app's first path schedule; **the app sees it from its next deploy**, and a run before that fails rather than being sent unsigned. ```ts import { createHmac, timingSafeEqual } from "node:crypto"; export function cronCallIsGenuine(req: Request, path: string): boolean { const ts = req.headers.get("rytiv-cron-timestamp") ?? ""; const sig = req.headers.get("rytiv-cron-signature") ?? ""; if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; const expected = `v1=${createHmac("sha256", process.env.RYTIV_CRON_SECRET!).update(`${ts}.${path}`).digest("hex")}`; return expected.length === sig.length && timingSafeEqual(Buffer.from(expected), Buffer.from(sig)); } ``` A call is retried up to three times (after 5 s, then 10 s) on a network error or a `5xx`; a `4xx` or a redirect is not retried. A slot that comes while the previous run of the same schedule still runs is skipped, not queued; a slot more than 10 minutes late counts as missed. ## A command on a schedule For heavy, rare work: at most hourly, and every run gets a machine of its own, sized as you say. ```bash ry cron add web "0 3 * * *" --timezone Europe/Berlin --command "npm run cleanup" --memory 8192 ry cron add web "@daily" -- node scripts/report.js --full ``` `--command` runs through `/bin/sh -c`; the form after `--` is the argument list without a shell. The machine runs the app's image with its secrets and environment, and the app's size unless `--memory` or `--vcpus` say otherwise. A run has `timeoutSec` (default 900, at most 3600). The minute field of the expression must name one minute. ## Manage schedules ```bash ry cron list web ry cron runs web sch-1a2b3c4d ry cron run web sch-1a2b3c4d ry cron pause web sch-1a2b3c4d ry cron resume web sch-1a2b3c4d ry cron rm web sch-1a2b3c4d ``` An app has at most 20 schedules; the last 100 runs of each are kept. `ry cron run` starts a run now, outside the expression (also on a paused schedule); resuming continues from the next slot, nothing missed is caught up. A run is `running`, `succeeded`, `failed`, `skipped` or `missed`. Removing a schedule takes its history with it. ## Jobs: one command, once **CLI** ```bash ry job run web -- npm run migrate ry job run web --memory 4096 --timeout 1800 -- node scripts/report.js ``` **curl** ```bash curl -X POST "https://api.rytiv.app/v1/apps/web/jobs" \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{"command": ["npm", "run", "migrate"], "timeoutSec": 900}' ``` **TypeScript** ```ts const res = await fetch("https://api.rytiv.app/v1/apps/web/jobs", { method: "POST", headers: { Authorization: `Bearer ${process.env.RYTIV_TOKEN}`, "Content-Type": "application/json" }, body: JSON.stringify({ command: ["npm", "run", "migrate"] }), }); const job = await res.json(); // { id, state: "queued", ... } ``` **Python** ```python import os, requests job = requests.post( "https://api.rytiv.app/v1/apps/web/jobs", headers={"Authorization": f"Bearer {os.environ['RYTIV_TOKEN']}"}, json={"command": ["npm", "run", "migrate"]}, ).json() ``` A job runs the command on a machine of its own with the app's image, secrets and environment, and answers `202` with the job. `ry job run` streams the output and exits with the command's own exit code (1 for `failed` or `timeout`); Ctrl-C leaves the job running. Poll `GET /v1/jobs/:id` until `state` is `done`, `failed` or `timeout`: | State | Meaning | | --- | --- | | `queued` | The machine is being placed. | | `running` | The command runs; `startedAt` is set. | | `done` | The command ran to its end; `exitCode` holds its verdict (a failing command is `done` with a non-zero code). | | `failed` | It did not run to the end: placement failed, the machine died, or the job was cancelled. `exitCode` is `null`, the reason is the last line of `output`. | | `timeout` | `timeoutSec` ended it. | "Green" means `state === "done" && exitCode === 0`. `output` keeps the first 1 MiB. In the API, `env` adds environment for this job, `links` names apps the job's machine may reach, `build` runs a specific build of the app; the CLI offers `-e`, `-t`, `-m` and `-c`. ```bash ry job list web ry job get job-1a2b3c4d ry job cancel job-1a2b3c4d ``` Cancelling (`DELETE /v1/jobs/:id`) ends a running job; its machine is gone after `finishedAt`, the job record stays. --- # How Rytiv works > The building blocks your agent works with, how a request becomes a running machine, what sleeps and what stays, and the project model that comes next. Everything on Rytiv belongs to an **organization** (a team in the console). An API key acts as one organization, and every name it uses is looked up inside that organization only. ## The building blocks **Apps** are the unit everything else hangs off. An app has a name that is unique across Rytiv and answers on `https://.rytiv.app`. It runs one or more **machines** from an image you deploy, or it is a static site served at the edge without a machine. Secrets, deploys, releases, logs, domains, schedules and jobs belong to an app. See [Apps](https://docs.rytiv.com/apps.md). **Computers** are apps of a special kind: one machine from the computer image, a home disk at `/data` that outlives the machine, and a desktop you can watch or take over. They are the place an agent works in. See [Computers](https://docs.rytiv.com/computers.md). **Sandboxes** are machines with a deadline. They start from a template (the image an app of yours runs, or a build), run commands, hold files, and are removed at their deadline. See [Sandboxes](https://docs.rytiv.com/sandboxes.md). **Postgres** databases run on machines of their own with their data on a volume. Every database can be branched (a copy-on-write copy, ready in seconds) and forked (a full copy from its archive, from any moment in the last 7 days). See [Postgres](https://docs.rytiv.com/postgres.md). **Volumes** are disks that outlive machines; **buckets** are S3-compatible object storage; **custom domains** put your names on an app. See the [guides](https://docs.rytiv.com/guides/buckets.md). ## Machines A machine is one virtual machine with its own kernel. It is `requested`, `scheduled`, `starting`, `running`, `suspending`, `suspended`, `stopping`, `stopped`, `deleted` or `failed`. A deploy creates machines for an app one at a time: the new machine must boot and answer on its port (and pass the health check, if set) before an old one is taken out of traffic; if it fails, the old version keeps serving. A machine that **declares a port** gets no traffic until that port answers. This is a promise, not only behaviour: a migration that runs before the app starts listening never sees traffic against a schema it does not expect. ## What sleeps and what stays An app sleeps only if you give it a sleep rule (`--sleep 5m`); a new app never sleeps on its own. A machine up to 2048 MB sleeps with its memory and resumes in well under a second on the next request; a larger machine stops instead and boots cold, its disk kept. A computer shuts down after 10 minutes without activity; its home disk stays. A database is suspended after 5 minutes without a query and woken by the next connection, where the region's hosts act on the policy. What stays through a stop, a replacement or a host failure: a volume, a computer's home, a database's data and archive, a bucket's objects, an agent machine's checkpoints. What does not: a machine's root disk (it is the image's), open programs, and anything on a sandbox at its deadline. ## Three ways in | Way | For | Where | | --- | --- | --- | | MCP server | Your coding agent | `https://api.rytiv.app/v1/mcp`, see the [quickstart](https://docs.rytiv.com/quickstart.md) | | CLI `ry` | You, in a terminal or in CI | [CLI reference](https://docs.rytiv.com/reference/cli.md) | | REST API | Your own code | [API reference](https://docs.rytiv.com/reference/api.md) | All three use the same API and the same keys, and they see the same apps, databases and machines. The console is where you sign in, create keys, watch a computer and top up credit. ## Where it runs Your apps, databases and files stay in the region of your workspace, in the EU at launch, and a machine never leaves its region. See [Regions](https://docs.rytiv.com/concepts/regions.md) and [Security and data](https://docs.rytiv.com/concepts/security.md). Every bound the platform enforces is on [Limits and quotas](https://docs.rytiv.com/concepts/limits.md); every error code on the [error catalog](https://docs.rytiv.com/concepts/errors.md). ## The project model Rytiv is becoming the place where your agent builds a whole project: you connect it once and say "build project XY on Rytiv". Each project has three parts with three levels of risk: - a [sandbox](https://docs.rytiv.com/concepts/sandbox.md) where the agent works freely, - a [twin](https://docs.rytiv.com/concepts/twins.md) for every task, a disposable copy of production, - [production](https://docs.rytiv.com/concepts/production.md), which serves your users. Reversible steps run without asking. Only an irreversible step waits for a [confirmation](https://docs.rytiv.com/concepts/confirmations.md) with your passkey. Projects as an object of the API are not available yet; the pieces (agent tokens with a class, database branches, preview apps, computers, automatic rollback after a deploy) are, and [Projects](https://docs.rytiv.com/concepts/projects.md) says how they fit together today. --- # Projects > A project holds everything one product needs on Rytiv: its sandbox, its twins and its production, so your agent can build it end to end. > **Not available yet:** Projects are not an object of the API yet. This page describes how they will work, so you can plan for them. Today your agent works with [apps](https://docs.rytiv.com/apps.md), [computers](https://docs.rytiv.com/computers.md), [sandboxes](https://docs.rytiv.com/sandboxes.md) and [Postgres](https://docs.rytiv.com/postgres.md) directly, and an [agent token](https://docs.rytiv.com/guides/agents-and-tokens.md) with a class and a list of apps draws the line between them. ## One sentence to start Connect your coding agent once, then ask it to "build project XY on Rytiv". The agent creates the project and works inside it. You do not set up environments, databases or preview addresses by hand. ## Three parts, three levels of risk | Part | What it is for | What the agent may do | | --- | --- | --- | | [Sandbox](https://docs.rytiv.com/concepts/sandbox.md) | Trying things | Everything | | [Twin](https://docs.rytiv.com/concepts/twins.md) | One task, against a copy of production | Everything, on the copy | | [Production](https://docs.rytiv.com/concepts/production.md) | Serving your users | Reversible steps; irreversible ones wait for you | ## The rule **Reversible steps run without asking.** A deploy that can be rolled back, a branch that can be dropped, a secret that can be changed again: none of these stop your agent. **Only irreversible steps need a tap.** Deleting a production database or dropping a table waits until you confirm it with your passkey. See [Confirmations](https://docs.rytiv.com/concepts/confirmations.md). The platform enforces this, not the agent: the key your agent holds cannot confirm its own request. --- # Project sandbox > The part of a project where your agent may do anything: build, break and try again. > **Not available yet:** The project sandbox comes with [projects](https://docs.rytiv.com/concepts/projects.md). It is not the same as the [Sandboxes](https://docs.rytiv.com/sandboxes.md) you can create today: those are short-lived machines for one task, and a project sandbox will use them. An agent token of class `sandbox` is how you give an agent this freedom today. ## What it is for The sandbox is the part of a project where nothing real is at stake. Your agent has full rights there: it can create and delete machines and databases, deploy, and start over, without asking you. ## What you can use today The pieces exist already: - [Sandboxes](https://docs.rytiv.com/sandboxes.md) for short-lived machines with a deadline. - [Computers](https://docs.rytiv.com/computers.md) for a machine whose files stay between sessions. - [Postgres branches](https://docs.rytiv.com/postgres.md#branch-a-database) for databases you can throw away. --- # Twins > A twin is a disposable copy of production for one task, with its own database branch, its own preview address, its own machine. > **Not available yet as one call:** Twins come with [projects](https://docs.rytiv.com/concepts/projects.md). Your agent will create one per task with a single tool call. Every piece of a twin exists today and can be assembled by hand, see below; an agent token of class `twin` is the key for it. ## What a twin is For every task, your agent gets a twin of production: - a **branch of the production database**, with an expiry, so the data is real but the original is never touched, - a **preview app** that only people with a signed link can open, - a **sandbox or computer** whose `DATABASE_URL` points at the branch. The agent can do anything inside a twin. When the task is done, the result goes to production with a promote step, and the twin goes away. ## Building one by hand today Every piece of a twin exists already, so you or your agent can assemble one today: 1. Branch the database with an expiry: [`create_branch`](https://docs.rytiv.com/reference/mcp-tools.md#create_branch), or `ry postgres branch` with `--expires-in`. 2. Deploy the change to an app of its own, and protect it with `ry protect signed `. 3. Attach the branch to that app with `ry attach --to `. The app gets the branch's connection URL as its `DATABASE_URL`. Rytiv's MCP tools never return a database password, and the connection URL of a branch is handed out only once, to whoever asks for it first. See [Postgres](https://docs.rytiv.com/postgres.md#connection-url-of-a-copy). --- # Production > The part of a project that serves your users. Your agent reads it freely and changes it only in ways that can be undone. > **Partly available:** Production as a part of a [project](https://docs.rytiv.com/concepts/projects.md) is not an object of the API yet. What exists today: an [agent token](https://docs.rytiv.com/guides/agents-and-tokens.md) of class `production` may only read, and a deploy that makes the app worse is rolled back on its own. The platform does not yet know which app is production; the token's class and app list say it. ## What your agent may do - **Read** without asking: logs, releases, and read-only SQL. - **Change** what can be undone: a deploy that can be rolled back, a promote from a twin. - **Ask you** before anything irreversible, see [Confirmations](https://docs.rytiv.com/concepts/confirmations.md). Whether an agent may read production data at all will be a setting, because production holds your customers' data. ## Safety nets - **Watching after a deploy.** For five minutes after the switch, Rytiv compares the app's error rate (the share of `5xx` answers, with at least 20 requests to judge) with the rate before. If it is clearly worse, the deploy is rolled back on its own; the verdict is in the deploy's `watch` field. - **The database is never rolled back automatically.** A point-in-time restore is offered instead, and the state before a restore is kept as a branch. - **A restore point before every production migration** comes with projects. ## What exists today - [Rollbacks](https://docs.rytiv.com/apps.md#releases-and-rollback) of an app to an earlier release, without a rebuild, by hand and automatically after a bad deploy. - [Point-in-time restore](https://docs.rytiv.com/postgres.md#restore-in-place) of a database in place, with the old state kept as a branch. - [Read-only SQL](https://docs.rytiv.com/reference/mcp-tools.md#execute_sql) for your agent: without a branch, `execute_sql` runs in a read-only transaction. - An [agent token](https://docs.rytiv.com/guides/agents-and-tokens.md) of class `production`, which may read and nothing else. --- # Confirmations > Irreversible steps wait for one tap with your passkey. Everything else runs without asking. > **Not available yet:** Confirmation requests are not an object of the API yet. This page describes how they will work. Today the platform draws the line with the agent token's class: class `production` may only read, and a change it asks for is refused with `agent-token-production-readonly` until a person makes it with their own key or in the console. ## Why only irreversible steps An agent that asks before every action is an agent nobody uses. So Rytiv sorts every action into two kinds: - **Reversible**: it can be undone. It runs right away. - **Irreversible**: it cannot be undone. It waits for you. ## What counts as irreversible The planned list: - deleting a production database, volume or app, - `DROP` or `TRUNCATE` in production, - moving a domain, - sending mail to many real recipients, - going over your budget. ## How a confirmation works 1. Your agent asks for the step. Rytiv does not run it, it creates a confirmation request with a number, what will happen, and a preview link. 2. You open the request and confirm it with your passkey, or reject it. 3. Only then does the step run. A confirmation can never be given with the agent's own key. The approval comes from you, not from the conversation. --- # Limits and quotas > Every bound the platform enforces, in one place. Organization limits, machine sizes, build machines, sandboxes, databases, tokens, schedules, jobs, domains, buckets and rate limits. These are the bounds the API enforces. A request outside them answers `400` with `bad-request` and the field named, or `403` and `429` where the table says so. Where a number can be raised for you, the table says "contact us". ## Organization | Limit | Value | When reached | | --- | --- | --- | | Computers | 2 | `403` with `org-limit-computers`; contact us for more | | Apps, computers included | 10 | `403` with `org-limit-apps`; contact us for more | | Sandboxes running at once | 100 | `429` with `too-many-sandboxes` | | Webhooks | 20 | | | Starting credit | 100 EUR, once per person | see [Credit and prices](https://docs.rytiv.com/billing.md) | The computer and app limits apply to organizations created in the console on starting credit. Only creation is limited: a computer or app that exists keeps running whatever its state, and waking it never counts. Database backing apps, sandboxes and previews do not count as apps. ## Machines | Setting | Range | Default | | --- | --- | --- | | Memory (`memoryMb`) | 128 to 16384 | 512 for a new app, 1024 suggested for Next.js | | vCPUs (`vcpus`) | 1 to 16 | 1 | | Disk (`diskMb`) | 512 to 131072 | the image's size plus headroom | | Volume (`volumeGb`) | 1 to 500 | | | Machines per app (`count`) | 1 to 20; exactly 1 with a volume | 1 | | Internal port | 1 to 65535 | 3000 | | Start command | up to 2000 characters | the image's or the detected one | | Kill timeout (`killTimeoutS`) | 1 to 300 | 30 | | Sleep after idle (`idleSeconds`) | 10 to 86400 | 300 (only if a sleep rule is chosen) | | Stop after sleeping (`stopAfterSeconds`) | greater than `idleSeconds`, up to 7 days | | | Sleeps with its memory | machines up to 2048 MB; larger ones stop instead | | | Health check `maxWaitMs` | 1000 to 300000 | 60000, 120000 with a volume | | Health probe `timeoutMs`, `intervalMs`, `healthyThreshold` | 100 to 30000, 100 to 10000, 1 to 10 | 2000, 500, 1 | | Restart retries | 1 to 10 | 3 | | App name | 2 to 63 characters, `a-z`, `0-9`, `-`, starting with a letter | | | Reserved app names | `api`, `console`, `s3`, `admin`, `app`, `db`, `docs`, `internal`, `mail`, `pub`, `ssh`, `status`, `www`, and `rytiv` or `rytiv-*` | | ## Computers | Size | vCPU | Memory | Home disk | | --- | --- | --- | --- | | `small` | 1 | 2304 MB | 50 GB | | `base` (default) | 2 | 4096 MB | 100 GB | | `large` | 4 | 8192 MB | 128 GB | A computer shuts down after 10 minutes without activity; its home stays. Computer names are 2 to 31 characters. ## Builds | Setting | Value | | --- | --- | | Build machine | 4096 MB, 4 vCPU | | Build machine for Next.js and Dockerfile projects | 8192 MB, 4 vCPU | | `--build-memory` | 1024 to 16384 MB | | Build timeout | 8 minutes; 15 for Next.js, 20 for a Dockerfile | | Build environment (the secrets a build sees) | 96 KiB encoded, else `413` | | Git deploy archive | 200 MB compressed, 2 GiB unpacked | | Static site | 1 GiB and 100,000 files | | Builds kept per app | 3 | ## Sandboxes | Setting | Range | Default | | --- | --- | --- | | Lifetime (`ttlSeconds`) | 10 to 86400 | 300 | | Memory, vCPUs, disk | as a machine | 512 MB, 1 vCPU | | Environment variables | up to 128, values up to 32768 characters | | | Extra ports | up to 8 | | | Metadata entries | up to 16 | | | Exec timeout (`timeoutMs`) | 1000 to 600000 | 60000 | | Exec output, stdin | 1 MiB each | | | One file | 64 MiB | | | Ended sandboxes listed for | 14 days | | ## Postgres | Setting | Range | Default | | --- | --- | --- | | Memory | 128 to 16384 MB | 1024 | | Volume (`sizeGb`) | 4 to 500 GB for Postgres | 10 | | Branches per database | 15 | | | Expiry of a branch or fork | 60 seconds to 30 days | none | | Fork from a point in time | within the archive, 7 days | | | Sleep after idle | 10 seconds to 1 day; second stage up to 7 days | 300 s, stop after 1 day | ## Tokens | Setting | Range | | --- | --- | | Name | 1 to 120 characters | | Scopes per key | up to 20 | | Expiry | 60 seconds to 365 days; never later than the key that creates it | | Apps on an agent token | 1 to 50 | | SSH tokens | 60 seconds to 72 hours, default 1 hour | ## Snapshots Labels are 1 to 63 characters of letters, digits, `.`, `-` and `_`, starting with a letter or digit; `auto-` is reserved. Automatic snapshots of agent machines are taken hourly; the newest per hour is kept for 24 hours, the newest per day for 30 days. ## Schedules and jobs | Setting | Value | | --- | --- | | Schedules per app | 20 | | Path call timeout | 60 s default, 240 s at most; down to every minute | | Command timeout | 900 s default, 3600 s at most; at most hourly | | Runs kept per schedule | 100 | | Cron expression, time zone | up to 200 and 64 characters | | Job command | up to 64 arguments; up to 8 linked apps | | Job timeout | 1 to 3600 s, default 900 | | Job output kept | 1 MiB | ## Domains and buckets | Setting | Value | | --- | --- | | Domain length | up to 253 characters, labels up to 63 | | Certificate renewal | when fewer than 30 days remain | | Previews per app | 10; idle previews close after 14 days | | Bucket name | 3 to 63 characters, `a-z`, `0-9`, `-`; `rytiv*` reserved | | Presigned URL | 1 second to 7 days, default 15 minutes | | Object key | up to 1024 bytes | | CORS rules per bucket | 100 | | Objects per list page | 100 default, 1000 at most | | Sending domains waiting for DNS | 3 per organization | ## Rate limits The product API has no general rate limit. These entrances have one and answer `429` with `Retry-After`: | Entrance | Limit | | --- | --- | | MCP tool calls | 120 per minute per organization | | Shell session starts | 120 per minute per organization; 64 open sessions per organization, 16 per machine | | Computer viewer links | 120 per minute per organization | | Device login codes | 20 per 10 minutes per address | | OAuth register, authorize, token | 30, 60 and 300 per 10 minutes per address | | Organizations created from the console | 10 per person per day | ## Credit | Setting | Value | | --- | --- | | Smallest top-up | 10 EUR | | Largest top-up | 1,000 EUR | | Auto-recharge amount | 10 to 500 EUR; monthly cap up to 2,000 EUR | | Warning | below 20 percent of the last top-up | --- # Error catalog > Every error code the API answers, with its HTTP status and what to do about it. Generated from the contract's code lists. Every error of the API is a JSON object with a sentence and a stable code: `{ "error": "name already taken", "code": "name-taken" }`. Branch on `code`, show `error`. A code your program does not know should fall back on the HTTP status; the generic code of a status is listed first. See [REST API basics](https://docs.rytiv.com/reference/api-basics.md) for the request and response format. ## Generic codes One per HTTP status. A route answers one of these when it has nothing more specific to say. | 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 (unknown, revoked or expired). | | 403 | `forbidden` | The key may not do this. | | 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. | | 409 | `conflict` | The request conflicts with the current state. | | 410 | `gone` | The object existed and is gone. | | 413 | `too-large` | The body is larger than the route accepts. | | 422 | `unprocessable` | The request is well-formed but cannot be carried out as it is. | | 426 | `upgrade-required` | The route needs a WebSocket upgrade; use the CLI. | | 429 | `rate-limited` | Too many requests; wait for `Retry-After` and try again. | | 500 | `internal` | Something failed on Rytiv's side. The sentence carries a reference you can quote. | | 501 | `not-implemented` | Not available on this installation. | | 502 | `upstream-failed` | A host, the object store or a connected service failed. | | 503 | `unavailable` | Not possible right now; try again shortly. | | 504 | `upstream-timeout` | A host or a connected service did not answer in time. | ## Codes shared by several routes | Status | Code | Meaning | | --- | --- | --- | | 409 | `name-taken` | The name is used somewhere on Rytiv (an app, a database, a volume or a bucket). | | 409 | `already-exists` | You already have it; an idempotent create may treat this as done. | | 400 | `name-reserved` | The platform keeps this name (for example `api`, `console`, `docs`, `www`, and every name that is `rytiv` or starts with `rytiv-`). | | 403 | `org-suspended` | The organization was suspended by the operator. | | 403 | `delegation-refused` | `X-Rytiv-Org` names an organization this key may not act for. | | 403 | `creator-proof` | Creating an organization needs the console's signed proof of the person. | | 403 | `token-scope` | The key's scopes do not cover this route, this mail domain or this upgrade. The sentence names the scope needed. | | 503 | `no-capacity` | No host in the region has room for the machine right now; try again. | | 503 | `lease-held` | Another host still holds the volume or checkpoint; try again shortly. | | 503 | `create-failed` | The host could not create the machine (image, start command or port). The sentence says which. | | 503 | `database-unavailable` | The platform database cannot be reached right now; retry after `Retry-After`. | | 408 | `wait-timeout` | The wait ran out before the machine reached the state; ask again. | | 409 | `not-a-copy` | The database is neither a fork nor a branch; its URL came with its create answer. | | 409 | `not-ready` | The copy is still restoring, or failed; there is nothing to hand out yet. | | 409 | `url-handed-out` | The URL of this copy was handed out already. It leaves the platform exactly once. | | 403 | `agent-token-production-readonly` | An agent token of class `production` may read; a change needs a person. | | 403 | `agent-token-production-data` | An agent token of class `production` without `prodDataRead` may not read what production holds. | | 403 | `agent-token-credential` | No agent token mints, reveals or revokes a credential, or administers the organization. | | 403 | `agent-token-app-scope` | The request is about an app outside the token's list, or about no app at all. | | 403 | `agent-token-route-unknown` | A route the agent rules do not know is refused. | | 403 | `agent-token-delegation` | An agent token acts in its own organization only. | | 503 | `agent-audit-unavailable` | The audit record of the action could not be written, so the action did not run; ask again shortly. | | 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 | `org-limit-apps` | The organization has as many apps as it may have (computers included). The body carries `limit` and `current`. | ## Machine actions Answered by `POST /v1/machines/:id/:action` and a machine create when the host could not start the machine. | Status | Code | Meaning | | --- | --- | --- | | 503 | `start-interrupted` | The start was interrupted on the host. | | 503 | `guest-exited` | The machine's process ended during the start. | | 503 | `port-timeout` | The machine did not answer on its port in time. | | 503 | `no-capacity` | No host in the region has room for the machine right now; try again. | | 503 | `host-refused` | The host refused the action. | | 503 | `host-unreachable` | The host did not answer. | | 409 | `machine-busy` | Another operation on the machine is in progress; try again. | | 503 | `create-failed` | The host could not create the machine (image, start command or port). The sentence says which. | ## Rollbacks Answered by `POST /v1/apps/:name/rollback`. | Status | Code | Meaning | | --- | --- | --- | | 404 | `release-not-found` | No release with that version or id. | | 409 | `rollback-target-current` | That release is the one that runs. | | 409 | `rollback-same-build` | That release runs the same build as the current one. | | 409 | `rollback-target-never-live` | That release never went live, so there is nothing to bring back. | | 409 | `release-artifact-gone` | The release's image is no longer available. | | 409 | `no-previous-release` | There is no earlier release that went live. | | 409 | `rollback-not-for-resources` | The app backs a database; databases are restored, not rolled back. | | 409 | `rollback-needs-machine-config` | The app has no machine whose settings a rollback could reuse. | | 409 | `deploy-in-flight` | Another deploy of this app is running. | ## Credit and billing Answered by the routes under `/v1/billing`. | Status | Code | Meaning | | --- | --- | --- | | 402 | `credit-blocked` | The organization's credit is at zero: nothing new starts until a top-up. Reads, stops and the billing routes still answer. | | 409 | `no-customer` | No payment relationship yet; a first top-up creates it. | | 403 | `billing-console-only` | A billing change is made in the console by an admin, not with an API key. | | 409 | `not-prepaid` | The organization has no credit account; only org creation or an operator's grant starts prepaid credit. | ## Computers Answered by the viewer and control routes of a computer. | Status | Code | Meaning | | --- | --- | --- | | 409 | `not-a-computer` | The machine is not a computer (it has no `COMPUTER_VIEW_KEY`), or it is a console or job machine. | | 409 | `machine-stopped` | The machine is stopped; start it first. | | 503 | `not-placed` | The machine has no host yet. | | 503 | `view-key-unreadable` | The computer's key cannot be decrypted right now. | | 409 | `view-key-too-short` | The computer's key is too short; the image would refuse to start with it. | | 409 | `several-machines` | Control needs exactly one machine; the app has several. | | 409 | `control-held` | Somebody else holds control; the body names `heldBy`, `until` and `leaseId`. Use `force` to take it. | | 409 | `control-busy` | Another change of control is in progress; try again after `Retry-After`. | | 409 | `computer-control-missing` | The machine runs a computer image from before control leases; deploy it again. | | 409 | `guest-agent-absent` | The machine has no guest channel; it was created before shell sessions existed. Deploy it again. | | 503 | `guest-channel-off` | The guest channel is switched off on the machine's host. | | 503 | `guest-unreachable` | The guest did not answer; try again after `Retry-After`. | | 502 | `guest-failed` | The control tool in the machine ran and failed. | | 409 | `managed-service` | A managed database; its machine is not opened to a shell. | | 503 | `resource-check-failed` | Could not tell whether the machine backs a managed database; try again. | ## Shell sessions Answered when a shell, a tunnel or a file transfer into a machine is refused (`ry exec`, `ry ssh`, `ry proxy`). | Status | Code | Meaning | | --- | --- | --- | | 404 | `machine-not-found` | No such machine in your organization. | | 409 | `console-machine` | The machine is a console or job machine; it takes no shell. | | 409 | `managed-service` | A managed database; its machine is not opened to a shell. | | 503 | `resource-check-failed` | Could not tell whether the machine backs a managed database; try again. | | 409 | `machine-stopped` | The machine is stopped; start it first. | | 409 | `machine-suspended-no-wake` | The machine is suspended and cannot be woken for this. | | 503 | `machine-not-placed` | The machine has no host yet. | | 429 | `too-many-sessions` | The machine has as many shell sessions open as it may. | | 503 | `host-unavailable` | The machine's host is not reachable right now. | | 503 | `audit-unavailable` | The audit record of the session could not be written, so it was not opened. | | 503 | `exec-disabled` | Shell sessions are switched off on this installation. | | 409 | `guest-agent-absent` | The machine has no guest channel; it was created before shell sessions existed. Deploy it again. | | 504 | `guest-agent-timeout` | The guest did not answer in time. | | 409 | `guest-agent-version` | The guest's agent is too old for this; deploy the machine again. | | 502 | `guest-agent-protocol` | The guest answered something the platform did not expect. | | 502 | `host-error` | The host failed to open the session. | | 504 | `host-timeout` | The host did not answer in time. | ## Validation errors A request that breaks a schema answers `400` with the code `bad-request`. The `error` sentence names every field that was wrong (`invalid request: memoryMb: Too big: expected number to be <=16384`), and `issues` carries the same problems as a list, one entry per field, each with `path` and `message`. Fix every named field at once; the server reports them all in one answer. --- # Regions > Where your machines, databases and files run. One EU region at launch; a machine never leaves the region it was created in. A region is the unit everything runs in: its own hosts, its own API, its own address space. Your workloads and data stay in the region of your workspace. Account and billing data stay in the EU whatever the region. ## At launch: EU Rytiv launches with one region in the EU. Every app, computer, sandbox, database and bucket of a workspace runs there, under `https://.rytiv.app`, and the API is `https://api.rytiv.app`. The region's code in the API is `ams`; the API field `region` on a machine carries it. A US region follows. When it is offered, a workspace is created in one region and stays there; one login reaches every workspace, and each region has its own API address. ## A machine never moves between regions A machine is placed in its region when it is created and stays there for its lifetime: a move to another host (`POST /v1/machines/:id/migrate`), a host failure, a deploy that replaces machines, all of it stays inside the region. Capacity is found in the region or not at all (`503` with `no-capacity`), never somewhere else. That is what makes the residency promise a property of the platform rather than a setting. ## What this means for you - Choose the region with the workspace; nothing after that asks again. - Internal addresses (`.internal`, app links) resolve inside the region only. - Static sites are served from the region's edge. It is one region, not a content delivery network. - Mail is sent from the region's own delivery hosts; the sending domain is yours. See [Security and data](https://docs.rytiv.com/concepts/security.md) for what stays encrypted and who can read what. --- # Security and data > Where your data lives, how secrets are kept, what a key can reach, how machines are isolated and what TLS covers. ## Data residency Your workloads and data stay in the region of your workspace, in the EU at launch; see [Regions](https://docs.rytiv.com/concepts/regions.md). Account and billing data stay in the EU whatever the region. Payment pages are served by the payment provider; no card data crosses the Rytiv API. ## Secrets - Secrets are encrypted at rest with AES-256-GCM under a key the platform holds outside the database, and decrypted only when they are handed to a machine that starts. - The API never returns a value: `GET /v1/apps/:name/secrets` lists names, when they changed, whether Rytiv manages them, and whether a build sees them. - **Runtime and build are separate.** Every secret reaches the running app. A build sees only the secrets marked for it (`build: true`; `ry secrets set --build`). A new secret is marked by default only when its name carries a public prefix such as `NEXT_PUBLIC_` or `VITE_`. Secrets the platform creates (`DATABASE_URL`, `S3_*`, `RYTIV_CRON_SECRET`) are runtime only. - Secrets reach a machine when it starts. A running machine keeps the environment it started with; a new deploy, or a stop and start, picks up the change. - Managed secrets (the connection URL of an attached database, the key of an attached bucket) follow their attachment and cannot be changed or removed by hand. ## Keys and what they reach - A key acts as one organization and sees nothing of another; a name of another organization answers like a name that does not exist. - Keys are stored as a hash; the value is shown once, when created. - Scopes narrow a key to reading, deploying one app, or sending mail; agent tokens carry a class and an app list the platform enforces on every request. See [Agents and tokens](https://docs.rytiv.com/guides/agents-and-tokens.md). - A key with scopes or an agent token can never mint, reveal or revoke a credential. - A shell, a tunnel or a file transfer into a machine needs a key with full access and is audited. ## Machines - Every app, computer, sandbox and database runs in its own virtual machine with its own kernel, not a container on a shared kernel. - Machines of one app reach each other over the private network; another app only through an explicit link (`ry link`). A database's internal name resolves only inside the apps attached to it. - A sandbox can be created with an outbound policy: everything, nothing, or an allowlist of domains and address ranges. ## TLS - `https://.rytiv.app` is covered by the platform's certificate from the first deploy; port 80 redirects to HTTPS. - A custom domain gets its own certificate over ACME once ownership is proven, renewed when fewer than 30 days remain. See [Custom domains and TLS](https://docs.rytiv.com/guides/custom-domains.md). - A Postgres database accepts TLS connections only. The connection URL uses `sslmode=verify-full` against a certificate authority that every machine carries at `/etc/rytiv/ca/postgres.crt`. ## Signing in The console signs in with a passkey, a one-time code by e-mail, or GitHub where it is switched on; new accounts have no password, and every account enrols a second factor. `ry login` uses a device code you confirm in the console at `https://console.rytiv.com/device`; the key it receives is a key of your team like any other and can be revoked under **Team, Tokens & keys**. ## Durability - A volume is copied to object storage on a schedule and before its machine sleeps; a database keeps a continuous archive of its write-ahead log (7 days) on top of that, which forks and point-in-time restores read. - An agent machine with durable checkpoints survives a host failure with its last checkpoint; a computer comes back with the last copy of its home. - Deleting an app, a database, a volume or a bucket cannot be undone. The CLI asks you to type the name; automation passes `--yes`. --- # REST API basics > The base URL, how requests are authenticated, the one error format every route answers with, and how to act for a managed organization. Everything Rytiv does goes through one HTTP API. The CLI, the console and the MCP server use the same routes you can call yourself. The routes themselves are on the [API reference](https://docs.rytiv.com/reference/api.md) pages, one per family, generated from the control plane's code. ## Base URL ```text https://api.rytiv.app/v1 ``` Requests and answers are JSON (`Content-Type: application/json`), except where a route says otherwise: file contents, source uploads and the OAuth token endpoint take other bodies, and the reference says so. ## Authentication Send an [API key](https://docs.rytiv.com/authentication.md) as a bearer token on every request: **curl** ```bash curl https://api.rytiv.app/v1/apps -H "Authorization: Bearer $RYTIV_TOKEN" ``` **TypeScript** ```ts const res = await fetch("https://api.rytiv.app/v1/apps", { headers: { Authorization: `Bearer ${process.env.RYTIV_TOKEN}` }, }); const apps = await res.json(); ``` **Python** ```python import os, requests res = requests.get( "https://api.rytiv.app/v1/apps", headers={"Authorization": f"Bearer {os.environ['RYTIV_TOKEN']}"}, ) apps = res.json() ``` **CLI** ```bash ry apps list ``` Every route acts as the organization of the key. A name that belongs to another organization answers exactly like a name that does not exist, usually `404`. A key with [scopes](https://docs.rytiv.com/guides/agents-and-tokens.md) reaches only the routes its scopes cover; the reference names the scope each route needs. ### Acting for a managed organization If your organization manages other organizations, a key of yours can act inside one of them with the header `X-Rytiv-Org: `. Naming your own organization changes nothing. Naming one your organization does not manage answers `403` with the code `delegation-refused`. An agent token cannot use the header at all (`agent-token-delegation`). ## Errors Every error is a JSON object with an English sentence and a stable code: ```json { "error": "name already taken", "code": "name-taken" } ``` Branch on `code`, show `error`. Some routes add fields that help with the next step (`issues`, `limit`, `current`, `heldBy`). New codes can appear; a code your program does not know should fall back on the HTTP status. The [error catalog](https://docs.rytiv.com/concepts/errors.md) lists every code. A request that fails validation answers `400` with the code `bad-request`. Its `error` sentence names every field that was wrong, and `issues` carries the same problems as a list. | Status | Meaning | | --- | --- | | `400` | The request is malformed or a field is out of range. | | `401` | No API key, or one that does not work. | | `402` | The organization's credit is at zero; only reads, stops and billing routes pass. | | `403` | The key may not do this: a scope, an agent token's class, an organization limit, or an organization yours does not manage. | | `404` | Not found, or not yours. | | `408` | A wait ran out (`GET /v1/machines/:id/wait`). | | `409` | The request conflicts with the current state, for example a taken name or a deploy that is already running. | | `429` | Too many: a limit of your organization is reached, or a rate limit. `Retry-After` says when to try again. | | `5xx` | Something on our side, or no capacity right now. The `error` sentence says which. | ## Waiting for work to finish A deploy, a machine create with `?wait=false`, a branch or a fork answer `202` and continue in the background. Poll the object they name until it reaches its final state: | Started by | Poll | Until | | --- | --- | --- | | `POST /v1/apps/:name/deploy` | `GET /v1/deploys/:id` | `status` is `done` or `error` | | `POST /v1/apps/:name/machines?wait=false` | `GET /v1/machines/:id/wait?state=running` | the machine answers, or `408` | | `POST /v1/resources/:name/branch` or `/fork` | `GET /v1/resources/:name` | `state` is `ready` or `failed` | | `POST /v1/apps/:name/jobs` | `GET /v1/jobs/:id` | `state` is `done`, `failed` or `timeout` | ## Rate limits The product API has no general rate limit. A few entrances do, and answer `429` with `Retry-After` when it is reached: the MCP endpoint (120 tool calls per minute per organization), shell sessions (120 starts per minute per organization), computer viewer links (120 per minute per organization), and the login and OAuth entrances per caller address. ## Routes by family | Family | Routes | Reference | | --- | --- | --- | | Apps | `/v1/apps`, attach, link, labels, maintenance | [Apps](https://docs.rytiv.com/reference/api/apps.md) | | Deploys and releases | detect, builds, deploy, deploys, releases, rollback, previews, Git | [Deploys and releases](https://docs.rytiv.com/reference/api/deploys.md) | | Machines | `/v1/apps/:name/machines`, `/v1/machines/:id`, logs, snapshots, files | [Machines](https://docs.rytiv.com/reference/api/machines.md) | | Computers | viewer and control links, control audit | [Computers](https://docs.rytiv.com/reference/api/computers.md) | | Sandboxes | `/v1/sandboxes` | [Sandboxes](https://docs.rytiv.com/reference/api/sandboxes.md) | | Postgres | `/v1/resources` | [Postgres](https://docs.rytiv.com/reference/api/postgres.md) | | Volumes | `/v1/volumes` | [Volumes](https://docs.rytiv.com/reference/api/volumes.md) | | Buckets | `/v1/buckets`, presign, objects, CORS | [Buckets](https://docs.rytiv.com/reference/api/buckets.md) | | Secrets | `/v1/apps/:name/secrets` | [Secrets](https://docs.rytiv.com/reference/api/secrets.md) | | Tokens and keys | `/v1/tokens`, agent actions, SSH keys, device login | [Tokens and keys](https://docs.rytiv.com/reference/api/tokens.md) | | Organization | `/v1/org`, managed organizations | [Organization](https://docs.rytiv.com/reference/api/organization.md) | | Billing and usage | `/v1/billing/*`, `/v1/usage` | [Billing and usage](https://docs.rytiv.com/reference/api/billing.md) | | Custom domains | `/v1/apps/:name/domains`, zones | [Custom domains](https://docs.rytiv.com/reference/api/domains.md) | | Cron and jobs | schedules, jobs | [Cron and jobs](https://docs.rytiv.com/reference/api/cron-and-jobs.md) | | Protection and network | protection, network policy, UDP | [Protection and network](https://docs.rytiv.com/reference/api/protection-and-network.md) | | Mail | `/v1/emails`, `/v1/mail/*` | [Mail](https://docs.rytiv.com/reference/api/mail.md) | | Webhooks | `/v1/webhooks` | [Webhooks](https://docs.rytiv.com/reference/api/webhooks.md) | | MCP | `POST /v1/mcp` | [MCP](https://docs.rytiv.com/reference/api/mcp.md), [MCP tools](https://docs.rytiv.com/reference/mcp-tools.md) | | OAuth and device login | `/v1/oauth/*`, `/v1/device/*` | [OAuth and device login](https://docs.rytiv.com/reference/api/oauth.md) | --- # API reference > Every endpoint of the Rytiv REST API, generated from the control plane's routes and the contract's schemas. Everything Rytiv does goes through one HTTP API at `https://api.rytiv.app/v1`. The CLI, the console and the MCP server use the same routes you can call yourself. This reference lists 183 endpoints, generated from the control plane's route registrations and the contract's schemas: every path, every field, every bound and every error code is what the code enforces. Start with [REST API basics](https://docs.rytiv.com/reference/api-basics.md) for authentication, the error format and how to act for a managed organization. Each family page below lists its endpoints with an example request and the shape of the answer. ## Families - [Apps](https://docs.rytiv.com/reference/api/apps.md): Create, list, label and delete apps; attach databases, buckets and volumes; link apps to each other. - [Deploys and releases](https://docs.rytiv.com/reference/api/deploys.md): Detect a project, build it, deploy an image, follow a deploy, list releases and roll back; previews and Git-connected deploys. - [Machines](https://docs.rytiv.com/reference/api/machines.md): The virtual machines behind an app: create, read, start, stop, suspend, destroy, migrate; logs, snapshots, files and shell sessions. - [Computers](https://docs.rytiv.com/reference/api/computers.md): Watch or take over a computer's desktop, and see who had control. - [Sandboxes](https://docs.rytiv.com/reference/api/sandboxes.md): Short-lived machines with a deadline: create from a template, run commands, read and write files, extend, end. - [Postgres](https://docs.rytiv.com/reference/api/postgres.md): Databases Rytiv runs for you: create, branch, fork, restore, expire, fetch a copy's connection URL, delete. - [Volumes](https://docs.rytiv.com/reference/api/volumes.md): Persistent disks: create, list, resize, attach to an app, detach, delete. - [Buckets](https://docs.rytiv.com/reference/api/buckets.md): S3-compatible object storage: create buckets, presign object URLs, list and delete objects, set CORS, attach to apps. - [Secrets](https://docs.rytiv.com/reference/api/secrets.md): Encrypted environment variables of an app: set, list names, remove, and choose which ones a build may see. - [Tokens and keys](https://docs.rytiv.com/reference/api/tokens.md): API keys with scopes and expiry, agent tokens with a class, the audit of agent actions, SSH keys and the device login. - [Organization](https://docs.rytiv.com/reference/api/organization.md): Your organization, its wake page and signed-access keys, organizations you manage for others, and deleting your own. - [Billing and usage](https://docs.rytiv.com/reference/api/billing.md): Prepaid credit in euro: balance, ledger, top-ups, auto-recharge, the payment portal, the billing preview and metered usage. - [Custom domains](https://docs.rytiv.com/reference/api/domains.md): Your own domains on an app: add, check, redirect, remove; prove a whole zone once. - [Cron and jobs](https://docs.rytiv.com/reference/api/cron-and-jobs.md): Schedules that call a path of an app or run a command on a machine of their own, their run history, and one-off jobs. - [Protection and network](https://docs.rytiv.com/reference/api/protection-and-network.md): Who may open an app (password, signed links), what its machines may reach, and public UDP endpoints. - [Mail](https://docs.rytiv.com/reference/api/mail.md): Send mail from your own domains, read the delivery log, receive inbound mail, and manage suppressions. - [Webhooks](https://docs.rytiv.com/reference/api/webhooks.md): Be called when something happens in your organization, and read the deliveries. - [MCP](https://docs.rytiv.com/reference/api/mcp.md): The MCP server your coding agent talks to: one endpoint, JSON-RPC over HTTP, the same key as every other route. - [OAuth and device login](https://docs.rytiv.com/reference/api/oauth.md): How a hosted MCP client connects without a pasted key, and how the CLI signs in with a code. --- # 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://.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==` to filter by label. A key with `deploy:` 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": "", "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==` to filter by label. A key with `deploy:` scopes sees only its apps. **Auth:** API key with full access or a deploy scope (`deploy` or `deploy:`), 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": "", "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:` 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/" \ -H "Authorization: Bearer $RYTIV_TOKEN" ``` ### Response `200` with the app: ```json { "name": "my-app", "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/" \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{"labels":{"":""}}' ``` ### 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/" \ -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//labels" \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{"labels":{"":""}}' ``` ### 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//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//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//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//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//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//link/" \ -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. | --- # 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:` 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 --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:`). 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":{"":""}}}' ``` ### 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:` 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//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:` 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//builds/" \ -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:` 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:` 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//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": "", "orgId": "", "app": "my-app", "image": "nginx:alpine", "status": "running", "created": [ "" ], "retired": [ "" ], "at": "2026-10-21T09:00:00Z", "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:` 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:` 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:` 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//deploys" \ -H "Authorization: Bearer $RYTIV_TOKEN" ``` ### Response `200` with a list of deploys, each: ```json [ { "id": "", "orgId": "", "app": "my-app", "image": "nginx:alpine", "status": "running", "created": [ "" ], "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:`), 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/" \ -H "Authorization: Bearer $RYTIV_TOKEN" ``` ### Response `200` with the deploy: ```json { "id": "", "orgId": "", "app": "my-app", "image": "nginx:alpine", "status": "running", "created": [ "" ], "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:` 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//releases" \ -H "Authorization: Bearer $RYTIV_TOKEN" ``` ### Response `200` with the release list: ```json { "app": "", "releases": [ { "id": "", "version": 1, "status": "running", "at": "2026-10-21T09:00:00Z", "startedAt": "2026-10-21T09:00:00Z", "image": "nginx:alpine", "source": { "kind": "git", "repo": "", "branch": "", "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:` 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//rollback" \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` ### Response `202` with the deploy that brings the release back: ```json { "id": "", "orgId": "", "app": "my-app", "image": "nginx:alpine", "status": "running", "created": [ "" ], "retired": [ "" ], "at": "2026-10-21T09:00:00Z", "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:` 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//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//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//previews/" \ -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//git" \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{"installationId":"","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//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//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//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/" \ -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":"","state":"","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/" \ -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. | --- # Machines > The virtual machines behind an app: create, read, start, stop, suspend, destroy, migrate; logs, snapshots, files and shell sessions. A machine is one virtual machine of an app. A deploy creates machines for you; this family is for reading them, acting on one, and for the machines you create yourself (a computer, an agent's workspace). A machine is `requested`, `scheduled`, `starting`, `running`, `suspending`, `suspended`, `stopping`, `stopped`, `deleted` or `failed`. The shell routes (`ry exec`, `ry ssh`, `ry proxy`) upgrade the connection to a WebSocket and are not plain JSON calls; use the CLI for them. ## Endpoints - [`POST /v1/apps/:name/machines`](#post-v1appsnamemachines): Creates a machine in the app and waits until it runs. Add `?wait=false` for `202` right away and poll `GET /v1/machines/:id/wait` - [`GET /v1/apps/:name/machines`](#get-v1appsnamemachines): The app's machines - [`GET /v1/machines/:id`](#get-v1machinesid): One machine - [`GET /v1/machines/:id/wait`](#get-v1machinesidwait): Holds the request until the machine reaches a state (`?state=`), at most `?timeout=` seconds; `408` when it runs out - [`POST /v1/machines/:id/:action`](#post-v1machinesidaction): Acts on a machine: `start`, `stop`, `suspend` or `destroy` - [`POST /v1/machines/:id/migrate`](#post-v1machinesidmigrate): Moves the machine to another host - [`GET /v1/apps/:name/logs`](#get-v1appsnamelogs): The console output of the app's machines, with a cursor per machine for the next call - [`POST /v1/machines/:id/checkpoints`](#post-v1machinesidcheckpoints): Takes a named snapshot of the machine: its disk, and with `memory: true` its running memory too - [`GET /v1/machines/:id/checkpoints`](#get-v1machinesidcheckpoints): The machine's named snapshots. Add `?auto=1` to include the automatic ones - [`DELETE /v1/machines/:id/checkpoints/:label`](#delete-v1machinesidcheckpointslabel): Forgets a snapshot - [`POST /v1/machines/:id/checkpoints/:label/restore`](#post-v1machinesidcheckpointslabelrestore): Brings the machine back to that snapshot. With `keepCurrent: true` (the default) the current state is saved first - [`GET /v1/machines/:id/files`](#get-v1machinesidfiles): Reads one file of the machine (`?path=`), as raw bytes - [`PUT /v1/machines/:id/files`](#put-v1machinesidfiles): Writes one file of the machine (`?path=`); the body is the file's bytes - [`GET /v1/machines/:id/files/list`](#get-v1machinesidfileslist): Lists a directory of the machine (`?path=`) - [`GET /v1/machines/:id/screenshot`](#get-v1machinesidscreenshot): A screenshot of the machine's desktop, for a machine that has one - [`GET /v1/machines/:id/exec`](#get-v1machinesidexec): Opens a shell session over a WebSocket. This is what `ry exec` and `ry ssh` use; it is not a plain JSON call - [`GET /v1/machines/:id/exec-sessions`](#get-v1machinesidexec-sessions): The shell sessions open on the machine ## POST /v1/apps/:name/machines Creates a machine in the app and waits until it runs. Add `?wait=false` for `202` right away and poll `GET /v1/machines/:id/wait`. **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. | ### Query parameters | Name | Description | | --- | --- | | `wait` | `false` answers `202` as soon as the row exists, before the boot; the default waits for the running machine. | ### Request body JSON, the shape `CreateMachineRequest` of the contract. | Field | Type | Required | Description | | --- | --- | --- | --- | | `image` | string | required | An OCI reference such as `nginx:alpine`, or `build:` for a build of your organization. 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. | | `region` | string | optional | The region to place the machine in. Without it, the organization's region. 2 to 12 characters. | ### Example request ```bash curl -X POST "https://api.rytiv.app/v1/apps//machines" \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{"image":"rytiv/computer:0.1.0"}' ``` ### Response `201` with the machine (plus `bootMs`, the time the boot took): ```json { "id": "", "appName": "my-app", "config": { "image": "nginx:alpine", "vcpus": 1, "memoryMb": 512, "env": { "": "" }, "internalPort": 3000, "suspendPolicy": { "mode": "suspend", "idleSeconds": 300 }, "durableCheckpoint": false, "console": false }, "desiredState": "requested", "observedState": "requested", "region": "", "createdAt": "2026-10-21T09:00:00Z" } ``` | Field | Type | Present | Description | | --- | --- | --- | --- | | `id` | string | always | The machine's id. | | `appName` | string | always | The app it belongs to. | | `config` | object | always | What it runs: image, size, environment and the settings of a deploy request. | | `config.image` | string | always | An OCI reference such as `nginx:alpine`, or `build:` for a build of your organization. 1 to 512 characters. | | `config.class` | `shared-cpu-1x` \| `shared-cpu-2x` \| `shared-cpu-4x` \| `shared-cpu-8x` \| `performance-1x` \| `performance-2x` \| `performance-4x` \| `performance-8x` | if set | A named size instead of `vcpus`; the two must not contradict each other. | | `config.vcpus` | integer | always | Virtual CPUs. 1 to 16, default `1`. | | `config.memoryMb` | integer | always | Memory in MB. 128 to 16384, default `512`. | | `config.env` | object of string | always | Plain environment variables. Use secrets for credentials. | | `config.diskMb` | integer | if set | The machine's disk in MB. Without it, the image's size plus headroom. 512 to 131072. | | `config.volumeGb` | integer | if set | A volume of this size, attached to the machine. 1 to 500. | | `config.start` | string | if set | The start command, run through `/bin/sh -c`. It replaces what detection chose or the image's `CMD`. 1 to 2000 characters. | | `config.internalPort` | integer | always | The port the app listens on. 1 to 65535, default `3000`. | | `config.suspendPolicy` | object | always | When the machine sleeps. Without it, a new app never sleeps; a deploy without it keeps the app's rule. | | `config.healthCheck` | object | if set | An HTTP check a new machine must pass before it gets traffic. | | `config.restart` | object | if set | What happens when the process ends. Without it, a crashed machine is restarted. | | `config.killSignal` | `SIGTERM` \| `SIGINT` \| `SIGQUIT` \| `SIGHUP` \| `SIGUSR1` \| `SIGUSR2` \| `SIGWINCH` \| `SIGKILL` | if set | The signal sent before a machine is stopped or replaced. Default: the image's `STOPSIGNAL`, else `SIGTERM`. | | `config.killTimeoutS` | integer | if set | Seconds between that signal and `SIGKILL`. 1 to 300. | | `config.durableCheckpoint` | boolean | always | Agent machine: the whole state (memory and disk) is checkpointed and survives a host failure. default `false`. | | `config.checkpointIntervalS` | integer | if set | 5 to 3600. | | `config.console` | boolean | always | default `false`. | | `config.command` | array of string | if set | Runs on a machine of its own, as an argument list. 1 to 64 items. | | `config.diskIsCache` | boolean | if set | | | `config.readiness` | `port` \| `boot` | if set | `boot`: ready when the machine boots. `port`: ready when `internalPort` answers. | | `config.exposePorts` | array of integer | if set | 0 to 8 items. | | `config.egress` | object | if set | Outbound network: `allow-all`, `deny-all`, or `allowlist` with `domains` and `cidrs`. | | `desiredState` | `requested` \| `scheduled` \| `starting` \| `running` \| `suspending` \| `suspended` \| `stopping` \| `stopped` \| `deleted` \| `failed` | always | What the platform wants the machine to be. | | `observedState` | `requested` \| `scheduled` \| `starting` \| `running` \| `suspending` \| `suspended` \| `stopping` \| `stopped` \| `deleted` \| `failed` | always | What it is right now. | | `host` | string | if set | Internal; not shown to a customer. | | `privateIp` | string | if set | The machine's address on the private network. | | `role` | `primary` \| `standby` | if set | `primary` or `standby`, for a redundant database only. | | `region` | string | always | Where it runs. Fixed for the machine's lifetime. 2 to 12 characters. | | `checkpointAt` | string (ISO 8601) | if set | Agent machines: when the last durable checkpoint was taken. | | `zone` | string | if set | The availability zone, where the host reports one. | | `diskUsedMb` | integer | if set | How full the disk is, an upper bound. 0 to 9007199254740991. | | `diskTotalMb` | integer | if set | The disk's size. 0 to 9007199254740991. | | `diskAt` | string (ISO 8601) | if set | When the disk numbers were reported. | | `restoreChunksDone` | integer | if set | 0 to 9007199254740991. | | `restoreChunksTotal` | integer | if set | 0 to 9007199254740991. | | `restoreAt` | string (ISO 8601) | if set | | | `readyAt` | string (ISO 8601) | if set | | | `net` | object or null | if set | | | `liveness` | `listening` \| `not-listening` \| `no-signal` | if set | | | `livenessReason` | string | if set | | | `livenessSince` | string (ISO 8601) | if set | | | `livenessAt` | string (ISO 8601) | if set | | | `createError` | string | if set | | | `createErrorCode` | string | if set | | | `createBootLog` | string | if set | | | `createWarning` | string | if set | | | `createdAt` | string (ISO 8601) | always | When it was created. | ### 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:` 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 | `conflict` | The request conflicts with the current state. | | 409 | `volume-needs-single-machine` | An app with a volume runs exactly one machine. | | 503 | `create-failed` | The host could not create the machine (image, start command or port). The sentence says which. | | 503 | `no-capacity` | No host in the region has room for the machine right now; try again. | ## GET /v1/apps/:name/machines The app's machines. **Auth:** API key with full access or the scope `deploy` or `deploy:` 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//machines" \ -H "Authorization: Bearer $RYTIV_TOKEN" ``` ### Response `200` with a list of machines, each: ```json [ { "id": "", "appName": "my-app", "config": { "image": "nginx:alpine", "vcpus": 1, "memoryMb": 512, "env": { "": "" }, "internalPort": 3000, "suspendPolicy": { "mode": "suspend", "idleSeconds": 300 }, "durableCheckpoint": false, "console": false }, "desiredState": "requested", "observedState": "requested", "region": "", "createdAt": "2026-10-21T09:00:00Z" } ] ``` | Field | Type | Present | Description | | --- | --- | --- | --- | | `id` | string | always | The machine's id. | | `appName` | string | always | The app it belongs to. | | `config` | object | always | What it runs: image, size, environment and the settings of a deploy request. | | `config.image` | string | always | An OCI reference such as `nginx:alpine`, or `build:` for a build of your organization. 1 to 512 characters. | | `config.class` | `shared-cpu-1x` \| `shared-cpu-2x` \| `shared-cpu-4x` \| `shared-cpu-8x` \| `performance-1x` \| `performance-2x` \| `performance-4x` \| `performance-8x` | if set | A named size instead of `vcpus`; the two must not contradict each other. | | `config.vcpus` | integer | always | Virtual CPUs. 1 to 16, default `1`. | | `config.memoryMb` | integer | always | Memory in MB. 128 to 16384, default `512`. | | `config.env` | object of string | always | Plain environment variables. Use secrets for credentials. | | `config.diskMb` | integer | if set | The machine's disk in MB. Without it, the image's size plus headroom. 512 to 131072. | | `config.volumeGb` | integer | if set | A volume of this size, attached to the machine. 1 to 500. | | `config.start` | string | if set | The start command, run through `/bin/sh -c`. It replaces what detection chose or the image's `CMD`. 1 to 2000 characters. | | `config.internalPort` | integer | always | The port the app listens on. 1 to 65535, default `3000`. | | `config.suspendPolicy` | object | always | When the machine sleeps. Without it, a new app never sleeps; a deploy without it keeps the app's rule. | | `config.healthCheck` | object | if set | An HTTP check a new machine must pass before it gets traffic. | | `config.restart` | object | if set | What happens when the process ends. Without it, a crashed machine is restarted. | | `config.killSignal` | `SIGTERM` \| `SIGINT` \| `SIGQUIT` \| `SIGHUP` \| `SIGUSR1` \| `SIGUSR2` \| `SIGWINCH` \| `SIGKILL` | if set | The signal sent before a machine is stopped or replaced. Default: the image's `STOPSIGNAL`, else `SIGTERM`. | | `config.killTimeoutS` | integer | if set | Seconds between that signal and `SIGKILL`. 1 to 300. | | `config.durableCheckpoint` | boolean | always | Agent machine: the whole state (memory and disk) is checkpointed and survives a host failure. default `false`. | | `config.checkpointIntervalS` | integer | if set | 5 to 3600. | | `config.console` | boolean | always | default `false`. | | `config.command` | array of string | if set | Runs on a machine of its own, as an argument list. 1 to 64 items. | | `config.diskIsCache` | boolean | if set | | | `config.readiness` | `port` \| `boot` | if set | `boot`: ready when the machine boots. `port`: ready when `internalPort` answers. | | `config.exposePorts` | array of integer | if set | 0 to 8 items. | | `config.egress` | object | if set | Outbound network: `allow-all`, `deny-all`, or `allowlist` with `domains` and `cidrs`. | | `desiredState` | `requested` \| `scheduled` \| `starting` \| `running` \| `suspending` \| `suspended` \| `stopping` \| `stopped` \| `deleted` \| `failed` | always | What the platform wants the machine to be. | | `observedState` | `requested` \| `scheduled` \| `starting` \| `running` \| `suspending` \| `suspended` \| `stopping` \| `stopped` \| `deleted` \| `failed` | always | What it is right now. | | `host` | string | if set | Internal; not shown to a customer. | | `privateIp` | string | if set | The machine's address on the private network. | | `role` | `primary` \| `standby` | if set | `primary` or `standby`, for a redundant database only. | | `region` | string | always | Where it runs. Fixed for the machine's lifetime. 2 to 12 characters. | | `checkpointAt` | string (ISO 8601) | if set | Agent machines: when the last durable checkpoint was taken. | | `zone` | string | if set | The availability zone, where the host reports one. | | `diskUsedMb` | integer | if set | How full the disk is, an upper bound. 0 to 9007199254740991. | | `diskTotalMb` | integer | if set | The disk's size. 0 to 9007199254740991. | | `diskAt` | string (ISO 8601) | if set | When the disk numbers were reported. | | `restoreChunksDone` | integer | if set | 0 to 9007199254740991. | | `restoreChunksTotal` | integer | if set | 0 to 9007199254740991. | | `restoreAt` | string (ISO 8601) | if set | | | `readyAt` | string (ISO 8601) | if set | | | `net` | object or null | if set | | | `liveness` | `listening` \| `not-listening` \| `no-signal` | if set | | | `livenessReason` | string | if set | | | `livenessSince` | string (ISO 8601) | if set | | | `livenessAt` | string (ISO 8601) | if set | | | `createError` | string | if set | | | `createErrorCode` | string | if set | | | `createBootLog` | string | if set | | | `createWarning` | string | if set | | | `createdAt` | string (ISO 8601) | always | When it was created. | ### 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/machines/:id One machine. **Auth:** API key with full access or the scope `read`. Every class of agent token may call it. ### Path parameters | Name | Description | | --- | --- | | `id` | The machine's id. | ### Example request ```bash curl -X GET "https://api.rytiv.app/v1/machines/" \ -H "Authorization: Bearer $RYTIV_TOKEN" ``` ### Response `200` with the machine: ```json { "id": "", "appName": "my-app", "config": { "image": "nginx:alpine", "vcpus": 1, "memoryMb": 512, "env": { "": "" }, "internalPort": 3000, "suspendPolicy": { "mode": "suspend", "idleSeconds": 300 }, "durableCheckpoint": false, "console": false }, "desiredState": "requested", "observedState": "requested", "region": "", "createdAt": "2026-10-21T09:00:00Z" } ``` | Field | Type | Present | Description | | --- | --- | --- | --- | | `id` | string | always | The machine's id. | | `appName` | string | always | The app it belongs to. | | `config` | object | always | What it runs: image, size, environment and the settings of a deploy request. | | `config.image` | string | always | An OCI reference such as `nginx:alpine`, or `build:` for a build of your organization. 1 to 512 characters. | | `config.class` | `shared-cpu-1x` \| `shared-cpu-2x` \| `shared-cpu-4x` \| `shared-cpu-8x` \| `performance-1x` \| `performance-2x` \| `performance-4x` \| `performance-8x` | if set | A named size instead of `vcpus`; the two must not contradict each other. | | `config.vcpus` | integer | always | Virtual CPUs. 1 to 16, default `1`. | | `config.memoryMb` | integer | always | Memory in MB. 128 to 16384, default `512`. | | `config.env` | object of string | always | Plain environment variables. Use secrets for credentials. | | `config.diskMb` | integer | if set | The machine's disk in MB. Without it, the image's size plus headroom. 512 to 131072. | | `config.volumeGb` | integer | if set | A volume of this size, attached to the machine. 1 to 500. | | `config.start` | string | if set | The start command, run through `/bin/sh -c`. It replaces what detection chose or the image's `CMD`. 1 to 2000 characters. | | `config.internalPort` | integer | always | The port the app listens on. 1 to 65535, default `3000`. | | `config.suspendPolicy` | object | always | When the machine sleeps. Without it, a new app never sleeps; a deploy without it keeps the app's rule. | | `config.healthCheck` | object | if set | An HTTP check a new machine must pass before it gets traffic. | | `config.restart` | object | if set | What happens when the process ends. Without it, a crashed machine is restarted. | | `config.killSignal` | `SIGTERM` \| `SIGINT` \| `SIGQUIT` \| `SIGHUP` \| `SIGUSR1` \| `SIGUSR2` \| `SIGWINCH` \| `SIGKILL` | if set | The signal sent before a machine is stopped or replaced. Default: the image's `STOPSIGNAL`, else `SIGTERM`. | | `config.killTimeoutS` | integer | if set | Seconds between that signal and `SIGKILL`. 1 to 300. | | `config.durableCheckpoint` | boolean | always | Agent machine: the whole state (memory and disk) is checkpointed and survives a host failure. default `false`. | | `config.checkpointIntervalS` | integer | if set | 5 to 3600. | | `config.console` | boolean | always | default `false`. | | `config.command` | array of string | if set | Runs on a machine of its own, as an argument list. 1 to 64 items. | | `config.diskIsCache` | boolean | if set | | | `config.readiness` | `port` \| `boot` | if set | `boot`: ready when the machine boots. `port`: ready when `internalPort` answers. | | `config.exposePorts` | array of integer | if set | 0 to 8 items. | | `config.egress` | object | if set | Outbound network: `allow-all`, `deny-all`, or `allowlist` with `domains` and `cidrs`. | | `desiredState` | `requested` \| `scheduled` \| `starting` \| `running` \| `suspending` \| `suspended` \| `stopping` \| `stopped` \| `deleted` \| `failed` | always | What the platform wants the machine to be. | | `observedState` | `requested` \| `scheduled` \| `starting` \| `running` \| `suspending` \| `suspended` \| `stopping` \| `stopped` \| `deleted` \| `failed` | always | What it is right now. | | `host` | string | if set | Internal; not shown to a customer. | | `privateIp` | string | if set | The machine's address on the private network. | | `role` | `primary` \| `standby` | if set | `primary` or `standby`, for a redundant database only. | | `region` | string | always | Where it runs. Fixed for the machine's lifetime. 2 to 12 characters. | | `checkpointAt` | string (ISO 8601) | if set | Agent machines: when the last durable checkpoint was taken. | | `zone` | string | if set | The availability zone, where the host reports one. | | `diskUsedMb` | integer | if set | How full the disk is, an upper bound. 0 to 9007199254740991. | | `diskTotalMb` | integer | if set | The disk's size. 0 to 9007199254740991. | | `diskAt` | string (ISO 8601) | if set | When the disk numbers were reported. | | `restoreChunksDone` | integer | if set | 0 to 9007199254740991. | | `restoreChunksTotal` | integer | if set | 0 to 9007199254740991. | | `restoreAt` | string (ISO 8601) | if set | | | `readyAt` | string (ISO 8601) | if set | | | `net` | object or null | if set | | | `liveness` | `listening` \| `not-listening` \| `no-signal` | if set | | | `livenessReason` | string | if set | | | `livenessSince` | string (ISO 8601) | if set | | | `livenessAt` | string (ISO 8601) | if set | | | `createError` | string | if set | | | `createErrorCode` | string | if set | | | `createBootLog` | string | if set | | | `createWarning` | string | if set | | | `createdAt` | string (ISO 8601) | always | When it was created. | ### 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/machines/:id/wait Holds the request until the machine reaches a state (`?state=`), at most `?timeout=` seconds; `408` when it runs out. **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. | ### Query parameters | Name | Description | | --- | --- | | `state` | The state to wait for, for example `running` or `stopped`. | | `timeout` | Seconds to hold the request, at most 60. | ### Example request ```bash curl -X GET "https://api.rytiv.app/v1/machines//wait" \ -H "Authorization: Bearer $RYTIV_TOKEN" ``` ### Response `200` with the machine, once it is in the state: ```json { "id": "", "appName": "my-app", "config": { "image": "nginx:alpine", "vcpus": 1, "memoryMb": 512, "env": { "": "" }, "internalPort": 3000, "suspendPolicy": { "mode": "suspend", "idleSeconds": 300 }, "durableCheckpoint": false, "console": false }, "desiredState": "requested", "observedState": "requested", "region": "", "createdAt": "2026-10-21T09:00:00Z" } ``` | Field | Type | Present | Description | | --- | --- | --- | --- | | `id` | string | always | The machine's id. | | `appName` | string | always | The app it belongs to. | | `config` | object | always | What it runs: image, size, environment and the settings of a deploy request. | | `config.image` | string | always | An OCI reference such as `nginx:alpine`, or `build:` for a build of your organization. 1 to 512 characters. | | `config.class` | `shared-cpu-1x` \| `shared-cpu-2x` \| `shared-cpu-4x` \| `shared-cpu-8x` \| `performance-1x` \| `performance-2x` \| `performance-4x` \| `performance-8x` | if set | A named size instead of `vcpus`; the two must not contradict each other. | | `config.vcpus` | integer | always | Virtual CPUs. 1 to 16, default `1`. | | `config.memoryMb` | integer | always | Memory in MB. 128 to 16384, default `512`. | | `config.env` | object of string | always | Plain environment variables. Use secrets for credentials. | | `config.diskMb` | integer | if set | The machine's disk in MB. Without it, the image's size plus headroom. 512 to 131072. | | `config.volumeGb` | integer | if set | A volume of this size, attached to the machine. 1 to 500. | | `config.start` | string | if set | The start command, run through `/bin/sh -c`. It replaces what detection chose or the image's `CMD`. 1 to 2000 characters. | | `config.internalPort` | integer | always | The port the app listens on. 1 to 65535, default `3000`. | | `config.suspendPolicy` | object | always | When the machine sleeps. Without it, a new app never sleeps; a deploy without it keeps the app's rule. | | `config.healthCheck` | object | if set | An HTTP check a new machine must pass before it gets traffic. | | `config.restart` | object | if set | What happens when the process ends. Without it, a crashed machine is restarted. | | `config.killSignal` | `SIGTERM` \| `SIGINT` \| `SIGQUIT` \| `SIGHUP` \| `SIGUSR1` \| `SIGUSR2` \| `SIGWINCH` \| `SIGKILL` | if set | The signal sent before a machine is stopped or replaced. Default: the image's `STOPSIGNAL`, else `SIGTERM`. | | `config.killTimeoutS` | integer | if set | Seconds between that signal and `SIGKILL`. 1 to 300. | | `config.durableCheckpoint` | boolean | always | Agent machine: the whole state (memory and disk) is checkpointed and survives a host failure. default `false`. | | `config.checkpointIntervalS` | integer | if set | 5 to 3600. | | `config.console` | boolean | always | default `false`. | | `config.command` | array of string | if set | Runs on a machine of its own, as an argument list. 1 to 64 items. | | `config.diskIsCache` | boolean | if set | | | `config.readiness` | `port` \| `boot` | if set | `boot`: ready when the machine boots. `port`: ready when `internalPort` answers. | | `config.exposePorts` | array of integer | if set | 0 to 8 items. | | `config.egress` | object | if set | Outbound network: `allow-all`, `deny-all`, or `allowlist` with `domains` and `cidrs`. | | `desiredState` | `requested` \| `scheduled` \| `starting` \| `running` \| `suspending` \| `suspended` \| `stopping` \| `stopped` \| `deleted` \| `failed` | always | What the platform wants the machine to be. | | `observedState` | `requested` \| `scheduled` \| `starting` \| `running` \| `suspending` \| `suspended` \| `stopping` \| `stopped` \| `deleted` \| `failed` | always | What it is right now. | | `host` | string | if set | Internal; not shown to a customer. | | `privateIp` | string | if set | The machine's address on the private network. | | `role` | `primary` \| `standby` | if set | `primary` or `standby`, for a redundant database only. | | `region` | string | always | Where it runs. Fixed for the machine's lifetime. 2 to 12 characters. | | `checkpointAt` | string (ISO 8601) | if set | Agent machines: when the last durable checkpoint was taken. | | `zone` | string | if set | The availability zone, where the host reports one. | | `diskUsedMb` | integer | if set | How full the disk is, an upper bound. 0 to 9007199254740991. | | `diskTotalMb` | integer | if set | The disk's size. 0 to 9007199254740991. | | `diskAt` | string (ISO 8601) | if set | When the disk numbers were reported. | | `restoreChunksDone` | integer | if set | 0 to 9007199254740991. | | `restoreChunksTotal` | integer | if set | 0 to 9007199254740991. | | `restoreAt` | string (ISO 8601) | if set | | | `readyAt` | string (ISO 8601) | if set | | | `net` | object or null | if set | | | `liveness` | `listening` \| `not-listening` \| `no-signal` | if set | | | `livenessReason` | string | if set | | | `livenessSince` | string (ISO 8601) | if set | | | `livenessAt` | string (ISO 8601) | if set | | | `createError` | string | if set | | | `createErrorCode` | string | if set | | | `createBootLog` | string | if set | | | `createWarning` | string | if set | | | `createdAt` | string (ISO 8601) | always | When it was created. | ### 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. | | 408 | `wait-timeout` | The wait ran out before the machine reached the state; ask again. | | 410 | `gone` | The object existed and is gone. | ## POST /v1/machines/:id/:action Acts on a machine: `start`, `stop`, `suspend` or `destroy`. **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 | | --- | --- | | `id` | The object's id. | | `action` | `start`, `stop`, `suspend` or `destroy`. | ### Example request ```bash curl -X POST "https://api.rytiv.app/v1/machines//" \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` ### Response `200` with the machine after the action: ```json { "id": "", "appName": "my-app", "config": { "image": "nginx:alpine", "vcpus": 1, "memoryMb": 512, "env": { "": "" }, "internalPort": 3000, "suspendPolicy": { "mode": "suspend", "idleSeconds": 300 }, "durableCheckpoint": false, "console": false }, "desiredState": "requested", "observedState": "requested", "region": "", "createdAt": "2026-10-21T09:00:00Z" } ``` | Field | Type | Present | Description | | --- | --- | --- | --- | | `id` | string | always | The machine's id. | | `appName` | string | always | The app it belongs to. | | `config` | object | always | What it runs: image, size, environment and the settings of a deploy request. | | `config.image` | string | always | An OCI reference such as `nginx:alpine`, or `build:` for a build of your organization. 1 to 512 characters. | | `config.class` | `shared-cpu-1x` \| `shared-cpu-2x` \| `shared-cpu-4x` \| `shared-cpu-8x` \| `performance-1x` \| `performance-2x` \| `performance-4x` \| `performance-8x` | if set | A named size instead of `vcpus`; the two must not contradict each other. | | `config.vcpus` | integer | always | Virtual CPUs. 1 to 16, default `1`. | | `config.memoryMb` | integer | always | Memory in MB. 128 to 16384, default `512`. | | `config.env` | object of string | always | Plain environment variables. Use secrets for credentials. | | `config.diskMb` | integer | if set | The machine's disk in MB. Without it, the image's size plus headroom. 512 to 131072. | | `config.volumeGb` | integer | if set | A volume of this size, attached to the machine. 1 to 500. | | `config.start` | string | if set | The start command, run through `/bin/sh -c`. It replaces what detection chose or the image's `CMD`. 1 to 2000 characters. | | `config.internalPort` | integer | always | The port the app listens on. 1 to 65535, default `3000`. | | `config.suspendPolicy` | object | always | When the machine sleeps. Without it, a new app never sleeps; a deploy without it keeps the app's rule. | | `config.healthCheck` | object | if set | An HTTP check a new machine must pass before it gets traffic. | | `config.restart` | object | if set | What happens when the process ends. Without it, a crashed machine is restarted. | | `config.killSignal` | `SIGTERM` \| `SIGINT` \| `SIGQUIT` \| `SIGHUP` \| `SIGUSR1` \| `SIGUSR2` \| `SIGWINCH` \| `SIGKILL` | if set | The signal sent before a machine is stopped or replaced. Default: the image's `STOPSIGNAL`, else `SIGTERM`. | | `config.killTimeoutS` | integer | if set | Seconds between that signal and `SIGKILL`. 1 to 300. | | `config.durableCheckpoint` | boolean | always | Agent machine: the whole state (memory and disk) is checkpointed and survives a host failure. default `false`. | | `config.checkpointIntervalS` | integer | if set | 5 to 3600. | | `config.console` | boolean | always | default `false`. | | `config.command` | array of string | if set | Runs on a machine of its own, as an argument list. 1 to 64 items. | | `config.diskIsCache` | boolean | if set | | | `config.readiness` | `port` \| `boot` | if set | `boot`: ready when the machine boots. `port`: ready when `internalPort` answers. | | `config.exposePorts` | array of integer | if set | 0 to 8 items. | | `config.egress` | object | if set | Outbound network: `allow-all`, `deny-all`, or `allowlist` with `domains` and `cidrs`. | | `desiredState` | `requested` \| `scheduled` \| `starting` \| `running` \| `suspending` \| `suspended` \| `stopping` \| `stopped` \| `deleted` \| `failed` | always | What the platform wants the machine to be. | | `observedState` | `requested` \| `scheduled` \| `starting` \| `running` \| `suspending` \| `suspended` \| `stopping` \| `stopped` \| `deleted` \| `failed` | always | What it is right now. | | `host` | string | if set | Internal; not shown to a customer. | | `privateIp` | string | if set | The machine's address on the private network. | | `role` | `primary` \| `standby` | if set | `primary` or `standby`, for a redundant database only. | | `region` | string | always | Where it runs. Fixed for the machine's lifetime. 2 to 12 characters. | | `checkpointAt` | string (ISO 8601) | if set | Agent machines: when the last durable checkpoint was taken. | | `zone` | string | if set | The availability zone, where the host reports one. | | `diskUsedMb` | integer | if set | How full the disk is, an upper bound. 0 to 9007199254740991. | | `diskTotalMb` | integer | if set | The disk's size. 0 to 9007199254740991. | | `diskAt` | string (ISO 8601) | if set | When the disk numbers were reported. | | `restoreChunksDone` | integer | if set | 0 to 9007199254740991. | | `restoreChunksTotal` | integer | if set | 0 to 9007199254740991. | | `restoreAt` | string (ISO 8601) | if set | | | `readyAt` | string (ISO 8601) | if set | | | `net` | object or null | if set | | | `liveness` | `listening` \| `not-listening` \| `no-signal` | if set | | | `livenessReason` | string | if set | | | `livenessSince` | string (ISO 8601) | if set | | | `livenessAt` | string (ISO 8601) | if set | | | `createError` | string | if set | | | `createErrorCode` | string | if set | | | `createBootLog` | string | if set | | | `createWarning` | string | if set | | | `createdAt` | string (ISO 8601) | always | When it was created. | ### 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 | `machine-busy` | Another operation on the machine is in progress; try again. | | 503 | `create-failed` | The host could not create the machine (image, start command or port). The sentence says which. | | 503 | `guest-exited` | The machine's process ended during the start. | | 503 | `host-refused` | The host refused the action. | | 503 | `host-unreachable` | The host did not answer. | | 503 | `no-capacity` | No host in the region has room for the machine right now; try again. | | 503 | `port-timeout` | The machine did not answer on its port in time. | | 503 | `start-interrupted` | The start was interrupted on the host. | ## POST /v1/machines/:id/migrate Moves the machine to another host. **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 | | --- | --- | | `id` | The object's id. | ### Example request ```bash curl -X POST "https://api.rytiv.app/v1/machines//migrate" \ -H "Authorization: Bearer $RYTIV_TOKEN" ``` ### Response `200` with a JSON object and the fields of the object the handler builds. ### 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 | `conflict` | The request conflicts with the current state. | | 409 | `create-failed` | The host could not create the machine (image, start command or port). The sentence says which. | | 409 | `ephemeral` | | | 503 | `no-capacity` | No host in the region has room for the machine right now; try again. | ## GET /v1/apps/:name/logs The console output of the app's machines, with a cursor per machine for the next call. **Auth:** API key with full access or the scope `deploy` or `deploy:` 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 | | --- | --- | | `machine` | Only this machine. | ### Example request ```bash curl -X GET "https://api.rytiv.app/v1/apps//logs" \ -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/machines/:id/checkpoints Takes a named snapshot of the machine: its disk, and with `memory: true` its running memory too. **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 | | --- | --- | | `id` | The object's id. | ### Request body JSON, the shape `CreateCheckpointRequest` of the contract. | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | string | required | A name for the snapshot: letters, digits, `.`, `-` and `_`, starting with a letter or digit. `auto-` is reserved. | | `comment` | string | optional | A note kept with the snapshot. at most 500 characters. | | `memory` | boolean | optional | `true` snapshots the running memory as well, so a restore resumes the very processes. default `false`. | ### Example request ```bash curl -X POST "https://api.rytiv.app/v1/machines//checkpoints" \ -H "Authorization: Bearer $RYTIV_TOKEN" \ -H "Content-Type: application/json" \ -d '{"label":"before-migration"}' ``` ### 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. | | 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. | ## GET /v1/machines/:id/checkpoints The machine's named snapshots. Add `?auto=1` to include the automatic ones. **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. | ### Query parameters | Name | Description | | --- | --- | | `includeAuto` | | ### Example request ```bash curl -X GET "https://api.rytiv.app/v1/machines//checkpoints" \ -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. | ## DELETE /v1/machines/:id/checkpoints/:label Forgets a snapshot. **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 | | --- | --- | | `id` | The object's id. | | `label` | The snapshot's label. | ### Example request ```bash curl -X DELETE "https://api.rytiv.app/v1/machines//checkpoints/