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.
Closed beta (ongoing). API key issuance is cohort-gated. New keys
are issued to approved cohorts only, so POST /v1/keys/bootstrap
(first key) and POST /v1/keys (additional keys) return a 403 for
callers who aren’t yet admitted — CLOSED_BETA for free /
waitlisted accounts, or API_PRO_COMING_SOON for paying Pro /
Pro+ accounts that don’t yet have a cohort grant:HTTP/1.1 403 Forbidden
X-Polysim-Code: CLOSED_BETA
Content-Type: application/json
{"error": "API access is in closed beta. New keys are issued to approved cohorts only. Apply via the waitlist; we'll email you when a cohort opens."}
Branch on the X-Polysim-Code response header (the body’s error is
the human message) and surface a “join the waitlist” prompt. While the
beta is closed, every non-admitted caller — including paying Pro / Pro+ —
gets CLOSED_BETA; API_PRO_COMING_SOON appears only once self-serve
issuance is enabled. Apply via the waitlist at
polysimulator.com/api-trading.
See Authentication → Closed Beta
for the full lifecycle, including the separate runtime
ACCESS_RESTRICTED allowlist gate on already-issued keys.
Create a Key
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"
}'
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"]).
Response (201 Created):
{
"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"
}
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.
| 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.
curl -H "X-API-Key: $API_KEY" https://api.polysimulator.com/v1/keys
[
{
"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.
curl -X DELETE -H "X-API-Key: $API_KEY" \
https://api.polysimulator.com/v1/keys/1
{
"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 |
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 | CLOSED_BETA — key issuance is in closed beta (the default for every non-admitted caller, including paying Pro / Pro+). The API_PRO_COMING_SOON variant appears only once self-serve issuance is enabled. Branch on the X-Polysim-Code response header. |
# 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