Skip to content

הסרה מרשימת תפוצה ורגולציה

החוק בישראל מחייב הודעות פרסומיות לשאת דרך יציאה שעובדת, ומחייב אתכם לכבד אותה. ה-API נותן לכם את המנגנון; העמוד הזה מסביר איך לחבר אותו כך ש"הסרה" תהיה באמת הסרה בכל המערכת שלכם.

זה לא ייעוץ משפטי

העמוד מתאר את התנהגות ה-API. חוק התקשורת (חוק הספאם) קובע מה עליכם לעשות בפועל, כולל תיעוד וחובת ההוכחה לגבי הסכמה. התייעצו עם מי שמייעץ לכם בנושאי רגולציה.

צעד 1 — לשים יציאה בהודעה

add_unsubscribe ב-sms וב-bulk מוסיף את מנגנון ההסרה:

ערךתוצאה
3מוסיף קישור הסרה.
2מוסיף הוראות הסרה בהודעה חוזרת.
כל ערך אחרלא מתווסף דבר.
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>New collection now in store.</message>
    <add_unsubscribe>3</add_unsubscribe>
    <campaign_name>august-newsletter</campaign_name>
</sms>
json
{
  "sms": {
    "user": { "username": "Leeroy" },
    "source": "DemoAPI",
    "destinations": { "cl_id": "21518" },
    "message": "New collection now in store.",
    "add_unsubscribe": "3",
    "campaign_name": "august-newsletter"
  }
}

ערך לא תקין מכשיל את השליחה בסטטוס 986 ולא שולח אותה בלי יציאה — וזה הכיוון הנכון.

איזה ערך?

3 (קישור) הוא הקלקה אחת ואינו דורש פרשנות, ולכן הוא מייצר נתוני הסרה נקיים יותר. 2 (תשובה) עובד במכשירים שבהם לחיצה על קישור מסורבלת, אבל מגיע כטקסט חופשי שצריך להמשיך להתאים — ראו טיפול בתשובות. אם חייבים לבחור אחד, בחרו 3.

הטקסט שמתווסף עולה תווים במגבלת 1005 התווים. השאירו מרווח.

צעד 2 — לתת ל-TextMe לדכא את השליחות שלה

כשמישהו מבקש להסירו, TextMe מוסיפה את המספר לרשימת החסימה של החשבון ומפסיקה למסור אליו. אינכם צריכים לסנן יעדים בעצמכם:

  • מספרים חסומים מושמטים בזמן השליחה.
  • שליחה שכל יעדיה חסומים נכשלת בסטטוס 8 ולא מדווחת הצלחה מדומה.
  • דוחות מסירה עבור יעדים חסומים חוזרים כסטטוס 201, נחסם לפי בקשה.

זה מכסה SMS. זה לא מכסה שום דבר אחר שאתם שולחים.

צעד 3 — להפיץ את ההסרה למערכות שלכם

זה הצעד שרוב האינטגרציות מפספסות

TextMe עוצרת את ההודעות שלה. הדואר שלכם, ההתראות שלכם, מרכז השירות שלכם וכל ספק SMS אחר שאתם משתמשים בו לא יודעים על זה כלום. מי שביקש שיניחו לו וממשיך לשמוע מכם בשלושה ערוצים אחרים — יש לו תלונה מוצדקת, ועל הנייר כיבדתם את הבקשה.

צרכו את ערוץ ה-Push של רשימת החסימה, שנשלח בכל פעם שמספר נחסם בכל דרך שזה קרה — קישור, תשובה, או פעולה יזומה:

http
POST https://your-app.example.com/textme/blacklist
Content-Type: application/x-www-form-urlencoded

