Opt-out & compliance
Israeli law requires marketing messages to carry a working way out, and requires you to honour it. The API gives you the mechanism; this page covers wiring it up so that "unsubscribe" actually means unsubscribed across your whole system.
This is not legal advice
It describes how the API behaves. Israel's Communications Law (the "spam law") sets out what you must actually do, including record-keeping and the burden of proving consent. Talk to whoever advises you on compliance.
Step 1 — Put an exit in the message
add_unsubscribe on sms and bulk appends the removal mechanism:
| Value | Effect |
|---|---|
3 | Adds a removal link. |
2 | Adds reply-to-remove instructions. |
| anything else | Nothing is added. |
<?xml version="1.0" encoding="UTF-8"?>
<sms>
<user>
<username>Leeroy</username>
</user>
<source>DemoAPI</source>
<destinations>
<cl_id>21518</cl_id>
</destinations>
<message>New collection now in store.</message>
<add_unsubscribe>3</add_unsubscribe>
<campaign_name>august-newsletter</campaign_name>
</sms>{
"sms": {
"user": { "username": "Leeroy" },
"source": "DemoAPI",
"destinations": { "cl_id": "21518" },
"message": "New collection now in store.",
"add_unsubscribe": "3",
"campaign_name": "august-newsletter"
}
}An invalid value fails the send with status 986 rather than sending without an exit — which is the right way round.
Which value?
3 (link) is one tap and needs no interpretation, so it produces cleaner opt-out data. 2 (reply) works on handsets where following a link is awkward, but arrives as free text you have to keep matching — see handling replies. If you must choose one, choose 3.
The appended text costs characters against the 1005-character limit. Leave room.
Step 2 — Let TextMe suppress its own sends
When someone opts out, TextMe adds the number to the account's blocklist and stops delivering to it. You do not have to filter destinations yourself:
- Blocked numbers are dropped at send time.
- A send whose destinations are all blocked fails with status
8rather than reporting a hollow success. - Delivery reports for blocked destinations come back as status
201, blocked on request.
That covers SMS. It does not cover anything else you send.
Step 3 — Propagate the opt-out to your own systems
This is the step most integrations miss
TextMe stops its messages. Your email, your push notifications, your call centre and any other SMS provider you use know nothing about it. Someone who asked to be left alone and keeps hearing from you on three other channels has a legitimate complaint — and, on paper, you honoured the request.
Consume the blocklist push feed, which fires whenever a number is blocked however it happened — link, reply, or manual action:
POST https://your-app.example.com/textme/blacklist
Content-Type: application/x-www-form-urlencoded
message=נחסם+מנוי&date=01/04/26 16:05:05&dest=9725xxxxxxxxapp.post('/textme/blacklist', (req, res) => {
const phone = toLocal(req.body.dest) // 9725xxxxxxxx → 05xxxxxxxx
const at = req.body.date
// Acknowledge before doing the work — a slow reply gets the feed disabled.
res.sendStatus(200)
queue.push({ kind: 'opt-out', phone, at })
})
// In the worker: suppress everywhere, not just SMS.
async function handleOptOut({ phone, at }) {
await contacts.suppress(phone, {
channels: ['sms', 'email', 'push', 'voice'],
reason: 'sms-opt-out',
recordedAt: at,
})
}@app.post("/textme/blacklist")
def blacklisted():
queue.put({
"kind": "opt-out",
"phone": to_local(request.form.get("dest")), # 9725… → 05…
"at": request.form.get("date"),
})
# Acknowledge before doing the work.
return "", 200
# In the worker: suppress everywhere, not just SMS.
def handle_opt_out(phone, at):
contacts.suppress(
phone,
channels=["sms", "email", "push", "voice"],
reason="sms-opt-out",
recorded_at=at,
)<?php
Route::post('/textme/blacklist', function (Illuminate\Http\Request $request) {
// 9725xxxxxxxx → 05xxxxxxxx before it touches your contact table.
SuppressContact::dispatch(
phone: to_local($request->input('dest')),
recordedAt: $request->input('date'),
);
return response()->noContent(200);
});Remember that dest arrives in international form (9725xxxxxxxx) while your records almost certainly hold the local form — normalise before matching, as in Incoming SMS & push.
Step 4 — Keep the audit trail
Two things are worth recording for every opt-out, because they are what you would need to produce if challenged:
- When it happened. Both the push feed and the
blacklistreport carry the date. - When and how consent was given in the first place. The API cannot tell you this — it lives in your own signup records.
The blocklist report gives you a periodic reconciliation:
<?xml version="1.0" encoding="UTF-8"?>
<blacklist>
<user>
<username>XXXXXX</username>
</user>
<from>02/06/22 00:00</from>
<to>15/12/22 23:59</to>
</blacklist>{
"blacklist": {
"user": {
"username": "XXXXXX"
},
"from": "02/06/22 00:00",
"to": "15/12/22 23:59"
}
}curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"blacklist": {
"user": {
"username": "XXXXXX"
},
"from": "02/06/22 00:00",
"to": "15/12/22 23:59"
}
}'// Node.js 18+ / browsers — no dependencies
const response = await fetch('https://my.textme.co.il/api', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.TEXTME_API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
blacklist: {
user: {
username: 'XXXXXX',
},
from: '02/06/22 00:00',
to: '15/12/22 23:59',
},
}),
})
const result = await response.json()
// Errors arrive as HTTP 200 too — the payload status is what counts
if (Number(result.status) !== 0) {
throw new Error(`TextMe ${result.status}: ${result.message}`)
}
console.log(result)<?php
// composer require guzzlehttp/guzzle
$client = new \GuzzleHttp\Client([
'headers' => [
'Authorization' => 'Bearer '.getenv('TEXTME_API_TOKEN'),
'Accept' => 'application/json',
],
]);
$response = $client->post('https://my.textme.co.il/api', [
'json' => [
'blacklist' => [
'user' => [
'username' => 'XXXXXX',
],
'from' => '02/06/22 00:00',
'to' => '15/12/22 23:59',
],
],
]);
$result = json_decode($response->getBody()->getContents(), true);
// Errors arrive as HTTP 200 too — the payload status is what counts
if ((int) $result['status'] !== 0) {
throw new RuntimeException("TextMe {$result['status']}: {$result['message']}");
}
print_r($result);<?php
use Illuminate\Support\Facades\Http;
$result = Http::withToken(config('services.textme.token'))
->acceptJson()
->post('https://my.textme.co.il/api', [
'blacklist' => [
'user' => [
'username' => 'XXXXXX',
],
'from' => '02/06/22 00:00',
'to' => '15/12/22 23:59',
],
])
->throw()
->json();
// Errors arrive as HTTP 200 too — the payload status is what counts
throw_if((int) $result['status'] !== 0, RuntimeException::class,
"TextMe {$result['status']}: {$result['message']}");
logger()->info('TextMe', $result);# pip install httpx
import os
import httpx
response = httpx.post(
"https://my.textme.co.il/api",
headers={"Authorization": f"Bearer {os.environ['TEXTME_API_TOKEN']}"},
json={
"blacklist": {
"user": {
"username": "XXXXXX",
},
"from": "02/06/22 00:00",
"to": "15/12/22 23:59",
},
},
)
response.raise_for_status()
result = response.json()
# Errors arrive as HTTP 200 too — the payload status is what counts
if int(result["status"]) != 0:
raise RuntimeError(f"TextMe {result['status']}: {result['message']}")
print(result)package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
payload, _ := json.Marshal(map[string]any{
"blacklist": map[string]any{
"user": map[string]any{
"username": "XXXXXX",
},
"from": "02/06/22 00:00",
"to": "15/12/22 23:59",
},
})
req, _ := http.NewRequest("POST", "https://my.textme.co.il/api", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+os.Getenv("TEXTME_API_TOKEN"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result struct {
Status json.Number `json:"status"`
Message string `json:"message"`
}
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
panic(err)
}
// Errors arrive as HTTP 200 too — the payload status is what counts
if result.Status.String() != "0" {
panic(fmt.Sprintf("TextMe %s: %s", result.Status, result.Message))
}
fmt.Println(result.Message)
}// Java 17+ — java.net.http, no dependencies (parse with Jackson/Gson)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class TextMeBlacklistGet {
public static void main(String[] args) throws Exception {
String body = """
{
"blacklist": {
"user": {
"username": "XXXXXX"
},
"from": "02/06/22 00:00",
"to": "15/12/22 23:59"
}
}
""";
HttpRequest request = HttpRequest.newBuilder(URI.create("https://my.textme.co.il/api"))
.header("Authorization", "Bearer " + System.getenv("TEXTME_API_TOKEN"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
// Errors arrive as HTTP 200 too — the payload status is what counts
System.out.println(response.body());
}
}// .NET 8+ — System.Net.Http
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var payload = """
{
"blacklist": {
"user": {
"username": "XXXXXX"
},
"from": "02/06/22 00:00",
"to": "15/12/22 23:59"
}
}
""";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("TEXTME_API_TOKEN"));
var response = await http.PostAsync("https://my.textme.co.il/api",
new StringContent(payload, Encoding.UTF8, "application/json"));
var result = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement;
var status = result.GetProperty("status").ToString();
// Errors arrive as HTTP 200 too — the payload status is what counts
if (status != "0")
{
var message = result.GetProperty("message").ToString();
throw new Exception($"TextMe {status}: {message}");
}
Console.WriteLine(result);require "net/http"
require "json"
uri = URI("https://my.textme.co.il/api")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('TEXTME_API_TOKEN')}"
request["Content-Type"] = "application/json"
request.body = JSON.dump({
"blacklist" => {
"user" => {
"username" => "XXXXXX",
},
"from" => "02/06/22 00:00",
"to" => "15/12/22 23:59",
},
})
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
result = JSON.parse(response.body)
# Errors arrive as HTTP 200 too — the payload status is what counts
raise "TextMe #{result['status']}: #{result['message']}" unless result["status"].to_i.zero?
pp result// [dependencies]
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
use serde_json::{json, Value};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let result: Value = reqwest::Client::new()
.post("https://my.textme.co.il/api")
.bearer_auth(std::env::var("TEXTME_API_TOKEN")?)
.json(&json!({
"blacklist": {
"user": {
"username": "XXXXXX"
},
"from": "02/06/22 00:00",
"to": "15/12/22 23:59"
}
}))
.send()
.await?
.json()
.await?;
// Errors arrive as HTTP 200 too — the payload status is what counts
if result["status"] != 0 {
return Err(format!("TextMe {}: {}", result["status"], result["message"]).into());
}
println!("{result}");
Ok(())
}<?xml version="1.0" encoding="UTF-8"?>
<blacklist>
<status>0</status>
<message></message>
<transactions>
<transaction>
<phone>5********</phone>
<date>03/12/14 16:38</date>
</transaction>
</transactions>
</blacklist>{
"status": 0,
"message": "",
"transactions": [
{
"phone": "5********",
"date": "22/08/22 11:00"
},
{
"phone": "5********",
"date": "23/11/22 12:37"
}
]
}Numbers come back masked
Entries are returned as 5********, not in full. The report is an audit of how many opted out and when — it is not a list you can diff against your own database. Use the push feed for that, and rely on the API to enforce the blocklist at send time.
Step 5 — Block on request, without waiting for a reply
Customers ask to be removed by phone, by email, at the counter. Add them yourself:
<?xml version="1.0" encoding="UTF-8"?>
<addNumBL>
<user>
<username>xxxxxx</username>
</user>
<phones>
<phone>5********</phone>
<phone>05********</phone>
</phones>
</addNumBL>{
"addNumBL": {
"user": {
"username": "xxxxxx"
},
"phones": {
"phone": [
"5********",
"05********"
]
}
}
}curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"addNumBL": {
"user": {
"username": "xxxxxx"
},
"phones": {
"phone": [
"5********",
"05********"
]
}
}
}'// 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({
addNumBL: {
user: {
username: 'xxxxxx',
},
phones: {
phone: [
'5********',
'05********',
],
},
},
}),
})
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' => [
'addNumBL' => [
'user' => [
'username' => 'xxxxxx',
],
'phones' => [
'phone' => [
'5********',
'05********',
],
],
],
],
]);
$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', [
'addNumBL' => [
'user' => [
'username' => 'xxxxxx',
],
'phones' => [
'phone' => [
'5********',
'05********',
],
],
],
])
->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={
"addNumBL": {
"user": {
"username": "xxxxxx",
},
"phones": {
"phone": [
"5********",
"05********",
],
},
},
},
)
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{
"addNumBL": map[string]any{
"user": map[string]any{
"username": "xxxxxx",
},
"phones": map[string]any{
"phone": []any{
"5********",
"05********",
},
},
},
})
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 TextMeBlacklistAdd {
public static void main(String[] args) throws Exception {
String body = """
{
"addNumBL": {
"user": {
"username": "xxxxxx"
},
"phones": {
"phone": [
"5********",
"05********"
]
}
}
}
""";
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 = """
{
"addNumBL": {
"user": {
"username": "xxxxxx"
},
"phones": {
"phone": [
"5********",
"05********"
]
}
}
}
""";
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({
"addNumBL" => {
"user" => {
"username" => "xxxxxx",
},
"phones" => {
"phone" => [
"5********",
"05********",
],
},
},
})
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!({
"addNumBL": {
"user": {
"username": "xxxxxx"
},
"phones": {
"phone": [
"5********",
"05********"
]
}
}
}))
.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(())
}Success is 946, not 0
addNumBL answers 946 — number has successfully added to blacklist. Code that treats any non-zero status as a failure will report a working call as broken, and may retry it pointlessly.
Unblocking
Removing someone from the blocklist means messaging a person who previously asked you to stop. rmNumBL therefore requires a reason:
<?xml version="1.0" encoding="UTF-8"?>
<rmNumBL>
<user>
<username>xxxxxx</username>
</user>
<phones>
<phone>5********</phone>
<phone>05********</phone>
</phones>
<reason>Customer opted back in by phone</reason>
</rmNumBL>{
"rmNumBL": {
"user": {
"username": "xxxxxx"
},
"phones": {
"phone": [
"5********",
"05********"
]
},
"reason": "Customer opted back in by phone"
}
}curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"rmNumBL": {
"user": {
"username": "xxxxxx"
},
"phones": {
"phone": [
"5********",
"05********"
]
},
"reason": "Customer opted back in by phone"
}
}'// 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({
rmNumBL: {
user: {
username: 'xxxxxx',
},
phones: {
phone: [
'5********',
'05********',
],
},
reason: 'Customer opted back in by phone',
},
}),
})
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' => [
'rmNumBL' => [
'user' => [
'username' => 'xxxxxx',
],
'phones' => [
'phone' => [
'5********',
'05********',
],
],
'reason' => 'Customer opted back in by phone',
],
],
]);
$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', [
'rmNumBL' => [
'user' => [
'username' => 'xxxxxx',
],
'phones' => [
'phone' => [
'5********',
'05********',
],
],
'reason' => 'Customer opted back in by phone',
],
])
->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={
"rmNumBL": {
"user": {
"username": "xxxxxx",
},
"phones": {
"phone": [
"5********",
"05********",
],
},
"reason": "Customer opted back in by phone",
},
},
)
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{
"rmNumBL": map[string]any{
"user": map[string]any{
"username": "xxxxxx",
},
"phones": map[string]any{
"phone": []any{
"5********",
"05********",
},
},
"reason": "Customer opted back in by phone",
},
})
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 TextMeBlacklistRemove {
public static void main(String[] args) throws Exception {
String body = """
{
"rmNumBL": {
"user": {
"username": "xxxxxx"
},
"phones": {
"phone": [
"5********",
"05********"
]
},
"reason": "Customer opted back in by phone"
}
}
""";
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 = """
{
"rmNumBL": {
"user": {
"username": "xxxxxx"
},
"phones": {
"phone": [
"5********",
"05********"
]
},
"reason": "Customer opted back in by phone"
}
}
""";
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({
"rmNumBL" => {
"user" => {
"username" => "xxxxxx",
},
"phones" => {
"phone" => [
"5********",
"05********",
],
},
"reason" => "Customer opted back in by phone",
},
})
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!({
"rmNumBL": {
"user": {
"username": "xxxxxx"
},
"phones": {
"phone": [
"5********",
"05********"
]
},
"reason": "Customer opted back in by phone"
}
}))
.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(())
}Its success statuses are also unusual: 0 when every number was removed, and 944 when some were not on the list to begin with.
Only unblock with evidence
Do it when you have a record of the person asking to come back — a fresh signup, a written request. reason is where that record goes. The burden of proving consent sits with the sender, so "they probably don't mind" is not a basis for re-subscribing anyone.
Related suppression mechanisms
Not everything that stops a message is an opt-out. These are worth distinguishing in your own reporting:
| Mechanism | What it is |
|---|---|
temp_bl | Your choice to skip anyone messaged in the last 1–14 days. Frequency capping, not consent. Filtering everyone out returns status 715. |
DLR 17 | The subscriber is blocked for marketing messages at the carrier or regulator level. |
DLR 201 | Blocked at the subscriber's own request — the opt-out showing up in delivery data. |
DLR 15 | A kosher (filtered) handset. Not a refusal — reach them with voice instead. |
Status 8 | Every destination in the send was blocklisted. An audience problem, not a code fault. |
15 deserves the emphasis: treating it as an opt-out throws away a reachable customer. It means this device does not do SMS, and tts.type 2 routes exactly those numbers to a voice call automatically.
A working checklist
- Every marketing send carries
add_unsubscribe—3unless you have a reason. - The blocklist push feed is consumed, and suppresses across all channels.
946and944are treated as success in your client.- Inbound
STOP/הסרkeywords are handled as a second path, for the2case. - DLR
201and17write back to the contact record. - DLR
15routes to voice rather than being marked as failed. - Status
8raises an audience alert, not a code alert. - Unblocking requires a recorded reason.
Next
- Incoming SMS & push — the reply path for
add_unsubscribevalue2 - Blocklist — the full operation reference
- DLR statuses — every blocked and refused code

