Sending
Send bulk SMS with POST /v1/batches
/v1/batchesSend to up to 10,000 numbers in one request. The whole cost is taken from the balance at once.
Request body
- toarray of stringsoptional
Numbers for one shared text. Use either to with text, or messages.
- textstringoptional
The shared text, required with to.
- messagesarray of objectsoptional
One {to, text} per recipient, for personal texts.
- namestringoptional
A label for the console, up to 100 characters.
- referencestringoptional
Your own id, up to 100 characters, returned on the batch and every message.
- send_atstringoptional
ISO 8601 time, 2 minutes to 45 days ahead.
curl https://bulksmsapi.cc/v1/batches \
-H "Authorization: Bearer $BULKSMSAPI_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: spring-sale-0412" \
-d '{
"name": "Spring sale",
"text": "Spring sale at Northwind: 20% off until Sunday. Opt out: northwind.example/sms",
"to": [
"+14155550123",
"+447400123456",
"+4915123456789"
]
}'const res = await fetch("https://bulksmsapi.cc/v1/batches", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BULKSMSAPI_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "spring-sale-0412",
},
body: JSON.stringify({
name: "Spring sale",
text: "Spring sale at Northwind: 20% off until Sunday. Opt out: northwind.example/sms",
to: ["+14155550123", "+447400123456", "+4915123456789"],
}),
});
const batch = await res.json();
console.log(res.status, batch.id, batch.recipients, batch.cost);import json, os, urllib.request
req = urllib.request.Request(
"https://bulksmsapi.cc/v1/batches",
data=json.dumps({
"name": "Spring sale",
"text": "Spring sale at Northwind: 20% off until Sunday. Opt out: northwind.example/sms",
"to": ["+14155550123", "+447400123456", "+4915123456789"],
}).encode(),
headers={
"Authorization": "Bearer " + os.environ["BULKSMSAPI_KEY"],
"Content-Type": "application/json",
"Idempotency-Key": "spring-sale-0412",
"User-Agent": "northwind-app/1.0",
},
method="POST",
)
with urllib.request.urlopen(req) as res:
batch = json.load(res)
print(res.status, batch["id"], batch["recipients"], batch["cost"])<?php
$ch = curl_init("https://bulksmsapi.cc/v1/batches");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("BULKSMSAPI_KEY"),
"Content-Type: application/json",
"Idempotency-Key: spring-sale-0412",
],
CURLOPT_POSTFIELDS => json_encode([
"name" => "Spring sale",
"text" => "Spring sale at Northwind: 20% off until Sunday. Opt out: northwind.example/sms",
"to" => ["+14155550123", "+447400123456", "+4915123456789"],
]),
]);
$batch = json_decode(curl_exec($ch), true);
echo $batch["id"], " ", $batch["recipients"], " ", $batch["cost"], "\n";package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
body, _ := json.Marshal(map[string]any{
"name": "Spring sale",
"text": "Spring sale at Northwind: 20% off until Sunday. Opt out: northwind.example/sms",
"to": []string{"+14155550123", "+447400123456", "+4915123456789"},
})
req, _ := http.NewRequest("POST", "https://bulksmsapi.cc/v1/batches", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("BULKSMSAPI_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "spring-sale-0412")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var batch map[string]any
json.NewDecoder(res.Body).Decode(&batch)
fmt.Println(res.StatusCode, batch["id"], batch["recipients"], batch["cost"])
}{
"id": "bat_6TQ2W8M1ZK4N",
"object": "batch",
"name": "Spring sale",
"status": "queued",
"source": "api",
"encoding": "GSM-7",
"recipients": 3,
"max_segments": 1,
"cost": "0.02741",
"refunded": "0.00000",
"currency": "USD",
"counts": { "scheduled": 0, "queued": 3, "sent": 0, "delivered": 0, "failed": 0, "canceled": 0 },
"send_at": null,
"reference": null,
"created_at": "2026-11-07T09:31:40.102Z",
"completed_at": null,
"set_aside": []
}Numbers that are set aside
Every number is cleaned and priced on its own. Numbers that cannot be sent are left out of the batch, not charged, and listed in set_aside with a reason:
| reason | Meaning |
|---|---|
| invalid_number | Not readable as + and 7 to 15 digits after cleaning. |
| duplicate | The same number appeared earlier in the request. |
| unsupported_country | The number does not belong to a priced country. |
| too_long | Its personal text needs more than 10 segments. |
| empty_text | Its personal text is empty. |
| suppressed | The number is on your do-not-send list. |
If no number is left, the request fails with the code of the first reason (for example invalid_number) and nothing is charged.
Personal text per number
Use messages to give each recipient their own text: names, times, amounts. Each text is counted and priced on its own, so a batch can mix one-segment and two-segment messages.
curl https://bulksmsapi.cc/v1/batches \
-H "Authorization: Bearer $BULKSMSAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Appointment reminders, 14 May",
"send_at": "2026-05-13T16:00:00Z",
"messages": [
{ "to": "+14155550123", "text": "Hi Ana, your dental check-up is tomorrow at 09:30." },
{ "to": "+14155550178", "text": "Hi Sam, your dental check-up is tomorrow at 11:00." }
]
}'Scheduling
With send_at the batch and its messages wait in status scheduled and move to the queue at that minute. The cost is taken when the request is accepted, so a scheduled batch always has its funds. A scheduled batch can be canceled until it starts.
/v1/batches/{id}/cancelCancels a scheduled batch: every message becomes canceled and the full cost returns to the balance as one refund line. A batch that has started answers 409 not_cancelable.
curl -X POST https://bulksmsapi.cc/v1/batches/bat_6TQ2W8M1ZK4N/cancel \
-H "Authorization: Bearer $BULKSMSAPI_KEY"Retrieve and list batches
/v1/batches/{id}The batch with live counts per status. Poll it for progress, or use webhooks.
/v1/batchesBatches of the account, newest first, with limit (1 to 100) and starting_after (a bat_ id).
Batch fields
- idstring
bat_ followed by 12 characters.
- statusstring
scheduled, queued, sending, completed or canceled.
- sourcestring
api or console.
- encodingstring
GSM-7, or UCS-2 when any text needs Unicode.
- recipientsinteger
Numbers accepted into the batch.
- max_segmentsinteger
Largest segment count among the texts.
- coststring
Total taken from the balance, USD.
- refundedstring
Total returned so far for failed or canceled messages.
- countsobject
Messages per status: scheduled, queued, sent, delivered, failed, canceled.
- send_atstring or null
Scheduled time, if any.
- completed_atstring or null
When the last message reached a final status.
Larger lists
Split lists over 10,000 numbers into several requests, each with its own Idempotency-Key (for example the list id and the part number). Messages are handed to the network in order at up to 1,200 per minute per account, so a 50,000-number campaign is on its way in under an hour. The console takes 50,000 numbers per campaign from a paste or CSV file.