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

Quickstart

Your first message
in five steps.

From an empty account to a delivered SMS and a webhook confirming it. Pick your language once; every sample on the page follows.

Before you start

  • An email address, to open the account
  • A terminal, or any HTTP client
  • A few dollars of crypto for real sends; the test key is free
  • About five minutes

Step 1 of 5

Create an account and an API key

Sign up with an email address and confirm it. In the panel open Developers, then Create key. Choose the send and read scopes. The key is shown once: copy it into an environment variable, never into source control.

  • Key copied
  • Exported as SMSMETEOR_KEY
Shell
# macOS, Linux
export SMSMETEOR_KEY="sk_test_4b8e...c19d"

# Windows PowerShell
$env:SMSMETEOR_KEY = "sk_test_4b8e...c19d"

Step 2 of 5

Fund the balance

Skip this step while you use the test key. For real sends open Balance, pick a coin and send the amount shown to the deposit address. It shows as pending when seen on the network and is credited, in USD, after confirmation: seconds on Tron or Solana, ten to sixty minutes on Bitcoin.

Check the balance from code, and set a low threshold in the panel to receive a balance.low event before a campaign stalls.

RequestGET /v1/balance
curl https://api.smsmeteor.com/v1/balance \
  -H "Authorization: Bearer $SMSMETEOR_KEY"
200 OK
{ "balance": "250.00", "currency": "USD",
  "pending": "0.00", "low_threshold": "25.00" }

Step 3 of 5

Send a message

One POST: the destination in E.164, a sender, the text. Always add an Idempotency-Key; if the network blips and you retry, the API returns the original message instead of sending a second one.

The response already tells you the segment count and the price. An uncovered destination returns 422 unsupported_destination and nothing is billed.

  • You received an id starting with msg_
  • status is queued, or test with a test key
POST /v1/messages
curl https://api.smsmeteor.com/v1/messages \
  -H "Authorization: Bearer $SMSMETEOR_KEY" \
  -H "Idempotency-Key: first-message-001" \
  -d to="+14155550142" \
  -d from="METEOR" \
  -d text="Hello from SMSMeteor."
import os, requests

