Developers

API reference

One endpoint to send, one to read status, HMAC-signed webhooks for receipts. Keys are issued the moment the account exists — there is nothing to approve and nobody to email.

v1https://api.worldsms.io/v1

Getting started

Three things stand between a new account and a delivered message: a token, some credit, and a key. None of them involves a human.

  1. Create an account — your browser generates the token, and that token is the account.
  2. Fund it. $40 minimum, in any of the coins on the payments page.
  3. Create an API key in the dashboard. It is shown once; we store only its hash, exactly like the token.

A key on an empty balance cannot send

Key creation is deliberately gated behind a funded account. An unused credential sitting around is a liability with no upside, so the dashboard will not mint one until there is something to spend.

Authentication

Bearer token on every request. The key identifies the account; there is no second factor, no signature on the request, and no IP allowlist — the key is the whole credential, so treat it as one.

Authorization
Authorization: Bearer ws_live_9f2c…
TransportHTTPS only. A request over plain HTTP is refused, not redirected — a redirect would have already leaked the key.
ScopeOne key spends one account's balance. Create one key per system that sends, so a leak is revoked narrowly.
RevocationImmediate. A revoked key returns 401 on the next request; in-flight batches already accepted still complete.
RecoveryNone. We hold a hash. A lost key is replaced, never recovered.

Send messages

POST/v1/messages

One call, one or many recipients, mixed destinations. Each recipient is priced at its own destination's rate and billed per segment. The call is accepted or refused as a whole; it never partially charges you.

Request body

FieldTypeNotes
tostring[]Required. E.164, up to 5,000 per call. Numbers that do not resolve to a priced destination are rejected before billing.
textstringRequired. UTF-8. Segment count follows GSM 03.38 — see segments.
fromstringAlphanumeric sender ID, up to 11 characters. Carried where the route allows it; see sender IDs.
varsobject[]Optional merge values, one object per recipient, substituted into {{name}} placeholders before segments are counted.
callback_urlstringOptional per-batch override of the account webhook.
referencestringOptional. Echoed back on every receipt so you can join to your own records.

Example

send.sh
# one call, many recipients, mixed destinations
curl -X POST https://api.worldsms.io/v1/messages \
  -H "Authorization: Bearer $WORLDSMS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "ACME",
    "to":   ["+447700900123", "+33612345678"],
    "text": "Your code is 480913"
  }'
202 Accepted
{
  "batch_id": "b_7Kq2xR9wLm",
  "accepted": 2,
  "rejected": 0,
  "segments": 1,
  "cost_usd": 0.0353
}

$0.0137 to the United Kingdom plus $0.0216 to France — the rate card applied per destination, not a blended average.

Batching

A batch is accepted immediately and drains at 200 messages a minute per account, so 50,000 recipients take about 4 h 10 min. Time-critical traffic should not queue behind a marketing send: use a separate key for it.

Message status

GET/v1/messages/{batch_id}

Polling is supported and rate-limited; webhooks are the intended path. The response rolls the batch up and lists per-recipient state.

200 OK
{
  "batch_id":    "b_7Kq2xR9wLm",
  "status":      "done",
  "sending":     0,
  "delivered":   1,
  "undelivered": 1,
  "refund_usd":  0.0216
}

refund_usd is settled once, when the batch finishes. It is already back in your balance by the time you can read it.

Balance

GET/v1/balance

Check before a large send. Balances are held in integer micro-dollars internally — rates go to four decimals, and a float has no business in a ledger — and returned here rounded to six.

200 OK
{ "balance_usd": 412.905400, "currency": "USD" }

Delivery receipts

A receipt is emitted per recipient as the carrier reports it. These are the only four states; there is no “unknown” bucket that quietly means “we lost it”.

StateMeaningBilled
queuedAccepted and waiting its turn in the batch.Yes
sentHanded to the carrier, no confirmation yet.Yes
deliveredThe handset acknowledged it.Yes
undeliveredDead, barred, or the sender was refused.Refunded

Numbers that never resolve to a priced destination are rejected at submission and never enter this lifecycle at all — they are not billed and then refunded, they are simply not billed.

Webhooks

Set an endpoint in the dashboard or per batch with callback_url. We POST JSON and expect a 2xx within 5 seconds. Non-2xx is retried six times with exponential backoff over roughly an hour, then dropped.

Payload

POST your endpoint
{
  "event":     "message.delivered",
  "batch_id":  "b_7Kq2xR9wLm",
  "to":        "+447700900123",
  "destination": "gb",
  "segments":  1,
  "cost_usd":  0.0137,
  "reference": "order-4471",
  "ts":        1785312041
}

Verifying the signature

Every delivery carries X-WorldSMS-Signature and X-WorldSMS-Timestamp. The signature is HMAC-SHA256(timestamp + "." + body) using your webhook secret. Compare in constant time, and reject a timestamp more than five minutes old so a captured delivery cannot be replayed at you later.

verify.php
// $secret is the webhook secret from the dashboard
$raw  = file_get_contents('php://input');
$ts   = $_SERVER['HTTP_X_WORLDSMS_TIMESTAMP'] ?? '';
$sig  = $_SERVER['HTTP_X_WORLDSMS_SIGNATURE'] ?? '';

// A stale timestamp is a replay, however well it is signed.
if (abs(time() - (int) $ts) > 300) { http_response_code(408); exit; }

$mine = hash_hmac('sha256', $ts . '.' . $raw, $secret);
if (!hash_equals($mine, $sig)) { http_response_code(401); exit; }

http_response_code(204);

Be idempotent

A retry after your endpoint timed out looks identical to a first delivery. Key on batch_id + to + event and treat a repeat as a no-op.

Errors

Every failure returns a JSON body with a stable error slug. Match on the slug, never on the prose — the prose is allowed to improve.

HTTPSlugWhat to do
400invalid_bodyMalformed JSON or a missing required field.
401key_rejectedAbsent, revoked or mistyped key. Not retryable.
402insufficient_creditThe batch costs more than the balance. Nothing was sent; top up and resubmit.
422no_valid_recipientsEvery number failed to resolve to a priced destination.
422sender_id_invalidfrom is longer than 11 characters or contains a digit-only string that would be read as a number.
429rate_limitedBack off. Retry-After carries the number of seconds.
503route_unavailableA destination is temporarily unroutable. Retryable.
402 Payment Required
{
  "error":       "insufficient_credit",
  "message":     "Batch costs $84.20, balance is $12.05.",
  "required_usd": 84.20,
  "balance_usd":  12.05
}

Limits

Requests60 per minute per key, burst 20. 429 with Retry-After past that.
Recipients per call5,000. Split larger lists across calls, or upload the file in the dashboard.
Body size2 MB.
Throughput200 messages a minute per account, regardless of how many keys submit.
KeysNo limit. One per sending system is the intended pattern.

Rate limits apply to API calls, not to messages: one call carrying 5,000 recipients costs one request against the limit.

Keys are issued instantly. There is no approval step.

Fund an account and the dashboard mints one on the spot.

Ask something specific

Create your account

One button. Your browser generates a 160-bit token, and that token is the account. There is nothing else to fill in and nobody to wait for.

EnglishEN · Change language

Language