Skip to content

קמפיינים ותזמון

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

מתן שם לקמפיין

campaign_name הוא אופציונלי, עד 50 תווים, וזמין גם ב-sms וגם ב-bulk.

xml
<campaign_name>august-newsletter-2026-08-17</campaign_name>
json
{ "campaign_name": "august-newsletter-2026-08-17" }

הפכו את השמות לייחודיים לכל ריצה

ביטול לפי שם מבטל כל קמפיין ממתין שתואם — בלי אישור, בלי הרצה יבשה. שם כמו newsletter יוריד יום אחד שליחה שלא התכוונתם לגעת בה.

שימו את התאריך, או מזהה ריצה, בשם: newsletter-2026-08-17 ולא newsletter.

בשימוש נכון, אותה התנהגות עצמה היא היתרון. ריצת bulk גדולה שפוצלה לכמה קריאות חולקת campaign_name אחד, וביטול אחד עוצר את כולה.

תזמון שליחה

timing דוחה שליחה. הפורמט הוא dd/mm/yy hh:mm — שנה בשתי ספרות, שעון 24 שעות:

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"
  }
}

השמיטו את timing וההודעה תצא מיד — מה שגם אומר שהיא תעבור את נקודת הביטול לפני שהספקתם לקרוא. התזמון הוא מה שקונה לכם את החלון לשנות את דעתכם.

פרמוט חותמת הזמן

dd/mm/yy, לא mm/dd/yy ולא ISO. 18/08/26 08:00 הוא 18 באוגוסט 2026. בנו אותה במפורש ולא בהסתמכות על ברירת מחדל של locale:

שפהמחרוזת הפורמט
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")

שני דברים נוספים קובעים מתי הודעה באמת יוצאת:

  • שעות שליחה מותרות. לחשבונות יש חלון; שליחה מחוצה לו מחזירה סטטוס 5. שליחה מתוזמנת לשעה מחוץ לחלון היא לא רעיון טוב.
  • temp_bl מדלג על יעדים שקיבלו הודעה ב-1–14 הימים האחרונים. הוא מוערך בזמן השליחה ולא בזמן התזמון — כך שנמען ששומע מכם בין התזמון לשליחה עדיין יסונן.

ב-bulk, timing הוא שדה ברמת האצווה, מחוץ ל-messages. אי אפשר לתזמן הודעות בודדות באצווה בנפרד; שעות שונות פירושן קריאות שונות.

SOAP מתנהג אחרת

sendBulkSms דרך SOAP מתעלם מ-timing לחלוטין. רק פעולת bulk בנקודת הקצה הראשית מכבדת אותו.

ביטול

לפי מזהה

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+ / דפדפנים — ללא תלויות
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()

// גם שגיאה חוזרת כ-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' => [
        'cancel' => [
            'user' => [
                'username' => 'Leeroy',
            ],
            'campaign_id' => '1234',
        ],
    ],
]);

$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', [
        'cancel' => [
            'user' => [
                'username' => 'Leeroy',
            ],
            'campaign_id' => '1234',
        ],
    ])
    ->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={
        "cancel": {
            "user": {
                "username": "Leeroy",
            },
            "campaign_id": "1234",
        },
    },
)
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{
		"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)
	}

	// גם שגיאה חוזרת כ-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 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());

        // גם שגיאה חוזרת כ-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 = """
    {
      "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();

// גם שגיאה חוזרת כ-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({
  "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)

# גם שגיאה חוזרת כ-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!({
          "cancel": {
            "user": {
              "username": "Leeroy"
            },
            "campaign_id": "1234"
          }
        }))
        .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"?>
<cancel>
    <status>0</status>
    <message>Campaign successfuly cancel</message>
</cancel>
json
{
  "status": 0,
  "message": "Campaign successfuly cancel"
}

לפי שם

מבטל כל מה שממתין תחת השם הזה, ומדווח כמה:

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+ / דפדפנים — ללא תלויות
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()

// גם שגיאה חוזרת כ-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' => [
        'cancel' => [
            'user' => [
                'username' => 'Leeroy',
            ],
            'campaign_name' => 'My Campaign',
        ],
    ],
]);

$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', [
        'cancel' => [
            'user' => [
                'username' => 'Leeroy',
            ],
            'campaign_name' => 'My Campaign',
        ],
    ])
    ->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={
        "cancel": {
            "user": {
                "username": "Leeroy",
            },
            "campaign_name": "My Campaign",
        },
    },
)
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{
		"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)
	}

	// גם שגיאה חוזרת כ-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 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());

        // גם שגיאה חוזרת כ-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 = """
    {
      "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();

// גם שגיאה חוזרת כ-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({
  "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)

# גם שגיאה חוזרת כ-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!({
          "cancel": {
            "user": {
              "username": "Leeroy"
            },
            "campaign_name": "My Campaign"
          }
        }))
        .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"?>
<cancel>
    <status>0</status>
    <message>Campaigns successfully cancel</message>
    <count>1</count>
</cancel>
json
{
  "status": 0,
  "message": "Campaigns successfully cancel",
  "count": 1
}

count הוא כמה נעצרו. 0 עם status: 0 אומר שלא נמצאה התאמה — או שהשם שגוי, או שהקמפיינים כבר יצאו.

קריאת התוצאה

