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+ or any modern browser. 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, so 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, so 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, so 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, so 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, so 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+ using 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, so the payload status is what counts
System.out.println(response.body());
}
}// .NET 8+ using 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, so 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, so 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, so 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+ or any modern browser. 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, so 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, so 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, so 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, so 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, so 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+ using 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, so the payload status is what counts
System.out.println(response.body());
}
}// .NET 8+ using 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, so 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, so 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, so 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, which works but turns 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

