Send a batch

POST /v1/batches

One body to up to 10,000 recipients, each with its own merge variables (or to contacts by tag), sent as individual messages through the normal pipeline. Numbers are normalized, deduplicated and checked against opt-outs before queueing; rejected rows are returned with reasons. Set dry_run to validate without sending.

Send your API key as a bearer token: Authorization: Bearer ssms_sk_.... Test keys run this endpoint against the sandbox; see Authentication for key modes and scopes.

Request body

JSON (Content-Type: application/json).

FieldTypeRequiredDescription
fromstringYesExample: +15005550100.
bodystringYesAt most 1600 characters. Example: Hi {{first_name}}, your order {{order_id}} has shipped..
namestringNoAt most 80 characters.
recipientsarray of objectNoAt most 10000 items.
recipients[].tostringYesExample: +14155550132.
recipients[].variablesobjectNo
tagsarray of stringNoSend to contacts carrying any of these tags (instead of recipients).
contact_idsarray of stringNo
segment_idstringNoSend to the contacts in this saved segment, evaluated when the batch is created (instead of recipients).
topic_idstringNoSubscription topic. Recipients unsubscribed from it are removed at validation (topic_unsubscribed) and skipped at send time, like opt-outs. Omit to apply opt-outs only. Example: tp_marketing.
scheduled_atstringNoFuture ISO timestamp or epoch ms, up to 30 days out.
dry_runbooleanNoValidate only: returns a BatchValidation and sends nothing.
check_line_typesbooleanNoReject landlines (first 500 unique numbers are checked).

Responses

StatusMeaningBody
200Dry run resultBatchValidation
201Batch createdBatch
400Invalid request or no valid recipientsError
401Missing, malformed, or revoked API keyError
402Billable live calls only: the prepaid credit balance cannot cover the call (insufficient_credits); nothing was done or chargedError
403from not owned, or key mode does not match the numberError
429Rate limit or quota exceededError

200: BatchValidation fields

FieldTypeDescription
objectbatch_validation
countsobject
counts.rowsinteger
counts.validinteger
counts.invalidinteger
counts.duplicatesinteger
counts.opted_outinteger
counts.topic_unsubscribedintegerUnsubscribed from the batch topic_id; 0 without a topic
counts.missing_variableinteger
counts.landlineinteger
counts.empty_after_mergeinteger
unknown_variablesarray of string
segmentsobject
segments.totalinteger
segments.maxinteger
segments.encodingGSM-7, UCS-2
estimated_cost_usdnumber
previewarray of object
preview[].tostring
preview[].bodystring
preview[].segmentsinteger
rejectedarray of object
rejected[].rowinteger0-based index into recipients
rejected[].tostring
rejected[].reasoninvalid, duplicate, opted_out, missing_variable, landline, empty_after_merge
rejected[].detailstring

201: Batch fields

FieldTypeDescription
idstringExample: bc_a1B2c3D4e5F6.
objectbatch
namestring
statusscheduled, sending, paused, complete, canceled
fromstring
bodystring
sourceapi, csv, contacts
testboolean
scheduled_atstring (date-time)Nullable.
created_atstring (date-time)
completed_atstring (date-time)Nullable.
countsobjecttotal = queued + sent + failed + opted_out + canceled, always. "sent" = accepted by the carrier; what happened next (delivered, failed, replies) is GET /v1/batches/{id}/results. "opted_out" also counts recipients unsubscribed from the batch topic.
counts.totalinteger
counts.queuedinteger
counts.sentinteger
counts.failedinteger
counts.opted_outinteger
counts.canceledinteger
rejectedobjectRows dropped at validation (not part of total), by reason. Nullable.
variablesarray of string
retriesinteger

Errors

StatusWhen
400Invalid request or no valid recipients
401Missing, malformed, or revoked API key
402Billable live calls only: the prepaid credit balance cannot cover the call (insufficient_credits); nothing was done or charged
403from not owned, or key mode does not match the number
429Rate limit or quota exceeded

Every error has the same JSON shape, and request_id matches the X-Request-Id response header. Errors lists every code and what to do about it.

json
{
  "error": {
    "code": "invalid_request",
    "message": "What went wrong, in plain words.",
    "param": "the_field",
    "request_id": "req_a1B2c3D4e5F6g7H8"
  }
}

Examples

curl

bash
curl -X POST "https://api.joinsimplesms.com/v1/batches" \
  -H "Authorization: Bearer $SIMPLESMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "from": "+15005550100",
  "body": "Hi {{first_name}}, your order {{order_id}} has shipped.",
  "topic_id": "tp_marketing"
}'

Node.js

The Node.js SDK does not wrap this endpoint yet; call it with fetch.

javascript
const res = await fetch('https://api.joinsimplesms.com/v1/batches', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SIMPLESMS_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "from": "+15005550100",
    "body": "Hi {{first_name}}, your order {{order_id}} has shipped.",
    "topic_id": "tp_marketing"
  }),
});

if (!res.ok) throw new Error((await res.json()).error.message);
const data = await res.json();

Python

The Python SDK does not wrap this endpoint yet; call it over HTTP.

python
import json, os, urllib.request

req = urllib.request.Request(
    "https://api.joinsimplesms.com/v1/batches",
    method="POST",
    headers={
        "Authorization": f"Bearer {os.environ['SIMPLESMS_API_KEY']}",
        "Content-Type": "application/json",
    },
    data=json.dumps({
        "from": "+15005550100",
        "body": "Hi {{first_name}}, your order {{order_id}} has shipped.",
        "topic_id": "tp_marketing"
    }).encode(),
)
with urllib.request.urlopen(req) as res:
    data = json.load(res)