Authentication
Every developer request authenticates with an API key. The key identifies
your tenant, carries a set of scopes, and pins
the environment (sb_test_ → TEST, sb_live_ → LIVE).
Sending the key
Present the key in either accepted header:
- curl
- TypeScript
- CLI
# Preferred header
curl https://api.sandbox.safebank.ai/v1/wallets \
-H "X-SafeBank-Api-Key: $SAFEBANK_API_KEY"
# Bearer form (also accepted)
curl https://api.sandbox.safebank.ai/v1/wallets \
-H "Authorization: Bearer $SAFEBANK_API_KEY"
import { SafeBank } from '@safebank/sdk'
// The transport attaches X-SafeBank-Api-Key to every request.
const sb = new SafeBank({ apiKey: process.env.SAFEBANK_API_KEY! })
# Credentials resolve: --api-key flag → SAFEBANK_API_KEY env → saved profile.
sb login sb_test_... # saves a profile
sb whoami # shows the active tenant + masked key
How authorization works
Each /v1 route runs two checks server-side:
PrincipalGuardresolves the caller to a principal. If the request presents ansb_(test|live)_…key (in either header), it authenticates as anapi_keyprincipal holding exactly its stored scopes. Otherwise it falls back to a Privy session (userprincipal — the dashboard), which implicitly holds every scope for its tenant.ScopesGuardenforces the route's@RequiredScopes(...). Anapi_keypasses when its scopes include the required scope (or the root*scope); ausersession always passes.
A key missing the required scope gets 403 with code INSUFFICIENT_SCOPE
(see Errors).
Scopes
Grant a key the least privilege it needs. The full taxonomy:
| Group | Scopes |
|---|---|
| Wallets | wallets:read, wallets:write, wallets:fund |
| Payments | payments:read, payments:create, payments:execute |
| Cards | cards:read, cards:issue, cards:manage |
| Policies | policies:read, policies:write, policies:evaluate |
| Agents | agents:read, agents:manage |
| Treasury | treasury:read, treasury:write |
| Invoices | invoices:read, invoices:write, invoices:release |
| Keys | keys:manage |
| Root | * (all scopes — avoid for shared keys) |
A few concrete mappings from the REST surface:
- Creating a wallet needs
wallets:write; reading needswallets:read. - Funding a wallet from the faucet needs
wallets:fund. - Creating a payment needs
payments:create; executing needspayments:execute; reading/listing needspayments:read. - Minting or rotating keys needs
keys:manage.
Creating and rotating keys
You need a key (with keys:manage) or a dashboard session to mint more keys.
The secret is returned once — store it immediately.
- curl
- TypeScript
curl -X POST https://api.sandbox.safebank.ai/v1/keys \
-H "X-SafeBank-Api-Key: $SAFEBANK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "CI sandbox",
"environment": "test",
"scopes": ["wallets:write", "wallets:fund", "payments:create"]
}'
const created = await sb.keys.create({
name: 'CI sandbox',
environment: 'test',
scopes: ['wallets:write', 'wallets:fund', 'payments:create'],
})
console.log(created.key) // shown once — persist it now
// Later: list, rotate, or revoke.
await sb.keys.list()
await sb.keys.rotate(created.id)
await sb.keys.revoke(created.id)
Response (CreatedApiKey):
{
"id": "c1a2b3d4-…",
"key": "sb_test_Ab12Cd34Ef56_xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"pubId": "Ab12Cd34Ef56",
"environment": "TEST",
"scopes": ["wallets:write", "wallets:fund", "payments:create"],
"expiresAt": null,
"createdAt": "2026-08-09T12:00:00.000Z"
}
Keys created for agents cannot hold the elevated scopes agents:manage,
keys:manage, or policies:write — an agent must not mint keys, manage other
agents, or author policy. This is enforced at key-creation time.
Handling secrets
- Never commit keys or embed them in client-side code. Keep them in a secret manager or env var (
SAFEBANK_API_KEY). - Use separate keys per service/environment so you can rotate or revoke one without downtime.
- Rotate on a schedule and on any suspected exposure —
sb.keys.rotate(id)issues a new secret and invalidates the old one.