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 scoped to one environment, encoded in the prefix:

  • sk_live_… — production. Real numbers, real charges.
  • sk_test_… — test. Same API surface, isolated from production data and billing.

A live key and a test key are never interchangeable.

Scopes

Each key carries a set of scopes from the Scope vocabulary. A request is allowed only if the key holds the scope the action requires.

ScopeGrants
calls:createplace outbound calls
calls:controlcontrol a live call (speak, transfer, DTMF, hang up)
messages:createsend SMS
numbers:readlist and read numbers
numbers:provisionprovision new numbers
numbers:releaserelease numbers
connections:readread connections
connections:writecreate and update connections
consent:writerecord and revoke consent
webhooks:writemanage webhook endpoints
billing:readread balance and usage
keys:adminmint, list, and revoke child keys
readbaseline read access

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:

{
  scopes: Scope[]            // at least one, from the table above
  resources?: {
    numbers?: string[]       // allow-list for number-scoped actions
  }
  spendLimit?: {
    amountCents: number
    resetPeriod?: 'monthly' | null
  }
}
  • scopes — the minimum is one scope; the key can do nothing outside them.
  • resources.numbers — an optional allow-list. When present, number-scoped actions are restricted to exactly those numbers.
  • spendLimit — a hard cap enforced at reserve time by the Ledger, so it holds even for in-flight spend. resetPeriod: '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 InsufficientFunds.

Child keys: delegation with a ceiling

A key that holds the keys:admin scope 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 a keys:admin key can only ever narrow what it already holds — never escalate. Hand your keys:admin key to your provisioning system, hand the scoped child keys it mints to individual agents.

ActionEndpoint
Mint a child keyPOST /api-tokens
List keysGET /api-tokens
Revoke a keyPOST /api-tokens/:id/revoke

POST /api-tokens returns a fresh key bound to the grant you pass. The plaintext key is shown once — 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",
    "grant": {
      "scopes": ["calls:create", "messages:create", "numbers:read", "read"],
      "resources": { "numbers": ["num_01H..."] },
      "spendLimit": { "amountCents": 5000, "resetPeriod": "monthly" }
    }
  }'
const res = await fetch('https://api.saperly.com/api-tokens', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SAPERLY_ADMIN_KEY!}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'agent-42',
    grant: {
      scopes: ['calls:create', 'messages:create', 'numbers:read', 'read'],
      resources: { numbers: ['num_01H...'] },
      spendLimit: { amountCents: 5000, resetPeriod: 'monthly' },
    },
  }),
})

const { key } = await res.json() // sk_live_… — shown once

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, 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 a keys:admin 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 scopes from their org role, not a grant:

RoleScopes
owner / adminall scopes
membercalls:create, calls:control, messages:create, numbers:read, connections:read, consent:write, billing:read, read

A member deliberately cannot provision or release numbers, or administer keys.

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.

Legacy service keys

Older sk_svc_ service keys still exist as a legacy management plane — list, create, revoke, and rotate only. They do not mint scoped keys and are not part of the current model. New integrations use scoped sk_ keys and POST /api-tokens for delegation.

On this page