> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cybr.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> These docs cover learner integrations through Hosted Lab Pages, the SDK, and the REST API. Content management is outside this integration scope.
> Read the setup page for the chosen approach before implementing it. Keep organization API keys and hosted mint secrets on the server.
> Install the SDK with `npm install @cybr/labs-sdk`. It runs on the server; the `/terminal`, `/terminal/xterm`, `/video`, and `/hosted-browser` entry points run in the browser.
> Cybr provides completion tracking and CTF verification. The integrating platform decides whether to award points.

# Tracking deployments

> Record each launch in your own database to check ownership, resume labs, and avoid duplicate launches.

The API cannot list a learner's deployments, and it accepts any deployment ID in your organization. Your server is the only place that knows which learner owns which deployment, so record each launch.

## Store each launch

| Column | Value |
| - | - |
| `deploymentId` | `deployment.id` from `launch()` |
| `learnerId` | The learner you launched for |
| `labId` | The lab |
| `launchedAt` | When `launch()` returned |

Add a unique constraint on active rows for each learner and lab. It stops two tabs from launching the same lab at once.

## Launch, or resume an active lab

Each launch counts toward your usage and limits. Before launching, look for an active row for the same learner and lab, and return it instead:

```ts theme={null}
const existing = await db.findActiveDeployment(learnerId, labId)
if (existing) {
  const status = await cybr.deployments.getStatus(existing.deploymentId)
  if (status.status !== 'failed') return existing
  await db.closeDeployment(existing.deploymentId)
}

const deployment = await cybr.deployments.launch({ labId, learnerId, membership: 'premium' })
await db.saveDeployment({
  deploymentId: deployment.id,
  learnerId,
  labId,
  launchedAt: new Date()
})
```

The same lookup resumes a running lab after the learner reloads the page.

## Check ownership on every request

For status, terminal, and end requests, find the row for the current learner and lab. Use only the `deploymentId` from that row:

```ts theme={null}
const row = await db.findActiveDeployment(learnerId, labId)
if (!row) return Response.json({ message: 'DEPLOYMENT_NOT_FOUND' }, { status: 404 })

const status = await cybr.deployments.getStatus(row.deploymentId)
```

Never act on a deployment ID the browser sends without this check.

## End labs

When the learner ends a lab early, for example with an **End lab** button, call `destroy()` and close the row:

```ts theme={null}
await cybr.deployments.destroy(row.deploymentId)
await db.closeDeployment(row.deploymentId)
```

A `not_found`, `conflict`, or `deployment_failed` error from `destroy()` means the lab is already over. Close the row in that case too.

When `lab.timeLimit` is set, Cybr ends the lab automatically at `launchedAt` plus `timeLimit` minutes. Show a countdown to that time, and close the row when it passes or a status check returns `failed`.

## Failed launches

A `timeout` or `network_error` from `launch()` is ambiguous. The launch may have started a deployment before the connection failed, and the API has no idempotency key.

Do not launch again automatically. Ask the learner to wait a minute before trying again. A deployment that did start ends at its time limit.

The [error guide](/guides/error-handling#ambiguous-writes) covers the same rule for other writes.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.