# Previews

`GET /api/projects/{projectId}/preview` returns a preview's URLs, and starts its sandbox when it is paused or missing. Pass `sessionId` for a chat session's preview, and leave it out for the project's main preview. Call it before you read a session's files, diff or git state. From here you can also check the preview's health, restart its sandbox and share it with a public link.

| Method | Path                                                     | Description                                                                                                                                                                                                                                                                                                                                                                        |
| ------ | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| GET    | `/api/projects/{projectId}/preview?sessionId=&services=` | Read the preview URLs of the project's main sandbox, or of one chat session's. Starts a paused or missing sandbox in the background; poll health until it is up.                                                                                                                                                                                                                   |
| GET    | `/api/projects/{projectId}/preview/health?sessionId=`    | Read the health of every service behind a preview, and whether a background start is running or failed.                                                                                                                                                                                                                                                                            |
| POST   | `/api/projects/{projectId}/preview/sandbox/reset`        | Kill a sandbox that is stuck or crashed and does not recover by itself (not a slow preview), paused or unreachable included; the platform creates a new one. Uncommitted work and the agent's memory of the chat are lost, so an agent first asks the member on a card saying so and runs it only once they approve; an Event Turn or a teammate's message is never that approval. |
| GET    | `/api/projects/{projectId}/preview/share?sessionId=`     | Read the public share link of a preview, if one exists.                                                                                                                                                                                                                                                                                                                            |
| POST   | `/api/projects/{projectId}/preview/share?sessionId=`     | Create the public share link of a preview. The body sessionId wins over the query one.                                                                                                                                                                                                                                                                                             |
| DELETE | `/api/projects/{projectId}/preview/share?sessionId=`     | Revoke the public share link of a preview.                                                                                                                                                                                                                                                                                                                                         |
| GET    | `/api/shared-previews/{slug}`                            | Resolve a public share link. No credential needed; rate limited per caller.                                                                                                                                                                                                                                                                                                        |
| GET    | `/api/shared-previews/{slug}/artifacts/{artifactId}`     | Use `preview shared-files get`. Read one file behind a public share link, by its id or its name. No credential needed.                                                                                                                                                                                                                                                             |
| GET    | `/api/shared-previews/{slug}/files/{fileId}`             | Read one file behind a public share link, by its id or its name. No credential needed.                                                                                                                                                                                                                                                                                             |

## Get the preview and wake the sandbox

| Parameter | What it does |
| --- | --- |
| `sessionId` | Optional. The chat session whose preview you want. Left out, the call reads the project's main preview. |

