Send to many numbers
POST /v1/messages/send-bulk sends one message to a contact list or a list of numbers, now, scheduled or at a set pace.
POST https://api.smsyanu.com/v1/messages/send-bulk
Authorization: Bearer yanu_live_YOUR_KEY
Content-Type: application/jsonThe whole batch is priced and charged when you send the request. Duplicate numbers are dropped, and numbers on your opt-out list are left out. Check the cost first with POST /v1/messages/bulk-precheck.
Request body
Pass contact_list_ids or recipients. If you pass both, recipients is used.
| Field | Type | Required | Meaning |
|---|---|---|---|
contact_list_ids | integer array | One of the two | Saved contact lists to send to. |
recipients | object array | One of the two | Numbers given in the request. Each has to, and optionally name and vars. |
text | string | Yes | The message. Merge fields are filled in for each person. See Merge fields. |
sender_id | string | No | Sender name, up to 11 characters. It must be approved on your account. Without it, the route default is used. |
schedule_mode | string | No | now (default), scheduled or drip. |
scheduled_at | string | When scheduled | ISO 8601 time with a time zone. At least 5 minutes and at most 7 days ahead. |
drip_rate | integer | When drip | Messages per minute, 1 to 5000. |
flash | boolean | No | Show on the screen without saving on the phone. |
validity_period | integer | No | Minutes (1 to 4320) before an undelivered message expires. |
priority | integer | No | 0 normal (default) to 3 highest. |
skip_unavailable | boolean | No | Leave out numbers that cannot be sent to, uncharged, instead of refusing the whole request. |
A scheduled send needs a contact list. Use now or drip with recipients. Binary messages are not supported on bulk sends.
Send to a list of numbers
{
"recipients": [
{ "to": "+265888123456", "name": "Chisomo", "vars": { "balance": "45,000" } },
{ "to": "0999123456", "name": "Thoko", "vars": { "balance": "12,500" } }
],
"text": "Dear {first_name|parent}, your balance is MWK {balance}.",
"sender_id": "YANU"
}Schedule a contact list
{
"contact_list_ids": [12],
"text": "School fees for term 2 are due on Friday.",
"sender_id": "YANU",
"schedule_mode": "scheduled",
"scheduled_at": "2026-12-01T08:00:00+02:00"
}To send at a set pace instead, use "schedule_mode": "drip" with "drip_rate": 300.
Response
HTTP 200:
{
"batch_id": "2d7f4a1e-8c33-4b1a-9c55-0e6f7a2b9d10",
"total_recipients": 2,
"estimated_cost": 40.0,
"status": "sending"
}| Field | Meaning |
|---|---|
batch_id | Use it to read the batch or to cancel a scheduled batch. |
total_recipients | Numbers that will be sent to, after duplicates and opt-outs are removed. |
estimated_cost | What was charged, in your balance currency. |
status | sending, scheduled or dripping. |
Merge fields
Write a field as {field}, or as {field|fallback} to give a value for anyone who has none. Fields come from the contact, or from vars and name in recipients.
If someone has no value and there is no fallback, the request is refused with missing_merge_field and nothing is charged. The answer names the field and an example number.
Merge fields change the length of each message, so people on one batch can use different numbers of segments. Each message is charged for its own segments.
Check the cost first
POST /v1/messages/bulk-precheck takes the same contact_list_ids or recipients, text and sender_id. It sends nothing and charges nothing.
{
"total_contacts": 2,
"total_segments": 2,
"total_cost": 40.0,
"wallet_currency": "MWK",
"wallet_balance": 5000.0,
"balance_after": 4960.0,
"sufficient_balance": true,
"invalid_count": 0,
"suppressed_count": 0,
"missing_fields": []
}suppressed_count is how many numbers were left out because they opted out.
Send each batch once
Add an Idempotency-Key header with a unique value per batch. If you send the same key again, you get the first result back with "idempotent_replay": true, and you are not charged twice. While the first request is still running, a repeat answers 409 with idempotency_in_progress.
Follow and cancel
| Method | Path | What it does |
|---|---|---|
GET | /v1/messages/batch/{batch_id} | Totals for the batch. |
POST | /v1/messages/batch/{batch_id}/cancel | Cancel a scheduled batch before it starts. The charge is refunded. |
A message that fails is refunded when the gateway refuses it or its final report says it failed. Set up a webhook to hear about each one.
Limits and errors
- Bulk requests are limited to 10 per minute by default. A batch can be large, so send one big request rather than many small ones.
- A message can be at most 10 segments. A longer one is refused with
message_too_long.
| HTTP | Code | What it means |
|---|---|---|
| 400 | missing_merge_field | Someone has no value for a field. Add a fallback or fix the data. |
| 400 | scheduled_needs_list | A scheduled send was given recipients. Use contact_list_ids. |
| 400 | sandbox_no_schedule | Test keys cannot schedule sends. |
| 402 | insufficient_balance | Your balance does not cover the batch. Nothing was charged. |
| 413 | message_too_long | A message renders to more than 10 segments. |
| 422 | destination_not_allowed | A number is outside Malawi. Fix it, or set skip_unavailable. |
See the common codes in Errors.