Skip to content

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.

xml
<campaign_name>august-newsletter-2026-08-17</campaign_name>
json
{ "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
<?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>
json
{
  "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:

LanguageFormat string
JavaScript (date-fns)format(when, 'dd/MM/yy HH:mm')
Pythonwhen.strftime("%d/%m/%y %H:%M")
PHP$when->format('d/m/y H:i')
JavaDateTimeFormatter.ofPattern("dd/MM/yy HH:mm")
C#when.ToString("dd/MM/yy HH:mm")
Rubywhen.strftime("%d/%m/%y %H:%M")
Gowhen.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_bl skips destinations messaged in the last 1–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
<?xml version="1.0" encoding="UTF-8"?>
<cancel>
    <user>
        <username>Leeroy</username>
    </user>
    <campaign_id>1234</campaign_id>
</cancel>
json
{
  "cancel": {
    "user": {
      "username": "Leeroy"
    },
    "campaign_id": "1234"
  }
}
bash
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"
  }
}'
js
// 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({
    cancel: {
      user: {
        username: 'Leeroy',
      },
      campaign_id: '1234',
    },
  }),
})

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
<?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 — the payload status is what counts
if ((int) $result['status'] !== 0) {
    throw new RuntimeException("TextMe {$result['status']}: {$result['message']}");
}

print_r($result);
php
<?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 — the payload status is what counts
throw_if((int) $result['status'] !== 0, RuntimeException::class,
    "TextMe {$result['status']}: {$result['message']}");

logger()->info('TextMe', $result);
python
# 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 — the payload status is what counts
if int(result["status"]) != 0:
    raise RuntimeError(f"TextMe {result['status']}: {result['message']}")

print(result)
go
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 — 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
// 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 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 — the payload status is what counts
        System.out.println(response.body());
    }
}
csharp
// .NET 8+ — 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 — the payload status is what counts
if (status != "0")
{
    var message = result.GetProperty("message").ToString();
    throw new Exception($"TextMe {status}: {message}");
}

Console.WriteLine(result);
ruby
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 — the payload status is what counts
raise "TextMe #{result['status']}: #{result['message']}" unless result["status"].to_i.zero?

pp result
rust
// [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 — the payload status is what counts
    if result["status"] != 0 {
        return Err(format!("TextMe {}: {}", result["status"], result["message"]).into());
    }

    println!("{result}");
    Ok(())
}
xml
<?xml version="1.0" encoding="UTF-8"?>
<cancel>
    <status>0</status>
    <message>Campaign successfuly cancel</message>
</cancel>
json
{
  "status": 0,
  "message": "Campaign successfuly cancel"
}

By name

Cancels everything pending under that name, and tells you how many:

xml
<?xml version="1.0" encoding="UTF-8"?>
<cancel>
    <user>
        <username>Leeroy</username>
    </user>
    <campaign_name>My Campaign</campaign_name>
</cancel>
json
{
  "cancel": {
    "user": {
      "username": "Leeroy"
    },
    "campaign_name": "My Campaign"
  }
}
bash
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"
  }
}'
js
// 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({
    cancel: {
      user: {
        username: 'Leeroy',
      },
      campaign_name: 'My Campaign',
    },
  }),
})

const result = await response.json()

// Errors arrive as HTTP 200 too — the payload status is what counts
if (Number(result.status) !== 0) {
  throw new Error(`TextMe ${result.status}: ${result.message}`)
}

console.log(result)
php
<?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 — the payload status is what counts
if ((int) $result['status'] !== 0) {
    throw new RuntimeException("TextMe {$result['status']}: {$result['message']}");
}

print_r($result);
php
<?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 — the payload status is what counts
throw_if((int) $result['status'] !== 0, RuntimeException::class,
    "TextMe {$result['status']}: {$result['message']}");

logger()->info('TextMe', $result);
python
# 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 — the payload status is what counts
if int(result["status"]) != 0:
    raise RuntimeError(f"TextMe {result['status']}: {result['message']}")

print(result)
go
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 — 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
// 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 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 — the payload status is what counts
        System.out.println(response.body());
    }
}
csharp
// .NET 8+ — 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 — the payload status is what counts
if (status != "0")
{
    var message = result.GetProperty("message").ToString();
    throw new Exception($"TextMe {status}: {message}");
}

Console.WriteLine(result);
ruby
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 — the payload status is what counts
raise "TextMe #{result['status']}: #{result['message']}" unless result["status"].to_i.zero?

