إرسال
SMS

Send Bulk SMS

Send the same SMS message to many recipients in one request

Endpoint

POST /api/sms/messages/bulk

Request body

FieldTypeRequiredDescription
messagestringYesNon-empty message content. Longer messages are billed as multiple SMS parts
senderstringYesSender ID (max 11 alphanumeric chars)
payment_typestringYeswallet or subscription
receiversarrayYesNon-empty array of phone numbers, each matching a provider pattern configured in the platform
expires_afterintegerNoSeconds this message stays worth delivering, 60604800. Defaults to 24 hours. Applies to every recipient in the batch; undelivered ones are then marked expired and refunded — see Message validity

Headers

HeaderRequiredDescription
AuthorizationYesBearer YOUR_API_TOKEN
Content-TypeYesapplication/json
AcceptYesapplication/json
Idempotency-KeyNoReplaying 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
  }
}
FieldDescription
message_idUUID for this batch — use it with Get Message
costTotal SMS parts charged: parts per message × accepted recipients
details.sentRecipients accepted for sending, after removing duplicates and numbers whose network the sender ID is not registered on
details.totalNumber of entries you submitted in receivers
details.skipped_unavailableRecipients 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.

On this page