> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.runtype.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.runtype.com/_mcp/server.

# Deploy a sandbox preview

> Run a server in a Runtype Sandbox container, get a public preview URL, redeploy changes, and destroy the container when you finish.

A sandbox deployment runs your code in a container and exposes one port at a public HTTPS preview URL. Use it to check a running server, such as a demo API, a mock server for a simulation, or a web app that a coding agent built. This is the session tier: the preview URL lives as long as its container, at most until the retention deadline. It is not durable hosting. To share a static HTML page, use the `builtin:publish_page` tool instead.

You can deploy from three places. Each one calls the same Runtype Sandbox endpoints:

| Where you deploy from | Deploy                                     | Destroy                                           |
| --------------------- | ------------------------------------------ | ------------------------------------------------- |
| Runtype MCP server    | `deploy_sandbox` tool                      | `destroy_sandbox` tool                            |
| TypeScript SDK        | `client.tools.deployCfSandbox()`           | `client.tools.cleanupCfSandbox()`                 |
| Runtype API           | `POST /v1/tools/sandbox/cf-sandbox/deploy` | `DELETE /v1/tools/sandbox/cf-sandbox/{sandboxId}` |

## Before you begin

Make sure that you have the following:

* A Runtype API key with the `TOOLS:EXECUTE` permission, in the `RUNTYPE_API_KEY` environment variable. For more information, see [Managing API keys](/user-guide/settings/managing-api-keys).
* A server that listens on a port other than `3000`. Runtype reserves port `3000` and port `2468`. If you omit `port`, Runtype exposes port `8080`.

## Deploy code

Send your code in the `code` field. Runtype writes it to `main.js`, `main.ts`, or `main.py`, based on `language`, and then starts it.

The following example deploys a Node.js server that listens on port `8080`:

#### TypeScript

**`deploy.ts`**

```typescript title="deploy.ts"
import { RuntypeClient } from '@runtypelabs/sdk'

const client = new RuntypeClient({
  apiKey: process.env.RUNTYPE_API_KEY,
})

const code = `
const http = require('http')
http
  .createServer((req, res) => res.end('Hello from a sandbox'))
  .listen(8080)
`

const result = await client.tools.deployCfSandbox({
  code,
  language: 'javascript',
  port: 8080,
})

console.log(result.sandboxId, result.previewUrl)
```

#### cURL

```bash
curl -X POST https://api.runtype.com/v1/tools/sandbox/cf-sandbox/deploy \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "require(\"http\").createServer((req, res) => res.end(\"Hello from a sandbox\")).listen(8080)",
    "language": "javascript",
    "port": 8080
  }'
```

Replace `YOUR_API_KEY` with your Runtype API key.

#### MCP

From an MCP client that is connected to the Runtype MCP server, call the `deploy_sandbox` tool with these arguments:

```json
{
  "code": "require('http').createServer((req, res) => res.end('Hello from a sandbox')).listen(8080)",
  "language": "javascript",
  "port": 8080
}
```

A successful deploy returns a response similar to the following:

```json
{
  "sandboxId": "2f1c9b7e-5d3a-4e8f-9a61-0c7d2b4e8f13",
  "previewUrl": "YOUR_PREVIEW_URL",
  "output": "",
  "status": "running",
  "stage": "expose_port",
  "effectivePolicy": {
    "retention": "unlimited",
    "origin": "legacy-default",
    "expiresAt": null,
    "metering": "legacy-untracked",
    "containerEpoch": 1,
    "freshContainer": true,
    "previewUrlChanged": false
  }
}
```

Open `previewUrl` in a browser to see the running server. Keep `sandboxId`. You need it to redeploy and to destroy the sandbox.

The preview URL is a subdomain URL that carries a token for the exposed port, so it reaches only that port. It requires no sign-in, so anyone who has the URL can open the server. Share it only with people who can see what the server returns.

The token is bound to the container. When a container stops and a later deploy starts a new one, the preview URL changes. The response reports this in `effectivePolicy`:

