saperly
Guides

Numbers

A number is a real phone number provisioned under your workspace and bound to one connection — provision, list, assign a handler, set a per-number webhook, and release, all over scoped sk_ keys.

A number is a real phone number provisioned under your workspace. Each number is bound to exactly one connection — its handler, the thing that answers — and can carry its own webhook. You provision numbers, list and read them, assign a connection, set a per-number webhook, and release them when you're done.

v1 SDKs call these "lines"

The published @saperly/sdk / saperly SDKs model a number fused with its handler as a single line. A line is a number plus the connection that answers it. The v2 endpoints below keep the two concepts separate. See the Node SDK.

Endpoints

All v2 endpoints are camelCase, authenticate with a scoped sk_ bearer key, and read the workspace from the key. Base URL: https://api.saperly.com.

Method & pathActionScopeReturns
GET /numberslistnumbers:readPhoneNumber[]
GET /numbers/:idgetnumbers:readPhoneNumber
POST /numbersprovisionnumbers:provisionPhoneNumber (201)
POST /numbers/:id/releasereleasenumbers:release{ status: "released" }
POST /numbers/:id/connectionassign connectionconnections:writePhoneNumber
POST /numbers/:id/webhookset webhookwebhooks:writePhoneNumber
POST /numbers/:id/sms-senderset SMS sender idnumbers:writePhoneNumber
POST /numbers/:id/caller-idset caller ID namenumbers:writePhoneNumber

Provision payload

{ "country": "US", "numberType": "local", "areaCode": "415" }
FieldDefaultNotes
country"US"ISO country to provision in
numberType"local"e.g. local
areaCodeoptional preferred area code

The PhoneNumber shape

{
  id: string
  phoneNumber: string
  connectionId: string | null
  webhookUrl: string | null
  country: string | null
  numberType: string | null
  monthlyPriceCents: number | null          // what you pay per month
  currency: string | null
  nextChargeAt: string | null               // ISO — next monthly sweep
  releasedAt: string | null                 // ISO — soft-delete marker
  createdAt: string                         // ISO
}

Sender identity

Two per-number settings control how a number presents itself on outbound traffic. Both are optional, both clear by sending null, and both fall back to the bare phone number when unset.

SMS sender id

For international SMS, you can send from an alphanumeric sender id instead of the phone number — a short brand name like Acme. It must be 1–11 alphanumeric characters. Send null to clear it, and SMS goes back to sending from the phone number.

POST /numbers/:id/sms-sender
{ "smsSenderId": "Acme" }

Alphanumeric sender ids are display-only (one-way) in some countries: recipients see the brand name but cannot reply to it. Where two-way SMS matters, send from the phone number.

Caller ID name (CNAM)

Set the outbound caller-ID name shown on the called party's handset. It must be 1–15 letters, digits, or spaces, and is registered as the line's CNAM listing. Send null to clear it, and the bare number shows.

POST /numbers/:id/caller-id
{ "callerIdName": "Acme Inc" }
# Set an SMS sender id for international SMS
curl -X POST "$BASE/numbers/$NUMBER_ID/sms-sender" \
  -H "Authorization: Bearer $SAPERLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "smsSenderId": "Acme" }'

# Set the outbound caller-ID name (CNAM)
curl -X POST "$BASE/numbers/$NUMBER_ID/caller-id" \
  -H "Authorization: Bearer $SAPERLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "callerIdName": "Acme Inc" }'

# Clear either one (the bare number shows / SMS sends from the number)
curl -X POST "$BASE/numbers/$NUMBER_ID/caller-id" \
  -H "Authorization: Bearer $SAPERLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "callerIdName": null }'

Pricing

Get a quote before provisioning so you know the price up front:

GET /pricing/quote?country=US&numberType=local

Returns a NumberQuote with your price:

{
  customerMonthlyCents: number  // your monthly price
  customerUpfrontCents: number  // your one-time provisioning price
}

See Billing for how the prepaid balance is funded and metered.

Provisioning, billing, and release

Provisioning reserves funds for the number's first charge and starts metering its monthly rent against your prepaid balance. The rent sweep is idempotent per billing period, so a number is never double-charged for the same month.

nextChargeAt records when the next monthly sweep is due. Each sweep settles the recurring cost against your balance.

Releasing a number sets releasedAt (a soft-delete marker) and stops the rent. The record stays for your history; no further charges accrue.

Worked example

Quote a price, provision the number, bind a connection so it can answer, set a webhook for delivery, and later release it.

const base = 'https://api.saperly.com'
const auth = { Authorization: `Bearer ${process.env.SAPERLY_API_KEY!}` }
const json = { ...auth, 'Content-Type': 'application/json' }

// 1. Quote the price
const quote = await fetch(
  `${base}/pricing/quote?country=US&numberType=local`,
  { headers: auth },
).then((r) => r.json())
// → { customerMonthlyCents, customerUpfrontCents, ... }

// 2. Provision a number (reserves funds + starts metering rent)
const number = await fetch(`${base}/numbers`, {
  method: 'POST',
  headers: json,
  body: JSON.stringify({ country: 'US', numberType: 'local', areaCode: '415' }),
}).then((r) => r.json())

// 3. Bind a connection (its handler)
await fetch(`${base}/numbers/${number.id}/connection`, {
  method: 'POST',
  headers: json,
  body: JSON.stringify({ connectionId: 'conn_…' }),
})

// 4. Set a per-number webhook
await fetch(`${base}/numbers/${number.id}/webhook`, {
  method: 'POST',
  headers: json,
  body: JSON.stringify({ url: 'https://example.com/hooks/saperly' }),
})

// 5. Later — release it (sets releasedAt, stops the rent)
await fetch(`${base}/numbers/${number.id}/release`, {
  method: 'POST',
  headers: auth,
})
export BASE=https://api.saperly.com

# 1. Quote the price
curl "$BASE/pricing/quote?country=US&numberType=local" \
  -H "Authorization: Bearer $SAPERLY_API_KEY"

# 2. Provision a number (reserves funds + starts metering rent)
curl -X POST "$BASE/numbers" \
  -H "Authorization: Bearer $SAPERLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "country": "US", "numberType": "local", "areaCode": "415" }'

# 3. Bind a connection (its handler)
curl -X POST "$BASE/numbers/$NUMBER_ID/connection" \
  -H "Authorization: Bearer $SAPERLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "connectionId": "conn_…" }'

# 4. Set a per-number webhook
curl -X POST "$BASE/numbers/$NUMBER_ID/webhook" \
  -H "Authorization: Bearer $SAPERLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/hooks/saperly" }'

# 5. Later — release it (sets releasedAt, stops the rent)
curl -X POST "$BASE/numbers/$NUMBER_ID/release" \
  -H "Authorization: Bearer $SAPERLY_API_KEY"

Errors

ErrorStatusDetail
NumberQuotaExceeded409{ limit, current }
NoNumbersAvailable404{ country, numberType }
InsufficientFunds402balance (or a key's spend cap) too low to reserve
NumberNotFound404no such number in this workspace

See Errors & idempotency for the shared error envelope and safe retries.

Next steps

  • Connections — the handler you bind to a number.
  • Billing — the prepaid ledger, reservations, and rent.
  • Authentication — the scopes and spend caps that gate these endpoints.
  • API reference — every numbers and pricing endpoint with a try-it playground.

On this page