Statusמשמעותמה לעשות
0בוטל.
955כבר בוטל.התייחסו כהצלחה; המצב הסופי הוא מה שרציתם.
966כבר נשלח.מאוחר מדי. אין מה לעשות מלבד לתעד.
970הביטול נכשל בצד השרת.נסו שוב פעם אחת, ואז פנו לתמיכה.
977מזהה שגוי, או שהוא שייך לחשבון אחר.בדקו את המזהה.
js
async function cancelCampaign(name) {
  const result = await textme({
    cancel: { user: { username: 'Leeroy' }, campaign_name: name },
  })

  // 955 אומר שהוא כבר בוטל — המצב שביקשתם, בכל מקרה.
  if (Number(result.status) === 955) return { cancelled: 0, alreadyDone: true }

  // 966 אומר שהוא יצא. לא ניתן לשחזור, אבל גם לא באג.
  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 אומר שהוא כבר בוטל — המצב שביקשתם, בכל מקרה.
    if status == 955:
        return {"cancelled": 0, "already_done": True}

    # 966 אומר שהוא יצא. לא ניתן לשחזור, אבל גם לא באג.
    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))}

תבנית בטוחה לשליחה מתוזמנת

תזמנו מוקדם מספיק כדי שביטול יהיה אפשרי באמת, ותנו שם לריצה כך שהביטול יהיה קריאה אחת:

js
const runId = `newsletter-${new Date().toISOString().slice(0, 10)}`

// 1. האם יש מספיק יתרה לכל הקהל?
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. אמתו את גוף הבקשה המדויק מול /api/test לפני שמתחייבים אליו.
await textmeTest(payload(runId))

// 3. תזמנו אותו, עם מזהה הריצה כשם הקמפיין.
const { shipment_id } = await textme(payload(runId))
await db.recordScheduledRun({ runId, shipmentId: shipment_id })

// בהמשך, אם משהו לא בסדר: קריאה אחת עוצרת כל פלח של הריצה.
await textme({ cancel: { user: { username: 'Leeroy' }, campaign_name: runId } })
python
from datetime import date

run_id = f"newsletter-{date.today().isoformat()}"

# 1. האם יש מספיק יתרה לכל הקהל?
balance = textme({"balance": {"user": {"username": "Leeroy"}}})
if int(balance["balance"]) < len(audience):
    raise RuntimeError(f"Need {len(audience)} credits, have {balance['balance']}")

# 2. אמתו את גוף הבקשה המדויק מול /api/test לפני שמתחייבים אליו.
textme_test(payload(run_id))

# 3. תזמנו אותו, עם מזהה הריצה כשם הקמפיין.
result = textme(payload(run_id))
db.record_scheduled_run(run_id=run_id, shipment_id=result["shipment_id"])

# בהמשך, אם משהו לא בסדר: קריאה אחת עוצרת כל פלח של הריצה.
textme({"cancel": {"user": {"username": "Leeroy"}, "campaign_name": run_id}})

קמפייני יום הולדת

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

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+ / דפדפנים — ללא תלויות
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()

// גם שגיאה חוזרת כ-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' => [
        'get_birthday_campaigns' => [
            'user' => [
                'username' => 'XXXXXX',
            ],
        ],
    ],
]);

$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', [
        'get_birthday_campaigns' => [
            'user' => [
                'username' => 'XXXXXX',
            ],
        ],
    ])
    ->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={
        "get_birthday_campaigns": {
            "user": {
                "username": "XXXXXX",
            },
        },
    },
)
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{
		"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)
	}

	// גם שגיאה חוזרת כ-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 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());

        // גם שגיאה חוזרת כ-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 = """
    {
      "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();

// גם שגיאה חוזרת כ-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({
  "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)

# גם שגיאה חוזרת כ-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!({
          "get_birthday_campaigns": {
            "user": {
              "username": "XXXXXX"
            }
          }
        }))
        .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"?>
<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": ""
      }
    ]
  }
}

שמות השדות כאן באותיות גדולות

CAMPAIGN_ID, CAMPAIGN_NAME, CREATED_ON, ACTIVE_DESTINATIONS — בשונה מסגנון האותיות הקטנות שנהוג בכל שאר ה-API.

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

עדכון גוף ההודעה:

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+ / דפדפנים — ללא תלויות
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()

// גם שגיאה חוזרת כ-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' => [
        'edit_birthday_campaign' => [
            'user' => [
                'username' => 'XXXX',
            ],
            'campaign_id' => 'XXXX',
            'message' => 'XXXX',
        ],
    ],
]);

$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', [
        'edit_birthday_campaign' => [
            'user' => [
                'username' => 'XXXX',
            ],
            'campaign_id' => 'XXXX',
            'message' => 'XXXX',
        ],
    ])
    ->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={
        "edit_birthday_campaign": {
            "user": {
                "username": "XXXX",
            },
            "campaign_id": "XXXX",
            "message": "XXXX",
        },
    },
)
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{
		"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)
	}

	// גם שגיאה חוזרת כ-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 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());

        // גם שגיאה חוזרת כ-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 = """
    {
      "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();

// גם שגיאה חוזרת כ-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({
  "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)

# גם שגיאה חוזרת כ-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!({
          "edit_birthday_campaign": {
            "user": {
              "username": "XXXX"
            },
            "campaign_id": "XXXX",
            "message": "XXXX"
          }
        }))
        .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"?>
<sms>
    <status>0</status>
    <message>birthday campaign successfully updated</message>
</sms>
json
{
  "status": 0,
  "message": "birthday campaign successfully updated"
}

רק הגוף משתנה. התזמון, הקהל והשם נשארים כפי שהם, ואין פעולת API לשינוי שלהם.

איזו ידית לשמור?

שליחה מחזירה shipment_id; הביטול מקבל campaign_id. בפועל שניהם מזהים את אותו קמפיין, וביטול לפי שם הוא ההרגל העמיד יותר:

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

שמרו את ה-shipment_id בכל מקרה — הוא מופיע בדוחות מסירה, ושם תרצו אותו.

הצעד הבא