• 228 destinations
  • No KYC, ever
  • Bitcoin, Monero, USDT and 4 more

SMS API

One HTTP call sends a message.
One webhook confirms it.

A REST API with bearer keys, predictable JSON, idempotent retries and delivery webhooks. Same balance and rates as the panel; nothing to negotiate, nothing to approve.

  • 50 requests per second per key
  • 1,000 messages per API request
  • Idempotency keys, IP allowlists

01Surface area

Six endpoints cover the whole product.

Everything the panel does, the API does. Nothing is API-only or panel-only.

MethodEndpointWhat it does
POST/v1/messagesSend one message. Returns the id, price and initial status.
POST/v1/messages/batchSend up to 1,000 messages in one request, each with its own text and sender.
GET/v1/messages/{id}Current status, carrier code, timestamps and price of one message.
GET/v1/messagesList and filter messages: status, destination, campaign, date range. Paginated.
GET/v1/ratesThe full rate card in JSON, or one country with /v1/rates/{iso}.
GET/v1/balanceBalance in USD, pending top-ups and the low-balance threshold you set.

02Message lifecycle

Six statuses. Each one means one thing.

A message moves forward only. You can rely on the order below whether you poll or listen to webhooks.

  • Webhooks deliver every transition with the carrier code when one exists.
  • Validity is 48 hours. After that an undelivered message becomes expired.
  • Billing happens at sent. Rejected messages are never charged.
  1. queuedAccepted, priced, waiting for a route slot.
  2. sentHanded to the carrier. Billed at this point.
  3. deliveredHandset confirmed receipt. Final.
  4. failedCarrier refused or could not reach the handset. Code attached. Final.
  5. expiredNot delivered within validity. Final.
  6. rejectedRefused before sending: invalid number, blocked content, empty balance. Not billed. Final.

03Errors

Stable codes, human messages, the field that caused it.

Every non-2xx response is JSON with the same shape. Retry on 429 and 5xx, fix and resend on 4xx.

HTTPCodeMeaningWhat to do
400invalid_numberThe destination is not a valid E.164 number.Normalise the number; the response names the field.
401unauthorizedMissing, revoked or IP-restricted key.Check the key and the allowlist in the panel.
402insufficient_balanceThe message would take the balance below zero.Top up. Set a low-balance webhook to avoid it.
409idempotency_conflictSame key reused with a different payload.Use a new key for a new message.
422unsupported_destinationNo route to that country or prefix.Check the coverage page for the destination.
422sender_not_allowedAlphanumeric sender not permitted on this route.Omit from; a numeric sender is assigned automatically.
429rate_limitedMore than 50 requests per second per key.Wait for Retry-After, or use the batch endpoint.
5xxinternalOur side. The message was not sent.Retry with the same Idempotency-Key.

04Samples

Copy, paste, replace the key.

The same request in the languages we see most. Every sample includes the idempotency key because you will want it the first time a network blips.

POST /v1/messages/batch
import requests

r = requests.post(
    "https://api.smsmeteor.com/v1/messages/batch",
    headers={"Authorization": f"Bearer {KEY}",
             "Idempotency-Key": "batch-2026-09-21-01"},
    json={"messages": [
        {"to": "+14155550142", "from": "METEOR", "text": "Order #4821 shipped."},
        {"to": "+447700900123", "from": "METEOR", "text": "Order #4822 shipped."},
    ]},
)
for m in r.json()["messages"]:
    print(m["id"], m["status"], m["price"])
const r = await fetch("https://api.smsmeteor.com/v1/messages/batch", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SMSMETEOR_KEY}`,
    "Idempotency-Key": "batch-2026-09-21-01",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ messages: [
    { to: "+14155550142", from: "METEOR", text: "Order #4821 shipped." },
    { to: "+447700900123", from: "METEOR", text: "Order #4822 shipped." },
  ]}),
});
const { messages } = await r.json();
$ch = curl_init("https://api.smsmeteor.com/v1/messages/batch");
curl_setopt_array($ch, [
  CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ["Authorization: Bearer $key",
    "Idempotency-Key: batch-2026-09-21-01", "Content-Type: application/json"],
  CURLOPT_POSTFIELDS => json_encode(["messages" => [
    ["to" => "+14155550142", "from" => "METEOR", "text" => "Order #4821 shipped."],
    ["to" => "+447700900123", "from" => "METEOR", "text" => "Order #4822 shipped."],
  ]]),
]);
$res = json_decode(curl_exec($ch), true);
body, _ := json.Marshal(map[string]any{"messages": []map[string]string{
    {"to": "+14155550142", "from": "METEOR", "text": "Order #4821 shipped."},
    {"to": "+447700900123", "from": "METEOR", "text": "Order #4822 shipped."},
}})
req, _ := http.NewRequest("POST", "https://api.smsmeteor.com/v1/messages/batch", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("SMSMETEOR_KEY"))
req.Header.Set("Idempotency-Key", "batch-2026-09-21-01")
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
201 Created
{ "batch_id": "bat_3Hq9Zr", "accepted": 2, "rejected": 0,
  "total_price": "0.0214", "currency": "USD",
  "messages": [ { "id": "msg_9Kd2fQ", "status": "queued", "price": "0.0084" },
                { "id": "msg_9Kd2fR", "status": "queued", "price": "0.0130" } ] }

05Questions, answered

What developers ask before the first request.

How do I get an API key?

Create it in the panel under Developers. Keys are bearer tokens, shown once, scoped to send, read or both, and can be restricted to an IP allowlist. Revoke or rotate at any time without touching your balance.

Does the API use the same balance and rates as the panel?

Yes. One account, one balance, one rate card. A message sent from code costs exactly what the rate table shows for that destination, and it appears in the same reports.

What is the rate limit?

50 requests per second per key for single sends. The batch endpoint accepts 1,000 messages per API request, so a million messages is a thousand requests. Above the limit you receive 429 with a Retry-After header.

What does the idempotency key do?

Send the same Idempotency-Key with a retried request and the API returns the original message instead of sending a second one. Keys are kept for 24 hours per account.

How do I know a message was delivered?

Poll GET /v1/messages/{id}, or register a webhook and receive message.delivered and message.failed events with the carrier code. Webhooks are the recommended path for anything above a few messages.

Is there a sandbox?

Every account has a test key. Messages sent with it are validated, priced and returned with status test, but never leave the platform and never cost anything.

Which languages do you support?

Any language that can make an HTTPS request. We publish ready-to-paste samples for cURL, Python, Node, PHP and Go, and the OpenAPI description if you prefer to generate a client.

Where are the errors documented?

Every error is JSON with a stable code, a human message and, when relevant, the field that caused it. The full list lives in the API reference; the most common ones are on this page.