Skip to content
Markets open
Quickstart · Messaging

Send an SMS

One POST sends a message on a wholesale route. Leave the route out and Smart Routing picks one for the destination; the response tells you the segments billed and exactly what they cost.
Calls
POST /comms/sms · GET /comms/sms/{messageId}
Scope
sms:send
Cost
The route rate per segment, plus 2% of the route rate, never more than $0.001 per message. The response states the exact cost.

Get an API key

Open an account, then create a key under Dashboard, API keys. Live keys (wmmn_live_sk_) send real traffic and charge your balance. Test keys (wmmn_test_sk_) run the same request through routing and pricing without delivering it, and draw on test credit, which comes with an invite code.

A key restricted to scopes needs sms:send. A test key runs the request through routing and pricing and returns simulated: true without sending anything.

Every sample reads the key from the environment. Keep it out of source control.

export PACKETEXCHANGE_API_KEY=wmmn_live_sk_...

Fund the balance

PacketExchange is prepaid. Top up from $5 by card or crypto, or from $100 by wire, under Dashboard, Billing. An agent can top itself up with x402. There is no contract and no monthly fee.

The message is charged to your prepaid balance at the route rate per segment, plus the platform fee. Long messages split into segments: 160 GSM-7 characters (153 per part when split) or 70 Unicode (67 per part).

CostThe route rate per segment, plus 2% of the route rate, never more than $0.001 per message. The response states the exact cost.

Make the request

Send to an E.164 number from a sender ID: a number, or up to 30 letters, digits and spaces. The X-Idempotency-Key header makes a retry safe: the same key never sends or bills the message twice.

curl https://packetexchange.io/api/v1/comms/sms \
  -H "Authorization: Bearer $PACKETEXCHANGE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -d '{
    "to": "+447700900123",
    "from": "Acme",
    "message": "Your order 1042 has shipped."
  }'
Save as request.sh, run bash request.shNeeds curl, plus jq for the multi-step samples

Handle the response

A 200 means the message was handed to the route. The status here is the send-time outcome; delivery comes later, from the carrier's receipt, on routes that return one.

200 OKPOST /comms/sms
{
  "success": true,
  "data": {
    "messageId": "<message id>",
    "to": "+447700900123",
    "from": "Acme",
    "status": "accepted",
    "segments": 1,
    "cost": "<USD, 6 decimals>",
    "submittedAt": "<ISO-8601 UTC>"
  }
}

Values in angle brackets are placeholders for your own ids, times and amounts.

messageId
Look the message up later with GET /comms/sms/{messageId}.
status
accepted, sent or failed at send time. GET /comms/sms/{messageId} later shows delivered or failed when the route returns a carrier receipt (routeReturnsReceipts says whether it does), and the sms.delivered and sms.failed webhooks fire on it. A message that fails on its receipt is refunded.
segments
How many segments were billed.
cost
What this message cost you, fee included, as a 6-decimal USD string. Do arithmetic with a decimal type.
simulated
Present and true on a test key: nothing was sent.

When something goes wrong

  • Anything other than 2xx carries { "success": false, "error": { "code", "message", "details" } }. VALIDATION_ERROR lists each bad field in details.
  • A 429 carries Retry-After in seconds. Wait that long, then retry with the same idempotency key.
  • Quote the X-Request-Id response header if you contact support.
400The error envelope
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input",
    "details": [
      { "path": "to", "message": "<why the field was refused>" }
    ]
  }
}

Go further

Start building

Run it with your own key

Open an account, add credit from $5 and create a key. You pay the carrier's rate plus a fee capped at $0.001.