Bulk & personalisation
Sending one message to many people is sms. Sending different messages needs a decision: build each one yourself with bulk, or store the variable parts on a contact list and let TextMe merge them.
Which approach?
bulk | sms + add_dynamic | |
|---|---|---|
| Where the variable data lives | Your database | TextMe's contact list |
| Message body | You compose each one | One template, merged server-side |
| Per-recipient sender | Yes — each message has its own source | No — one source for the send |
| Per-recipient short links | Yes | No |
| Batch ceiling | 2,500 message objects per call | Whatever the list holds |
| Best for | Transactional: order numbers, one-time codes, delivery windows | Marketing: a template with a name and a city in it |
The rule of thumb: if the variable value comes from a system of record you already own, use bulk. If it is a stable attribute of the contact — first name, branch, city — the contact list is less work and far less payload.
Approach 1 — bulk
One call, up to 2,500 message objects, each with its own text, sender and destinations.
<?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(())
}<?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"
}Building the batch
The mechanical part is turning your own rows into message objects. The batch-level settings — scheduling, recency filtering, campaign name — sit outside messages:
const orders = await db.ordersReadyToShip()
const payload = {
bulk: {
user: { username: 'Leeroy' },
messages: {
sms: orders.map((order) => ({
source: 'DemoAPI',
destinations: {
// The external id is what lets you reconcile this exact message later.
phone: { $: { id: `order-${order.id}` }, _: order.phone },
},
message: `Order ${order.reference} ships today. Track it: [link-t]`,
links: {
link: [{ $: { id: 't' }, _: order.trackingUrl }],
},
})),
},
// Batch-level, outside `messages`.
campaign_name: `shipping-${new Date().toISOString().slice(0, 10)}`,
temp_bl: '1',
},
}
const result = await textme(payload)from datetime import date
orders = db.orders_ready_to_ship()
payload = {
"bulk": {
"user": {"username": "Leeroy"},
"messages": {
"sms": [
{
"source": "DemoAPI",
"destinations": {
# The external id is what lets you reconcile this message.
"phone": {"$": {"id": f"order-{order.id}"}, "_": order.phone},
},
"message": f"Order {order.reference} ships today. Track it: [link-t]",
"links": {"link": [{"$": {"id": "t"}, "_": order.tracking_url}]},
}
for order in orders
]
},
# Batch-level, outside `messages`.
"campaign_name": f"shipping-{date.today().isoformat()}",
"temp_bl": "1",
}
}
result = textme(payload)<?php
$orders = $db->ordersReadyToShip();
$payload = [
'bulk' => [
'user' => ['username' => 'Leeroy'],
'messages' => [
'sms' => array_map(fn ($order) => [
'source' => 'DemoAPI',
'destinations' => [
// The external id lets you reconcile this exact message later.
'phone' => ['$' => ['id' => "order-{$order->id}"], '_' => $order->phone],
],
'message' => "Order {$order->reference} ships today. Track it: [link-t]",
'links' => [
'link' => [['$' => ['id' => 't'], '_' => $order->trackingUrl]],
],
], $orders),
],
// Batch-level, outside `messages`.
'campaign_name' => 'shipping-'.date('Y-m-d'),
'temp_bl' => '1',
],
];
$result = textme($payload);Chunking past 2,500
The ceiling counts message objects, not recipients. Split a larger run into several calls and give them all the same campaign_name — then one cancel by name stops the whole run if you need it to.
const BATCH = 2000 // under the 2,500 ceiling, with room to spare
const campaign = `shipping-${new Date().toISOString().slice(0, 10)}`
const shipments = []
for (let i = 0; i < orders.length; i += BATCH) {
const result = await textme({
bulk: {
user: { username: 'Leeroy' },
messages: { sms: orders.slice(i, i + BATCH).map(toMessage) },
campaign_name: campaign, // shared, so one cancel stops every chunk
},
})
shipments.push(result.shipment_id)
}BATCH = 2000 # under the 2,500 ceiling, with room to spare
campaign = f"shipping-{date.today().isoformat()}"
shipments = []
for start in range(0, len(orders), BATCH):
result = textme({
"bulk": {
"user": {"username": "Leeroy"},
"messages": {"sms": [to_message(o) for o in orders[start:start + BATCH]]},
"campaign_name": campaign, # shared, so one cancel stops every chunk
}
})
shipments.append(result["shipment_id"])Check the balance first
A batch that outruns your credit fails as a whole with status 4, not partway through. balance before a large run turns that into a clean pre-flight failure.
Approach 2 — dynamic fields
When the variable part is an attribute of the contact rather than of an event, store it once and template the message.
Each contact on a list carries up to six values, df1–df6, referenced in the body as [DYNAMIC_FIELD1] … [DYNAMIC_FIELD6]:
<destination>
<phone>055XXXXXXX</phone>
<df1>Israel</df1>
<df2>Israeli</df2>
<df3>Haifa</df3>
</destination>{
"destination": {
"phone": "055XXXXXXX",
"df1": "Israel",
"df2": "Israeli",
"df3": "Haifa"
}
}Then send one templated message to the list:
<?xml version="1.0" encoding="UTF-8"?>
<sms>
<user>
<username>Leeroy</username>
</user>
<source>DemoAPI</source>
<destinations>
<cl_id>21518</cl_id>
</destinations>
<message>Hello [DYNAMIC_FIELD1], your order is ready for collection in [DYNAMIC_FIELD3].</message>
<add_dynamic>1</add_dynamic>
<campaign_name>collection-reminder</campaign_name>
</sms>{
"sms": {
"user": { "username": "Leeroy" },
"source": "DemoAPI",
"destinations": { "cl_id": "21518" },
"message": "Hello [DYNAMIC_FIELD1], your order is ready for collection in [DYNAMIC_FIELD3].",
"add_dynamic": "1",
"campaign_name": "collection-reminder"
}
}add_dynamic has strict requirements
It works only with exactly one cl_id and no individual phone elements. The merge values have to come from a single list, so a send that mixes a list with loose numbers cannot personalise.
Full detail on building and maintaining lists is in Work with contact lists.
Reconciling a batch
Whichever approach you take, a batch returns one shipment_id for the whole thing. Per-recipient outcomes come from the external ids you set:
<?xml version="1.0" encoding="UTF-8"?>
<dlr>
<user>
<username>Leeroy</username>
</user>
<transactions>
<external_id>some id 1</external_id>
<external_id>some id 2</external_id>
</transactions>
<from>01/01/14 00:00</from>
<to>01/01/14 23:59</to>
</dlr>{
"dlr": {
"user": {
"username": "Leeroy"
},
"transactions": {
"external_id": [
"some id 1",
"some id 2"
]
},
"from": "01/01/14 00:00",
"to": "01/01/14 23:59"
}
}curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"dlr": {
"user": {
"username": "Leeroy"
},
"transactions": {
"external_id": [
"some id 1",
"some id 2"
]
},
"from": "01/01/14 00:00",
"to": "01/01/14 23:59"
}
}'// 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({
dlr: {
user: {
username: 'Leeroy',
},
transactions: {
external_id: [
'some id 1',
'some id 2',
],
},
from: '01/01/14 00:00',
to: '01/01/14 23:59',
},
}),
})
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' => [
'dlr' => [
'user' => [
'username' => 'Leeroy',
],
'transactions' => [
'external_id' => [
'some id 1',
'some id 2',
],
],
'from' => '01/01/14 00:00',
'to' => '01/01/14 23:59',
],
],
]);
$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', [
'dlr' => [
'user' => [
'username' => 'Leeroy',
],
'transactions' => [
'external_id' => [
'some id 1',
'some id 2',
],
],
'from' => '01/01/14 00:00',
'to' => '01/01/14 23:59',
],
])
->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={
"dlr": {
"user": {
"username": "Leeroy",
},
"transactions": {
"external_id": [
"some id 1",
"some id 2",
],
},
"from": "01/01/14 00:00",
"to": "01/01/14 23:59",
},
},
)
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{
"dlr": map[string]any{
"user": map[string]any{
"username": "Leeroy",
},
"transactions": map[string]any{
"external_id": []any{
"some id 1",
"some id 2",
},
},
"from": "01/01/14 00:00",
"to": "01/01/14 23:59",
},
})
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 TextMeDlr {
public static void main(String[] args) throws Exception {
String body = """
{
"dlr": {
"user": {
"username": "Leeroy"
},
"transactions": {
"external_id": [
"some id 1",
"some id 2"
]
},
"from": "01/01/14 00:00",
"to": "01/01/14 23:59"
}
}
""";
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 = """
{
"dlr": {
"user": {
"username": "Leeroy"
},
"transactions": {
"external_id": [
"some id 1",
"some id 2"
]
},
"from": "01/01/14 00:00",
"to": "01/01/14 23:59"
}
}
""";
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({
"dlr" => {
"user" => {
"username" => "Leeroy",
},
"transactions" => {
"external_id" => [
"some id 1",
"some id 2",
],
},
"from" => "01/01/14 00:00",
"to" => "01/01/14 23:59",
},
})
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!({
"dlr": {
"user": {
"username": "Leeroy"
},
"transactions": {
"external_id": [
"some id 1",
"some id 2"
]
},
"from": "01/01/14 00:00",
"to": "01/01/14 23:59"
}
}))
.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(())
}With ids shaped like order-10052, mapping a report row back to your own record is a string split. Without them, a batch of 2,000 messages produces 2,000 report rows you can only match on phone number — workable, but ambiguous the moment one person appears twice.
One id per destination, always
It costs nothing at send time and it is the only thing that makes a large run auditable afterwards. Make it a habit rather than a decision.
Suppressing recent recipients
temp_bl skips destinations that already heard from you in the last n days, 1 to 14. It is applied per destination at send time, so one value covers an audience assembled from several sources:
{ "temp_bl": "3" }If it filters everyone out the call fails with status 715 rather than quietly sending nothing — which is the behaviour you want, because "nothing sent" and "everything suppressed" need different responses.
Next
- Work with contact lists — building the lists that make
add_dynamicpossible - Track delivery — turning external ids into reconciled outcomes
- Campaigns & scheduling — sending a batch later, and calling it off

