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

# Sandbox

> A throwaway credential bound to a throwaway partition, with a reset you can run as often as you like.

**A sandbox key is an ordinary API key with two differences: it acts in an isolated partition of your organization, and it cannot leave it.**

That is what makes it safe to hand to a test suite. Nothing a sandbox key writes touches your real graph, and nothing it reads can reach a real subject.

## Minting one

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl -X POST https://api.get-exo.com/v1/sandbox/keys \
  -H "X-Exo-API-Key: $EXO_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"label": "CI"}'
```

The response carries the plaintext exactly once, like any minted key.

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "id": "3f1c0a...",
  "apiKey": "exo_sandbox_...",
  "label": "CI",
  "scopes": ["read", "write"],
  "subject": "__sandbox__"
}
```

Four things are fixed and not negotiable through the request:

| Property | Value              | Why                                                                                            |
| -------- | ------------------ | ---------------------------------------------------------------------------------------------- |
| Prefix   | `exo_sandbox_`     | Recognisable on sight, in a log or a config file.                                              |
| Scopes   | `read` and `write` | Not caller-selectable. A test key must never mint keys, provision subjects or erase real data. |
| Lifetime | 30 days            | The pitch is a throwaway credential for a throwaway partition. It expires.                     |
| Subject  | `__sandbox__`      | The reserved partition it is bound to.                                                         |

Minting needs the `admin` scope. See [`POST /v1/sandbox/keys`](/api-reference/keys/create-sandbox-key).

## The containment rule

**A sandbox key presenting `X-Exo-Subject` for anything but its own partition is refused with 403 [`permission_denied`](/errors/permission_denied).**

Without that rule "sandbox" would be a label rather than a boundary: one leaked test key could read, and once exports shipped, dump, every provisioned subject in the organization. The check asks the database rather than trusting the key prefix, so it holds even for a key minted through another route.

A sandbox key also cannot mint API keys, for the same reason.

## Resetting it

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl -X POST https://api.get-exo.com/v1/sandbox/reset \
  -H "X-Exo-API-Key: $EXO_ADMIN_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
```

The response reports what went:

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "subject": "__sandbox__",
  "rowsDeleted": 1284,
  "tablesCleared": 9,
  "tablesSkipped": 0,
  "jobsDeleted": 12,
  "keysRetained": 2,
  "orphansDeleted": 0
}
```

**The reset keeps the sandbox user and its keys.** That is the point: your test credential keeps working against a now-empty partition, so a suite does not have to re-mint between runs. Running it on an already-empty sandbox is a no-op that returns zeroes.

Reset is replay-safe. A resubmitted `Idempotency-Key` returns the original counts rather than running the wipe again.

## What reset does not clear

Reset removes rows it can attribute to the sandbox partition. Some derived structure has no attribution: domains the sandbox content helped form, segmentation rows, and clustered agent patterns are organization-wide aggregates, and deleting them would take a real user's cognition with them.

Those are left in place, deliberately. If you genuinely need a clean slate for the whole organization, that is [`POST /v1/purge`](/api-reference/admin/purge-data) with `scope: "org"`, which is a two-phase destructive ceremony rather than a test convenience.

## Routes

| Route                                                                        | What it does                                                          |
| ---------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| [`POST /v1/sandbox/keys`](/api-reference/keys/create-sandbox-key)            | Mint a sandbox key. Plaintext shown once. Not idempotency-replayable. |
| [`GET /v1/sandbox/keys`](/api-reference/keys/list-sandbox-keys)              | List sandbox keys, active and revoked.                                |
| [`DELETE /v1/sandbox/keys/{key_id}`](/api-reference/keys/delete-sandbox-key) | Revoke one. Idempotent 204, even for an unknown id.                   |
| [`POST /v1/sandbox/reset`](/api-reference/keys/reset-sandbox)                | Wipe the partition, keep the keys.                                    |

All four require `admin`.

## One conflict to know about

`__sandbox__` is a reserved subject id. If it was already provisioned as an ordinary subject, the sandbox cannot claim it without colliding with real data, and the routes refuse with 409 [`sandbox_subject_conflict`](/errors/sandbox_subject_conflict) rather than proceeding.

## Next

<Columns cols={2}>
  <Card title="Authentication" icon="key" href="/authentication">
    How keys are minted, presented and revoked.
  </Card>

  <Card title="Scopes" icon="shield" href="/scopes">
    Why a sandbox key is capped at read and write.
  </Card>
</Columns>
