Bulk send
Sends many different messages in a single call — each with its own text, its own sender and its own destinations. When every recipient gets the same body, sms is the operation you want; sending a batch of identical messages through bulk works but wastes the batch budget.
POST https://my.textme.co.il/apibulk Send a batch
The body wraps a messages element holding up to 2,500 sms objects. Each one is a miniature version of the sms payload: sender, destinations, text and optional links. Settings that apply to the batch as a whole — scheduling, recency filtering, campaign name — sit at the top level, outside messages.
Parameters
| Name | Type | Description | Required |
|---|---|---|---|
bulk | object | Root element. | ✔️ |
user | object | Contains the user element. | ✔️ |
username | string | The username of the account by which you are recognized in the system. | ✔️ |
messages | array of objects | Contains all the sms objects. The number of objects must not exceed 2500. | ✔️ |
messages.sms.source | string | Sender for this message. Maximum 11 characters, digits and English letters only, no +. Must be a verified sender. | ✔️ |
messages.sms.destinations | object | Destinations for this message. May hold multiple phone elements. | ✔️ |
messages.sms.phone | string | A destination number, formatted 5xxxxxxx or 05xxxxxxx. | ✔️ |
messages.sms.id | string | Attribute of phone. Your own external id for this destination, for later delivery reports. | ➖ |
messages.sms.message | string | The body for this message. Maximum 1005 characters. | ✔️ |
messages.sms.links | object | Links to shorten inside this message. [link-id] in the body marks where each appears. | ➖ |
messages.sms.cl_id | string | Send this message to a contact list as well. | ➖ |
timing | string | Schedule the whole batch, formatted dd/mm/yy hh:mm. Sent immediately when absent. | ➖ |
add_unsubscribe | int | Opt-out mechanism for the batch: 3 for a link, 2 for reply-to-remove. | ➖ |
temp_bl | string | Skip destinations messaged in the last n days, 1–14. | ➖ |
includes_international | int | 1 to allow non-Israeli destinations, 0 otherwise. | ➖ |
campaign_name | string | Name for the batch on system reports, and the handle for cancelling by name. Maximum 50 characters. | ➖ |
Request example
Two messages, each to its own destinations, the second carrying a short link:
<?xml version="1.0" encoding="UTF-8"?>
<bulk>
<user>
<username>Leeroy</username>
</user>
<messages>
<sms>
<source>DemoAPI</source>
<destinations>
<phone id="external id1">5xxxxxxxx</phone>
<phone id="external id2">5xxxxxxxx</phone>
<phone>5xxxxxxxx</phone>
<phone id="">5xxxxxxxx</phone>
</destinations>
<message>This is a sample message</message>
</sms>
<sms>
<source>DemoAPI</source>
<destinations>
<phone id="">5xxxxxxxx</phone>
</destinations>
<message>This is a different message sent. [link-a1]</message>
<links>
<link id="a1">https://www.example.com/path/to/resource</link>
</links>
</sms>
</messages>
<timing>10/10/17 10:10</timing>
<temp_bl>2</temp_bl>
<includes_international>1</includes_international>
<campaign_name>Sample Bulk Campaign</campaign_name>
</bulk>{
"bulk": {
"user": {
"username": "Leeroy"
},
"messages": {
"sms": [
{
"source": "DemoAPI",
"destinations": {
"phone": [
{
"$": {
"id": "external id1"
},
"_": "5xxxxxxxx"
},
{
"$": {
"id": "external id2"
},
"_": "5xxxxxxxx"
},
{
"_": "5xxxxxxxx"
},
{
"$": {
"id": ""
},
"_": "5xxxxxxxx"
}
]
},
"message": "This is a sample message"
},
{
"source": "DemoAPI",
"destinations": {
"phone": {
"$": {
"id": ""
},
"_": "5xxxxxxxx"
}
},
"message": "This is a different message sent. [link-a1]",
"links": {
"link": [
{
"$": {
"id": "a1"
},
"_": "https://www.example.com/path/to/resource"
}
]
}
}
]
},
"timing": "10/10/17 10:10",
"temp_bl": "2",
"includes_international": "1",
"campaign_name": "Sample Bulk Campaign"
}
}curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"bulk": {
"user": {
"username": "Leeroy"
},
"messages": {
"sms": [
{
"source": "DemoAPI",
"destinations": {
"phone": [
{
"$": {
"id": "external id1"
},
"_": "5xxxxxxxx"
},
{
"$": {
"id": "external id2"
},
"_": "5xxxxxxxx"
},
{
"_": "5xxxxxxxx"
},
{
"$": {
"id": ""
},
"_": "5xxxxxxxx"
}
]
},
"message": "This is a sample message"
},
{
"source": "DemoAPI",
"destinations": {
"phone": {
"$": {
"id": ""
},
"_": "5xxxxxxxx"
}
},
"message": "This is a different message sent. [link-a1]",
"links": {
"link": [
{
"$": {
"id": "a1"
},
"_": "https://www.example.com/path/to/resource"
}
]
}
}
]
},
"timing": "10/10/17 10:10",
"temp_bl": "2",
"includes_international": "1",
"campaign_name": "Sample Bulk Campaign"
}
}'// Node.js 18+ / browsers — no dependencies
const response = await fetch('https://my.textme.co.il/api', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.TEXTME_API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
bulk: {
user: {
username: 'Leeroy',
},
messages: {
sms: [
{
source: 'DemoAPI',
destinations: {
phone: [
{
$: {
id: 'external id1',
},
_: '5xxxxxxxx',
},
{
$: {
id: 'external id2',
},
_: '5xxxxxxxx',
},
{
_: '5xxxxxxxx',
},
{
$: {
id: '',
},
_: '5xxxxxxxx',
},
],
},
message: 'This is a sample message',
},
{
source: 'DemoAPI',
destinations: {
phone: {
$: {
id: '',
},
_: '5xxxxxxxx',
},
},
message: 'This is a different message sent. [link-a1]',
links: {
link: [
{
$: {
id: 'a1',
},
_: 'https://www.example.com/path/to/resource',
},
],
},
},
],
},
timing: '10/10/17 10:10',
temp_bl: '2',
includes_international: '1',
campaign_name: 'Sample Bulk Campaign',
},
}),
})
const result = await response.json()
// Errors arrive as HTTP 200 too — the payload status is what counts
if (Number(result.status) !== 0) {
throw new Error(`TextMe ${result.status}: ${result.message}`)
}
console.log(result)<?php
// composer require guzzlehttp/guzzle
$client = new \GuzzleHttp\Client([
'headers' => [
'Authorization' => 'Bearer '.getenv('TEXTME_API_TOKEN'),
'Accept' => 'application/json',
],
]);
$response = $client->post('https://my.textme.co.il/api', [
'json' => [
'bulk' => [
'user' => [
'username' => 'Leeroy',
],
'messages' => [
'sms' => [
[
'source' => 'DemoAPI',
'destinations' => [
'phone' => [
[
'$' => [
'id' => 'external id1',
],
'_' => '5xxxxxxxx',
],
[
'$' => [
'id' => 'external id2',
],
'_' => '5xxxxxxxx',
],
[
'_' => '5xxxxxxxx',
],
[
'$' => [
'id' => '',
],
'_' => '5xxxxxxxx',
],
],
],
'message' => 'This is a sample message',
],
[
'source' => 'DemoAPI',
'destinations' => [
'phone' => [
'$' => [
'id' => '',
],
'_' => '5xxxxxxxx',
],
],
'message' => 'This is a different message sent. [link-a1]',
'links' => [
'link' => [
[
'$' => [
'id' => 'a1',
],
'_' => 'https://www.example.com/path/to/resource',
],
],
],
],
],
],
'timing' => '10/10/17 10:10',
'temp_bl' => '2',
'includes_international' => '1',
'campaign_name' => 'Sample Bulk Campaign',
],
],
]);
$result = json_decode($response->getBody()->getContents(), true);
// Errors arrive as HTTP 200 too — the payload status is what counts
if ((int) $result['status'] !== 0) {
throw new RuntimeException("TextMe {$result['status']}: {$result['message']}");
}
print_r($result);<?php
use Illuminate\Support\Facades\Http;
$result = Http::withToken(config('services.textme.token'))
->acceptJson()
->post('https://my.textme.co.il/api', [
'bulk' => [
'user' => [
'username' => 'Leeroy',
],
'messages' => [
'sms' => [
[
'source' => 'DemoAPI',
'destinations' => [
'phone' => [
[
'$' => [
'id' => 'external id1',
],
'_' => '5xxxxxxxx',
],
[
'$' => [
'id' => 'external id2',
],
'_' => '5xxxxxxxx',
],
[
'_' => '5xxxxxxxx',
],
[
'$' => [
'id' => '',
],
'_' => '5xxxxxxxx',
],
],
],
'message' => 'This is a sample message',
],
[
'source' => 'DemoAPI',
'destinations' => [
'phone' => [
'$' => [
'id' => '',
],
'_' => '5xxxxxxxx',
],
],
'message' => 'This is a different message sent. [link-a1]',
'links' => [
'link' => [
[
'$' => [
'id' => 'a1',
],
'_' => 'https://www.example.com/path/to/resource',
],
],
],
],
],
],
'timing' => '10/10/17 10:10',
'temp_bl' => '2',
'includes_international' => '1',
'campaign_name' => 'Sample Bulk Campaign',
],
])
->throw()
->json();
// Errors arrive as HTTP 200 too — the payload status is what counts
throw_if((int) $result['status'] !== 0, RuntimeException::class,
"TextMe {$result['status']}: {$result['message']}");
logger()->info('TextMe', $result);# pip install httpx
import os
import httpx
response = httpx.post(
"https://my.textme.co.il/api",
headers={"Authorization": f"Bearer {os.environ['TEXTME_API_TOKEN']}"},
json={
"bulk": {
"user": {
"username": "Leeroy",
},
"messages": {
"sms": [
{
"source": "DemoAPI",
"destinations": {
"phone": [
{
"$": {
"id": "external id1",
},
"_": "5xxxxxxxx",
},
{
"$": {
"id": "external id2",
},
"_": "5xxxxxxxx",
},
{
"_": "5xxxxxxxx",
},
{
"$": {
"id": "",
},
"_": "5xxxxxxxx",
},
],
},
"message": "This is a sample message",
},
{
"source": "DemoAPI",
"destinations": {
"phone": {
"$": {
"id": "",
},
"_": "5xxxxxxxx",
},
},
"message": "This is a different message sent. [link-a1]",
"links": {
"link": [
{
"$": {
"id": "a1",
},
"_": "https://www.example.com/path/to/resource",
},
],
},
},
],
},
"timing": "10/10/17 10:10",
"temp_bl": "2",
"includes_international": "1",
"campaign_name": "Sample Bulk Campaign",
},
},
)
response.raise_for_status()
result = response.json()
# Errors arrive as HTTP 200 too — the payload status is what counts
if int(result["status"]) != 0:
raise RuntimeError(f"TextMe {result['status']}: {result['message']}")
print(result)package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
payload, _ := json.Marshal(map[string]any{
"bulk": map[string]any{
"user": map[string]any{
"username": "Leeroy",
},
"messages": map[string]any{
"sms": []any{
map[string]any{
"source": "DemoAPI",
"destinations": map[string]any{
"phone": []any{
map[string]any{
"$": map[string]any{
"id": "external id1",
},
"_": "5xxxxxxxx",
},
map[string]any{
"$": map[string]any{
"id": "external id2",
},
"_": "5xxxxxxxx",
},
map[string]any{
"_": "5xxxxxxxx",
},
map[string]any{
"$": map[string]any{
"id": "",
},
"_": "5xxxxxxxx",
},
},
},
"message": "This is a sample message",
},
map[string]any{
"source": "DemoAPI",
"destinations": map[string]any{
"phone": map[string]any{
"$": map[string]any{
"id": "",
},
"_": "5xxxxxxxx",
},
},
"message": "This is a different message sent. [link-a1]",
"links": map[string]any{
"link": []any{
map[string]any{
"$": map[string]any{
"id": "a1",
},
"_": "https://www.example.com/path/to/resource",
},
},
},
},
},
},
"timing": "10/10/17 10:10",
"temp_bl": "2",
"includes_international": "1",
"campaign_name": "Sample Bulk Campaign",
},
})
req, _ := http.NewRequest("POST", "https://my.textme.co.il/api", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+os.Getenv("TEXTME_API_TOKEN"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result struct {
Status json.Number `json:"status"`
Message string `json:"message"`
}
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
panic(err)
}
// Errors arrive as HTTP 200 too — the payload status is what counts
if result.Status.String() != "0" {
panic(fmt.Sprintf("TextMe %s: %s", result.Status, result.Message))
}
fmt.Println(result.Message)
}// Java 17+ — java.net.http, no dependencies (parse with Jackson/Gson)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class TextMeSendBulk {
public static void main(String[] args) throws Exception {
String body = """
{
"bulk": {
"user": {
"username": "Leeroy"
},
"messages": {
"sms": [
{
"source": "DemoAPI",
"destinations": {
"phone": [
{
"$": {
"id": "external id1"
},
"_": "5xxxxxxxx"
},
{
"$": {
"id": "external id2"
},
"_": "5xxxxxxxx"
},
{
"_": "5xxxxxxxx"
},
{
"$": {
"id": ""
},
"_": "5xxxxxxxx"
}
]
},
"message": "This is a sample message"
},
{
"source": "DemoAPI",
"destinations": {
"phone": {
"$": {
"id": ""
},
"_": "5xxxxxxxx"
}
},
"message": "This is a different message sent. [link-a1]",
"links": {
"link": [
{
"$": {
"id": "a1"
},
"_": "https://www.example.com/path/to/resource"
}
]
}
}
]
},
"timing": "10/10/17 10:10",
"temp_bl": "2",
"includes_international": "1",
"campaign_name": "Sample Bulk Campaign"
}
}
""";
HttpRequest request = HttpRequest.newBuilder(URI.create("https://my.textme.co.il/api"))
.header("Authorization", "Bearer " + System.getenv("TEXTME_API_TOKEN"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
// Errors arrive as HTTP 200 too — the payload status is what counts
System.out.println(response.body());
}
}// .NET 8+ — System.Net.Http
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var payload = """
{
"bulk": {
"user": {
"username": "Leeroy"
},
"messages": {
"sms": [
{
"source": "DemoAPI",
"destinations": {
"phone": [
{
"$": {
"id": "external id1"
},
"_": "5xxxxxxxx"
},
{
"$": {
"id": "external id2"
},
"_": "5xxxxxxxx"
},
{
"_": "5xxxxxxxx"
},
{
"$": {
"id": ""
},
"_": "5xxxxxxxx"
}
]
},
"message": "This is a sample message"
},
{
"source": "DemoAPI",
"destinations": {
"phone": {
"$": {
"id": ""
},
"_": "5xxxxxxxx"
}
},
"message": "This is a different message sent. [link-a1]",
"links": {
"link": [
{
"$": {
"id": "a1"
},
"_": "https://www.example.com/path/to/resource"
}
]
}
}
]
},
"timing": "10/10/17 10:10",
"temp_bl": "2",
"includes_international": "1",
"campaign_name": "Sample Bulk Campaign"
}
}
""";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("TEXTME_API_TOKEN"));
var response = await http.PostAsync("https://my.textme.co.il/api",
new StringContent(payload, Encoding.UTF8, "application/json"));
var result = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement;
var status = result.GetProperty("status").ToString();
// Errors arrive as HTTP 200 too — the payload status is what counts
if (status != "0")
{
var message = result.GetProperty("message").ToString();
throw new Exception($"TextMe {status}: {message}");
}
Console.WriteLine(result);require "net/http"
require "json"
uri = URI("https://my.textme.co.il/api")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('TEXTME_API_TOKEN')}"
request["Content-Type"] = "application/json"
request.body = JSON.dump({
"bulk" => {
"user" => {
"username" => "Leeroy",
},
"messages" => {
"sms" => [
{
"source" => "DemoAPI",
"destinations" => {
"phone" => [
{
"$" => {
"id" => "external id1",
},
"_" => "5xxxxxxxx",
},
{
"$" => {
"id" => "external id2",
},
"_" => "5xxxxxxxx",
},
{
"_" => "5xxxxxxxx",
},
{
"$" => {
"id" => "",
},
"_" => "5xxxxxxxx",
},
],
},
"message" => "This is a sample message",
},
{
"source" => "DemoAPI",
"destinations" => {
"phone" => {
"$" => {
"id" => "",
},
"_" => "5xxxxxxxx",
},
},
"message" => "This is a different message sent. [link-a1]",
"links" => {
"link" => [
{
"$" => {
"id" => "a1",
},
"_" => "https://www.example.com/path/to/resource",
},
],
},
},
],
},
"timing" => "10/10/17 10:10",
"temp_bl" => "2",
"includes_international" => "1",
"campaign_name" => "Sample Bulk Campaign",
},
})
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
result = JSON.parse(response.body)
# Errors arrive as HTTP 200 too — the payload status is what counts
raise "TextMe #{result['status']}: #{result['message']}" unless result["status"].to_i.zero?
pp result// [dependencies]
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
use serde_json::{json, Value};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let result: Value = reqwest::Client::new()
.post("https://my.textme.co.il/api")
.bearer_auth(std::env::var("TEXTME_API_TOKEN")?)
.json(&json!({
"bulk": {
"user": {
"username": "Leeroy"
},
"messages": {
"sms": [
{
"source": "DemoAPI",
"destinations": {
"phone": [
{
"$": {
"id": "external id1"
},
"_": "5xxxxxxxx"
},
{
"$": {
"id": "external id2"
},
"_": "5xxxxxxxx"
},
{
"_": "5xxxxxxxx"
},
{
"$": {
"id": ""
},
"_": "5xxxxxxxx"
}
]
},
"message": "This is a sample message"
},
{
"source": "DemoAPI",
"destinations": {
"phone": {
"$": {
"id": ""
},
"_": "5xxxxxxxx"
}
},
"message": "This is a different message sent. [link-a1]",
"links": {
"link": [
{
"$": {
"id": "a1"
},
"_": "https://www.example.com/path/to/resource"
}
]
}
}
]
},
"timing": "10/10/17 10:10",
"temp_bl": "2",
"includes_international": "1",
"campaign_name": "Sample Bulk Campaign"
}
}))
.send()
.await?
.json()
.await?;
// Errors arrive as HTTP 200 too — the payload status is what counts
if result["status"] != 0 {
return Err(format!("TextMe {}: {}", result["status"], result["message"]).into());
}
println!("{result}");
Ok(())
}Response
<?xml version="1.0" encoding="UTF-8"?>
<sms>
<status>0</status>
<message>SMS bulk will be sent</message>
<shipment_id>xxxxxxx</shipment_id>
</sms>{
"status": 0,
"message": "SMS bulk will be sent",
"shipment_id": "XXXXXXX"
}| Field | Type | Description |
|---|---|---|
status | int | 0 — the batch was accepted. |
message | string | SMS bulk will be sent. |
shipment_id | string | Identifies the campaign created for the whole batch. |
One id for the whole batch
shipment_id names the batch, not the individual messages inside it. To reconcile per destination, set an id attribute on every phone and pull delivery reports by external id.
Errors
| Status | When |
|---|---|
2 | A required element is missing; message names it. |
4 | Not enough credit for the batch. |
5 | Outside the account's permitted sending hours. |
8 | Every destination in the batch is blocklisted. |
9 | A destination is too short or too long. |
515 | A source is not a verified sender. |
714 | temp_bl is outside 1–14. |
715 | Every destination was filtered out by temp_bl. |
989 | A message body is empty or over 1005 characters, or campaign_name is over 50. |
Field notes
Batch limits
The hard ceiling is 2,500 sms objects per call — objects, not recipients. A single object addressing 400 numbers counts as one. Splitting a large run into several calls is fine; each returns its own shipment_id, and giving them all the same campaign_name lets you cancel the whole run with one request.
Per-message vs batch-level settings
This split trips people up:
| Setting | Where it lives |
|---|---|
source, message, destinations, links | Inside each sms object |
timing, temp_bl, includes_international, campaign_name, add_unsubscribe | On the bulk root |
You cannot schedule individual messages within a batch at different times. Different send times mean different calls.
The SOAP interface behaves differently here
sendBulkSms over SOAP ignores timing entirely. The bulk operation documented on this page honours it at the batch level.
Personalisation
bulk personalises by construction: you build the exact text for each recipient. That is the right tool when the variable part comes from your own database — an order number, a delivery window, a one-time code.
When the variable part already lives in TextMe, sms with add_dynamic is less work: store the merge values as dynamic fields on a contact list and send one templated message. See Bulk & personalisation for how to choose.

