הודעות נכנסות ו-Push
הודעות נעות בשני הכיוונים. אנשים מגיבים לקמפיינים, שולחים מילות מפתח למספר שלכם, ומבקשים להסירם — ו-TextMe יכולה גם להחזיק את התעבורה הזו כדי שתתשאלו אותה, וגם לדחוף אותה לכתובת שבשליטתכם ברגע שהיא מגיעה.
שתי דרכים לקבל
תשאול — incoming | Push — Push API | |
|---|---|---|
| מי יוזם | אתם, לפי לוח זמנים | TextMe, בזמן ההגעה |
| השהיה | לפי תכיפות התשאול | שניות |
| שלמות | מלאה — אפשר לתשאל כל חלון מחדש | לכל היותר פעם אחת, ומושבת בשקט לאחר כשלים חוזרים |
| דורש כתובת ציבורית | לא | כן |
| הקמה | ללא | מסירת הכתובת ל-TextMe |
רוב האינטגרציות בייצור מפעילות את שניהם: Push להשהיה קצרה, ותשאול תקופתי כרשת ביטחון.
תשאול הודעות נכנסות
<?xml version="1.0" encoding="UTF-8"?>
<incoming>
<user>
<username>Leeroy</username>
</user>
<from>01/11/22 00:00</from>
<to>01/11/22 23:59</to>
</incoming>{
"incoming": {
"user": {
"username": "Leeroy"
},
"from": "01/11/22 00:00",
"to": "01/11/22 23:59"
}
}curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"incoming": {
"user": {
"username": "Leeroy"
},
"from": "01/11/22 00:00",
"to": "01/11/22 23:59"
}
}'// 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({
incoming: {
user: {
username: 'Leeroy',
},
from: '01/11/22 00:00',
to: '01/11/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
// 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' => [
'incoming' => [
'user' => [
'username' => 'Leeroy',
],
'from' => '01/11/22 00:00',
'to' => '01/11/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
use Illuminate\Support\Facades\Http;
$result = Http::withToken(config('services.textme.token'))
->acceptJson()
->post('https://my.textme.co.il/api', [
'incoming' => [
'user' => [
'username' => 'Leeroy',
],
'from' => '01/11/22 00:00',
'to' => '01/11/22 23:59',
],
])
->throw()
->json();
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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={
"incoming": {
"user": {
"username": "Leeroy",
},
"from": "01/11/22 00:00",
"to": "01/11/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)package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
payload, _ := json.Marshal(map[string]any{
"incoming": map[string]any{
"user": map[string]any{
"username": "Leeroy",
},
"from": "01/11/22 00:00",
"to": "01/11/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 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 TextMeIncoming {
public static void main(String[] args) throws Exception {
String body = """
{
"incoming": {
"user": {
"username": "Leeroy"
},
"from": "01/11/22 00:00",
"to": "01/11/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());
}
}// .NET 8+ — System.Net.Http
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var payload = """
{
"incoming": {
"user": {
"username": "Leeroy"
},
"from": "01/11/22 00:00",
"to": "01/11/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);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({
"incoming" => {
"user" => {
"username" => "Leeroy",
},
"from" => "01/11/22 00:00",
"to" => "01/11/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// [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!({
"incoming": {
"user": {
"username": "Leeroy"
},
"from": "01/11/22 00:00",
"to": "01/11/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 version="1.0" encoding="UTF-8"?>
<incoming>
<status>0</status>
<message></message>
<transactions>
<transaction>
<source>05********</source>
<destination>05********</destination>
<message>This is a sample message</message>
<date>03/12/14 16:38</date>
</transaction>
</transactions>
</incoming>{
"status": 0,
"message": "",
"transactions": [
{
"source": "test",
"destination": "9725XXXXXXX",
"message": "test",
"date": "01/05/23 00:00"
}
]
}| שדה | תיאור |
|---|---|
source | המספר ששלח את ההודעה. |
destination | המספר שלכם שאליו היא הגיעה. |
message | הטקסט שהתקבל. |
date | מועד ההגעה, dd/mm/yy hh:mm. |
מגבלות החלון
שבוע אחד לכל בקשה, שמגיע עד שנה אחורה. כמו בדוחות מסירה אין מצביע — החלון הוא העימוד, ולכן עברו על היסטוריה בפלחים שאינם חופפים.
source ו-destination הפוכים ביחס לשליחה
בדרך צאת, source הוא אתם. בדרך כניסה, source הוא הלקוח ו-destination הוא המספר שלכם. בלבול ביניהם הוא באג קלאסי של תו אחד.
קבלת הודעות Push
רשמו כתובת אצל TextMe וכל הודעה נכנסת תישלח אליה ב-POST, בפורמט form-encoded:
POST https://your-app.example.com/textme/incoming
Content-Type: application/x-www-form-urlencoded
message=STOP&date=01/04/26 16:05:05&phone=9725xxxxxxxx&dest=9725xxxxxxxxשמות השדות ב-Push שונים מאלה שבתשאול
גוף ה-Push משתמש ב-phone עבור השולח וב-dest עבור המספר שלכם — במקום ש-incoming המתושאלת קוראת להם source ו-destination. נרמלו בשכבת הקצה ושאר הקוד יוכל להפסיק להתעניין בזה.
import express from 'express'
const app = express()
app.use(express.urlencoded({ extended: false })) // form-encoded, לא JSON
app.post('/textme/incoming', (req, res) => {
// ב-Push הם phone/dest; בדוח המתושאל הם source/destination.
const inbound = {
from: req.body.phone,
to: req.body.dest,
text: req.body.message,
receivedAt: req.body.date,
}
// אשרו קודם — תשובה איטית או כושלת מביאה להשבתת הערוץ.
res.sendStatus(200)
queue.push({ kind: 'incoming', ...inbound })
})from flask import Flask, request
app = Flask(__name__)
@app.post("/textme/incoming")
def incoming():
# ב-Push הם phone/dest; בדוח המתושאל הם source/destination.
queue.put({
"kind": "incoming",
"from": request.form.get("phone"),
"to": request.form.get("dest"),
"text": request.form.get("message"),
"received_at": request.form.get("date"),
})
# אשרו קודם — תשובה איטית או כושלת מביאה להשבתת הערוץ.
return "", 200<?php
// routes/web.php — החריגו את הנתיב מאימות CSRF.
Route::post('/textme/incoming', function (Illuminate\Http\Request $request) {
// ב-Push הם phone/dest; בדוח המתושאל הם source/destination.
HandleInboundMessage::dispatch(
from: $request->input('phone'),
to: $request->input('dest'),
text: $request->input('message'),
receivedAt: $request->input('date'),
);
// בתור, לא מעובד — התשובה חוזרת מיד.
return response()->noContent(200);
});שלושה כללים לכל כתובת Push
- החזירו
200מהר. כל דבר אחר הוא כשל; אחרי כמה כשלים TextMe מפסיקה לשלוח אליכם לגמרי ובשקט. הכניסו לתור, אשרו, עבדו אחר כך. - שום דבר לא מאמת את הבקשה. אין טוקן, אין חתימה. השתמשו בנתיב שלא ניתן לנחש, הגדירו רשימת כתובות מורשות אם אפשר, ואל תתנו ל-Push לבדו להפעיל משהו בלתי הפיך.
- היו אידמפוטנטיים. שליחה חוזרת אינה ניתנת להבחנה מהודעה חדשה. השתמשו ב-
phone+date+messageכמפתח.
טיפול בתשובות
מילות מפתח להסרה
הדבר החשוב ביותר לטפל בו. כשהודעה נושאת add_unsubscribe בערך 2, נמענים מבקשים להסירם בהודעה חוזרת — והתשובות האלה מגיעות בערוץ הזה.
TextMe מוסיפה את המספר לרשימת החסימה שלה אוטומטית, ולכן השליחות שלה נעצרות. מה שהיא לא יכולה לעשות הוא לעדכן את מסד הנתונים שלכם, ולכן איש הקשר ימשיך לקבל את הדואר, ההתראות וכל השאר שאתם שולחים.
const STOP_WORDS = ['stop', 'הסר', 'הסירו', 'unsubscribe']
function isOptOut(text) {
return STOP_WORDS.includes(text.trim().toLowerCase())
}
async function handleInbound({ from, text }) {
if (isOptOut(text)) {
// TextMe כבר עצרה את השליחות שלה — זה נוגע לכל שאר הערוצים.
await contacts.suppress(from, { reason: 'sms-opt-out', channel: 'all' })
return
}
await conversations.record(from, text)
}STOP_WORDS = {"stop", "הסר", "הסירו", "unsubscribe"}
def is_opt_out(text):
return text.strip().lower() in STOP_WORDS
def handle_inbound(sender, text):
if is_opt_out(text):
# TextMe כבר עצרה את השליחות שלה — זה מכסה כל ערוץ אחר.
contacts.suppress(sender, reason="sms-opt-out", channel="all")
return
conversations.record(sender, text)בנוסף קיים ערוץ Push נפרד לרשימת החסימה שנשלח בכל פעם שמנוי נחסם, בכל דרך שזה קרה — לחיצה על קישור, תשובה, או פעולה יזומה. צריכת הערוץ הזה אמינה יותר מהתאמת מילות מפתח, כי הוא תופס גם הסרות שלא מופיעות כלל כהודעה נכנסת.
פורמטי המספרים שונים בין הכיוונים
phone נכנס מגיע בצורה בינלאומית — 9725xxxxxxxx. הפורמט שאתם שולחים אליו הוא מקומי — 05xxxxxxx או 5xxxxxxx. הם לא יתאימו בהשוואת מחרוזות נאיבית מול טבלת אנשי הקשר שלכם.
// נכנס הוא 9725xxxxxxxx; אתם שולחים אל 05xxxxxxxx. נרמלו לפני התאמה.
function toLocal(phone) {
const digits = String(phone).replace(/\D/g, '')
if (digits.startsWith('972')) return `0${digits.slice(3)}`
if (digits.startsWith('0')) return digits
return `0${digits}`
}
const contact = await contacts.findByPhone(toLocal(inbound.from))import re
def to_local(phone):
"""נכנס הוא 9725xxxxxxxx; אתם שולחים אל 05xxxxxxxx."""
digits = re.sub(r"\D", "", str(phone))
if digits.startswith("972"):
return "0" + digits[3:]
if digits.startswith("0"):
return digits
return "0" + digits
contact = contacts.find_by_phone(to_local(inbound["from"]))שיחות דו-כיווניות
ל-TextMe אין מושג של שיחה או שרשור: הודעה נכנסת היא אירוע עומד בפני עצמו שנושא מספר טלפון וחותמת זמן. השרשור הוא באחריותכם — השתמשו במספר המנורמל כמפתח, והחליטו על זמן ההמתנה שאחריו הודעה חדשה מתחילה שיחה חדשה.
כדי להשיב, שלחו sms רגילה עם source שמוגדר למספר שאליו הם כתבו, כך שההתכתבות תישאר בשרשור אחד במכשיר.
סריקת רשת ביטחון
Push הוא "לכל היותר פעם אחת". כל מה שהגיע בזמן שהכתובת שלכם לא הייתה תקינה לא נשלח מחדש, ולכן שלבו אותו עם תשאול תקופתי:
// רשת ביטחון שעתית לכל מה ש-Push פספס.
const report = await textme({
incoming: {
user: { username: 'Leeroy' },
from: format(hourAgo, 'dd/MM/yy HH:mm'),
to: format(now, 'dd/MM/yy HH:mm'),
},
})
for (const t of report.transactions ?? []) {
// אידמפוטנטי לפי phone + date + text, ולכן ראייה חוזרת לא עושה כלום.
await inbox.record({ from: t.source, to: t.destination, text: t.message, at: t.date })
}# רשת ביטחון שעתית לכל מה ש-Push פספס.
report = textme({
"incoming": {
"user": {"username": "Leeroy"},
"from": hour_ago.strftime("%d/%m/%y %H:%M"),
"to": now.strftime("%d/%m/%y %H:%M"),
}
})
for t in report.get("transactions", []):
# אידמפוטנטי לפי phone + date + text, ולכן ראייה חוזרת לא עושה כלום.
inbox.record(sender=t["source"], to=t["destination"], text=t["message"], at=t["date"])מכיוון שהתשאול וה-Push חולקים על שמות השדות, נרמלו את שניהם למבנה פנימי אחד בשכבת הקצה — כפי ששתי הדוגמאות למעלה עושות.
הצעד הבא
- הסרה ורגולציה — מה לעשות כשיודעים שמישהו רוצה לצאת
- Push API — כל שלושת הערוצים ודוגמאות מקבל בשמונה שפות
- מעקב אחר מסירה — החצי היוצא של אותו מחזור

