Campaigns & scheduling
Every send creates a campaign. Give it a name and a send time, and you get two things worth having: a handle for cancelling it, and a label that shows up in the account's reports.
Naming a campaign
campaign_name is optional, up to 50 characters, and available on both sms and bulk.
<campaign_name>august-newsletter-2026-08-17</campaign_name>{ "campaign_name": "august-newsletter-2026-08-17" }Make names unique per run
Cancelling by name cancels every pending campaign that matches, no confirmation, no dry run. A name like newsletter will one day take out a send you did not mean to touch.
Put the date, or a run id, in the name: newsletter-2026-08-17, not newsletter.
Used well, that same behaviour is the feature. A large bulk run split across several calls shares one campaign_name, and a single cancel stops all of it.
Scheduling a send
timing defers a send. The format is dd/mm/yy hh:mm. Two-digit year, 24-hour clock:
<?xml version="1.0" encoding="UTF-8"?>
<sms>
<user>
<username>Leeroy</username>
</user>
<source>DemoAPI</source>
<destinations>
<cl_id>21518</cl_id>
</destinations>
<message>Doors open at 10:00 tomorrow.</message>
<timing>18/08/26 08:00</timing>
<campaign_name>opening-reminder-2026-08-18</campaign_name>
</sms>{
"sms": {
"user": { "username": "Leeroy" },
"source": "DemoAPI",
"destinations": { "cl_id": "21518" },
"message": "Doors open at 10:00 tomorrow.",
"timing": "18/08/26 08:00",
"campaign_name": "opening-reminder-2026-08-18"
}
}Leave timing out and the message goes immediately, which also means it is past cancelling before you could call. Scheduling is what buys you the window to change your mind.
Formatting the timestamp
dd/mm/yy, not mm/dd/yy or ISO. 18/08/26 08:00 is 18 August 2026. Build it explicitly rather than relying on a locale default:
| Language | Format string |
|---|---|
| JavaScript (date-fns) | format(when, 'dd/MM/yy HH:mm') |
| Python | when.strftime("%d/%m/%y %H:%M") |
| PHP | $when->format('d/m/y H:i') |
| Java | DateTimeFormatter.ofPattern("dd/MM/yy HH:mm") |
| C# | when.ToString("dd/MM/yy HH:mm") |
| Ruby | when.strftime("%d/%m/%y %H:%M") |
| Go | when.Format("02/01/06 15:04") |
Two other things govern when a message actually goes out:
- Permitted sending hours. Accounts have a window; sending outside it returns status
5. A scheduled send timed outside the window is not a good idea. temp_blskips destinations messaged in the last 1 to 14 days. Evaluated at send time, not schedule time, so a recipient who hears from you between scheduling and sending is still filtered.
For bulk, timing is a batch-level field, outside messages. Individual messages in a batch cannot be scheduled separately; different times mean different calls.
SOAP behaves differently
sendBulkSms over SOAP ignores timing entirely. Only the bulk operation on the main endpoint honours it.
Cancelling
By id
<?xml version="1.0" encoding="UTF-8"?>
<cancel>
<user>
<username>Leeroy</username>
</user>
<campaign_id>1234</campaign_id>
</cancel>{
"cancel": {
"user": {
"username": "Leeroy"
},
"campaign_id": "1234"
}
}curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"cancel": {
"user": {
"username": "Leeroy"
},
"campaign_id": "1234"
}
}'// 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({
cancel: {
user: {
username: 'Leeroy',
},
campaign_id: '1234',
},
}),
})
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' => [
'cancel' => [
'user' => [
'username' => 'Leeroy',
],
'campaign_id' => '1234',
],
],
]);
$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', [
'cancel' => [
'user' => [
'username' => 'Leeroy',
],
'campaign_id' => '1234',
],
])
->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={
"cancel": {
"user": {
"username": "Leeroy",
},
"campaign_id": "1234",
},
},
)
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{
"cancel": map[string]any{
"user": map[string]any{
"username": "Leeroy",
},
"campaign_id": "1234",
},
})
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 TextMeCampaignCancelById {
public static void main(String[] args) throws Exception {
String body = """
{
"cancel": {
"user": {
"username": "Leeroy"
},
"campaign_id": "1234"
}
}
""";
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 = """
{
"cancel": {
"user": {
"username": "Leeroy"
},
"campaign_id": "1234"
}
}
""";
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({
"cancel" => {
"user" => {
"username" => "Leeroy",
},
"campaign_id" => "1234",
},
})
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!({
"cancel": {
"user": {
"username": "Leeroy"
},
"campaign_id": "1234"
}
}))
.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"?>
<cancel>
<status>0</status>
<message>Campaign successfuly cancel</message>
</cancel>{
"status": 0,
"message": "Campaign successfuly cancel"
}By name
Cancels everything pending under that name, and tells you how many:
<?xml version="1.0" encoding="UTF-8"?>
<cancel>
<user>
<username>Leeroy</username>
</user>
<campaign_name>My Campaign</campaign_name>
</cancel>{
"cancel": {
"user": {
"username": "Leeroy"
},
"campaign_name": "My Campaign"
}
}curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"cancel": {
"user": {
"username": "Leeroy"
},
"campaign_name": "My 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({
cancel: {
user: {
username: 'Leeroy',
},
campaign_name: 'My 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' => [
'cancel' => [
'user' => [
'username' => 'Leeroy',
],
'campaign_name' => 'My 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', [
'cancel' => [
'user' => [
'username' => 'Leeroy',
],
'campaign_name' => 'My 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={
"cancel": {
"user": {
"username": "Leeroy",
},
"campaign_name": "My 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{
"cancel": map[string]any{
"user": map[string]any{
"username": "Leeroy",
},
"campaign_name": "My 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 TextMeCampaignCancelByName {
public static void main(String[] args) throws Exception {
String body = """
{
"cancel": {
"user": {
"username": "Leeroy"
},
"campaign_name": "My 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 = """
{
"cancel": {
"user": {
"username": "Leeroy"
},
"campaign_name": "My 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({
"cancel" => {
"user" => {
"username" => "Leeroy",
},
"campaign_name" => "My 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!({
"cancel": {
"user": {
"username": "Leeroy"
},
"campaign_name": "My 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"?>
<cancel>
<status>0</status>
<message>Campaigns successfully cancel</message>
<count>1</count>
</cancel>{
"status": 0,
"message": "Campaigns successfully cancel",
"count": 1
}count is how many were stopped. 0 with status: 0 means nothing matched. Either the name is wrong or the campaigns have already gone out.
Reading the outcome
| Status | Meaning | What to do |
|---|---|---|
0 | Cancelled. | , |
955 | Already cancelled. | Treat as success; the end state is what you wanted. |
966 | Already sent. | Too late. Nothing to do but note it. |
970 | Cancellation failed server-side. | Retry once, then contact support. |
977 | Wrong id, or it belongs to another account. | Check the id. |
async function cancelCampaign(name) {
const result = await textme({
cancel: { user: { username: 'Leeroy' }, campaign_name: name },
})
// 955 means it was already cancelled. The state you asked for either way.
if (Number(result.status) === 955) return { cancelled: 0, alreadyDone: true }
// 966 means it has gone out. Not recoverable, but not a bug either.
if (Number(result.status) === 966) return { cancelled: 0, tooLate: true }
if (Number(result.status) !== 0) {
throw new Error(`TextMe ${result.status}: ${result.message}`)
}
return { cancelled: Number(result.count ?? 0) }
}def cancel_campaign(name):
result = textme({
"cancel": {"user": {"username": "Leeroy"}, "campaign_name": name}
})
status = int(result["status"])
# 955 means it was already cancelled. The state you asked for either way.
if status == 955:
return {"cancelled": 0, "already_done": True}
# 966 means it has gone out. Not recoverable, but not a bug either.
if status == 966:
return {"cancelled": 0, "too_late": True}
if status != 0:
raise RuntimeError(f"TextMe {status}: {result['message']}")
return {"cancelled": int(result.get("count", 0))}A safe scheduled-send pattern
Schedule far enough ahead that cancelling is genuinely possible, and name the run so cancelling is one call:
const runId = `newsletter-${new Date().toISOString().slice(0, 10)}`
// 1. Enough credit for the whole audience?
const balance = await textme({ balance: { user: { username: 'Leeroy' } } })
if (Number(balance.balance) < audience.length) {
throw new Error(`Need ${audience.length} credits, have ${balance.balance}`)
}
// 2. Validate the exact payload against /api/test before committing to it.
await textmeTest(payload(runId))
// 3. Schedule it, with the run id as the campaign name.
const { shipment_id } = await textme(payload(runId))
await db.recordScheduledRun({ runId, shipmentId: shipment_id })
// Later, if something is wrong: one call stops every chunk of the run.
await textme({ cancel: { user: { username: 'Leeroy' }, campaign_name: runId } })from datetime import date
run_id = f"newsletter-{date.today().isoformat()}"
# 1. Enough credit for the whole audience?
balance = textme({"balance": {"user": {"username": "Leeroy"}}})
if int(balance["balance"]) < len(audience):
raise RuntimeError(f"Need {len(audience)} credits, have {balance['balance']}")
# 2. Validate the exact payload against /api/test before committing to it.
textme_test(payload(run_id))
# 3. Schedule it, with the run id as the campaign name.
result = textme(payload(run_id))
db.record_scheduled_run(run_id=run_id, shipment_id=result["shipment_id"])
# Later, if something is wrong: one call stops every chunk of the run.
textme({"cancel": {"user": {"username": "Leeroy"}, "campaign_name": run_id}})Birthday campaigns
Recurring sends triggered by a contact's date of birth. They are created in the console; the API lists them and rewrites their message.
<?xml version="1.0" encoding="UTF-8"?>
<get_birthday_campaigns>
<user>
<username>XXXXXX</username>
</user>
</get_birthday_campaigns>{
"get_birthday_campaigns": {
"user": {
"username": "XXXXXX"
}
}
}curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"get_birthday_campaigns": {
"user": {
"username": "XXXXXX"
}
}
}'// 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({
get_birthday_campaigns: {
user: {
username: 'XXXXXX',
},
},
}),
})
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' => [
'get_birthday_campaigns' => [
'user' => [
'username' => 'XXXXXX',
],
],
],
]);
$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', [
'get_birthday_campaigns' => [
'user' => [
'username' => 'XXXXXX',
],
],
])
->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={
"get_birthday_campaigns": {
"user": {
"username": "XXXXXX",
},
},
},
)
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{
"get_birthday_campaigns": 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, 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 TextMeBirthdayList {
public static void main(String[] args) throws Exception {
String body = """
{
"get_birthday_campaigns": {
"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, 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 = """
{
"get_birthday_campaigns": {
"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, 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({
"get_birthday_campaigns" => {
"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, 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!({
"get_birthday_campaigns": {
"user": {
"username": "XXXXXX"
}
}
}))
.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></message>
<birthday_campaigns>
<birthday_campaign>
<CAMPAIGN_ID>XXXX</CAMPAIGN_ID>
<CAMPAIGN_NAME>XXXX</CAMPAIGN_NAME>
<CREATED_ON>18/10/23 14:40:39</CREATED_ON>
<ACTIVE_DESTINATIONS>X</ACTIVE_DESTINATIONS>
</birthday_campaign>
</birthday_campaigns>
</sms>{
"status": 0,
"message": "",
"birthday_campaigns": {
"birthday_campaign": [
{
"CAMPAIGN_ID": "xxxxx",
"CAMPAIGN_NAME": "xxxxx",
"CREATED_ON": "18/10/23 14:40:39",
"ACTIVE_DESTINATIONS": ""
}
]
}
}Field names are upper case here
CAMPAIGN_ID, CAMPAIGN_NAME, CREATED_ON, ACTIVE_DESTINATIONS. Unlike the lower-case style used everywhere else in the API.
ACTIVE_DESTINATIONS is how many contacts the campaign currently targets, which makes it a useful health check: a birthday campaign that has quietly fallen to zero is not going to send anything.
Updating the message body:
<?xml version="1.0" encoding="UTF-8"?>
<edit_birthday_campaign>
<user>
<username>XXXX</username>
</user>
<campaign_id>XXXX</campaign_id>
<message>XXXX</message>
</edit_birthday_campaign>{
"edit_birthday_campaign": {
"user": {
"username": "XXXX"
},
"campaign_id": "XXXX",
"message": "XXXX"
}
}curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"edit_birthday_campaign": {
"user": {
"username": "XXXX"
},
"campaign_id": "XXXX",
"message": "XXXX"
}
}'// 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({
edit_birthday_campaign: {
user: {
username: 'XXXX',
},
campaign_id: 'XXXX',
message: 'XXXX',
},
}),
})
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' => [
'edit_birthday_campaign' => [
'user' => [
'username' => 'XXXX',
],
'campaign_id' => 'XXXX',
'message' => 'XXXX',
],
],
]);
$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', [
'edit_birthday_campaign' => [
'user' => [
'username' => 'XXXX',
],
'campaign_id' => 'XXXX',
'message' => 'XXXX',
],
])
->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={
"edit_birthday_campaign": {
"user": {
"username": "XXXX",
},
"campaign_id": "XXXX",
"message": "XXXX",
},
},
)
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{
"edit_birthday_campaign": map[string]any{
"user": map[string]any{
"username": "XXXX",
},
"campaign_id": "XXXX",
"message": "XXXX",
},
})
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 TextMeBirthdayEdit {
public static void main(String[] args) throws Exception {
String body = """
{
"edit_birthday_campaign": {
"user": {
"username": "XXXX"
},
"campaign_id": "XXXX",
"message": "XXXX"
}
}
""";
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 = """
{
"edit_birthday_campaign": {
"user": {
"username": "XXXX"
},
"campaign_id": "XXXX",
"message": "XXXX"
}
}
""";
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({
"edit_birthday_campaign" => {
"user" => {
"username" => "XXXX",
},
"campaign_id" => "XXXX",
"message" => "XXXX",
},
})
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!({
"edit_birthday_campaign": {
"user": {
"username": "XXXX"
},
"campaign_id": "XXXX",
"message": "XXXX"
}
}))
.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>birthday campaign successfully updated</message>
</sms>{
"status": 0,
"message": "birthday campaign successfully updated"
}Only the body changes. Schedule, audience and name are untouched, and there is no API operation to change them.
Which handle should you keep?
A send returns shipment_id; cancellation takes campaign_id. In practice both name the same campaign, and cancelling by name is the more robust habit:
- It works across a run split into several
bulkcalls. - It does not depend on having stored an id.
- It is derivable from your own run identifier, so you can cancel from a script that never saw the original response.
Store the shipment_id anyway. It appears in delivery reports, which is where you will want it.
Next
- Bulk & personalisation: building the run you are scheduling
- Track delivery: what happened once it went out
- Campaigns endpoint: the full operation reference

