Sending SMS
Once a port completes, the number is yours to use — starting with SMS. Send
from any active or active_unconfigured number in your
registry, receive inbound texts and delivery
receipts as webhooks, and check the price of any destination before you send.
Check the price first
Rates are set per destination country (per SMS segment) and controlled by the platform. A destination without a rate cannot be messaged.
# The full rate deck
curl "https://api.example.com/v2/sms/rates" \
-H "Authorization: Bearer pk_live_YOUR_KEY"
# Price one specific number (URL-encode + as %2B)
curl "https://api.example.com/v2/sms/rates/check?to=%2B14155552671" \
-H "Authorization: Bearer pk_live_YOUR_KEY"
{
"phone_number": "+14155552671",
"country_code": "US",
"number_type": "local",
"supported": true,
"rate": 0.0079,
"currency": "USD"
}
supported: false means no rate is configured — a send to that destination
returns 422.
Your wallet (prepaid)
Sending is prepaid: every message charges rate × segments against your
account wallet. Wallets are funded by platform ops (contact support to top
up) — there is no self-serve payment. A send that would overdraw the balance
is refused with 402 and nothing is sent or charged.
curl "https://api.example.com/v2/wallet" \
-H "Authorization: Bearer pk_live_YOUR_KEY"
# {"balance": 4.9921, "currency": "USD"}
Every balance change is a ledger entry — ops top-ups (admin_credit),
per-message charges (sms_charge, linked to the message via message_id)
and automatic refunds:
curl "https://api.example.com/v2/wallet/transactions?kind=sms_charge" \
-H "Authorization: Bearer pk_live_YOUR_KEY"
You can also see the balance and the full ledger in the dashboard — the Wallet page in the sidebar (the Overview page shows the balance too).
Three rules worth knowing:
- Failed messages are refunded automatically — when a message ends in
failedyou get the full cost back (onesms_refundentry). - Inbound is free — replies to your numbers never touch the wallet.
- Retries never double-charge — replaying
POST /v2/messageswith the sameIdempotency-Keyreturns the stored response without a new charge, and duplicate provider receipts can't refund twice.
Send a message
from is a number in your registry — its id or the E.164 string. Always set
an Idempotency-Key so retries can't double-send (see
Errors & idempotency).
curl -X POST https://api.example.com/v2/messages \
-H "Authorization: Bearer pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: msg-2026-09-04-001" \
-d '{
"from": "+447912345678",
"to": "+14155552671",
"text": "Your verification code is 482913"
}'
{
"id": "3b8b02de-77e6-44a5-b0e5-30f1f4bfa2ad",
"direction": "outbound",
"from_number": "+447912345678",
"to_number": "+14155552671",
"text": "Your verification code is 482913",
"segments": 1,
"status": "queued",
"destination_country": "US",
"rate": 0.0079,
"cost": 0.0079,
"currency": "USD",
"error": null,
"created_at": "2026-09-04T10:15:00Z"
}
You're charged rate × segments. Segment counting follows the SMS standard:
160 characters per message for the GSM-7 alphabet (153 each when
concatenated), 70 (67) when the text needs Unicode.
Common errors:
| Status | Why |
|---|---|
402 |
Wallet balance doesn't cover the message — ops top-up needed |
404 |
from is not a number on your account |
409 |
The number can't send yet (e.g. still porting_in) |
422 |
Invalid destination, or no rate configured for it |
Delivery status
status moves queued → sent → delivered, or ends in failed (with
error set). Track it two ways:
- Webhooks (recommended):
sms.sent,sms.delivered,sms.failed— full payloads in Webhook event examples. - Polling:
GET /v2/messages/{id}.
Not every carrier confirms delivery — a message can stay sent and still
have arrived.
Receiving SMS
Anything texted to your platform numbers arrives as an sms.received
webhook (register endpoints per Webhooks) and in your
message history. Inbound messages are free — including replies sent from
carrier short codes (the 5–6 digit sender is preserved as-is in
from_number). Note the reverse is not true: you cannot send to a short
code (422 — not a routable E.164 destination).
curl "https://api.example.com/v2/messages?direction=inbound&limit=20" \
-H "Authorization: Bearer pk_live_YOUR_KEY"
The history endpoint filters by direction, status, phone_number
(either end of the conversation), and created_after/created_before.
Billing
The wallet ledger is the money trail; in addition, every outbound message
writes a usage record (kind: "sms_outbound") with the segments and cost in
its metadata — the same GET /v2/usage_records feed that tracks your
porting activity.
