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 & path | Action | Scope | Returns |
|---|---|---|---|
GET /numbers | list | numbers:read | PhoneNumber[] |
GET /numbers/:id | get | numbers:read | PhoneNumber |
POST /numbers | provision | numbers:provision | PhoneNumber (201) |
POST /numbers/:id/release | release | numbers:release | { status: "released" } |
POST /numbers/:id/connection | assign connection | connections:write | PhoneNumber |
POST /numbers/:id/webhook | set webhook | webhooks:write | PhoneNumber |
POST /numbers/:id/sms-sender | set SMS sender id | numbers:write | PhoneNumber |
POST /numbers/:id/caller-id | set caller ID name | numbers: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.
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
| 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.