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

# API Keys

> Create, list, and revoke API keys for programmatic access.

# API Keys

API keys are the primary authentication mechanism for the PolySimulator API. Each key is tied to a user account, has configurable permissions, and can be revoked instantly.

<Note>
  **Closed beta (ongoing).** API key issuance is cohort-gated, so
  `POST /v1/keys/bootstrap` and `POST /v1/keys` return a `403` for
  callers who aren't yet admitted — branch on the `X-Polysim-Code`
  response header. See [Authentication → Closed Beta](/authentication#closed-beta)
  for the full lifecycle (the `CLOSED_BETA` / `API_PRO_COMING_SOON`
  issuance gates and the separate runtime `ACCESS_RESTRICTED` allowlist
  gate on already-issued keys).
</Note>

***

## Create a Key

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.polysimulator.com/v1/keys \
    -H "X-API-Key: $API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "my-trading-bot",
      "permissions": ["read", "trade"],
      "tier": "pro"
    }'
  ```

  ```python Python theme={null}
  import requests, os

  resp = requests.post(
      f"{os.environ['POLYSIM_BASE_URL']}/v1/keys",
      headers={"X-API-Key": os.environ["POLYSIM_API_KEY"]},
      json={
          "name": "my-trading-bot",
          "permissions": ["read", "trade"],
          "tier": "pro",
      },
  )
  resp.raise_for_status()
  key_data = resp.json()
  print(f"Save this key: {key_data['raw_key']}")
  ```
</CodeGroup>

<Note>
  **Free-tier keys are read-only.** A `free` key requesting `trade`
  returns `403 TIER_REQUIRES_UPGRADE` — so a `["read", "trade"]` key
  needs a paid tier (`pro` / `pro_plus` / `enterprise`). Omit
  `permissions` to take the per-tier default (`free` → `["read"]`,
  paid → `["read", "trade"]`).
</Note>

**Response (201 Created):**

```json theme={null}
{
  "id": 1,
  "raw_key": "ps_live_kJ9mNx2pQrStUvWxYz01Ab3CdEfGhI4j...",
  "key_prefix": "ps_live_kJ9mNx2p",
  "name": "my-trading-bot",
  "rate_limit_tier": "pro",
  "permissions": ["read", "trade"],
  "created_at": "2026-02-06T12:00:00Z"
}
```

<Warning>
  The `raw_key` field is shown **exactly once**. Store it securely — it cannot
  be retrieved again. Only the SHA-256 hash is stored in the database.
</Warning>

| Field             | Description                                                           |
| ----------------- | --------------------------------------------------------------------- |
| `id`              | Numeric key ID (used for revocation)                                  |
| `raw_key`         | Full API key — save this immediately                                  |
| `key_prefix`      | First 16 chars (used for identification in listings)                  |
| `name`            | Your label for this key                                               |
| `rate_limit_tier` | `free`, `pro`, `pro_plus`, or `enterprise` (see `GET /v1/keys/tiers`) |
| `permissions`     | Array of granted permissions                                          |
| `created_at`      | ISO 8601 timestamp                                                    |

***

## List Keys

Returns all keys for your account. Only prefixes are shown — never the full key.

```bash theme={null}
curl -H "X-API-Key: $API_KEY" https://api.polysimulator.com/v1/keys
```

```json theme={null}
[
  {
    "id": 1,
    "key_prefix": "ps_live_kJ9mNx2p",
    "name": "my-trading-bot",
    "permissions": ["read", "trade"],
    "rate_limit_tier": "pro",
    "is_active": true,
    "created_at": "2026-02-06T12:00:00Z",
    "expires_at": null
  }
]
```

***

## Revoke a Key

Permanently deactivates a key. **This action cannot be undone.**

```bash theme={null}
curl -X DELETE -H "X-API-Key: $API_KEY" \
  https://api.polysimulator.com/v1/keys/1
```

```json theme={null}
{
  "message": "Key revoked successfully"
}
```

***

## Key Limits

| Constraint        | Value                                                                                                                                                                                                        |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Max keys per user | 5                                                                                                                                                                                                            |
| Key format        | `ps_live_<64 hex chars>`                                                                                                                                                                                     |
| Storage           | SHA-256 hash only                                                                                                                                                                                            |
| Expiration        | Not set at creation — `expires_at` is server-managed (populated on a rotated key's old half during its 24h overlap, and on beta keys as their cutoff). Use `POST /v1/keys/{id}/rotate` for short-lived keys. |

***

## Error Handling

| Status | Endpoint                                   | Meaning                                                                                                                                                                                                        |
| ------ | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `201`  | `POST /v1/keys`                            | Key created — save `raw_key` immediately                                                                                                                                                                       |
| `400`  | `POST /v1/keys`                            | Max 5 keys reached, or unknown tier (`X-Polysim-Code: VALIDATION_FAILED`)                                                                                                                                      |
| `400`  | `POST /v1/keys/bootstrap`                  | You already have key(s) — use `POST /v1/keys` with `X-API-Key`                                                                                                                                                 |
| `401`  | Any                                        | Invalid, expired, or deactivated API key                                                                                                                                                                       |
| `401`  | `POST /v1/keys/bootstrap`                  | Invalid or expired Supabase JWT                                                                                                                                                                                |
| `403`  | `POST /v1/keys`, `POST /v1/keys/bootstrap` | Key issuance is [closed-beta](/authentication#closed-beta) gated (`CLOSED_BETA` / `API_PRO_COMING_SOON`), or `TIER_REQUIRES_UPGRADE` for a free key requesting `trade`. Branch on the `X-Polysim-Code` header. |

```python theme={null}
# Robust key creation with error handling
resp = requests.post(
    f"{BASE_URL}/v1/keys",
    headers={"X-API-Key": API_KEY},
    json={"name": "my-bot", "permissions": ["read", "trade"]},
)

if resp.status_code == 201:
    key_data = resp.json()
    print(f"Save this key NOW: {key_data['raw_key']}")
elif resp.status_code == 400:
    err = resp.json()
    print(f"Cannot create key: {err.get('message', err.get('error'))}")
elif resp.status_code == 401:
    print("API key is invalid or expired — re-authenticate")
elif resp.status_code == 403:
    # Key issuance is closed-beta gated; branch on the stable machine
    # code in the `X-Polysim-Code` header (the body's `error` is the
    # human message).
    code = resp.headers.get("X-Polysim-Code")
    if code == "CLOSED_BETA":
        # Your account isn't in an admitted cohort yet. Apply via the
        # waitlist at https://polysimulator.com/api-trading.
        print("Closed beta — apply at https://polysimulator.com/api-trading")
    elif code == "API_PRO_COMING_SOON":
        # Paying Pro / Pro+ without a cohort grant — API access is
        # rolling out in cohorts. Check /account/billing.
        print("Pro API access is rolling out — see /account/billing")
    elif code == "TIER_REQUIRES_UPGRADE":
        # Free tier is read-only: requesting ["read", "trade"] without a
        # paid tier is rejected. Omit "trade" or upgrade first.
        print("Free tier cannot create trade-scoped keys — upgrade or drop 'trade'")
    elif code == "INSUFFICIENT_PERMISSION":
        print("Key lacks the required permission for this endpoint")
    else:
        print(f"403 [{code}]: {resp.json().get('error')}")
else:
    print(f"Unexpected error: {resp.status_code}")
```

***

## Next Steps

* [Rate Limits](/concepts/rate-limits) — Understand your tier's request budget
* [Authentication](/authentication) — Security model deep dive
