Docs

Send your first SMS in a few minutes.

Quickstart

Use your test key

Replace sk_test_YOUR_TEST_KEY with a key from your dashboard. Sign up and the dashboard docs show these snippets with your real test key filled in.

One request sends an SMS to any Mongolian number; the operator is picked for you. The example sends to the magic number 00000000, which is accepted and then delivered about 2 seconds later, without reaching a real phone.

bash
curl https://web-production-c8403.up.railway.app/v1/messages \
  -H "Authorization: Bearer sk_test_YOUR_TEST_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042" \
  -d '{"to": "00000000", "body": "Таны захиалга баталгаажлаа"}'

The response is 202 with the message object (status: "queued"). Send the same Idempotency-Key again and you get the stored response back instead of a second SMS. Mongolian Cyrillic switches the message to UCS-2 (70 characters per segment instead of 160); pass "transliterate": true to send it as Latin and pay for fewer segments.

Check a message

bash
curl https://web-production-c8403.up.railway.app/v1/messages/msg_YOUR_MESSAGE_ID \
  -H "Authorization: Bearer sk_test_YOUR_TEST_KEY"

Statuses are honest. Most operators send no delivery report, so their final status is accepted (the operator took it). G-Mobile reports delivered or undelivered. unknown means the request may have reached the operator; it is never retried automatically, so nobody gets the SMS twice. failed, expired and canceled messages are refunded.

Test mode and magic numbers

Test keys (sk_test_…) go through the same queue, worker, statuses and webhooks as live keys, but never reach an operator and cost nothing. A normal Mongolian number is routed (so you see the operator) and then accepted without being sent. These numbers simulate every outcome:

NumberWhat happensFinal status
00000000accepted, then delivered ~2s laterdelivered
00000001failed / operator_rejectedfailed / operator_rejected
00000002unknown / operator_timeoutunknown / operator_timeout
00000003accepted after a 30s delay (slow operator)accepted
00000004rejected at the API with recipient_opted_out (no message created)HTTP 400 recipient_opted_out
00000005retryable twice, then accepted (attempts: 3)accepted
00000006accepted, then undelivered ~2s laterundelivered
0000xxxxAny other number starting with 0000HTTP 400 invalid_phone

Live keys reject magic numbers with invalid_phone.

One-time passwords

POST /v1/otp generates and sends a code (4 to 10 digits, valid 60 to 900 seconds). We store only a hash. POST /v1/otp/check verifies it: 5 wrong tries fail the verification, a correct code works once. Per number: one send per 30 seconds, 5 per 10 minutes, 20 per day. In test mode the response includes test_code.

bash
# 1. Send a code (test mode answers with "test_code")
curl https://web-production-c8403.up.railway.app/v1/otp \
  -H "Authorization: Bearer sk_test_YOUR_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to": "00000000", "app_name": "MyShop"}'
# -> {"id": "ver_…", "status": "pending", "expires_at": "…", "test_code": "123456"}

# 2. Check what the user typed
curl https://web-production-c8403.up.railway.app/v1/otp/check \
  -H "Authorization: Bearer sk_test_YOUR_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"id": "ver_…", "code": "123456"}'
# -> {"id": "ver_…", "status": "approved", "valid": true}

Webhooks

Add an endpoint in the dashboard (or POST /v1/webhooks) to get message.*, otp.approved, batch.completed, inbound.received and balance.low events. Each delivery is signed with Standard Webhooks headers (webhook-id, webhook-timestamp, webhook-signature). Answer with any 2xx within 15 seconds; failures are retried after 5s, 5m, 30m, 2h, 5h, 10h and 10h. Live endpoints must use https:// and a public host.

Express + @smsmulti/sdk
import express from 'express';
import { verifyWebhook } from '@smsmulti/sdk';

const app = express();

// Keep the raw body: re-serialized JSON will not verify.
app.post('/webhooks/sms', express.raw({ type: 'application/json' }), (req, res) => {
  const ok = verifyWebhook({
    payload: req.body,
    headers: req.headers,
    secret: process.env.SMSMULTI_WEBHOOK_SECRET, // whsec_…
  });
  if (!ok) return res.status(400).end();

  const event = JSON.parse(req.body.toString('utf8'));
  if (event.type === 'message.delivered') {
    // event.data is the message object
  }
  res.status(200).end(); // any 2xx; we retry anything else
});

Local development

smsmulti listen streams your project's events and forwards each one to a local URL, signed exactly like a production delivery, so you can build your webhook handler without a public URL.

bash
npm install -g @smsmulti/cli
export SMSMULTI_API_KEY=sk_test_YOUR_TEST_KEY
export SMSMULTI_BASE_URL=https://web-production-c8403.up.railway.app

# Streams your events and POSTs each one, signed like a real webhook, to your local server
smsmulti listen --forward-to http://localhost:4000/webhooks/sms

# Send from the terminal too
smsmulti send --to 00000000 --body "Hello from the CLI"

Reference

Pricing

A flat 50₮ per segment on every operator, debited when the message is accepted by the API. Test mode is free.