message=נחסם+מנוי&date=01/04/26 16:05:05&dest=9725xxxxxxxx
js
app.post('/textme/blacklist', (req, res) => {
  const phone = toLocal(req.body.dest) // 9725xxxxxxxx → 05xxxxxxxx
  const at = req.body.date

  // אשרו לפני העבודה — תשובה איטית מביאה להשבתת הערוץ.
  res.sendStatus(200)

  queue.push({ kind: 'opt-out', phone, at })
})

// ב-worker: דיכוי בכל מקום, לא רק ב-SMS.
async function handleOptOut({ phone, at }) {
  await contacts.suppress(phone, {
    channels: ['sms', 'email', 'push', 'voice'],
    reason: 'sms-opt-out',
    recordedAt: at,
  })
}
python
@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"),
    })

    # אשרו לפני העבודה.
    return "", 200


# ב-worker: דיכוי בכל מקום, לא רק ב-SMS.
def handle_opt_out(phone, at):
    contacts.suppress(
        phone,
        channels=["sms", "email", "push", "voice"],
        reason="sms-opt-out",
        recorded_at=at,
    )
php
<?php

Route::post('/textme/blacklist', function (Illuminate\Http\Request $request) {
    // 9725xxxxxxxx → 05xxxxxxxx לפני שזה נוגע בטבלת אנשי הקשר.
    SuppressContact::dispatch(
        phone: to_local($request->input('dest')),
        recordedAt: $request->input('date'),
    );

    return response()->noContent(200);
});

זכרו ש-dest מגיע בצורה בינלאומית (9725xxxxxxxx) בעוד הרשומות שלכם כמעט בוודאות מחזיקות את הצורה המקומית — נרמלו לפני התאמה, כמו בהודעות נכנסות ו-Push.

צעד 4 — לשמור על מסלול ביקורת

שני דברים כדאי לתעד בכל הסרה, כי הם מה שתצטרכו להציג אם תיתקלו בערעור:

  • מתי זה קרה. גם ערוץ ה-Push וגם דוח ה-blacklist נושאים את התאריך.
  • מתי ואיך ניתנה ההסכמה מלכתחילה. ה-API לא יכול לספר לכם את זה — זה נמצא ברשומות ההרשמה שלכם.

דוח רשימת החסימה נותן לכם התאמה תקופתית:

xml
<?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>
json
{
  "blacklist": {
    "user": {
      "username": "XXXXXX"
    },
    "from": "02/06/22 00:00",
    "to": "15/12/22 23:59"
  }
}
bash
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"
  }
}'
js
// Node.js 18+ / דפדפנים — ללא תלויות
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()

// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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' => [
        'blacklist' => [
            'user' => [
                'username' => 'XXXXXX',
            ],
            'from' => '02/06/22 00:00',
            'to' => '15/12/22 23:59',
        ],
    ],
]);

$result = json_decode($response->getBody()->getContents(), true);

// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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', [
        'blacklist' => [
            'user' => [
                'username' => 'XXXXXX',
            ],
            'from' => '02/06/22 00:00',
            'to' => '15/12/22 23:59',
        ],
    ])
    ->throw()
    ->json();

// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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={
        "blacklist": {
            "user": {
                "username": "XXXXXX",
            },
            "from": "02/06/22 00:00",
            "to": "15/12/22 23:59",
        },
    },
)
response.raise_for_status()
result = response.json()

# גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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{
		"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)
	}

	// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
	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, ללא תלויות (פענוח עם 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());

        // גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
        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 = """
    {
      "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();

// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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({
  "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)

# גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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!({
          "blacklist": {
            "user": {
              "username": "XXXXXX"
            },
            "from": "02/06/22 00:00",
            "to": "15/12/22 23:59"
          }
        }))
        .send()
        .await?
        .json()
        .await?;

    // גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
    if result["status"] != 0 {
        return Err(format!("TextMe {}: {}", result["status"], result["message"]).into());
    }

    println!("{result}");
    Ok(())
}
xml
<?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>
json
{
  "status": 0,
  "message": "",
  "transactions": [
    {
      "phone": "5********",
      "date": "22/08/22 11:00"
    },
    {
      "phone": "5********",
      "date": "23/11/22 12:37"
    }
  ]
}

המספרים חוזרים ממוסכים

הרשומות מוחזרות כ-5******** ולא במלואן. הדוח הוא ביקורת של כמה ביקשו להסירם ומתי — הוא לא רשימה שאפשר להשוות מול מסד הנתונים שלכם. השתמשו בערוץ ה-Push לצורך הזה, וסמכו על ה-API שיאכוף את רשימת החסימה בזמן השליחה.

צעד 5 — לחסום לפי בקשה, בלי לחכות לתשובה

לקוחות מבקשים להוסר בטלפון, במייל, בדלפק. הוסיפו אותם בעצמכם:

xml
<?xml version="1.0" encoding="UTF-8"?>
<addNumBL>
    <user>
        <username>xxxxxx</username>
    </user>
    <phones>
        <phone>5********</phone>
        <phone>05********</phone>
    </phones>
</addNumBL>
json
{
  "addNumBL": {
    "user": {
      "username": "xxxxxx"
    },
    "phones": {
      "phone": [
        "5********",
        "05********"
      ]
    }
  }
}
bash
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********"
      ]
    }
  }
}'
js
// Node.js 18+ / דפדפנים — ללא תלויות
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()

// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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' => [
        'addNumBL' => [
            'user' => [
                'username' => 'xxxxxx',
            ],
            'phones' => [
                'phone' => [
                    '5********',
                    '05********',
                ],
            ],
        ],
    ],
]);

$result = json_decode($response->getBody()->getContents(), true);

// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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', [
        'addNumBL' => [
            'user' => [
                'username' => 'xxxxxx',
            ],
            'phones' => [
                'phone' => [
                    '5********',
                    '05********',
                ],
            ],
        ],
    ])
    ->throw()
    ->json();

// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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={
        "addNumBL": {
            "user": {
                "username": "xxxxxx",
            },
            "phones": {
                "phone": [
                    "5********",
                    "05********",
                ],
            },
        },
    },
)
response.raise_for_status()
result = response.json()

# גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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{
		"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)
	}

	// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
	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, ללא תלויות (פענוח עם 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());

        // גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
        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 = """
    {
      "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();

// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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({
  "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)

# גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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!({
          "addNumBL": {
            "user": {
              "username": "xxxxxx"
            },
            "phones": {
              "phone": [
                "5********",
                "05********"
              ]
            }
          }
        }))
        .send()
        .await?
        .json()
        .await?;

    // גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
    if result["status"] != 0 {
        return Err(format!("TextMe {}: {}", result["status"], result["message"]).into());
    }

    println!("{result}");
    Ok(())
}

ההצלחה היא 946, לא 0

addNumBL עונה 946number has successfully added to blacklist. קוד שמתייחס לכל סטטוס שאינו אפס ככשל ידווח על קריאה תקינה כשבורה, ואולי ינסה אותה שוב לחינם.

הסרת חסימה

הסרת מישהו מרשימת החסימה פירושה לשלוח הודעה לאדם שביקש בעבר שתפסיקו. לכן rmNumBL דורשת reason:

xml
<?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>
json
{
  "rmNumBL": {
    "user": {
      "username": "xxxxxx"
    },
    "phones": {
      "phone": [
        "5********",
        "05********"
      ]
    },
    "reason": "Customer opted back in by phone"
  }
}
bash
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"
  }
}'
js
// Node.js 18+ / דפדפנים — ללא תלויות
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()

// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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' => [
        'rmNumBL' => [
            'user' => [
                'username' => 'xxxxxx',
            ],
            'phones' => [
                'phone' => [
                    '5********',
                    '05********',
                ],
            ],
            'reason' => 'Customer opted back in by phone',
        ],
    ],
]);

$result = json_decode($response->getBody()->getContents(), true);

// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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', [
        'rmNumBL' => [
            'user' => [
                'username' => 'xxxxxx',
            ],
            'phones' => [
                'phone' => [
                    '5********',
                    '05********',
                ],
            ],
            'reason' => 'Customer opted back in by phone',
        ],
    ])
    ->throw()
    ->json();

// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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={
        "rmNumBL": {
            "user": {
                "username": "xxxxxx",
            },
            "phones": {
                "phone": [
                    "5********",
                    "05********",
                ],
            },
            "reason": "Customer opted back in by phone",
        },
    },
)
response.raise_for_status()
result = response.json()

# גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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{
		"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)
	}

	// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
	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, ללא תלויות (פענוח עם 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());

        // גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
        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 = """
    {
      "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();

// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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({
  "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)

# גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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!({
          "rmNumBL": {
            "user": {
              "username": "xxxxxx"
            },
            "phones": {
              "phone": [
                "5********",
                "05********"
              ]
            },
            "reason": "Customer opted back in by phone"
          }
        }))
        .send()
        .await?
        .json()
        .await?;

    // גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
    if result["status"] != 0 {
        return Err(format!("TextMe {}: {}", result["status"], result["message"]).into());
    }

    println!("{result}");
    Ok(())
}

גם סטטוסי ההצלחה שלה יוצאי דופן: 0 כשכל המספרים הוסרו, ו-944 כשחלקם לא היו ברשימה מלכתחילה.

להסיר חסימה רק עם הוכחה

עשו זאת כשיש לכם תיעוד של האדם שמבקש לחזור — הרשמה חדשה, בקשה בכתב. reason הוא המקום שבו התיעוד הזה נרשם. חובת ההוכחה לגבי הסכמה מוטלת על השולח, ולכן "הם בטח לא מתנגדים" אינו בסיס להרשמה מחדש של אף אחד.

מנגנוני דיכוי קרובים

לא כל דבר שעוצר הודעה הוא הסרה. כדאי להבחין ביניהם בדיווח שלכם:

מנגנוןמה זה
temp_blהבחירה שלכם לדלג על מי שקיבל הודעה ב-1–14 הימים האחרונים. הגבלת תכיפות, לא הסכמה. סינון של כולם מחזיר סטטוס 715.
סטטוס מסירה 17המנוי חסום להודעות פרסומיות ברמת המפעיל או הרגולטור.
סטטוס מסירה 201נחסם לפי בקשת המנוי עצמו — ההסרה כפי שהיא מופיעה בנתוני המסירה.
סטטוס מסירה 15מכשיר כשר (מסונן). זו אינה סירוב — הגיעו אליו בקול במקום.
סטטוס 8כל היעדים בשליחה היו ברשימת החסימה. בעיית קהל, לא תקלת קוד.

15 מצדיק הדגשה: התייחסות אליו כאל הסרה זורקת לקוח שניתן להשגה. הוא אומר המכשיר הזה לא עושה SMS, ו-tts.type בערך 2 מנתב בדיוק את המספרים האלה לשיחה קולית אוטומטית.

צ'ק-ליסט עובד

  1. כל שליחה שיווקית נושאת add_unsubscribe3 אלא אם יש סיבה אחרת.
  2. ערוץ ה-Push של רשימת החסימה נצרך, ומדכא בכל הערוצים.
  3. 946 ו-944 מטופלים כהצלחה בקוד שלכם.
  4. מילות מפתח נכנסות STOP / הסר מטופלות כמסלול שני, למקרה של 2.
  5. סטטוסי מסירה 201 ו-17 נכתבים בחזרה לרשומת איש הקשר.
  6. סטטוס מסירה 15 מנותב לקול ולא מסומן ככשל.
  7. סטטוס 8 מעלה התראת קהל, לא התראת קוד.
  8. הסרת חסימה דורשת סיבה מתועדת.

הצעד הבא