SSoftLinkSMSDocs

Send bulk SMS

Send campaigns and announcements to thousands of recipients, in safe, resumable batches.

One request sends one message to up to 1,000 recipients. For bigger lists, send in batches.

One batch

curl https://api.softlinksms.co.ke/api/v1/sms \
  -H "X-Api-Key: $SOFTLINK_API_KEY" \
  -H "Idempotency-Key: spring-sale-batch-0" \
  -H "Content-Type: application/json" \
  -d '{
    "recipients": ["+254712345678", "0798765432", "254701234567"],
    "message": "Spring sale: 20% off everything until Sunday. Reply STOP to opt out."
  }'

What happens:

  • Every number is validated first. If any number is invalid, the whole request is rejected with 400 and the bad numbers listed, and nothing is sent or charged. See Phone numbers.
  • Duplicates are removed. Each unique number gets one message.
  • The whole batch is charged at once from your wallet. If the balance can't cover it, you get 402 insufficient_balance and nothing is sent.
  • You get one object back for the whole send: its batch_id, recipients, total_price and currency, and a messages list with one message per recipient. Every message and webhook event carries the same batch_id.

Many batches

Split the list into chunks of 1,000 and give each chunk a stable idempotency key, such as campaign id plus chunk number. If your job crashes halfway, rerun it from the start: chunks that already went out are replayed from their keys rather than sent again.

import os
import time
import requests

API = "https://api.softlinksms.co.ke/api/v1/sms"
HEADERS = {"X-Api-Key": os.environ["SOFTLINK_API_KEY"]}

def send_campaign(campaign_id: str, numbers: list[str], body: str) -> None:
    for start in range(0, len(numbers), 1000):
        chunk = numbers[start : start + 1000]
        while True:
            response = requests.post(
                API,
                headers={**HEADERS, "Idempotency-Key": f"{campaign_id}-{start // 1000}"},
                json={"recipients": chunk, "body": body},
                timeout=30,
            )
            if response.status_code == 429:
                time.sleep(int(response.headers.get("Retry-After", "1")))
                continue
            response.raise_for_status()
            break

At 30 send requests per second per account (see Rate limits), that's up to 30,000 recipients a second.

Promotional rules

Bulk messages are usually promotional, the default. Promotional SMS in Kenya must only go to people who agreed to receive it and must tell them how to stop:

  • Keep your own opt-in records.
  • Include opt-out instructions, such as "Reply STOP to opt out".
  • Honour opt-outs before your next send.

Don't send promotions as transactional to get around these rules; it gets transactional access revoked.

After the send

Messages move from pending to sent within seconds, then to delivered or failed as networks report back. For a campaign, webhooks are far cheaper than polling 1,000 messages. Use batch_id to group results by send.

On this page