pp result
rust
// [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 — the payload status is what counts
    if result["status"] != 0 {
        return Err(format!("TextMe {}: {}", result["status"], result["message"]).into());
    }

    println!("{result}");
    Ok(())
}
xml
<?xml version="1.0" encoding="UTF-8"?>
<cancel>
    <status>0</status>
    <message>Campaigns successfully cancel</message>
    <count>1</count>
</cancel>
json
{
  "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

StatusMeaningWhat to do
0Cancelled.
955Already cancelled.Treat as success; the end state is what you wanted.
966Already sent.Too late. Nothing to do but note it.
970Cancellation failed server-side.Retry once, then contact support.
977Wrong id, or it belongs to another account.Check the id.
js
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) }
}
python
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:

js
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 } })
python
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
<?xml version="1.0" encoding="UTF-8"?>
<get_birthday_campaigns>
    <user>
        <username>XXXXXX</username>
    </user>
</get_birthday_campaigns>
json
{
  "get_birthday_campaigns": {
    "user": {
      "username": "XXXXXX"
    }
  }
}
bash
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"
    }
  }
}'
js
// 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({
    get_birthday_campaigns: {
      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
<?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 — the payload status is what counts
if ((int) $result['status'] !== 0) {
    throw new RuntimeException("TextMe {$result['status']}: {$result['message']}");
}

print_r($result);
php
<?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 — the payload status is what counts
throw_if((int) $result['status'] !== 0, RuntimeException::class,
    "TextMe {$result['status']}: {$result['message']}");

logger()->info('TextMe', $result);
python
# 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 — the payload status is what counts
if int(result["status"]) != 0:
    raise RuntimeError(f"TextMe {result['status']}: {result['message']}")

print(result)
go
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 — 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
// 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 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 — the payload status is what counts
        System.out.println(response.body());
    }
}
csharp
// .NET 8+ — 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 — the payload status is what counts
if (status != "0")
{
    var message = result.GetProperty("message").ToString();
    throw new Exception($"TextMe {status}: {message}");
}

Console.WriteLine(result);
ruby
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 — the payload status is what counts
raise "TextMe #{result['status']}: #{result['message']}" unless result["status"].to_i.zero?

pp result
rust
// [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 — the payload status is what counts
    if result["status"] != 0 {
        return Err(format!("TextMe {}: {}", result["status"], result["message"]).into());
    }

    println!("{result}");
    Ok(())
}
xml
<?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>
json
{
  "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
<?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>
json
{
  "edit_birthday_campaign": {
    "user": {
      "username": "XXXX"
    },
    "campaign_id": "XXXX",
    "message": "XXXX"
  }
}
bash
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"
  }
}'
js
// 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({
    edit_birthday_campaign: {
      user: {
        username: 'XXXX',
      },
      campaign_id: 'XXXX',
      message: 'XXXX',
    },
  }),
})

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
<?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 — the payload status is what counts
if ((int) $result['status'] !== 0) {
    throw new RuntimeException("TextMe {$result['status']}: {$result['message']}");
}

print_r($result);
php
<?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 — the payload status is what counts
throw_if((int) $result['status'] !== 0, RuntimeException::class,
    "TextMe {$result['status']}: {$result['message']}");

logger()->info('TextMe', $result);
python
# 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 — the payload status is what counts
if int(result["status"]) != 0:
    raise RuntimeError(f"TextMe {result['status']}: {result['message']}")

print(result)
go
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 — 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
// 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 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 — the payload status is what counts
        System.out.println(response.body());
    }
}
csharp
// .NET 8+ — 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 — the payload status is what counts
if (status != "0")
{
    var message = result.GetProperty("message").ToString();
    throw new Exception($"TextMe {status}: {message}");
}

Console.WriteLine(result);
ruby
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 — the payload status is what counts
raise "TextMe #{result['status']}: #{result['message']}" unless result["status"].to_i.zero?

pp result
rust
// [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 — the payload status is what counts
    if result["status"] != 0 {
        return Err(format!("TextMe {}: {}", result["status"], result["message"]).into());
    }

    println!("{result}");
    Ok(())
}
xml
<?xml version="1.0" encoding="utf-8"?>
<sms>
    <status>0</status>
    <message>birthday campaign successfully updated</message>
</sms>
json
{
  "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 bulk calls.
  • 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