Send Bulk SMS
Send the same SMS message to many recipients in one request
Endpoint
POST /api/sms/messages/bulkRequest body
| Field | Type | Required | Description |
|---|---|---|---|
message | string | Yes | Non-empty message content. Longer messages are billed as multiple SMS parts |
sender | string | Yes | Sender ID (max 11 alphanumeric chars) |
payment_type | string | Yes | wallet or subscription |
receivers | array | Yes | Non-empty array of phone numbers, each matching a provider pattern configured in the platform |
expires_after | integer | No | Seconds this message stays worth delivering, 60–604800. Defaults to 24 hours. Applies to every recipient in the batch; undelivered ones are then marked expired and refunded — see Message validity |
Headers
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer YOUR_API_TOKEN |
Content-Type | Yes | application/json |
Accept | Yes | application/json |
Idempotency-Key | No | Replaying the same key with the same body returns the original result instead of sending again |
Example
curl -X POST https://sms.lamah.com/api/sms/messages/bulk \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"message": "Your order has been shipped.",
"sender": "MyShop",
"payment_type": "wallet",
"receivers": [
"00218912345678",
"00218922654321",
"00218944678901"
]
}'Response
{
"message_id": "b9e8a1f2-4c60-4b0b-9f51-0c4f2f6c9e7a",
"cost": 3,
"details": {
"sent": 3,
"total": 3,
"skipped_unavailable": 0
}
}| Field | Description |
|---|---|
message_id | UUID for this batch — use it with Get Message |
cost | Total SMS parts charged: parts per message × accepted recipients |
details.sent | Recipients accepted for sending, after removing duplicates and numbers whose network the sender ID is not registered on |
details.total | Number of entries you submitted in receivers |
details.skipped_unavailable | Recipients dropped because their network operator is temporarily unreachable — a subset of the gap between sent and total |
details.sent can be lower than details.total. Numbers that are duplicates, or that belong to a network your sender ID is not registered on, are dropped silently — they are not itemised in the response. Compare the two values to detect partial acceptance.
A non-zero skipped_unavailable is retryable, unlike the rest of the gap. Those numbers were valid and reachable by your sender ID; the operator was simply down at the time. Re-send them once it is back — with a new Idempotency-Key, since the original key is bound to the batch that already went out.
A 200 means the batch was accepted and your balance reserved, not that the messages have reached the network. Poll Get Message with the returned message_id for per-recipient delivery status.
Error responses
422 Unprocessable Entity — Validation failed
{
"message": "The receivers.0 must be valid in system providers.",
"errors": {
"receivers.0": ["The receivers.0 must be valid in system providers."]
}
}404 Not Found — Sender not found
{ "message": "Sender not found" }400 Bad Request — No compatible recipients
Returned when none of the recipients belong to a network the sender ID is registered on, and none were dropped for operator unavailability.
{ "message": "The sender and receivers are not compatible." }400 Bad Request — Insufficient balance
{ "message": "Insufficient balance" }401 Unauthorized
{ "message": "Invalid or missing token." }409 Conflict — Idempotency key reused
{ "message": "Idempotency key is already in use" }503 Service Unavailable — Every recipient unreachable
Returned when the batch is left with nothing to send and at least one recipient was dropped because its network operator is temporarily unreachable. Nothing is charged. Retry the whole batch once the operator is back.
{ "message": "The provider for these receivers is temporarily unavailable" }A batch that still has at least one reachable recipient returns 200 instead, with the dropped ones counted in details.skipped_unavailable.
503 Service Unavailable — Ingest unavailable
{ "message": "SMS ingest unavailable" }Batching large lists
The API does not enforce a fixed cap on receivers, but very large arrays make for slow requests and large request bodies. Split long lists into batches:
const chunk = (arr, size) =>
Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =>
arr.slice(i * size, i * size + size)
);
for (const [i, batch] of chunk(allReceivers, 1000).entries()) {
await sendBulk({ ...payload, receivers: batch }, { idempotencyKey: `campaign-42-batch-${i}` });
}If your company is not yet verified, receivers is replaced with your company's registered phone number alone. The request still returns 200 and still consumes balance.