---
url: https://textme-docs.matat.io/he/use-cases/incoming-and-push.md
description: >-
  קבלת הודעות נכנסות בתשאול או ב-Push, טיפול במילות הסרה, ונרמול פורמטי המספרים
  שנבדלים בין הכיוונים.
---

# הודעות נכנסות ו-Push

הודעות נעות בשני הכיוונים. אנשים מגיבים לקמפיינים, שולחים מילות מפתח למספר שלכם, ומבקשים להסירם. ו-TextMe יכולה גם להחזיק את התעבורה הזו כדי שתתשאלו אותה, וגם לדחוף אותה לכתובת שבשליטתכם ברגע שהיא מגיעה.

## שתי דרכים לקבל

| | תשאול ([`incoming`](../endpoints/reports.md) | Push) [Push API](../endpoints/push.md) |
|---|---|---|
| מי יוזם | אתם, לפי לוח זמנים | TextMe, בזמן ההגעה |
| השהיה | לפי תכיפות התשאול | שניות |
| שלמות | מלאה. אפשר לתשאל כל חלון מחדש | לכל היותר פעם אחת, ומושבת בשקט לאחר כשלים חוזרים |
| דורש כתובת ציבורית | לא | כן |
| הקמה | ללא | מסירת הכתובת ל-TextMe |

רוב האינטגרציות בייצור מפעילות את **שניהם**: Push להשהיה קצרה, ותשאול תקופתי כרשת ביטחון.

## תשאול הודעות נכנסות

::: code-group

```xml [XML]
<?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>
```

```json [JSON]
{
  "incoming": {
    "user": {
      "username": "Leeroy"
    },
    "from": "01/11/22 00:00",
    "to": "01/11/22 23:59"
  }
}
```

```bash [cURL]
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"
  }
}'
```

```js [JavaScript]
// 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 [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' => [
        '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 [Laravel]
<?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);
```

```python [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={
        "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)
```

```go [Go]
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 [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 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());
    }
}
```

```csharp [C#]
// .NET 8+ using 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);
```

```ruby [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({
  "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
```

```rust [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!({
          "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(())
}
```

:::

::: code-group

```xml [XML]
<?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>
```

```json [JSON]
{
  "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`. |

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

::: tip `source` ו-`destination` הפוכים ביחס לשליחה {#number-formats-differ-between-directions}
בדרך צאת, `source` הוא *אתם*. בדרך כניסה, `source` הוא *הלקוח* ו-`destination` הוא המספר שלכם. בלבול ביניהם הוא באג קלאסי של תו אחד.
:::

## קבלת הודעות Push

רשמו כתובת אצל TextMe וכל הודעה נכנסת תישלח אליה ב-POST, בפורמט form-encoded:

```http
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
```

::: warning שמות השדות ב-Push שונים מאלה שבתשאול
גוף ה-Push משתמש ב-`phone` עבור השולח וב-`dest` עבור המספר שלכם. במקום ש-`incoming` המתושאלת קוראת להם `source` ו-`destination`. נרמלו בשכבת הקצה ושאר הקוד יוכל להפסיק להתעניין בזה.
:::

::: code-group

```js [JavaScript]
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 })
})
```

```python [Python]
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 [Laravel]
<?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);
});
```

:::

::: danger שלושה כללים לכל כתובת Push

1. **החזירו `200` מהר.** כל דבר אחר הוא כשל; אחרי כמה כשלים TextMe מפסיקה לשלוח אליכם *לגמרי ובשקט*. הכניסו לתור, אשרו, עבדו אחר כך.
2. **שום דבר לא מאמת את הבקשה.** אין טוקן, אין חתימה. השתמשו בנתיב שלא ניתן לנחש, הגדירו רשימת כתובות מורשות אם אפשר, ואל תתנו ל-Push לבדו להפעיל משהו בלתי הפיך.
3. **היו אידמפוטנטיים.** שליחה חוזרת אינה ניתנת להבחנה מהודעה חדשה. השתמשו ב-`phone` + `date` + `message` כמפתח.
   :::

## טיפול בתשובות

### מילות מפתח להסרה {#opt-out-keywords}

הדבר החשוב ביותר לטפל בו. כשהודעה נושאת [`add_unsubscribe`](../endpoints/send.md#opt-out-footers) בערך `2`, נמענים מבקשים להסירם בהודעה חוזרת, והתשובות האלה מגיעות בערוץ הזה.

TextMe מוסיפה את המספר ל[רשימת החסימה](../endpoints/blacklist.md) שלה אוטומטית, ולכן *השליחות שלה* נעצרות, מה שהיא לא יכולה לעשות הוא לעדכן את מסד הנתונים *שלכם*, ולכן איש הקשר ימשיך לקבל את הדואר, ההתראות וכל השאר שאתם שולחים.

::: code-group

```js [JavaScript]
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)
}
```

```python [Python]
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 נפרד לרשימת החסימה](../endpoints/push.md#post-blocklist-additions) שנשלח בכל פעם שמנוי נחסם, בכל דרך שזה קרה. לחיצה על קישור, תשובה, או פעולה יזומה. צריכת הערוץ הזה אמינה יותר מהתאמת מילות מפתח, כי הוא תופס גם הסרות שלא מופיעות כלל כהודעה נכנסת.

### פורמטי המספרים שונים בין הכיוונים

`phone` נכנס מגיע בצורה בינלאומית (`9725xxxxxxxx`. הפורמט שאתם *שולחים* אליו הוא מקומי) `05xxxxxxx` או `5xxxxxxx`. הם לא יתאימו בהשוואת מחרוזות נאיבית מול טבלת אנשי הקשר שלכם.

::: code-group

```js [JavaScript]
// נכנס הוא 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))
```

```python [Python]
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`](../endpoints/send.md) רגילה עם `source` שמוגדר למספר שאליו הם כתבו, כך שההתכתבות תישאר בשרשור אחד במכשיר.

## סריקת רשת ביטחון

Push הוא "לכל היותר פעם אחת". כל מה שהגיע בזמן שהכתובת שלכם לא הייתה תקינה לא נשלח מחדש, ולכן שלבו אותו עם תשאול תקופתי:

::: code-group

```js [JavaScript]
// רשת ביטחון שעתית לכל מה ש-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 })
}
```

```python [Python]
# רשת ביטחון שעתית לכל מה ש-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 חולקים על שמות השדות, נרמלו את שניהם למבנה פנימי אחד בשכבת הקצה. כפי ששתי הדוגמאות למעלה עושות.

## הצעד הבא

* **[הסרה ורגולציה](./opt-out-and-compliance.md)**: מה לעשות כשיודעים שמישהו רוצה לצאת
* **[Push API](../endpoints/push.md)**: כל שלושת הערוצים ודוגמאות מקבל בשמונה שפות
* **[מעקב אחר מסירה](./delivery-reports.md)**: החצי היוצא של אותו מחזור
