# Ask for a sandbox key

`POST /sandboxes`

Takes **no credential**: it is how a script, a CLI or a coding agent that holds no key yet asks for one.

**A person approves.** The answer is `202` with `status: "authorization_required"`: a `verifyUrl` to show the person you are working for, and a `deviceCode` to keep. They open the link, a page of its own that says who is asking, sign in and approve or decline (no phone number and no enrolment: the sign-in is enough for a test key). Poll `POST /sandboxes/token` with the `deviceCode` every `interval` seconds; it answers `pending` until they approve and then hands over the key, once. The key is an ordinary sandbox key on their developer account: no expiry, visible and revocable in the console, and capped at 5,000 requests a day until the account verifies a phone. An authorization lives ten minutes.

**Anonymous sandboxes** are a server option that is off by default. Where the operator enabled them the answer is `201` with `status: "ready"` and an `apiKey` at once, for a sandbox nobody owns yet: it has small limits, expires at `expiresAt`, may not register webhooks or Connect apps or have documents read, and a person keeps it by opening `claimUrl`, which replaces its key. Read `status` and handle both.

## Body (optional)

| Field | Type | | Description |
|---|---|---|---|
| `name` | string |  | What you are building, shown to the person on the approval screen. Letters, digits, space, dot, dash; 60 characters. |

## Response

`201`: `{ "ok": true, … }`

| Field | Type | | Description |
|---|---|---|---|
| `ok` | boolean | **required** |  |
| `status` | string | **required** |  One of: `ready`. |
| `mode` | string | **required** |  One of: `sandbox`. |
| `apiKey` | string | **required** | The `bk_test_` key. Shown here once: send it as `X-Api-Key`. |
| `keyPrefix` | string |  | The first characters of the key, as the console lists it. |
| `expiresAt` | string | **required** | When the key stops working unless the sandbox is claimed. |
| `claimUrl` | string |  | A person opens it and signs in to keep the sandbox. Claiming replaces the key. |
| `claimToken` | string |  | The token inside `claimUrl`. |
| `limits` | object |  |  |
| `limits.ratePerMin` | integer |  | Requests per minute while unclaimed. |
| `limits.dailyCap` | integer |  | Requests per day while unclaimed. |
| `apiBase` | string |  | The base URL for every other call. |
| `mcpUrl` | string |  | The MCP endpoint, which takes the same key. |
| `docs` | string |  | The documentation index for agents. |
| `next` | array of string |  | What to do with the key, in order. |

Errors: `429`, `503`. Every failure answers `{ "error", "reason", "hint", "param", "doc_url", "retryable", "request_id" }`: branch on `reason`, and read `hint` for what to change (https://docs.billos.co.il/errors.md).

## Examples

### cURL

```bash
curl -X POST "$BASE/sandboxes" \
```
### Node

```node
const res = await fetch(`${BASE}/sandboxes`, {
  method: 'POST',
  headers: {
  },
})
const data = await res.json()
if (!data.ok) throw new Error(data.reason)
```
### Python

```python
import requests
res = requests.post(
    f"{BASE}/sandboxes",
)
data = res.json()
```