Skip to content

Sending SMS

One endpoint sends everything: POST /api/v1/messages.

A single message

bash
curl -X POST https://sms.xash.network/api/v1/messages \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "sms",
    "to": "+263771234567",
    "body": "Your verification code is 482913"
  }'

Bulk

to also takes an array, up to 1,000 numbers per call:

json
{
  "channel": "sms",
  "to": ["+263771234567", "+27821234567", "+263712345678"],
  "body": "Loadshedding starts at 18:00 in your area today.",
  "reference": "outage-2026-08-30"
}

One call makes one batch: one debit, one batch_id, one message per recipient. Duplicate numbers within a call are sent once. Numbers outside +263 and +27 come back in rejected with a code and are not billed:

json
{
  "batch_id": "9d2f7c1e-...",
  "accepted": 2,
  "rejected": [
    { "to": "+14155550123", "code": "UNSUPPORTED_DESTINATION" }
  ],
  "segments": 1,
  "total_cost": "0.0600",
  "currency": "USD",
  "balance_after": "4.94"
}

For more than 1,000 recipients, loop the call or upload a CSV on the dashboard (up to 10,000 rows).

reference is an optional label of yours, up to 64 characters. It comes back on batch lookups.

Dry runs

"dry_run": true prices the batch and returns the same cost breakdown without sending or billing:

json
{
  "dry_run": true,
  "accepted": 2,
  "rejected": [],
  "segments": 2,
  "total_cost": "0.1200",
  "currency": "USD"
}

Segments

An SMS is billed per segment. Plain (GSM-7) text fits 160 characters in one segment, or 153 per segment when the message spans several. Emoji or non-Latin script switches the message to UCS-2: 70 characters for one segment, 67 per segment after that.

Cost = recipients × segments × the per-country rate. The response always tells you the segment count, and the dashboard composer counts as you type. Messages are capped at 5 segments.

Idempotency

Network timeouts happen. To make a send safe to retry, put a unique key in the Idempotency-Key header:

Idempotency-Key: order-8213-notify

A retry with the same key and the same payload replays the original response instead of billing twice (the replay carries an Idempotency-Replayed: true header). The same key with a different payload gets 409 IDEMPOTENCY_CONFLICT.

Checking status

  • GET /api/v1/batches/{batch_id} — the batch with every recipient's outcome
  • GET /api/v1/messages/{message_id} — one message

Statuses are explained on Delivery & webhooks.