Developers
SMS API for developers
A small, predictable REST API. Send your first SMS in two minutes with cURL, Node.js, PHP, Python or Go.
Quick start
- 1
Create an account
Sign up and verify your mobile number to get trial credit.
- 2
Create an API key
In the dashboard, open API keys and create a key with the messages:send scope. It is shown once; store it as BITSMS_API_KEY.
- 3
Send a message
POST /v1/messages with the number and text. You get 202 and a message ID; follow its status by API or webhook.
Send an SMS
Numbers can be 01XXXXXXXXX, 8801XXXXXXXXX or +8801XXXXXXXXX. Omit sender_id to use the default non-masking sender.
curl https://sms-api.bitcommerz.com/v1/messages \
-H "Authorization: Bearer $BITSMS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-5521-shipped" \
-d '{"to": "01712345678", "text": "Your order #5521 has shipped."}'// Node.js 18+ (built-in fetch)
const res = await fetch("https://sms-api.bitcommerz.com/v1/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BITSMS_API_KEY}`,
"Content-Type": "application/json",
// Same key on a retry = no duplicate SMS, no double charge.
"Idempotency-Key": "order-5521-shipped",
},
body: JSON.stringify({ to: "01712345678", text: "Your order #5521 has shipped." }),
});
const msg = await res.json();
console.log(res.status, msg.id, msg.status); // 202 "0192…" "queued"<?php
$ch = curl_init("https://sms-api.bitcommerz.com/v1/messages");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("BITSMS_API_KEY"),
"Content-Type: application/json",
"Idempotency-Key: order-5521-shipped",
],
CURLOPT_POSTFIELDS => json_encode([
"to" => "01712345678",
"text" => "আপনার অর্ডার #5521 পাঠানো হয়েছে।",
]),
]);
$body = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE); // 202
echo $body["id"], " ", $body["parts"], " part(s)\n";import os, requests
res = requests.post(
"https://sms-api.bitcommerz.com/v1/messages",
headers={
"Authorization": f"Bearer {os.environ['BITSMS_API_KEY']}",
"Idempotency-Key": "order-5521-shipped",
},
json={"to": "01712345678", "text": "Your order #5521 has shipped."},
timeout=10,
)
msg = res.json()
print(res.status_code, msg["id"], msg["status"]) # 202 ... queuedbody, _ := json.Marshal(map[string]string{
"to": "01712345678",
"text": "Your order #5521 has shipped.",
})
req, _ := http.NewRequest("POST", "https://sms-api.bitcommerz.com/v1/messages", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("BITSMS_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "order-5521-shipped")
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err) // safe to retry with the same Idempotency-Key
}
defer res.Body.Close()
var msg struct{ ID, Status string }
json.NewDecoder(res.Body).Decode(&msg)
fmt.Println(res.StatusCode, msg.ID, msg.Status) // 202 ... queuedCheck a message
Statuses move forward only: queued, sent, then delivered, failed or expired. Webhooks are the better way to follow them.
curl https://sms-api.bitcommerz.com/v1/messages/$MESSAGE_ID \
-H "Authorization: Bearer $BITSMS_API_KEY"const res = await fetch(`https://sms-api.bitcommerz.com/v1/messages/${messageId}`, {
headers: { Authorization: `Bearer ${process.env.BITSMS_API_KEY}` },
});
const msg = await res.json();
// queued → sent → delivered | failed | expired
console.log(msg.status, msg.status_reason);<?php
$ch = curl_init("https://sms-api.bitcommerz.com/v1/messages/" . $messageId);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("BITSMS_API_KEY")],
]);
$msg = json_decode(curl_exec($ch), true);
echo $msg["status"], "\n"; // queued, sent, delivered, failed, expiredres = requests.get(
f"https://sms-api.bitcommerz.com/v1/messages/{message_id}",
headers={"Authorization": f"Bearer {os.environ['BITSMS_API_KEY']}"},
timeout=10,
)
msg = res.json()
print(msg["status"], msg.get("status_reason"))req, _ := http.NewRequest("GET", "https://sms-api.bitcommerz.com/v1/messages/"+messageID, nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("BITSMS_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
var msg struct {
Status string `json:"status"`
StatusReason *string `json:"status_reason"`
}
json.NewDecoder(res.Body).Decode(&msg)
fmt.Println(msg.Status)Send and verify an OTP
BitSMS generates the code and never returns it. Verify returns verified: true or false; after 5 wrong codes or expiry the OTP is closed.
# 1. Send: BitSMS generates the code and never returns it.
curl https://sms-api.bitcommerz.com/v1/otp \
-H "Authorization: Bearer $BITSMS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: login-7f3a91" \
-d '{"to": "01712345678", "app_name": "MyShop"}'
# → 202 {"otp_id": "…", "message_id": "…", "expires_at": "…"}
# 2. Verify what the user typed.
curl https://sms-api.bitcommerz.com/v1/otp/$OTP_ID/verify \
-H "Authorization: Bearer $BITSMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"code": "482913"}'
# → 200 {"verified": true}const headers = {
Authorization: `Bearer ${process.env.BITSMS_API_KEY}`,
"Content-Type": "application/json",
};
// 1. Send
const sent = await fetch("https://sms-api.bitcommerz.com/v1/otp", {
method: "POST",
headers: { ...headers, "Idempotency-Key": `login-${sessionId}` },
body: JSON.stringify({ to: "01712345678", app_name: "MyShop" }),
}).then((r) => r.json());
// 2. Verify
const result = await fetch(`https://sms-api.bitcommerz.com/v1/otp/${sent.otp_id}/verify`, {
method: "POST",
headers,
body: JSON.stringify({ code: userInput }),
}).then((r) => r.json());
if (result.verified) { /* sign the user in */ }<?php
function bitsms(string $path, array $body, array $extra = []): array {
$ch = curl_init("https://sms-api.bitcommerz.com/v1" . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => array_merge([
"Authorization: Bearer " . getenv("BITSMS_API_KEY"),
"Content-Type: application/json",
], $extra),
CURLOPT_POSTFIELDS => json_encode($body),
]);
return json_decode(curl_exec($ch), true);
}
$otp = bitsms("/otp", ["to" => "01712345678", "app_name" => "MyShop"],
["Idempotency-Key: login-" . session_id()]);
$check = bitsms("/otp/" . $otp["otp_id"] . "/verify", ["code" => $_POST["code"]]);
if ($check["verified"] ?? false) { /* sign the user in */ }auth = {"Authorization": f"Bearer {os.environ['BITSMS_API_KEY']}"}
otp = requests.post(
"https://sms-api.bitcommerz.com/v1/otp",
headers={**auth, "Idempotency-Key": f"login-{session_id}"},
json={"to": "01712345678", "app_name": "MyShop"},
timeout=10,
).json()
check = requests.post(
f"https://sms-api.bitcommerz.com/v1/otp/{otp['otp_id']}/verify",
headers=auth,
json={"code": user_input},
timeout=10,
).json()
if check["verified"]:
... # sign the user intype otp struct {
OtpID string `json:"otp_id"`
}
var sent otp
post("/v1/otp", "login-"+sessionID, map[string]any{"to": "01712345678", "app_name": "MyShop"}, &sent)
var result struct {
Verified bool `json:"verified"`
AttemptsRemaining int `json:"attempts_remaining"`
}
post("/v1/otp/"+sent.OtpID+"/verify", "", map[string]any{"code": userInput}, &result)
// post marshals body, sets Authorization (and Idempotency-Key when non-empty),
// sends to https://sms-api.bitcommerz.com and decodes the JSON response into out.Verify webhook signatures
Each delivery carries a BitSMS-Signature header. Check it against the raw body with your endpoint secret before trusting the event.
# Each delivery is signed:
# BitSMS-Signature: t=1730000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
# v1 = hex(HMAC-SHA256(webhook_secret, "<t>.<raw request body>"))
# Reject if t is more than 5 minutes old. During secret rotation there are
# two v1= values; accept either.import crypto from "node:crypto";
// Use the RAW body (e.g. express.raw({ type: "application/json" })).
export function verifyBitsms(rawBody, header, secret) {
const parts = header.split(",").map((p) => p.split("="));
const t = Number(parts.find(([k]) => k === "t")?.[1]);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
return parts
.filter(([k]) => k === "v1")
.some(([, sig]) => {
const got = Buffer.from(sig, "hex");
return got.length === expected.length && crypto.timingSafeEqual(got, expected);
});
}<?php
function verify_bitsms(string $rawBody, string $header, string $secret): bool {
$t = null; $sigs = [];
foreach (explode(",", $header) as $part) {
[$k, $v] = array_pad(explode("=", $part, 2), 2, "");
if ($k === "t") $t = (int) $v;
if ($k === "v1") $sigs[] = $v;
}
if (!$t || abs(time() - $t) > 300) return false;
$expected = hash_hmac("sha256", $t . "." . $rawBody, $secret);
foreach ($sigs as $sig) {
if (hash_equals($expected, $sig)) return true;
}
return false;
}
$ok = verify_bitsms(file_get_contents("php://input"),
$_SERVER["HTTP_BITSMS_SIGNATURE"] ?? "", getenv("BITSMS_WEBHOOK_SECRET"));import hashlib, hmac, time
def verify_bitsms(raw_body: bytes, header: str, secret: str) -> bool:
items = [p.split("=", 1) for p in header.split(",") if "=" in p]
t = next((int(v) for k, v in items if k == "t"), 0)
if not t or abs(time.time() - t) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, v) for k, v in items if k == "v1")func VerifyBitsms(rawBody []byte, header, secret string) bool {
var t int64
var sigs []string
for _, part := range strings.Split(header, ",") {
k, v, _ := strings.Cut(part, "=")
switch k {
case "t":
t, _ = strconv.ParseInt(v, 10, 64)
case "v1":
sigs = append(sigs, v)
}
}
if t == 0 || math.Abs(float64(time.Now().Unix()-t)) > 300 {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
fmt.Fprintf(mac, "%d.", t)
mac.Write(rawBody)
expected := mac.Sum(nil)
for _, s := range sigs {
got, err := hex.DecodeString(s)
if err == nil && hmac.Equal(got, expected) {
return true
}
}
return false
}How the API behaves
Authentication
Send your key as Authorization: Bearer bsk_live_…. Keys have scopes and can be limited to your server IPs. Never put a key in a browser or mobile app.
Idempotency
Every send needs an Idempotency-Key. Retrying with the same key and body returns the original result, so a network timeout never causes a duplicate SMS or a double charge.
Errors
Errors share one shape: a stable code such as insufficient_credit or invalid_destination, a readable message and a request ID to quote to support.
Rate limits
Each key has a request rate. Responses include X-RateLimit headers; when you go over, you get 429 with Retry-After.