Appearance
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-notifyA 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.