KeepMyNumber
Home Sign in

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 failed you get the full cost back (one sms_refund entry).
  • Inbound is free — replies to your numbers never touch the wallet.
  • Retries never double-charge — replaying POST /v2/messages with the same Idempotency-Key returns 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.