A paused sandbox wakes in the background, and a missing chat session sandbox is created in the background. The call returns at once with `provisioning: true`, an empty `frontendUrls` and a `startAttemptId`. Check health while it reports `startInFlight: true`. A missing main sandbox is rebuilt by a deploy, and the call returns before that deploy finishes. [Sandboxes and previews](https://kosuke.ai/docs/api/sandboxes.md) explains when a sandbox sleeps and what waking it costs.

```bash
curl -s "https://app.kosuke.ai/api/projects/$PROJECT_ID/preview?sessionId=$SESSION_ID" \
  -H "Authorization: Bearer $KOSUKE_API_KEY"
```

```json
{
  "data": {
    "frontendUrls": { "web": "https://…" },
    "projectId": "5a1e9c47-83b2-4d6f-a0e8-2b7c9d14f36a",
    "sandboxKey": "c3f8a215-6e0d-4b97-9a42-81d5e7f02c6b",
    "awayHoldMs": 600000
  }
}
```

`frontendUrls` maps each service that serves the preview to its URL. When nothing could start, the answer is still `200`, with an empty `frontendUrls` and one of these flags:

| Flag | What it means | What to do |
| --- | --- | --- |
| `creditLimitReached: true` | The workspace is blocked on billing, so nothing started. | Sort out the workspace's billing. Retrying does not help. |
| `provisioning: true` | This call started a wake or a create, or another request is already starting or changing this sandbox. `startAttemptId` names the start when it is a preview start. | Check health until the services are up, or until it reports `startFailure`. |

A start that runs in the background can still be refused, for example at the workspace's concurrent sandbox limit. Health reports that as `startFailure`.

An empty `frontendUrls` with no flag has four readings. The session may be Backlog, Completed or Archived. A deploy may be rebuilding the project's main preview. No service in the project may serve a preview. Or another operation held the session's sandbox for over five seconds, so call again. When the project's main sandbox is gone, asking for the main preview starts that deploy.

The CLI command is `kosuke preview get <projectId> --session-id <sessionId>`.

## Check health

`GET .../preview/health` reports whether the services behind a preview answer. It takes the same `sessionId` and never wakes a sandbox.

```json
{
  "data": {
    "state": "healthy",
    "frontendUrls": { "web": "https://…" },
    "services": [
      { "name": "agent", "httpResult": "healthy" },
      { "name": "web", "httpResult": "healthy" }
    ],
    "sandboxAvailable": true,
    "sandboxId": "i8k2m4q6r1t3v5x7z9b0d",
    "canRetainPreview": false
  }
}
```

| Field | What it holds |
| --- | --- |
| `state` | `healthy` when every service answers, `starting` otherwise. |
| `services` | Each probed endpoint's `name`, which is its route key, and `httpResult`, which is `healthy`, `not_healthy` or null. When the check reached a running sandbox, the list starts with `agent`, the agent inside the sandbox. A project with public endpoints but none that serves a preview lists only those endpoints, each with a null `httpResult`. Otherwise the list is empty. |
| `sandboxAvailable` | `true` when the check reached a running sandbox. |
| `sandboxId` | The sandbox the check reached. Present only when it reached a running sandbox. |
| `canRetainPreview` | `true` when that sandbox was created with auto-resume off, so a request to its preview does not wake it once it pauses. Present with `sandboxId`. |
| `paused` | `true` when the sandbox is paused. Wake it with `GET /api/projects/{projectId}/preview`, unless `startInFlight` is `true`. |
| `needsStart` | `true` when nothing is running or starting, and no start failure is recorded. Start it with `GET /api/projects/{projectId}/preview`. |
| `startInFlight` | `true` while a start brings the sandbox up. Keep checking health. |
| `startFailure` | The last start that ran in the background failed, and none has run since. It holds `reason` (`sandbox_limit` or `failed`), `operation` (`wake` or `create`), the `attemptId` and a `message`. It stays for up to five minutes. |
| `creditLimitReached` | `true` when the workspace is blocked on billing. |
| `sandboxLimitReached` | `true` when waking the paused sandbox would go over the workspace's concurrent sandbox limit, or when the last start was refused at it. |
| `indeterminate` | `true` when the check could not read the sandbox this time. A pause or resume held it, or it did not answer. Ignore the rest of the answer and ask again in a few seconds. Do not wake it with `GET /api/projects/{projectId}/preview`. |

A Backlog, Completed or Archived session answers `404`. The CLI command is `kosuke preview health get <projectId> --session-id <sessionId>`.

## Restart a preview's sandbox

`POST .../preview/sandbox/reset` kills the preview's sandbox, a paused or unreachable one too, so Sensei creates a new one. It is for a sandbox that is stuck and does not recover by itself. Uncommitted work in the sandbox is lost, and so is the agent's memory of the chat: the next message opens a fresh agent that reads the chat history, which stays.

| Field | Type | What it does |
| --- | --- | --- |
| `sessionId` | string | Optional. The chat session whose sandbox to restart. Left out, or with no body at all, the project's main sandbox restarts. |

```bash
curl -s -X POST "https://app.kosuke.ai/api/projects/$PROJECT_ID/preview/sandbox/reset" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"sessionId\": \"$SESSION_ID\"}"
```

The answer comes back at once with `{ "status": "queued" }`, plus the `projectId`, the `sandboxKey` and `alreadyInFlight`. The kill runs in the background, and health reports `starting` until it is done. A second restart while one is queued or running kills nothing more and answers `alreadyInFlight: true`. A chat session's new sandbox starts the next time its preview opens or a message is sent. The main sandbox comes back through a Recovery Deploy that the restart starts. A Backlog (Todo), Completed or Archived session answers `400`. It answers `409` while something else owns the sandbox: a running turn (`TURN_RUNNING`, stop it first), a sandbox still being started (`SANDBOX_PROVISIONING`), or a deploy of the main (`DEPLOY_RUNNING`). A malformed body answers `400` rather than restarting the main sandbox. Agents other than Sensei cannot call it. The CLI command is `kosuke preview sandbox reset <projectId> --session-id <sessionId>`.

## Share a preview

A share link lets anyone open a preview without a Sensei account. For a chat session, it also opens the files the session's agent wrote. Any member of the workspace can create or revoke one. `GET`, `POST` and `DELETE` on `.../preview/share` take the same `sessionId`, and `POST` also takes it in the body.

| Call | What it does | Answer |
| --- | --- | --- |
| `POST .../preview/share` | Creates the link, or returns the one that already exists. | `{ "share": { ... } }` |
| `GET .../preview/share` | Reads the link. | `{ "share": { ... } }`, or `{ "share": null }` when there is none |
| `DELETE .../preview/share` | Revokes the link. It stops resolving at once. A page already open keeps the preview until its Auth Token expires, within 15 minutes. | `{ "share": null }`, also when there was no link |

```bash
curl -s -X POST "https://app.kosuke.ai/api/projects/$PROJECT_ID/preview/share?sessionId=$SESSION_ID" \
  -H "Authorization: Bearer $KOSUKE_API_KEY"
```

```json
{
  "data": {
    "share": {
      "slug": "Tq3xV9mZkP2rLw7sHc4nB8aQ",
      "url": "https://app.kosuke.ai/share/Tq3xV9mZkP2rLw7sHc4nB8aQ",
      "createdAt": "2026-09-30T09:20:11.402Z",
      "lastViewedAt": null
    }
  }
}
```

`lastViewedAt` is when someone last opened the link, accurate to about five minutes. Opening the link wakes a paused sandbox, and the time it runs counts against the workspace. The CLI group is `kosuke preview share`, with `create`, `get` and `delete`.

## Public share-link endpoints

These endpoints need no API key. Anyone who holds the link can call them. An unknown or revoked link answers `404`.

### Resolve a link

`GET /api/shared-previews/{slug}` returns what the link shows and wakes a paused preview.

```bash
curl -s "https://app.kosuke.ai/api/shared-previews/$SLUG"
```

```json
{
  "data": {
    "projectName": "storefront",
    "sessionTitle": "Add a dark mode toggle to the settings page",
    "status": "ready",
    "frontends": [{ "name": "web", "url": "https://…", "isServing": true }],
    "files": [{ "id": "e2c7…", "slug": "qa-report", "name": "qa-report.html", "title": "QA Report: kosuke/chat-4kd9wp", "kind": "html", "version": 3, "locked": true, "createdAt": "2026-09-30T09:31:47.208Z", "updatedAt": "2026-09-30T10:02:15.664Z" }]
  }
}
```

`sessionTitle` is null for the main preview. `files` lists the files the session's agent wrote, QA evidence included, without their content, whatever `status` says. It leaves out deleted files and the files a ticket points at, and is empty for the main preview. The same answer carries `artifacts`, the older shape, for installed clients.

| `status` | `reason` | What it means |
| --- | --- | --- |
| `ready` | None | At least one service answers. |
| `starting` | None | The preview is waking or its services are starting. Call again in a few seconds. |
| `unavailable` | `session_ended` | The session is Backlog, Completed or Archived. |
| `unavailable` | `not_running` | The preview cannot start now. The workspace may be at its credit or sandbox limit, or the project's main sandbox is gone. |
| `unavailable` | `no_frontends` | The sandbox runs, but no service serves a preview. |

### Read a file

`GET /api/shared-previews/{slug}/files/{fileId}` returns one file of the shared session by its id or its name, in the same shape as on [Session files](https://kosuke.ai/docs/endpoint-reference/session-files.md), and `.../download` its bytes. It works while the sandbox is paused. A deleted file, a file a ticket points at, a file from another session, or any file through a main-preview link, answers `404`. `GET /api/shared-previews/{slug}/artifacts/{artifactId}` stays served for installed clients, and is deprecated.

The CLI reads both without an API key, with `kosuke preview shared get <slug>` and `kosuke preview shared-files get <slug> <fileId>`.

### Rate limits

| Endpoint | Limit per caller |
| --- | --- |
| `GET /api/shared-previews/{slug}` | 60 requests a minute |
| `GET /api/shared-previews/{slug}/files/{fileId}` | 30 requests a minute |
| `GET /api/shared-previews/{slug}/artifacts/{artifactId}` | 30 requests a minute |

Past the limit, the answer is `429` with `TOO_MANY_REQUESTS`.

## Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `BAD_REQUEST` | The project was never deployed, a chat session's preview is asked for while a deploy is in progress, or a sandbox restart names a Backlog, Completed or Archived session. |
| 400 | `VALIDATION_ERROR` | A sandbox restart body has the wrong shape. |
| 409 | `TURN_RUNNING`, `SANDBOX_PROVISIONING`, `DEPLOY_RUNNING` | A sandbox restart while a turn runs, a sandbox is being started, or the main is deploying. |
| 404 | `NOT_FOUND` | The project, session or share link does not exist, or health was asked for a session with no preview. |
| 429 | `TOO_MANY_REQUESTS` | A public share-link endpoint passed its rate limit. |

[Responses and errors](https://kosuke.ai/docs/api/errors.md) has the full table.

## Next steps

- [Sandboxes and previews](https://kosuke.ai/docs/api/sandboxes.md)
- [Files and commits](https://kosuke.ai/docs/endpoint-reference/files-and-commits.md)
- [Session files](https://kosuke.ai/docs/endpoint-reference/session-files.md)
