saperly
Guides

Authentication

Saperly uses one tier of scoped sk_ API keys, each carrying a grant of scopes, an optional number allow-list, and an optional spend cap. The workspace is always read from the key, and a key with keys:admin can mint child keys bounded by its own grant.

Every Saperly request authenticates with Authorization: Bearer sk_…. There is one tier of credential: a scoped API key that agents use to place calls, send SMS, and manage numbers, connections, and consent. Each key carries a grant that bounds exactly what it can do. The workspace an action belongs to is always read from the key — never from client input — so a key can only ever touch its own workspace's data, even when you issue keys at fleet scale.

The key is the tenant

You never pass an org or workspace id. Saperly resolves the workspace from the key on every request, which is what makes fleet-scale key issuance safe: a key is structurally incapable of reaching another tenant's data.

Sending the key

Send the key as a bearer token on every request:

curl https://api.saperly.com/numbers \
  -H "Authorization: Bearer $SAPERLY_API_KEY"

Keys are bearer credentials, prefixed sap_sk_live_…. The workspace, scopes, number allow-list, and spend cap all ride on the key itself — the workspace is always resolved from the token, never from client input.

Scopes

Each key carries a set of scopes. A request is allowed only if the key holds the scope the action requires. On the wire a key holds one or more of three coarse scopes:

ScopeGrants
readlist and read resources (numbers, connections, messages, calls, usage, consent)
writemutating actions — place calls, send SMS, provision/release numbers, record consent, write connections
admineverything write allows, plus minting, listing, and revoking child keys

The grant

A key's grant is more than scopes. It also bounds which numbers the key can touch and how much it can spend. On the wire (the POST /api-tokens body) the grant is flat:

{
  name: string                                  // 1–100 chars
  scopes: ('read' | 'write' | 'admin')[]        // at least one
  numberScope?: string[] | null                 // allow-list for number-scoped actions
  spendLimitCents?: number | null               // hard spend cap, in cents
  spendLimitResetPeriod?: 'monthly' | null      // cap window
}
  • scopes — the minimum is one scope; the key can do nothing outside them.
  • numberScope — an optional allow-list. When present, number-scoped actions are restricted to exactly those numbers.
  • spendLimitCents — a hard cap enforced at reserve time by the Ledger, so it holds even for in-flight spend. spendLimitResetPeriod: 'monthly' resets the counter at the UTC month boundary; null (or omitted) means a single lifetime cap.

Because the spend limit is checked when funds are reserved, a key can never overrun its cap mid-call — the reservation simply fails with SpendLimitExceeded.

Child keys: delegation with a ceiling

A key with the admin scope (the key-administration capability, keys:admin) can mint child keys — one key per agent, each with a narrower grant. This is how you hand out keys at fleet scale without minting a separate admin credential per agent.

The child is bounded by the parent's ceiling: its scopes, number allow-list, and spend cap may not exceed the minting key's own grant. This is enforced server-side, so an admin key can only ever narrow what it already holds — never escalate. Hand your admin key to your provisioning system, hand the scoped child keys it mints to individual agents.

ActionEndpointSDK
Mint a child keyPOST /api-tokenskeys.mint(...)
List keysGET /api-tokenskeys.list()
Revoke a keyPOST /api-tokens/:id/revokekeys.revoke(...)

POST /api-tokens returns a fresh key bound to the grant you pass. The plaintext token is in the response — store it immediately.

curl -X POST https://api.saperly.com/api-tokens \
  -H "Authorization: Bearer $SAPERLY_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "agent-42",
    "scopes": ["read", "write"],
    "numberScope": ["num_01H..."],
    "spendLimitCents": 5000,
    "spendLimitResetPeriod": "monthly"
  }'
import { configure, keys } from '@trysaperly/sdk'

configure({ apiKey: process.env.SAPERLY_ADMIN_KEY! })

const { data, error } = await keys.mint({
  body: {
    name: 'agent-42',
    scopes: ['read', 'write'],
    numberScope: ['num_01H...'],
    spendLimitCents: 5000,
    spendLimitResetPeriod: 'monthly',
  },
})

if (error) {
  // typed error body — e.g. AuthorizationDenied when the grant exceeds the ceiling
  console.error(error)
} else {
  const token = data.token // sap_sk_live_… — present in the response, store it now
}

The child inherits the minting key's workspace — you never pass an org or workspace id, which is precisely what closes the cross-tenant hole. If the grant you request exceeds the parent's ceiling (scopes ⊄ parent, a number outside the parent's allow-list, or a higher spend cap), the mint is rejected.

Where keys come from

A key is created either by an org admin in the dashboard (Settings → Keys) or programmatically by an admin-scoped key via POST /api-tokens. Use the dashboard for a handful of keys; mint child keys when you're issuing one per agent at fleet scale.

Role presets (human members)

Human members get their abilities from their org role, not from a key grant:

RoleAllows
owner / admineverything — read, write, and key administration
memberread + the everyday write actions (place calls, send SMS, read numbers/connections, record consent), but not key administration

A member deliberately cannot administer keys; key administration is reserved for owner / admin (or a keys:admin-equivalent admin-scoped key).

Next steps

  • Numbers — provision and manage numbers with a scoped key.
  • Billing — the prepaid ledger and how spend limits are enforced.
  • Core concepts — workspaces, tenancy, and the org-from-token rule.
  • API reference — every api-tokens endpoint with a try-it playground.

On this page