r = requests.post(
    "https://api.smsmeteor.com/v1/messages",
    headers={"Authorization": f"Bearer {os.environ['SMSMETEOR_KEY']}",
             "Idempotency-Key": "first-message-001"},
    json={"to": "+14155550142", "from": "METEOR",
          "text": "Hello from SMSMeteor."},
)
msg = r.json()
print(msg["id"], msg["status"], msg["price"])
const r = await fetch("https://api.smsmeteor.com/v1/messages", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SMSMETEOR_KEY}`,
    "Idempotency-Key": "first-message-001",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ to: "+14155550142", from: "METEOR",
    text: "Hello from SMSMeteor." }),
});
const msg = await r.json();
console.log(msg.id, msg.status, msg.price);
$ch = curl_init('https://api.smsmeteor.com/v1/messages');
curl_setopt_array($ch, [
  CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('SMSMETEOR_KEY'),
    'Idempotency-Key: first-message-001', 'Content-Type: application/json'],
  CURLOPT_POSTFIELDS => json_encode(['to' => '+14155550142',
    'from' => 'METEOR', 'text' => 'Hello from SMSMeteor.']),
]);
$msg = json_decode(curl_exec($ch), true);
echo $msg['id'], ' ', $msg['status'], ' ', $msg['price'];
201 Created
{
  "id": "msg_9Kd2fQ",
  "status": "queued",
  "to": "+14155550142",
  "from": "METEOR",
  "segments": 1,
  "encoding": "gsm7",
  "price": "0.0084",
  "currency": "USD",
  "created_at": "2026-09-21T11:42:07Z"
}

Step 4 of 5

Read the status

Fetch the message by id. Statuses move forward only: queued sent then delivered, failed or expired. Most deliveries confirm within seconds; some carriers report in batches.

Polling is fine for one message. For anything more, use the webhook in the next step.

GET /v1/messages/{id}
curl https://api.smsmeteor.com/v1/messages/msg_9Kd2fQ \
  -H "Authorization: Bearer $SMSMETEOR_KEY"
r = requests.get(
    "https://api.smsmeteor.com/v1/messages/msg_9Kd2fQ",
    headers={"Authorization": f"Bearer {os.environ['SMSMETEOR_KEY']}"},
)
print(r.json()["status"])
const r = await fetch("https://api.smsmeteor.com/v1/messages/msg_9Kd2fQ", {
  headers: { Authorization: `Bearer ${process.env.SMSMETEOR_KEY}` },
});
console.log((await r.json()).status);
$ch = curl_init('https://api.smsmeteor.com/v1/messages/msg_9Kd2fQ');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('SMSMETEOR_KEY')]]);
echo json_decode(curl_exec($ch), true)['status'];
200 OK
{ "id": "msg_9Kd2fQ", "status": "delivered",
  "sent_at": "2026-09-21T11:42:08Z",
  "delivered_at": "2026-09-21T11:42:10Z",
  "carrier_code": null, "price": "0.0084" }

Step 5 of 5

Receive the webhook

In the panel open Developers, Webhooks, Add endpoint. Give an HTTPS URL, subscribe to message.delivered and message.failed, and copy the endpoint secret. Verify the signature before trusting the payload, then answer any 2xx within 5 seconds.

  • Endpoint answers 200
  • Signature verified with the raw body
Your endpoint
# Simulate a signed event against your local endpoint
TS=$(date +%s)
BODY='{"id":"evt_test","type":"message.delivered","data":{"message_id":"msg_9Kd2fQ"}}'
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$HOOK_SECRET" | awk '{print $2}')

curl -X POST http://localhost:3000/hooks/sms \
  -H "X-Meteor-Timestamp: $TS" \
  -H "X-Meteor-Signature: t=$TS,v1=$SIG" \
  -H "Content-Type: application/json" -d "$BODY"
import hmac, hashlib, time
from flask import Flask, request, abort

app = Flask(__name__)

@app.post("/hooks/sms")
def hook():
    ts = request.headers["X-Meteor-Timestamp"]
    sig = request.headers["X-Meteor-Signature"].split("v1=")[1]
    mac = hmac.new(SECRET.encode(), f"{ts}.".encode() + request.data,
                   hashlib.sha256).hexdigest()
    if abs(time.time() - int(ts)) > 300 or not hmac.compare_digest(mac, sig):
        abort(400)
    event = request.get_json()
    print(event["type"], event["data"]["message_id"])
    return "", 200
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const app = express();
app.post("/hooks/sms", express.raw({ type: "application/json" }), (req, res) => {
  const ts = req.get("X-Meteor-Timestamp");
  const sig = req.get("X-Meteor-Signature").split("v1=")[1];
  const mac = createHmac("sha256", process.env.HOOK_SECRET)
    .update(`${ts}.${req.body}`).digest("hex");
  if (!timingSafeEqual(Buffer.from(mac), Buffer.from(sig))) return res.sendStatus(400);
  const event = JSON.parse(req.body);
  console.log(event.type, event.data.message_id);
  res.sendStatus(200);
});
$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_METEOR_TIMESTAMP'];
$sig = explode('v1=', $_SERVER['HTTP_X_METEOR_SIGNATURE'])[1];
$mac = hash_hmac('sha256', $ts . '.' . $raw, getenv('HOOK_SECRET'));
if (abs(time() - (int) $ts) > 300 || !hash_equals($mac, $sig)) { http_response_code(400); exit; }
$event = json_decode($raw, true);
error_log($event['type'] . ' ' . $event['data']['message_id']);
http_response_code(200);
Event body
{ "id": "evt_4Qm8Xz", "type": "message.delivered",
  "created_at": "2026-09-21T11:42:10Z",
  "data": { "message_id": "msg_9Kd2fQ", "status": "delivered",
            "to": "+14155550142", "price": "0.0084" } }

Troubleshooting

401 unauthorized

The header must read Authorization: Bearer sk_… with no quotes around the key. Check the key was not revoked, has the send scope, and that your IP is on its allowlist if one is set.

402 insufficient_balance

The live balance is lower than the message price. Use the test key while integrating, or top up; a deposit shown as pending is not spendable yet.

400 invalid_number

Numbers must be E.164: a plus sign, country code, national number without the trunk zero, no spaces. +447700900123, not 07700 900123.

The message stays at sent

Some operators return receipts in batches or not at all; the country page says which. After 48 hours without a receipt it becomes expired.

The recipient sees a number, not my name

The destination does not deliver alphanumeric senders, or needs them registered. The from field of the message shows the sender actually used. See sender ID and routes.

My webhook never fires

The endpoint must be HTTPS with a valid certificate and answer within 5 seconds. The panel lists each attempt with the response we saw; replay from there once fixed.

Next steps