How to Send Bulk SMS with a Single API Call
By SendAPI Engineering · Last updated March 12, 2026 · 6 min read
Learn how to send bulk SMS messages programmatically — batching, rate limits, personalization, delivery tracking, and handling failures at scale.
What Is Bulk SMS?
Bulk SMS means sending the same (or personalized) message to many recipients simultaneously via API. Common use cases:
- Marketing campaigns — promotions, flash sales, re-engagement
- Operational alerts — service outages, maintenance windows
- Event reminders — appointment confirmations, webinar reminders
- Mass notifications — emergency alerts, system status updates
The key distinction from individual transactional SMS: bulk sending is broadcast-oriented and often non-personalized, though modern APIs support per-recipient variable substitution.
Why SMS Remains a Top Broadcast Channel
SMS consistently outperforms email for time-sensitive bulk messages. According to SimpleTexting's 2024 SMS Marketing Statistics report, SMS has an average open rate of 98% compared to 20–22% for email, and an average response rate of 45% compared to 6% for email. Average time-to-open is 3 minutes for SMS vs 90 minutes for email (Klaviyo, 2024). For flash sales, urgent alerts, and appointment reminders, SMS bulk sending delivers measurably better customer engagement than equivalent email campaigns.
The global bulk SMS market was valued at $62.7 billion in 2023 and is expected to reach $105.3 billion by 2030 (Grand View Research), driven by increasing adoption for customer engagement and transactional notifications across retail, logistics, and fintech.
The Naive Approach (Don't Do This)
A common first implementation loops over recipients and fires individual API calls:
// ❌ DON'T: sequential individual sends
for (const phone of recipients) {
await client.sms.send({ to: phone, body: 'Your message here' })
}
Problems with this approach:
- Slow — sequential, no concurrency
- Fragile — one failure breaks the loop
- Rate limit prone — hammers the API endpoint
- No batch delivery optimization — carrier routing done one at a time
The Right Approach: Batch API
SendAPI's bulk SMS endpoint accepts an array of recipients and handles batching, carrier routing, and retry internally:
import SendAPI from '@sendapi/node'
const client = new SendAPI({ apiKey: process.env.SENDAPI_KEY })
const result = await client.sms.sendBulk({
messages: [
{ to: '+14155552671', body: 'Your order ORD-001 has shipped!' },
{ to: '+447911123456', body: 'Your order ORD-002 has shipped!' },
{ to: '+61412345678', body: 'Your order ORD-003 has shipped!' },
],
options: {
senderId: 'ACME', // Alphanumeric sender (where supported)
deliveryReceipts: true, // Webhook delivery events per message
idempotencyKey: 'batch-2026-03-12-shipment', // Safe to retry
}
})
console.log(result.batchId) // Track this batch
console.log(result.accepted) // Count accepted
console.log(result.rejected) // Array of failed numbers with reasons
Personalization at Scale
For personalized messages (name, order number, etc.), use template variables:
const messages = orders.map(order => ({
to: order.customerPhone,
template: 'shipment_notification',
variables: {
name: order.customerName,
orderId: order.id,
trackingUrl: order.trackingUrl,
}
}))
const result = await client.sms.sendBulk({ messages })
The template is defined once in your SendAPI dashboard:
> "Hi {{name}}, your order {{orderId}} is on its way. Track it: {{trackingUrl}}"
Handling Delivery Receipts
For large batches, you need to know which messages were delivered and which failed. Set up a webhook endpoint:
app.post('/webhook/sms-delivery', (req, res) => {
const { messageId, to, status, timestamp, error } = req.body
// status: 'delivered' | 'failed' | 'undelivered' | 'queued'
if (status === 'failed') {
console.log(`Failed to deliver to ${to}: ${error.code}`)
// Log to your system, retry with alternative channel
}
res.sendStatus(200)
})
Common failure reasons:
- Invalid number — number doesn't exist or is disconnected
- Carrier rejection — blocked by destination carrier (content filter)
- Opt-out — recipient has opted out of your sender ID
- Network error — temporary, will be retried automatically
Rate Limits and Throughput
SendAPI's bulk SMS handles throttling internally, but you should understand the limits:
| Plan | Bulk SMS throughput |
|---|---|
| Starter | 100 messages/minute |
| Growth | 1,000 messages/minute |
| Business | 5,000 messages/minute |
| Enterprise | Custom |
For very large sends (100k+), split your list into batches of 10,000 and send with a small delay between batches:
const BATCH_SIZE = 10_000
const batches = chunk(recipients, BATCH_SIZE)
for (const batch of batches) {
const messages = batch.map(phone => ({ to: phone, body: messageBody }))
await client.sms.sendBulk({ messages })
await sleep(2000) // 2s pause between batches
}
International Bulk SMS Considerations
When sending internationally:
- Sender ID rules vary by country — alphanumeric IDs are banned in the US, India, and some other markets. Use a long code or short code instead.
- Opt-out compliance — different countries have different requirements. In the US, include "Reply STOP to unsubscribe." Many countries have similar requirements.
- Timing — respect local time zones. Sending at 3am local time is a fast route to spam complaints.
- Content filtering — some carriers filter SMS containing URLs, certain keywords, or shortlinks. Use full URLs and avoid spam trigger words.
// Respect time zones when scheduling
import { zonedTimeToUtc } from 'date-fns-tz'
const localSendTime = '2026-03-15 09:00'
const sendAt = zonedTimeToUtc(localSendTime, 'America/New_York')
await client.sms.sendBulk({
messages,
options: { scheduledAt: sendAt.toISOString() }
})
Idempotency (Critical for Bulk Sends)
Always set an idempotency key on bulk sends. If your server crashes mid-send or your network times out, you can safely retry the same request without double-sending:
await client.sms.sendBulk({
messages,
options: {
idempotencyKey: `campaign-spring-sale-${Date.now()}`
}
})
SendAPI deduplicates requests with the same key within a 24-hour window.
Monitoring a Bulk Send
After firing a bulk batch, monitor progress via the batch status endpoint:
const status = await client.sms.getBatchStatus(result.batchId)
console.log({
total: status.total,
sent: status.sent,
delivered: status.delivered,
failed: status.failed,
pending: status.pending,
})
Or view real-time stats in the SendAPI dashboard under SMS → History.