API key controls
When you mint a key you can scope it: cap its monthly spend, restrict it to a set of models, and give it an expiry. These are enforced on every metered call. A key created without any of them behaves exactly as it always has: unlimited, all models, no expiry.
Creating a scoped key
POST /v1/keys accepts these optional fields alongside name:
spend_limit_usd: spend cap in whole USDspend_window: how often that cap resets:day,week,month(default), ortotal(a lifetime cap that never resets). A refreshing budget (day/week) lets an unattended agent recover instead of exhausting its cap forever; delegated child keys inherit their parent's window and can't loosen itallowed_models: an allowlist of Sable model ids;sandboxis also a valid entry, gating sandbox computeexpires_in_days: days until the key stops workingrate_limit_per_min: this key's own request/minute limit (see Rate limits); omit to use the global default
curl https://api.buildsable.com/v1/keys \
-H "Authorization: Bearer $SABLE_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "trading-agent-prod",
"spend_limit_usd": 50,
"allowed_models": ["sable-llama-3.3-70b", "sable-deepseek-v3"],
"expires_in_days": 90
}'The plaintext key is returned exactly once at creation. Store it immediately. See Authentication.
Enforcement
The controls are checked on every metered route: /v1/chat/completions,
/v1/embeddings, /v1/messages (which delegates to the
chat handler), and /v1/sandboxes, where the sandbox
allowlist entry gates access:
| Condition | Status | Error type |
|---|---|---|
Monthly spend would exceed spend_limit_usd | 402 | spend_limit_exceeded |
Requested model not in allowed_models | 403 | scope_denied |
| Call made after the key's expiry | 401 | — |
Spend is measured against the same metered cost that appears in each receipt and rolls up in usage, so a key's cap and your account totals always agree.
A monthly cap binds the key and its whole delegated subtree: usage by every delegated sub-key beneath a key counts against that key's cap, and every ancestor's cap is enforced on each request. A child can never spend past any budget above it, however the tree fans out.
# HTTP 402 Payment Required
{
"error": {
"type": "spend_limit_exceeded",
"message": "monthly spend limit reached for this key"
}
}The spend cap is a monthly window. A key that hits its limit resumes at
the start of the next month; revoke and re-mint if you need it sooner, or
raise the cap on a fresh key. Expiry is one-way: an expired key returns
401 and can't be reactivated.
Rotating a key
POST /v1/keys/:id/rotate (session-authed, like minting) replaces a key
without re-deriving its scopes by hand. It mints a scope-identical
replacement (same spend cap, allowlist, rate limit, and tier), returns the new
plaintext exactly once, and expires the old key after a grace window so
in-flight deployments can switch over without a hard cut.
curl https://api.buildsable.com/v1/keys/$KEY_ID/rotate \
-H "Authorization: Bearer $SABLE_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{"grace_minutes": 30}'grace_minutes defaults to 10 and is capped at 1440 (one day). The
grace window never extends an earlier expiry: a key already set to expire
sooner still expires at its original time.