Skip to content
Splashify Pro
Docs

Send Bulk SMS

Send one approved SMS template to many numbers with a single request, each with its own variables. Up to 1,000 numbers per request.

We check the whole list first, then send in the background. You get a bulk_id straight away and can follow progress with SMS status, or get each result on your server with delivery reports.

Any approved template works here, whatever its type: promotional, transactional, service implicit or service explicit. The type, sender ID and price come from the template.

Note: SMS must be Active on your account and the template must be Approved. See Set up SMS and SMS templates.

Endpoint

POST

https://api.splashifypro.com/api/v1/public/sms/send-bulk

Headers

Request body

json
{
  "dlt_template_id": "1107160000000000002",
  "name": "Order alerts 26 Sep",
  "messages": [
    { "to": "+919876543210", "variables": ["Aarav", "AB-1042"] },
    { "to": "+919876543211", "variables": ["Diya", "AB-1043"] },
    { "to": "+919876543212", "variables": ["Kabir", "AB-1044"] }
  ]
}

What we check before sending

Nothing is sent until the whole request passes these checks:

  1. The template can send. It is approved, its sender ID is approved, and your DLT chain is verified.
  2. Every row has the right variables. A row with too many or too few, or with a value that is empty or over 30 characters, is refused with 400 and code: "invalid_variables". The message and row name the first bad row. Nothing is queued.
  3. Every number is an Indian mobile. Numbers that are not are skipped and counted in skipped.invalid.
  4. Duplicates are sent once. The first row with a number is sent. Later rows with the same number count in skipped.duplicate.
  5. Opted-out numbers are skipped. People who asked not to get your messages count in skipped.opted_out and are not charged.
  6. Your wallet covers the estimate. This is checked on the numbers left after opted-out ones are taken out, so your balance only needs to cover the messages that will go. If it does not, the request is refused with 402 and nothing is queued.
  7. Something is left to send. If every row was skipped, the request is refused with 400 and code: "no_recipients".

Every skipped row is listed in skipped_rows, so you can fix your data.

Examples

cURL

bash
curl -X POST https://api.splashifypro.com/api/v1/public/sms/send-bulk \
  -H "Authorization: Basic YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-alerts-26-sep" \
  -d '{
    "dlt_template_id": "1107160000000000002",
    "name": "Order alerts 26 Sep",
    "messages": [
      { "to": "+919876543210", "variables": ["Aarav", "AB-1042"] },
      { "to": "+919876543211", "variables": ["Diya", "AB-1043"] }
    ]
  }'

Node.js / TypeScript

text
type Row = { to: string; variables: string[] };

// idempotencyKey: make it once for this list, and send the same one on every retry.
async function sendOrderAlerts(rows: Row[], idempotencyKey: string) {
  const res = await fetch("https://api.splashifypro.com/api/v1/public/sms/send-bulk", {
    method: "POST",
    headers: {
      "Authorization":   `Basic ${process.env.SPLASHIFY_API_KEY!}`,
      "Content-Type":    "application/json",
      "Idempotency-Key": idempotencyKey,
    },
    body: JSON.stringify({
      dlt_template_id: "1107160000000000002",
      name:            "Order alerts 26 Sep",
      messages:        rows, // up to 1,000
    }),
  });

  const data = await res.json();
  if (!res.ok) throw new Error(`${res.status} ${data.code ?? ""}: ${data.message ?? ""}`);
  return data.bulk_id; // follow it with GET /api/v1/public/sms/bulk/:bulk_id
}

Python

python
import os, requests

# idempotency_key: make it once for this list, and send the same one on every retry.
def send_order_alerts(rows: list[dict], idempotency_key: str) -> str:
    r = requests.post(
        "https://api.splashifypro.com/api/v1/public/sms/send-bulk",
        headers={
            "Authorization":   f"Basic {os.environ['SPLASHIFY_API_KEY']}",
            "Content-Type":    "application/json",
            "Idempotency-Key": idempotency_key,
        },
        json={
            "dlt_template_id": "1107160000000000002",
            "name":            "Order alerts 26 Sep",
            "messages":        rows,  # up to 1,000
        },
        timeout=60,
    )
    data = r.json()
    if r.status_code != 200:
        raise RuntimeError(f"{r.status_code} {data.get('code')}: {data.get('message')}")
    return data["bulk_id"]

For more than 1,000 numbers, split the list and send one request per 1,000.

Successful response (200 OK)

json
{
  "success": true,
  "bulk_id": "9d2f4b61-1c7e-4a35-8f90-6b3e2d7c5a18",
  "name": "Order alerts 26 Sep",
  "queued": 2,
  "skipped": { "opted_out": 1, "invalid": 0, "duplicate": 0 },
  "skipped_rows": [
    { "row": 3, "to": "+919876543212", "reason": "opted_out" }
  ],
  "estimated_parts": 2,
  "estimated_cost": 0.32,
  "status": "queued"
}

Retry safely

A bulk send can take a while to check. If your request times out or you get a 5xx, you cannot tell whether the list was queued. Send an Idempotency-Key header, and you can simply send the same request again with the same key:

  • If the first request went through, you get its answer back: the same bulk_id, the send's status as it is now, and "replayed": true. The response also has the header Idempotency-Replayed: true. Nothing is sent or charged again.
  • If the first request failed, the key is not kept, and the retry is a normal send.
  • If the first request is still running, you get 409 with code: "idempotency_in_progress". Wait a few seconds and try again with the same key.
  • The same key with a different body gets 422 with code: "idempotency_key_reused". Use a new key for a new list.

A key is kept for 24 hours. Use one key per list you decide to send, such as order-alerts-26-sep, and reuse it on every retry of that list. See Idempotency.

Errors

Errors look like this. Switch on code, and show message to a person:

json
{
  "success": false,
  "code": "invalid_variables",
  "message": "Row 2: this template needs 2 variable(s) and 1 were given.",
  "row": 2
}

A 402 also tells you the estimate, and for insufficient_balance your balance. The estimate leaves out the opted-out numbers we found. When your balance cannot cover the list, we stop checking for opt-outs as soon as that is clear, or do not check at all when it cannot cover one message, so the estimate can include numbers that opted out:

json
{
  "success": false,
  "code": "insufficient_balance",
  "message": "Your wallet balance is too low for this send. Nothing was queued. Add funds and try again.",
  "estimated_parts": 1000,
  "estimated_cost": 160,
  "balance": 42.5
}

The checks every Public API request goes through answer the same way as on Send SMS: 401 for a missing or invalid API key, 403 when your plan does not include the Public API, your IP is not on the allowlist or your account is suspended, and 429 over your plan's rate limit. One bulk request counts as one request toward that limit.

Rate limits

Bulk sends have their own limit: 10 a minute for your account. It counts only bulk sends, and the 60 a minute limit on single sends does not apply here. Over it, you get 429 with a Retry-After header and this body:

json
{
  "success": false,
  "code": "rate_limited",
  "error": "rate_limit_exceeded",
  "message": "Too many bulk sends. You can make 10 a minute. Wait a moment and try again.",
  "retry_after": 42,
  "limit": 10
}

Nothing is queued by a request that gets 429. To send more numbers, put up to 1,000 in each request.

Good to know

  • Send an Idempotency-Key. Without one, a second identical request is a second send, and everyone on it gets the SMS twice. With one, a retry gets the first answer back. See Retry safely.
  • Promotional SMS do not reach numbers on Do Not Disturb. Use a transactional or service template for messages people asked for.
  • Opted-out numbers are never charged. They are left out before anything is sent.