| Field               | Description                                                                                                                                                                                                                                        |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `containerEpoch`    | The container the preview URL belongs to. A new number means a new container and a new URL.                                                                                                                                                        |
| `freshContainer`    | `true` when this deploy started a new container. `false` when it reused the running one.                                                                                                                                                           |
| `previewUrlChanged` | `true` when the preview URL differs from the one that the previous successful deploy to this sandbox returned, for example after a container restart or a port change. Update every place where you saved the old URL. `false` for a first deploy. |

Don't save a preview URL as an MCP server URL, an OAuth audience, or an external tool URL that must work for more than a day.

The following fields shape the deployment:

| Field          | Type   | Description                                                                                                                                       |
| -------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code`         | string | Required. The contents of the main file.                                                                                                          |
| `language`     | string | `javascript`, `typescript`, or `python`. Defaults to `typescript`.                                                                                |
| `port`         | number | The port that your server listens on. Defaults to `8080`.                                                                                         |
| `packageJson`  | string | The contents of `package.json`. Runtype runs `npm install` before it starts your code. Ignored for Python.                                        |
| `files`        | object | Extra files, as relative paths mapped to string contents. Paths can't start with `/` or contain `..`.                                             |
| `startCommand` | string | Replaces the default start command: `node main.js`, `npx tsx main.ts`, or `python3 main.py`.                                                      |
| `sandboxId`    | string | The ID from an earlier deploy. Runtype deploys into that container instead of creating one.                                                       |
| `retention`    | string | `10m`, `1h`, `24h`, or `unlimited`. Runtype destroys the sandbox when retention ends. Requires lifecycle policy.                                  |
| `sleepPolicy`  | object | `{ "mode": "idle", "after": "5m" }` (also `15m`, `30m`, `1h`), or `{ "mode": "never" }` to keep the container running. Requires lifecycle policy. |

`retention` and `sleepPolicy` require lifecycle policy for your organization. Without it, a request that sets either one fails with `403` and `Explicit sandbox lifecycle policy is not enabled for this organization`.

## Redeploy changes

To update the code in a sandbox, deploy again and pass the `sandboxId` from the first response. Runtype writes your files into the same container, stops the previous server and every process that it started, and then runs the start command again. If another process in the sandbox still answers on `port`, the deploy fails at the `start_process` stage instead of starting a server that can't bind the port.

The deploy waits up to 30 seconds for the new server to answer on `port`. If the server exits first, or doesn't answer in time, the deploy fails at the `wait_for_port` stage and `output` includes the server's logs.

If the container was still running, the preview URL stays the same. If it had stopped, the redeploy starts a new container and returns a new URL, and `effectivePolicy.previewUrlChanged` is `true`.

## How long a sandbox stays up

If you omit `retention`, a Runtype Sandbox deploy has no Runtype expiry. The response reports this as `"retention": "unlimited"` with `"expiresAt": null`. The container itself doesn't run forever:

* When the preview receives no requests for a period of time, the container sleeps and your server stops. The default is 10 minutes. Set `sleepPolicy` to `{ "mode": "idle", "after": "1h" }` to wait longer.
* The container can also stop when your server exits or crashes.

A stopped server doesn't restart on its own, and the preview URL stops responding. To bring it back, deploy again with the same `sandboxId`. The new container has a new preview URL.

To keep a server reachable at one URL for a bounded run, such as a simulation or an eval benchmark, set `retention` to `10m`, `1h`, or `24h` and `sleepPolicy` to `{ "mode": "never" }`. The container keeps running without requests until the retention deadline in `effectivePolicy.expiresAt`. From that deadline on, the preview URL returns `404`, the same answer as an unknown token, and Runtype destroys the sandbox. Each successful redeploy starts a new retention window. A redeploy that fails keeps the previous retention window and `sleepPolicy`, for the sandbox and for billing. A request that sets `{ "mode": "never" }` with `unlimited` or no retention fails with `400`.

A kept-alive sandbox is billed for the whole time that it runs. A crash still stops the server and changes the URL on the next deploy. For a server that must stay up for more than a day, survive a crash, or keep its URL, use a managed deployment instead.

## Destroy a sandbox

Destroy the sandbox when you no longer need the preview. Destroying stops and removes the container, and you can't recover it.

#### TypeScript

**`destroy.ts`**

```typescript title="destroy.ts"
await client.tools.cleanupCfSandbox('YOUR_SANDBOX_ID')
```

#### cURL

```bash
curl -X DELETE https://api.runtype.com/v1/tools/sandbox/cf-sandbox/YOUR_SANDBOX_ID \
  -H "Authorization: Bearer YOUR_API_KEY"
