Send SMS from your own code
One HTTPS request sends to one number or ten thousand. Same balance, same reports and same segment billing as the dashboard, with the safeguards a production system needs.
curl -X POST https://api.bulletsms.com/api/v1/sms/send \
-H "Authorization: Bearer $BULLETSMS_TOKEN" \
-H "Idempotency-Key: order-4821-shipped" \
-H "Content-Type: application/json" \
-d '{
"to": ["9801234567"],
"message": "Your order #4821 has shipped.",
"senderId": "BulletSMS"
}'Response · 200
{
"success": true,
"data": {
"campaignId": "cm2x8k1q40001…",
"queued": 1,
"billedUnits": 1,
"invalidNumbers": [],
"duplicatesRemoved": 0,
"balanceRemaining": 4999
}
}From token to report in four steps
The API is a door into the same account the dashboard uses, not a separate product with its own balance.
- 1
Create a token
Generate an API token in your dashboard and limit it to your server's IPs, domains or sender IDs.
- 2
Send a request
POST the numbers and the message. The response says what was queued and how many segments were billed.
- 3
Get told what happened
Receive signed webhooks as messages go out, or look up any message or send by its ID.
- 4
See it in the dashboard
API sends appear in the same reports as your campaigns, ready to filter and export.
The parts every production integration needs
Safe retries, locked-down tokens and signed events from your first request, not an enterprise add-on.
Idempotency keys
Send an Idempotency-Key and a retried request is applied once. A network blip never double-sends.
Scoped tokens
Limit a token to your IPs, domains and sender IDs, and give it a daily SMS limit.
Signed webhooks
Campaign, message and OTP events reach your URL, signed with HMAC-SHA256.
Personalised sends
Up to 5,000 numbers, each with its own message, in one request.
Scheduling
Pass scheduledAt to send later, up to 90 days ahead.
Billing in every response
Each response says how many segments were billed and what's left in your balance.
Status lookups
Check one message or a whole send by its ID, any time.
Limits per token
Each token has its own rate limit, so one busy integration can't slow down another.
Hear about it instead of asking
Add a webhook URL to a token and events arrive as they happen. Each one is signed with your webhook secret, and failed deliveries are retried with back-off.
campaign.queuedA send was accepted and queued.campaign.sendingMessages in the send started going out.campaign.completedEvery message in the send has a final status.message.deliveredOne message was delivered (sends of up to 100 numbers).message.failedOne message failed (sends of up to 100 numbers).otp.deliveredA verification code was delivered.otp.failedA verification code could not be delivered.
POST /your/webhook
X-BulletSMS-Event: campaign.completed
X-BulletSMS-Signature: 5d41402abc4b2a76b9719d911017c592…
{
"event": "campaign.completed",
"data": {
"campaignId": "cm2x8k1q40001…",
"totalRecipients": 500,
"sent": 500,
"delivered": 497,
"failed": 3
},
"sentAt": "2026-10-29T09:15:42.008Z"
}import crypto from "node:crypto";
// Use the raw request body, exactly as received.
function isFromBulletSms(rawBody, signature, secret) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return (
signature?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
);
}A small, predictable surface
Every path is relative to https://api.bulletsms.com/api/v1.
- POST
/sms/sendSend one message to up to 10,000 numbers. - POST
/sms/send-dynamicSend a different message to each number, in one request. - GET
/sms/status/:messageIdStatus of one message. - GET
/sms/campaign/:campaignIdReport for a whole send: counts and per-number status. - GET
/balanceYour current SMS credit balance.
SMS API questions
Per token, send requests are limited to 5 a second, and other calls such as status lookups to 60 a minute. You can also give a token a daily SMS limit. Going over returns a 429 (or a 403 for the daily limit) with an error code your code can handle.
Send an Idempotency-Key header with each send. If a send with the same key and body was already accepted, it isn't sent again; the same key with a different body is rejected with a 409.
Look it up with GET /sms/status/:messageId, or a whole send with GET /sms/campaign/:campaignId. Or add a webhook URL to your token and receive message and campaign events as they happen.
Any language that can make an HTTPS request. The API reference has examples in cURL, Node.js, PHP and Python.
Yes. They draw on the same balance and appear in the same reports as sends from the dashboard.
Make your first request today
Create an account and verify your number. Once it's approved and topped up, generate a token and send.
