Work with contact lists
A contact list is an audience stored on the TextMe side. Each member carries up to six dynamic fields — a first name, a city, a branch — which a single templated message can merge in per recipient.
This walkthrough builds a list, personalises a send from it, keeps it up to date, and covers the parts of the model that surprise people.
Step 1 — Create the list
newCL creates one or more lists and can populate them in the same call. Dynamic fields go on each contact as df1 through df6:
<?xml version="1.0" encoding="UTF-8"?>
<newCL>
<user>
<username>xxxxxx</username>
</user>
<cl>
<name>name1</name>
<destinations>
<destination>
<phone>055XXXXXXX</phone>
<df1>Israel</df1>
<df2>Israeli</df2>
<df3>Haifa</df3>
</destination>
<destination>
<phone>55XXXXXXX</phone>
</destination>
</destinations>
</cl>
<cl>
<name>name2</name>
<destinations>
<destination>
<phone>055XXXXXXX</phone>
</destination>
</destinations>
</cl>
</newCL>{
"newCL": {
"user": {
"username": "xxxxxx"
},
"cl": [
{
"name": "name1",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX",
"df1": "Israel",
"df2": "Israeli",
"df3": "Haifa"
},
{
"phone": "55XXXXXXX"
}
]
}
},
{
"name": "name2",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX"
}
]
}
}
]
}
}curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"newCL": {
"user": {
"username": "xxxxxx"
},
"cl": [
{
"name": "name1",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX",
"df1": "Israel",
"df2": "Israeli",
"df3": "Haifa"
},
{
"phone": "55XXXXXXX"
}
]
}
},
{
"name": "name2",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX"
}
]
}
}
]
}
}'// 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({
newCL: {
user: {
username: 'xxxxxx',
},
cl: [
{
name: 'name1',
destinations: {
destination: [
{
phone: '055XXXXXXX',
df1: 'Israel',
df2: 'Israeli',
df3: 'Haifa',
},
{
phone: '55XXXXXXX',
},
],
},
},
{
name: 'name2',
destinations: {
destination: [
{
phone: '055XXXXXXX',
},
],
},
},
],
},
}),
})
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' => [
'newCL' => [
'user' => [
'username' => 'xxxxxx',
],
'cl' => [
[
'name' => 'name1',
'destinations' => [
'destination' => [
[
'phone' => '055XXXXXXX',
'df1' => 'Israel',
'df2' => 'Israeli',
'df3' => 'Haifa',
],
[
'phone' => '55XXXXXXX',
],
],
],
],
[
'name' => 'name2',
'destinations' => [
'destination' => [
[
'phone' => '055XXXXXXX',
],
],
],
],
],
],
],
]);
$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', [
'newCL' => [
'user' => [
'username' => 'xxxxxx',
],
'cl' => [
[
'name' => 'name1',
'destinations' => [
'destination' => [
[
'phone' => '055XXXXXXX',
'df1' => 'Israel',
'df2' => 'Israeli',
'df3' => 'Haifa',
],
[
'phone' => '55XXXXXXX',
],
],
],
],
[
'name' => 'name2',
'destinations' => [
'destination' => [
[
'phone' => '055XXXXXXX',
],
],
],
],
],
],
])
->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={
"newCL": {
"user": {
"username": "xxxxxx",
},
"cl": [
{
"name": "name1",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX",
"df1": "Israel",
"df2": "Israeli",
"df3": "Haifa",
},
{
"phone": "55XXXXXXX",
},
],
},
},
{
"name": "name2",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX",
},
],
},
},
],
},
},
)
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{
"newCL": map[string]any{
"user": map[string]any{
"username": "xxxxxx",
},
"cl": []any{
map[string]any{
"name": "name1",
"destinations": map[string]any{
"destination": []any{
map[string]any{
"phone": "055XXXXXXX",
"df1": "Israel",
"df2": "Israeli",
"df3": "Haifa",
},
map[string]any{
"phone": "55XXXXXXX",
},
},
},
},
map[string]any{
"name": "name2",
"destinations": map[string]any{
"destination": []any{
map[string]any{
"phone": "055XXXXXXX",
},
},
},
},
},
},
})
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 TextMeClCreate {
public static void main(String[] args) throws Exception {
String body = """
{
"newCL": {
"user": {
"username": "xxxxxx"
},
"cl": [
{
"name": "name1",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX",
"df1": "Israel",
"df2": "Israeli",
"df3": "Haifa"
},
{
"phone": "55XXXXXXX"
}
]
}
},
{
"name": "name2",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX"
}
]
}
}
]
}
}
""";
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 = """
{
"newCL": {
"user": {
"username": "xxxxxx"
},
"cl": [
{
"name": "name1",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX",
"df1": "Israel",
"df2": "Israeli",
"df3": "Haifa"
},
{
"phone": "55XXXXXXX"
}
]
}
},
{
"name": "name2",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX"
}
]
}
}
]
}
}
""";
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({
"newCL" => {
"user" => {
"username" => "xxxxxx",
},
"cl" => [
{
"name" => "name1",
"destinations" => {
"destination" => [
{
"phone" => "055XXXXXXX",
"df1" => "Israel",
"df2" => "Israeli",
"df3" => "Haifa",
},
{
"phone" => "55XXXXXXX",
},
],
},
},
{
"name" => "name2",
"destinations" => {
"destination" => [
{
"phone" => "055XXXXXXX",
},
],
},
},
],
},
})
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!({
"newCL": {
"user": {
"username": "xxxxxx"
},
"cl": [
{
"name": "name1",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX",
"df1": "Israel",
"df2": "Israeli",
"df3": "Haifa"
},
{
"phone": "55XXXXXXX"
}
]
}
},
{
"name": "name2",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX"
}
]
}
}
]
}
}))
.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"?>
<newCL>
<status>0</status>
<message>conact list successfully created</message>
<errors>
<error>The phone is too long or too short or contain characters and therefore not added</error>
</errors>
<identifiers>
<identifier>17419</identifier>
</identifiers>
</newCL>{
"status": 0,
"message": "conact list successfully created",
"errors": [
"The phone is too long or too short or contain characters and therefore not added"
],
"identifiers": "17419"
}Two things to read in that response
identifiers holds the new list's id — the cl_id you send to. It appears nowhere else; capture it now or go hunting with getCL later.
errors holds the rows that were rejected — a malformed number, usually. The call still returns status: 0, because the list was created. Treating 0 as "all contacts stored" will silently lose recipients.
const result = await textme({
newCL: {
user: { username: 'Leeroy' },
cl: [{
name: 'august-collection',
destinations: {
destination: customers.map((c) => ({
phone: c.phone,
df1: c.firstName,
df2: c.lastName,
df3: c.city,
})),
},
}],
},
})
// `identifiers` is the only place the new id appears — store it.
const listId = Array.isArray(result.identifiers)
? result.identifiers[0]
: result.identifiers
// status 0 does not mean every row landed.
for (const rejected of result.errors ?? []) {
console.warn('contact rejected:', rejected)
}result = textme({
"newCL": {
"user": {"username": "Leeroy"},
"cl": [{
"name": "august-collection",
"destinations": {
"destination": [
{
"phone": c.phone,
"df1": c.first_name,
"df2": c.last_name,
"df3": c.city,
}
for c in customers
]
},
}],
}
})
# `identifiers` is the only place the new id appears — store it.
identifiers = result["identifiers"]
list_id = identifiers[0] if isinstance(identifiers, list) else identifiers
# status 0 does not mean every row landed.
for rejected in result.get("errors", []):
print("contact rejected:", rejected)<?php
$result = textme([
'newCL' => [
'user' => ['username' => 'Leeroy'],
'cl' => [[
'name' => 'august-collection',
'destinations' => [
'destination' => array_map(fn ($c) => [
'phone' => $c->phone,
'df1' => $c->firstName,
'df2' => $c->lastName,
'df3' => $c->city,
], $customers),
],
]],
],
]);
// `identifiers` is the only place the new id appears — store it.
$listId = is_array($result['identifiers'])
? $result['identifiers'][0]
: $result['identifiers'];
// status 0 does not mean every row landed.
foreach ($result['errors'] ?? [] as $rejected) {
error_log("contact rejected: {$rejected}");
}Step 2 — Send to the list
A cl_id is a destination like any other. This sends the same body to everyone on list 21518:
<destinations>
<cl_id>21518</cl_id>
</destinations>{
"destinations": { "cl_id": "21518" }
}Lists and individual numbers can be mixed freely in one send — two lists and a handful of extra numbers is a valid destinations block.
Step 3 — Personalise with dynamic fields
Reference the stored values positionally in the message body and set add_dynamic to 1:
<?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] [DYNAMIC_FIELD2], your order is ready for collection in [DYNAMIC_FIELD3].</message>
<add_dynamic>1</add_dynamic>
<campaign_name>august-collection</campaign_name>
</sms>{
"sms": {
"user": { "username": "Leeroy" },
"source": "DemoAPI",
"destinations": { "cl_id": "21518" },
"message": "Hello [DYNAMIC_FIELD1] [DYNAMIC_FIELD2], your order is ready for collection in [DYNAMIC_FIELD3].",
"add_dynamic": "1",
"campaign_name": "august-collection"
}
}With df1 = Israel, df2 = Israeli, df3 = Haifa, that contact receives:
Hello Israel Israeli, your order is ready for collection in Haifa.
add_dynamic has strict requirements
- Exactly one
cl_id. Two lists cannot be merged in one personalised send. - No individual
phoneelements. Loose numbers have no dynamic fields, so mixing them is rejected.
Any other value than 1 is read as "off", and the placeholders go out as literal text — which is the failure mode to watch for. Send to yourself once before sending to the list.
Length and empty fields
The merged value counts against the 1005-character limit, and the length varies per recipient. A template that fits comfortably for Dan may not for a long name — leave headroom.
A contact with no df1 gets an empty substitution, so Hello [DYNAMIC_FIELD1], becomes Hello ,. Store a sensible fallback at list-build time rather than hoping; there is no default-value syntax.
Step 4 — Keep the list current
Adding contacts
<?xml version="1.0" encoding="UTF-8"?>
<addNumCL>
<user>
<username>xxxxxx</username>
</user>
<cl>
<id>21518</id>
<destinations>
<destination>
<phone>055XXXXXXX</phone>
<df1>Israel</df1>
<df2>Israeli</df2>
<df3>Haifa</df3>
</destination>
<destination>
<phone>55XXXXXXX</phone>
</destination>
</destinations>
</cl>
<cl>
<id>21500</id>
<destinations>
<destination>
<phone>055XXXXXXX</phone>
</destination>
</destinations>
</cl>
</addNumCL>{
"addNumCL": {
"user": {
"username": "xxxxxx"
},
"cl": [
{
"id": "21518",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX",
"df1": "Israel",
"df2": "Israeli",
"df3": "Haifa"
},
{
"phone": "55XXXXXXX"
}
]
}
},
{
"id": "21500",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX"
}
]
}
}
]
}
}curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"addNumCL": {
"user": {
"username": "xxxxxx"
},
"cl": [
{
"id": "21518",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX",
"df1": "Israel",
"df2": "Israeli",
"df3": "Haifa"
},
{
"phone": "55XXXXXXX"
}
]
}
},
{
"id": "21500",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX"
}
]
}
}
]
}
}'// 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({
addNumCL: {
user: {
username: 'xxxxxx',
},
cl: [
{
id: '21518',
destinations: {
destination: [
{
phone: '055XXXXXXX',
df1: 'Israel',
df2: 'Israeli',
df3: 'Haifa',
},
{
phone: '55XXXXXXX',
},
],
},
},
{
id: '21500',
destinations: {
destination: [
{
phone: '055XXXXXXX',
},
],
},
},
],
},
}),
})
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' => [
'addNumCL' => [
'user' => [
'username' => 'xxxxxx',
],
'cl' => [
[
'id' => '21518',
'destinations' => [
'destination' => [
[
'phone' => '055XXXXXXX',
'df1' => 'Israel',
'df2' => 'Israeli',
'df3' => 'Haifa',
],
[
'phone' => '55XXXXXXX',
],
],
],
],
[
'id' => '21500',
'destinations' => [
'destination' => [
[
'phone' => '055XXXXXXX',
],
],
],
],
],
],
],
]);
$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', [
'addNumCL' => [
'user' => [
'username' => 'xxxxxx',
],
'cl' => [
[
'id' => '21518',
'destinations' => [
'destination' => [
[
'phone' => '055XXXXXXX',
'df1' => 'Israel',
'df2' => 'Israeli',
'df3' => 'Haifa',
],
[
'phone' => '55XXXXXXX',
],
],
],
],
[
'id' => '21500',
'destinations' => [
'destination' => [
[
'phone' => '055XXXXXXX',
],
],
],
],
],
],
])
->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={
"addNumCL": {
"user": {
"username": "xxxxxx",
},
"cl": [
{
"id": "21518",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX",
"df1": "Israel",
"df2": "Israeli",
"df3": "Haifa",
},
{
"phone": "55XXXXXXX",
},
],
},
},
{
"id": "21500",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX",
},
],
},
},
],
},
},
)
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{
"addNumCL": map[string]any{
"user": map[string]any{
"username": "xxxxxx",
},
"cl": []any{
map[string]any{
"id": "21518",
"destinations": map[string]any{
"destination": []any{
map[string]any{
"phone": "055XXXXXXX",
"df1": "Israel",
"df2": "Israeli",
"df3": "Haifa",
},
map[string]any{
"phone": "55XXXXXXX",
},
},
},
},
map[string]any{
"id": "21500",
"destinations": map[string]any{
"destination": []any{
map[string]any{
"phone": "055XXXXXXX",
},
},
},
},
},
},
})
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 TextMeClAddNumbers {
public static void main(String[] args) throws Exception {
String body = """
{
"addNumCL": {
"user": {
"username": "xxxxxx"
},
"cl": [
{
"id": "21518",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX",
"df1": "Israel",
"df2": "Israeli",
"df3": "Haifa"
},
{
"phone": "55XXXXXXX"
}
]
}
},
{
"id": "21500",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX"
}
]
}
}
]
}
}
""";
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 = """
{
"addNumCL": {
"user": {
"username": "xxxxxx"
},
"cl": [
{
"id": "21518",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX",
"df1": "Israel",
"df2": "Israeli",
"df3": "Haifa"
},
{
"phone": "55XXXXXXX"
}
]
}
},
{
"id": "21500",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX"
}
]
}
}
]
}
}
""";
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({
"addNumCL" => {
"user" => {
"username" => "xxxxxx",
},
"cl" => [
{
"id" => "21518",
"destinations" => {
"destination" => [
{
"phone" => "055XXXXXXX",
"df1" => "Israel",
"df2" => "Israeli",
"df3" => "Haifa",
},
{
"phone" => "55XXXXXXX",
},
],
},
},
{
"id" => "21500",
"destinations" => {
"destination" => [
{
"phone" => "055XXXXXXX",
},
],
},
},
],
},
})
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!({
"addNumCL": {
"user": {
"username": "xxxxxx"
},
"cl": [
{
"id": "21518",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX",
"df1": "Israel",
"df2": "Israeli",
"df3": "Haifa"
},
{
"phone": "55XXXXXXX"
}
]
}
},
{
"id": "21500",
"destinations": {
"destination": [
{
"phone": "055XXXXXXX"
}
]
}
}
]
}
}))
.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(())
}Numbers already on the list come back in errors and are skipped, so re-adding is safe.
Removing contacts
Note the shape difference — rmNumCL puts phone elements directly inside destinations, with no destination wrapper:
<?xml version="1.0" encoding="UTF-8"?>
<rmNumCL>
<user>
<username>xxxxxx</username>
</user>
<cl>
<id>21518</id>
<destinations>
<phone>055XXXXXXX</phone>
</destinations>
</cl>
<cl>
<id>21500</id>
<destinations>
<phone>055XXXXXXX</phone>
<phone>55XXXXXXX</phone>
</destinations>
</cl>
</rmNumCL>{
"rmNumCL": {
"user": {
"username": "xxxxxx"
},
"cl": [
{
"id": "21518",
"destinations": {
"phone": "055XXXXXXX"
}
},
{
"id": "21500",
"destinations": {
"phone": [
"055XXXXXXX",
"55XXXXXXX"
]
}
}
]
}
}curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"rmNumCL": {
"user": {
"username": "xxxxxx"
},
"cl": [
{
"id": "21518",
"destinations": {
"phone": "055XXXXXXX"
}
},
{
"id": "21500",
"destinations": {
"phone": [
"055XXXXXXX",
"55XXXXXXX"
]
}
}
]
}
}'// 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({
rmNumCL: {
user: {
username: 'xxxxxx',
},
cl: [
{
id: '21518',
destinations: {
phone: '055XXXXXXX',
},
},
{
id: '21500',
destinations: {
phone: [
'055XXXXXXX',
'55XXXXXXX',
],
},
},
],
},
}),
})
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' => [
'rmNumCL' => [
'user' => [
'username' => 'xxxxxx',
],
'cl' => [
[
'id' => '21518',
'destinations' => [
'phone' => '055XXXXXXX',
],
],
[
'id' => '21500',
'destinations' => [
'phone' => [
'055XXXXXXX',
'55XXXXXXX',
],
],
],
],
],
],
]);
$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', [
'rmNumCL' => [
'user' => [
'username' => 'xxxxxx',
],
'cl' => [
[
'id' => '21518',
'destinations' => [
'phone' => '055XXXXXXX',
],
],
[
'id' => '21500',
'destinations' => [
'phone' => [
'055XXXXXXX',
'55XXXXXXX',
],
],
],
],
],
])
->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={
"rmNumCL": {
"user": {
"username": "xxxxxx",
},
"cl": [
{
"id": "21518",
"destinations": {
"phone": "055XXXXXXX",
},
},
{
"id": "21500",
"destinations": {
"phone": [
"055XXXXXXX",
"55XXXXXXX",
],
},
},
],
},
},
)
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{
"rmNumCL": map[string]any{
"user": map[string]any{
"username": "xxxxxx",
},
"cl": []any{
map[string]any{
"id": "21518",
"destinations": map[string]any{
"phone": "055XXXXXXX",
},
},
map[string]any{
"id": "21500",
"destinations": map[string]any{
"phone": []any{
"055XXXXXXX",
"55XXXXXXX",
},
},
},
},
},
})
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 TextMeClRemoveNumbers {
public static void main(String[] args) throws Exception {
String body = """
{
"rmNumCL": {
"user": {
"username": "xxxxxx"
},
"cl": [
{
"id": "21518",
"destinations": {
"phone": "055XXXXXXX"
}
},
{
"id": "21500",
"destinations": {
"phone": [
"055XXXXXXX",
"55XXXXXXX"
]
}
}
]
}
}
""";
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 = """
{
"rmNumCL": {
"user": {
"username": "xxxxxx"
},
"cl": [
{
"id": "21518",
"destinations": {
"phone": "055XXXXXXX"
}
},
{
"id": "21500",
"destinations": {
"phone": [
"055XXXXXXX",
"55XXXXXXX"
]
}
}
]
}
}
""";
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({
"rmNumCL" => {
"user" => {
"username" => "xxxxxx",
},
"cl" => [
{
"id" => "21518",
"destinations" => {
"phone" => "055XXXXXXX",
},
},
{
"id" => "21500",
"destinations" => {
"phone" => [
"055XXXXXXX",
"55XXXXXXX",
],
},
},
],
},
})
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!({
"rmNumCL": {
"user": {
"username": "xxxxxx"
},
"cl": [
{
"id": "21518",
"destinations": {
"phone": "055XXXXXXX"
}
},
{
"id": "21500",
"destinations": {
"phone": [
"055XXXXXXX",
"55XXXXXXX"
]
}
}
]
}
}))
.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(())
}Changing a contact's dynamic fields
There is no update operation. Remove the number and add it back with the new values:
// There is no update: adding an existing number is rejected, not merged.
await textme({
rmNumCL: {
user: { username: 'Leeroy' },
cl: [{ id: listId, destinations: { phone: contact.phone } }],
},
})
await textme({
addNumCL: {
user: { username: 'Leeroy' },
cl: [{
id: listId,
destinations: {
destination: [{ phone: contact.phone, df1: contact.firstName, df3: contact.city }],
},
}],
},
})# There is no update: adding an existing number is rejected, not merged.
textme({
"rmNumCL": {
"user": {"username": "Leeroy"},
"cl": [{"id": list_id, "destinations": {"phone": contact.phone}}],
}
})
textme({
"addNumCL": {
"user": {"username": "Leeroy"},
"cl": [{
"id": list_id,
"destinations": {
"destination": [
{"phone": contact.phone, "df1": contact.first_name, "df3": contact.city}
]
},
}],
}
})There is a gap between the two calls where the contact is not on the list. For a list used by scheduled campaigns, do the churn well before the send window.
Step 5 — Read lists back
getCL returns every list on the account, but the shape is worth knowing before you call it: one row per contact, not per list, with cl_id and name repeated on every row.
<?xml version="1.0" encoding="UTF-8"?>
<getCL>
<user>
<username>xxxxxx</username>
</user>
</getCL>{
"getCL": {
"user": {
"username": "xxxxxx"
}
}
}curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"getCL": {
"user": {
"username": "xxxxxx"
}
}
}'// 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({
getCL: {
user: {
username: 'xxxxxx',
},
},
}),
})
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' => [
'getCL' => [
'user' => [
'username' => 'xxxxxx',
],
],
],
]);
$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', [
'getCL' => [
'user' => [
'username' => 'xxxxxx',
],
],
])
->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={
"getCL": {
"user": {
"username": "xxxxxx",
},
},
},
)
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{
"getCL": map[string]any{
"user": map[string]any{
"username": "xxxxxx",
},
},
})
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 TextMeClGetAll {
public static void main(String[] args) throws Exception {
String body = """
{
"getCL": {
"user": {
"username": "xxxxxx"
}
}
}
""";
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 = """
{
"getCL": {
"user": {
"username": "xxxxxx"
}
}
}
""";
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({
"getCL" => {
"user" => {
"username" => "xxxxxx",
},
},
})
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!({
"getCL": {
"user": {
"username": "xxxxxx"
}
}
}))
.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"?>
<getCL>
<status>0</status>
<message></message>
<contact_lists>
<contact_list>
<cl_id>21518</cl_id>
<phone>55XXXXXXX</phone>
<name>name1</name>
</contact_list>
<contact_list>
<cl_id>21500</cl_id>
<phone>055XXXXXXX</phone>
<name>name2</name>
</contact_list>
</contact_lists>
</getCL>{
"status": 0,
"message": "",
"contact_lists": [
{
"cl_id": "21518",
"phone": "55XXXXXXX",
"name": "name1"
},
{
"cl_id": "21500",
"phone": "555XXXXXX",
"name": "name2"
}
]
}Group by cl_id to reconstruct lists:
const { contact_lists: rows } = await textme({
getCL: { user: { username: 'Leeroy' } },
})
// One row per contact — fold them back into lists.
const lists = new Map()
for (const row of rows) {
if (!lists.has(row.cl_id)) {
lists.set(row.cl_id, { id: row.cl_id, name: row.name, phones: [] })
}
lists.get(row.cl_id).phones.push(row.phone)
}from collections import defaultdict
rows = textme({"getCL": {"user": {"username": "Leeroy"}}})["contact_lists"]
# One row per contact — fold them back into lists.
lists = defaultdict(lambda: {"name": None, "phones": []})
for row in rows:
entry = lists[row["cl_id"]]
entry["name"] = row["name"]
entry["phones"].append(row["phone"])This response grows with contacts, not lists
An account with 50,000 contacts returns 50,000 rows, whichever question you were asking. Cache the cl_id from newCL at creation time and you will rarely need getCL at all; when you only want one list, getCLbyID is far cheaper.
Things that surprise people
Dynamic fields belong to the membership, not the contact. The same number on two lists carries independent df1–df6 values. That is useful — formal on one list, familiar on another — but it means updating a value is a per-list operation.
Reading does not return dynamic fields. getCL and getCLbyID answer with cl_id, phone and name. The stored df1–df6 are not echoed back, so TextMe cannot be your system of record for them. Keep the authoritative copy on your side.
The blocklist still applies. Sending to a list does not bypass opt-outs; blocked members are dropped at send time. If every member is blocked the send fails with status 8.
Deleting a list does not delete the contacts' history. Delivery reports for messages already sent remain queryable by their external ids.
Next
- Bulk & personalisation — when to template from a list versus compose each message
- Campaigns & scheduling — sending to a list on a schedule
- Opt-out & compliance — keeping lists lawful as people leave