```

#### MCP

Call the `destroy_sandbox` tool:

```json
{ "sandboxId": "YOUR_SANDBOX_ID" }
```

Replace `YOUR_SANDBOX_ID` with the `sandboxId` from the deploy response. A successful call returns `{ "success": true }`.

## Deploy to a Daytona sandbox

The TypeScript SDK and the API can also deploy to a Daytona sandbox with `client.tools.deploySandbox()` or `POST /v1/tools/sandbox/deploy`. The request takes the same fields as a Runtype Sandbox deploy. Runtype uses your Daytona key if you saved one in **Settings > Integrations**, and a Runtype platform key otherwise. For more information, see [Integrations (PostHog, Weaviate, Daytona)](/user-guide/settings/integrations-posthog-weaviate-daytona).

A Daytona deploy differs from a Runtype Sandbox deploy in these ways:

* Runtype destroys a Daytona sandbox after its retention window ends, 10 minutes after the last deploy. Each redeploy with the same `sandboxId` starts a new 10-minute window. The response shows the deadline in `effectivePolicy.expiresAt`.
* After Runtype destroys the sandbox, a redeploy with its `sandboxId` returns `404` and `Sandbox not found`. Deploy without `sandboxId` to get a new one.
* To destroy a Daytona sandbox early, call `client.tools.cleanupSandbox()` or `DELETE /v1/tools/sandbox/{sandboxId}`.

## Troubleshoot a deploy

A deploy that fails inside the container still returns HTTP `200`. The response has `"status": "error"`, an `error` message, `output` with the logs so far, and `stage` set to the step that failed. The following table lists the stages and their common causes:

| Stage            | Common cause                                                                                                       |
| ---------------- | ------------------------------------------------------------------------------------------------------------------ |
| `validate_port`  | `port` is `3000` or `2468`. Pick another port, such as `8080`.                                                     |
| `validate_files` | A `files` path is absolute or contains `..`.                                                                       |
| `npm_install`    | A dependency in `packageJson` failed to install. Read `output`.                                                    |
| `start_process`  | The start command failed to launch, the previous server didn't stop, or another process uses `port`. Read `error`. |
| `wait_for_port`  | The server exited, or didn't answer on `port` within 30 seconds. Read `output`.                                    |
| `expose_port`    | Runtype couldn't expose the port. Check that your server listens on `port`.                                        |

The API rejects some requests before it deploys anything:

* `403` with `The account sandbox spend limit has been reached`: your account reached its spend limit. For more information, see [Billing and plans](/user-guide/settings/billing-and-plans).
* `403` with `Explicit sandbox lifecycle policy is not enabled for this organization`: remove `retention` and `sleepPolicy` from the request.
* `400` with `requires finite retention (10m, 1h or 24h)`: `sleepPolicy` is `{ "mode": "never" }`, but `retention` is `unlimited` or missing. Set `retention` to `10m`, `1h`, or `24h`.

If the deploy succeeds but the preview URL doesn't respond, the server might have crashed after it started, or the container might be asleep. Deploy again with the same `sandboxId`, and read `output` for errors.

## Next steps

* [Runtype MCP server](/developer-guides/integrations/runtype-mcp-server): connect an MCP client that can call `deploy_sandbox`
* [Marathon (CLI long-running tasks)](/user-guide/agents/marathon-long-running-tasks): let a CLI agent deploy previews with `--sandbox`
* [Working with tools](/developer-guides/guides/working-with-tools): run built-in and custom tools from flows and agents