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.
Numbers and connections are separate
There is no single "line" object. A number is the phone number; a connection is the brain that answers it. You provision the number, create the connection, then attach one to the other. The same connection can answer many numbers — see Connections.
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 & path | Action | Scope | Returns |
|---|---|---|---|
GET /numbers | list | read | PhoneNumber[] |
GET /numbers/:id | get | read | PhoneNumber |
POST /numbers | provision | write | PhoneNumber (201) |
POST /numbers/:id/release | release | write | { status: "released" } |
POST /numbers/:id/connection | assign connection | write | PhoneNumber |
POST /numbers/:id/webhook | set webhook | write | PhoneNumber |
POST /numbers/:id/sms-sender | set SMS sender id | write | PhoneNumber |
POST /numbers/:id/caller-id | set caller ID name | write | PhoneNumber |
Provision payload
{ "country": "US", "numberType": "local", "areaCode": "415" }| Field | Default | Notes |
|---|---|---|
country | "US" | ISO country to provision in |
numberType | "local" | e.g. local |
areaCode | — | optional 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=localReturns 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.
import { configure, numbers, pricing } from '@trysaperly/sdk'
configure({ apiKey: process.env.SAPERLY_API_KEY! })
// 1. Quote the price
const { data: quote } = await pricing.quote({
query: { country: 'US', numberType: 'local' },
})
// → { customerMonthlyCents, customerUpfrontCents }
// 2. Provision a number (reserves funds + starts metering rent)
const { data: number } = await numbers.provision({
body: { country: 'US', numberType: 'local', areaCode: '415' },
})
// 3. Bind a connection (its handler)
await numbers.assignConnection({
path: { id: number!.id },
body: { connectionId: 'conn_…' },
})
// 4. Set a per-number webhook
await numbers.setWebhook({
path: { id: number!.id },
body: { url: 'https://example.com/hooks/saperly' },
})
// 5. Later — release it (sets releasedAt, stops the rent)
await numbers.release({ path: { id: number!.id } })import os
import httpx
BASE = "https://api.saperly.com"
headers = {
"Authorization": f"Bearer {os.environ['SAPERLY_API_KEY']}",
"Content-Type": "application/json",
}
with httpx.Client(base_url=BASE, headers=headers) as http:
# 1. Quote the price
quote = http.get(
"/pricing/quote", params={"country": "US", "numberType": "local"}
).json()
# → { "customerMonthlyCents": ..., "customerUpfrontCents": ... }
# 2. Provision a number (reserves funds + starts metering rent)
number = http.post(
"/numbers",
json={"country": "US", "numberType": "local", "areaCode": "415"},
).json()
# 3. Bind a connection (its handler)
http.post(
f"/numbers/{number['id']}/connection",
json={"connectionId": "conn_…"},
)
# 4. Set a per-number webhook
http.post(
f"/numbers/{number['id']}/webhook",
json={"url": "https://example.com/hooks/saperly"},
)
# 5. Later — release it (sets releasedAt, stops the rent)
http.post(f"/numbers/{number['id']}/release")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
| Error | Status | Detail |
|---|---|---|
NumberQuotaExceeded | 409 | { limit, current } |
NoNumbersAvailable | 404 | { country, numberType } |
InsufficientFunds | 402 | balance (or a key's spend cap) too low to reserve |
NumberNotFound | 404 | no 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
numbersandpricingendpoint with a try-it playground.
Billing
Saperly is prepaid — you top up a balance and usage meters against it through a reserve → settle → release ledger that can never overspend a balance or a scoped key's spend cap.
Messaging
Send and list SMS from your Saperly numbers over the v2 API — consent and disclosures are enforced automatically.