For AI agents: a documentation index is available at the root level at /llms.txt and /llms-full.txt. Append /llms.txt to any URL for a page-level index, or .md for the markdown version of any page.
WebsiteDashboardGet API key
  • Get Started
    • Welcome
    • Quickstart
    • Agent onboarding
    • Service keys
    • Core Concepts
  • Guides
    • Hosted Mode
    • Webhook Mode
    • Audio Mode
    • SMS and Conversations
    • Compliance and Consent
    • Billing and Usage
    • Voice zones
    • SMS zones
  • Reference
    • Authentication
    • API Overview
    • Errors and Testing
  • API Reference
  • Changelog
    • Cloudflare Insights CSP
    • Agent-native key management
    • Postpaid auto-charge
    • Cents-honest pricing
    • Stripe payments foundation
    • Hosted/webhook mode rename
    • Upgrading to v0.5.7.0
LogoLogo
WebsiteDashboardGet API key
On this page
  • What are service keys?
  • The safety invariant
  • How permissions work
  • Permissions reference
  • Code examples
  • Mint a child API key
  • Rotate a child key
  • Revoke a child key
  • Best practices
  • What service keys cannot do
Get Started

Service keys

Was this page helpful?
Previous

Core Concepts

Next
Built with

Like Stripe restricted keys + Exa team keys. First time applied to telephony.

What are service keys?

v0.5.7.0 introduced a two-tier credential model so an AI agent can mint, rotate, and revoke its own scoped API keys without human intervention. The bootstrap step stays in the portal; everything after that is API-driven.

A service key is a root credential for credential management. It never places calls, never sends SMS, never moves money. Its only job is to call /v1/keys on your behalf so an agent can manage its own credentials at runtime.

CredentialPrefixWhat it can doHow to get one
API keysk_test_… / sk_live_…Place calls, send SMS, manage lines, query usage, read /v1/auditPortal, OR minted by a service key via POST /v1/keys
Service keysk_svc_test_… / sk_svc_live_…Mint, list, get, update, rotate, revoke child API keysPortal only (Settings → Service Keys)

Both credential types authenticate the same way on the wire: Authorization: Bearer <key>. The prefix tells the auth dispatcher which tier you’re on.

The safety invariant

Service keys cannot mint other service keys via API. This is enforced at the schema level (there is no parent_service_key_id column on the service keys table), so no API surface can ever expose that path. The pattern mirrors Stripe restricted keys and Exa service keys: bootstrap is one-way through a human surface, by design.

If you lose every service key for an account, recovery requires logging into the portal and rotating a new one. There is no API path back.

How permissions work

Child API keys carry their own scope. The service key that mints them does not relax any account-level guardrails. A child key is the intersection of the requested scope and what the account allows.

line_id scopes a child key to a single line. The line must exist, be owned by the same account as the service key, match the service key’s environment, and not be released. Otherwise POST /v1/keys returns 404 not_found on the line (without sticky-caching, so retries succeed once the binding is fixed). Omit line_id for an account-wide child key.

monthly_cap_cents is per-key and independent of the account cap. Spend on a child key counts against both its own cap and the account cap, whichever hits first.

Service keys themselves are limited to the six /v1/keys credential-management endpoints. Calling any data-plane endpoint with a service key returns 401 invalid_api_key. The tier dispatcher rejects the wrong-tier token before the handler runs.

Permissions reference

The API accepts exactly four permission tiers on POST /v1/keys and PATCH /v1/keys/{id}. A fifth value, legacy_full, appears on responses for keys minted before v0.5.7.0 but cannot be set via the API.

PermissionSettable on POST/PATCHReturned on responsesWhat it does
fullyesyesAll call, SMS, line, and account operations
call_onlyyesyesPlace and inspect calls; no SMS, no line mutation
sms_onlyyesyesSend and inspect SMS; no calls, no line mutation
read_onlyyesyesGETs only; no mutations
legacy_fullnoyesBackfill marker for keys minted before v0.5.7.0

line_id. Setting line_id on a child key scopes every call and message it sends to that one line. Useful for one-agent-per-line topologies. PATCH with line_id: null to unbind (account-wide). Omitting line_id on POST creates an account-wide key.

monthly_cap_cents. Per-key spend cap in cents (minimum 1, maximum 1,000,000). When a child key hits its cap, calls return 402 agent_cap_exceeded until the cycle rolls. The account cap still applies on top, so a child cap never lets you exceed the account budget.

Code examples

Bootstrap a service key in the portal, save its plaintext as SAPERLY_SERVICE_KEY, then have your agent mint, rotate, and revoke child keys at runtime.

Mint a child API key

1import { Saperly } from "@saperly/sdk";
2
3const svc = new Saperly({ apiKey: process.env.SAPERLY_SERVICE_KEY! });
4
5const child = await svc.keys.create({
6 name: "voice-agent-prod",
7 lineId: "550e8400-e29b-41d4-a716-446655440000",
8 permissions: "call_only",
9 monthlyCapCents: 500,
10});
11
12// First and only chance to read the plaintext. Save it now.
13console.log(child.plaintextKey);

Rotate a child key

Rotate when you suspect compromise. The old plaintext stops working immediately and a new one is returned:

1const rotated = await svc.keys.rotate(child.id);
2console.log(rotated.plaintextKey); // new plaintext, old is dead

Revoke a child key

Revoke when you’re done. Child revocation takes effect immediately, with no grace window. Calls and SMS already accepted before revocation continue to completion; only new requests are rejected.

1await svc.keys.delete(child.id);

Every key operation writes an append-only row to compliance_events. You can revoke a key but you cannot erase the event that created it. Every child API key carries created_by_service_key_id, so any call placed with a child key traces back to the service key that minted the credential.

Best practices

Rotation cadence. Rotate long-lived account API keys on a 90-day cadence. Rotate service keys immediately on any team-member offboarding. Child keys minted by service keys can rotate on every deploy if your runtime supports it. That’s the whole point of the model.

Storage. Store service keys as SAPERLY_SERVICE_KEY in your secrets manager. Store the regular API key your agent runtime uses as SAPERLY_API_KEY. Never commit either to source control. Never log the plaintext.

When to use each tier.

  • Account API key. Single-agent integrations and one-shot scripts. One key per environment.
  • Line-scoped child API key (minted by a service key). One agent per line. The child key is line_id-bound and cap-limited, so blast radius is bounded even if it leaks.
  • Service key. Anywhere your agent needs to mint, rotate, or revoke its own credentials at runtime.

If a key leaks. Rotate or revoke immediately, then audit. The audit feed at GET /v1/audit (called with a regular API key, not the service key) shows every key lifecycle event and every call traced back to the credential that placed it. For a leaked child key, revoke just that one. For a leaked service key, rotate it in the portal. Existing child keys keep working, but rotate any you suspect were minted under the compromise window.

What service keys cannot do

Service keys are limited to the six /v1/keys credential-management endpoints. Calling any of these with a service key returns 401 invalid_api_key:

  • Place a call (POST /v1/calls)
  • Run a one-off conversation call (POST /v1/calls/conversation)
  • Send SMS (POST /v1/messages)
  • Mutate a line (POST/PATCH/DELETE /v1/lines)
  • Read the compliance event stream (GET /v1/audit, which requires a regular API key)
  • Mint another service key (no API path exists)

For the full bootstrap-to-revoke walkthrough, see Agent onboarding. For idempotency rules, error codes, and the audit chain, see the v0.5.7.0 changelog entry.