Skip to content

Push API ​

ההפך משאר התיעוד הזה: במקום שאתם תקראו ל-TextMe, TextMe קוראת לכתובת שבשליטתכם. חמישה ערוצים יכולים להישלח ב-Push, מה שמייתר את הצורך לתשאל את הדוחות כליל.

אתם רושמים כתובת בעצמכם, או עם הפעולה push_url או בממשק הניהול תחת הגדרות, Push API. אין צורך לפנות ל-TextMe.

רישום כתובת ​

כותרת העמוד היא URL assignment to. בחרו משתמש ברשימת ה-Users בצד, ואז השתמשו ב-Addition of URL. הכתובות הרשומות מופיעות בטבלה עם עמודת Service, כלומר כתובת משויכת לערוץ מסוים ולא קולטת את כל הערוצים יחד, ולמשתמש יכולה להיות יותר מכתובת אחת.

תפריט URL type מציע בדיוק חמישה ערכים:

URL typeהערוץ
dlrדוחות מסירה של SMS
incoming_smsSMS נכנס
blacklistהוספות לרשימת החסימה
dlr_whatsappדוחות מסירה של WhatsApp
incoming_whatsappWhatsApp נכנס

שני ערוצי ה-WhatsApp הם רישומים נפרדים. כתובת dlr אינה מקבלת דוחות מסירה של WhatsApp.

הכתובות הן לכל משתמש, לא לכל חשבון

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

בטבלה יש גם עמודת Status ועמודת Actions, ושם מופיע ערוץ שכובה אחרי כשלי מסירה חוזרים, ושם מטפלים בו.

רישום עם push_url ​

עושה בדיוק מה שעושה מסך הקונסולה שלמעלה, בלי לפתוח אותו. קריאה אחת רושמת כתובת אחת עבור ערוץ אחד.

http
POST https://my.textme.co.il/api

פרמטרים ​

שםסוגתיאורחובה
push_urlobjectמכיל את כל האלמנטים האחרים.✔️
userobjectמכיל את אלמנט המשתמש.✔️
usernamestringשם המשתמש של החשבון שבו אתם מזוהים במערכת.✔️
typestringהערוץ שברצונכם לקבל בכתובת הזו. אחד מתוך dlr, incoming_sms, blacklist, dlr_whatsapp, incoming_whatsapp.✔️
urlstringהכתובת שאליה נשלח. חייבת להיות כתובת http או https ציבורית.✔️

דוגמת בקשה ​

xml
<?xml version="1.0" encoding="UTF-8"?>
<push_url>
    <user>
        <username>Leeroy</username>
    </user>
    <type>dlr</type>
    <url>https://www.example.com/path/to/resource</url>
</push_url>
json
{
  "push_url": {
    "user": {
      "username": "Leeroy"
    },
    "type": "dlr",
    "url": "https://www.example.com/path/to/resource"
  }
}
bash
curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
  "push_url": {
    "user": {
      "username": "Leeroy"
    },
    "type": "dlr",
    "url": "https://www.example.com/path/to/resource"
  }
}'
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({
    push_url: {
      user: {
        username: 'Leeroy',
      },
      type: 'dlr',
      url: 'https://www.example.com/path/to/resource',
    },
  }),
})

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' => [
        'push_url' => [
            'user' => [
                'username' => 'Leeroy',
            ],
            'type' => 'dlr',
            'url' => 'https://www.example.com/path/to/resource',
        ],
    ],
]);

$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', [
        'push_url' => [
            'user' => [
                'username' => 'Leeroy',
            ],
            'type' => 'dlr',
            'url' => 'https://www.example.com/path/to/resource',
        ],
    ])
    ->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={
        "push_url": {
            "user": {
                "username": "Leeroy",
            },
            "type": "dlr",
            "url": "https://www.example.com/path/to/resource",
        },
    },
)
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{
		"push_url": map[string]any{
			"user": map[string]any{
				"username": "Leeroy",
			},
			"type": "dlr",
			"url": "https://www.example.com/path/to/resource",
		},
	})

	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 TextMePushUrlAdd {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "push_url": {
                "user": {
                  "username": "Leeroy"
                },
                "type": "dlr",
                "url": "https://www.example.com/path/to/resource"
              }
            }
            """;

        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+ using System.Net.Http
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

var payload = """
    {
      "push_url": {
        "user": {
          "username": "Leeroy"
        },
        "type": "dlr",
        "url": "https://www.example.com/path/to/resource"
      }
    }
    """;

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({
  "push_url" => {
    "user" => {
      "username" => "Leeroy",
    },
    "type" => "dlr",
    "url" => "https://www.example.com/path/to/resource",
  },
})

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!({
          "push_url": {
            "user": {
              "username": "Leeroy"
            },
            "type": "dlr",
            "url": "https://www.example.com/path/to/resource"
          }
        }))
        .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>push url has successfully added</message>
</sms>
json
{
  "status": 0,
  "message": "push url has successfully added"
}

הפעולה מוסיפה כתובת. היא לעולם אינה משנה כתובת קיימת.

אם לחשבון כבר יש כתובת עבור אותו type, הקריאה מחזירה סטטוס 517 ושום דבר לא משתנה. הכתובת הישנה נשארת בדיוק במקומה.

כדי להעביר ערוץ למקום אחר, מחקו קודם את הרשומה הקיימת בממשק הניהול, ורק אז קראו ל-push_url.

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

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

איך הודעת Push מגיעה ​

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

כל ערוץ שולח את אותם שמות שדות כמו בתשובת ה-XML המתושאלת, בצורה שטוחה: שדות טופס, לא XML ולא JSON. קראו אותם בדיוק כפי שאתם קוראים שליחת טופס HTML.

שום דבר לא מאמת את הבקשה

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

  • השתמשו בנתיב שלא ניתן לנחש, והתייחסו אליו כאל פרט גישה.
  • אם אפשר, הגדירו רשימת כתובות המקור של TextMe בשכבת הקצה.
  • הפכו את הטיפול לאידמפוטנטי (לפי external_id או לפי phone + date) כי הודעה שנשלחה שוב אינה ניתנת להבחנה מהודעה חדשה.
  • אף פעם לא לפעול על סמך Push לבדו במשהו בלתי הפיך; אמתו קודם מול dlr.

החזירו 200, ומהר

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

אשרו קודם, עבדו אחר כך: כתבו את גוף הבקשה לתור, החזירו 200, ובצעו את העבודה האמיתית מחוץ לבקשה.

כתובות המקור ​

כל הערוצים נשלחים מהכתובות האלה:

46.31.96.138
46.31.96.222
46.31.97.35
46.31.97.108
46.31.97.109
46.31.97.205

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

מה להתיר, את המארחים או את הבלוקים ​

שש הכתובות יושבות בשני בלוקי /24 סמוכים בשני אתרים, ומכאן שיש שש ולא אחת:

בלוקאתרהכתובות ברשימה
46.31.96.0/24Telzar 019 Infrastructure, פתח תקווה.138, .222
46.31.97.0/24Telzar 019 Infrastructure, חיפה.35, .108, .109, .205

שניהם רשומים כ-ASSIGNED PA ל-TELZAR-INFRA1, מתוחזקים על ידי Telzar-MNT, בתוך 46.31.96.0/21 שמוכרז על ידי AS51825 עם רשומת RPKI ROA.

מכאן שיש לכם בחירה:

  • שש כתובות ה-/32 מדויקות, והכלל ההדוק ביותר שאפשר לכתוב. הן גם מארחים בודדים, ולא מפורסמת תקופת הודעה מוקדמת לשינוי שלהן.
  • שני בלוקי ה-/24 הם הקצאות רישום. הם אינם זזים אלא אם ההקצאה עצמה משתנה, ולכן הם שורדים החלפת מספור של מארח. המחיר הוא שאתם מקבלים גם מארחי תשתית אחרים של Telzar בבלוקים האלה.

בחרו ב-/32 אם אתם יכולים לחיות עם בדיקה חוזרת של הרשימה, וב-/24 אם שקט בלתי מוסבר בדוחות המסירה שלכם גרוע יותר מכלל מעט רחב יותר.

לא מפורסמת תקופת הודעה מוקדמת לשינוי הרשימה

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

POST דוחות מסירה ​

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

שדהתיאור
external_idהמזהה שהגדרתם על אלמנט ה-<phone> בזמן השליחה.
statusסטטוס המסירה. ראו סטטוסי מסירה.
he_messageהסטטוס בעברית.
en_messageהסטטוס באנגלית.
dateמועד רישום הסטטוס, dd/mm/yy hh:mm:ss.
phoneהיעד, בצורה בינלאומית, לדוגמה 9725xxxxxxxx.
operaorהמפעיל שטיפל בו. מאויית בלי ה-t: ראו את ההערה למטה.
shipment_idהקמפיין שההודעה שייכת לו.

כ-URL, המקבילה נראית כך:

apache
http://your-app.example.com/textme/dlr?external_id=1234&status=102&he_message=הגיע+ליעד&en_message=Delivered
&date=01/04/14 16:05:05&phone=9725xxxxxxxx&operaor=Telzar&shipment_id=xxxxxxxxx

operaor, לא operator

שדה המפעיל מאויית שגוי בגוף ה-Push. תשובת dlr המתושאלת מאייתת אותו operator. קוד שקורא דוחות משני המקורות צריך לקבל את שני האיותים. קריאת operator בלבד מ-Push תחזיר כלום בשקט.

POST הודעות נכנסות ​

נשלח כשמישהו שולח הודעה לאחד המספרים שלכם. אותו מידע ש-incoming מחזירה.

שדהתיאור
messageהטקסט שהתקבל.
dateמועד ההגעה, dd/mm/yy hh:mm:ss.
phoneהמספר ששלח אותה.
destהמספר שלכם שאליו היא הגיעה.
apache
http://your-app.example.com/textme/incoming?message=This+is+a+sample+message&date=01/04/14 16:05:05&phone=9725xxxxxxxx&dest=9725xxxxxxxx

POST הוספות לרשימת החסימה ​

נשלח כשמנוי נחסם. בדרך כלל מפני שביקש להסירו מהודעה שנשאה add_unsubscribe.

שדהתיאור
messageהערה על החסימה, בעברית. לדוגמה נחסם מנוי.
dateמועד ההתרחשות, dd/mm/yy hh:mm:ss.
destהמספר שנחסם.
apache
http://your-app.example.com/textme/blacklist?message=נחסם+מנוי&date=01/04/14 16:05:05&dest=9725xxxxxxxx

זה אות ההסרה שכדאי לחבר קודם

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

קבלת הודעת Push ​

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

js
// Express. שימו לב ל-express.urlencoded, לא express.json
import express from 'express'

const app = express()
app.use(express.urlencoded({ extended: false }))

app.post('/textme/dlr', (req, res) => {
  const { external_id, status, en_message, date, phone, shipment_id } = req.body

  // `operaor` הוא האיות ב-Push; `operator` הוא האיות בדוח המתושאל.
  const carrier = req.body.operaor ?? req.body.operator

  // אשרו קודם. כל דבר איטי יותר מסתכן במחזור ניסיונות ואז השבתה.
  res.sendStatus(200)

  queue.push({ external_id, status, en_message, date, phone, carrier, shipment_id })
})

app.post('/textme/incoming', (req, res) => {
  const { message, date, phone, dest } = req.body
  res.sendStatus(200)
  queue.push({ kind: 'incoming', message, date, from: phone, to: dest })
})

app.post('/textme/blacklist', (req, res) => {
  const { dest, date } = req.body
  res.sendStatus(200)
  queue.push({ kind: 'opt-out', phone: dest, date })
})
php
<?php
// PHP רגיל. הגוף הוא form-encoded, ולכן הוא מגיע ל-$_POST.

$report = [
    'external_id' => $_POST['external_id'] ?? null,
    'status' => $_POST['status'] ?? null,
    'he_message' => $_POST['he_message'] ?? null,
    'en_message' => $_POST['en_message'] ?? null,
    'date' => $_POST['date'] ?? null,
    'phone' => $_POST['phone'] ?? null,
    // `operaor` הוא האיות ב-Push; `operator` הוא האיות בדוח המתושאל.
    'carrier' => $_POST['operaor'] ?? $_POST['operator'] ?? null,
    'shipment_id' => $_POST['shipment_id'] ?? null,
];

// אשרו לפני ביצוע עבודה אמיתית.
http_response_code(200);
header('Content-Length: 0');
header('Connection: close');
flush();

queue_delivery_report($report);
php
<?php

// routes/web.php. החריגו את הנתיב מאימות CSRF.
Route::post('/textme/dlr', function (Illuminate\Http\Request $request) {
    ProcessDeliveryReport::dispatch([
        'external_id' => $request->input('external_id'),
        'status' => $request->input('status'),
        'en_message' => $request->input('en_message'),
        'date' => $request->input('date'),
        'phone' => $request->input('phone'),
        // `operaor` הוא האיות ב-Push; `operator` הוא האיות המתושאל.
        'carrier' => $request->input('operaor', $request->input('operator')),
        'shipment_id' => $request->input('shipment_id'),
    ]);

    // בתור, לא מעובד, התשובה חוזרת מיד.
    return response()->noContent(200);
});

Route::post('/textme/blacklist', function (Illuminate\Http\Request $request) {
    SuppressContact::dispatch($request->input('dest'), $request->input('date'));

    return response()->noContent(200);
});
python
# Flask, request.form, לא request.json
from flask import Flask, request

app = Flask(__name__)


@app.post("/textme/dlr")
def delivery_report():
    form = request.form
    queue.put({
        "external_id": form.get("external_id"),
        "status": form.get("status"),
        "en_message": form.get("en_message"),
        "date": form.get("date"),
        "phone": form.get("phone"),
        # `operaor` הוא האיות ב-Push; `operator` הוא האיות המתושאל.
        "carrier": form.get("operaor") or form.get("operator"),
        "shipment_id": form.get("shipment_id"),
    })

    # אשרו מיד; ה-worker עושה את השאר.
    return "", 200


@app.post("/textme/blacklist")
def opt_out():
    queue.put({"kind": "opt-out", "phone": request.form.get("dest")})
    return "", 200
go
package main

import (
	"log"
	"net/http"
)

func deliveryReport(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		// עדיין מאשרים: תשובה שאינה 200 מתחילה מחזור ניסיונות ואז השבתה.
		w.WriteHeader(http.StatusOK)
		log.Println("textme: unparsable push:", err)
		return
	}

	// `operaor` הוא האיות ב-Push; `operator` הוא האיות בדוח המתושאל.
	carrier := r.FormValue("operaor")
	if carrier == "" {
		carrier = r.FormValue("operator")
	}

	report := map[string]string{
		"external_id": r.FormValue("external_id"),
		"status":      r.FormValue("status"),
		"en_message":  r.FormValue("en_message"),
		"date":        r.FormValue("date"),
		"phone":       r.FormValue("phone"),
		"carrier":     carrier,
		"shipment_id": r.FormValue("shipment_id"),
	}

	w.WriteHeader(http.StatusOK)
	go enqueue(report)
}

func main() {
	http.HandleFunc("/textme/dlr", deliveryReport)
	log.Fatal(http.ListenAndServe(":8080", nil))
}
csharp
// ASP.NET Core minimal API
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/textme/dlr", async (HttpRequest request, IReportQueue queue) =>
{
    var form = await request.ReadFormAsync();

    // `operaor` הוא האיות ב-Push; `operator` הוא האיות בדוח המתושאל.
    var carrier = form["operaor"].FirstOrDefault() ?? form["operator"].FirstOrDefault();

    // מעבירים לתור ולא מעבדים. התשובה לא צריכה לחכות לעבודה.
    queue.Enqueue(new DeliveryReport(
        ExternalId: form["external_id"],
        Status: form["status"],
        EnMessage: form["en_message"],
        Date: form["date"],
        Phone: form["phone"],
        Carrier: carrier,
        ShipmentId: form["shipment_id"]));

    return Results.Ok();
});

app.Run();
ruby
require "sinatra"

post "/textme/dlr" do
  # `operaor` הוא האיות ב-Push; `operator` הוא האיות בדוח המתושאל.
  carrier = params["operaor"] || params["operator"]

  Queue.push(
    external_id: params["external_id"],
    status: params["status"],
    en_message: params["en_message"],
    date: params["date"],
    phone: params["phone"],
    carrier: carrier,
    shipment_id: params["shipment_id"],
  )

  # אשרו מיד.
  status 200
  body ""
end

post "/textme/blacklist" do
  Queue.push(kind: "opt-out", phone: params["dest"], date: params["date"])
  status 200
  body ""
end
java
// Spring Boot. @RequestParam קורא שדות form-encoded
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
public class TextMePushController {

    private final ReportQueue queue;

    public TextMePushController(ReportQueue queue) {
        this.queue = queue;
    }

    @PostMapping("/textme/dlr")
    public ResponseEntity<Void> deliveryReport(
            @RequestParam(required = false) String external_id,
            @RequestParam(required = false) String status,
            @RequestParam(required = false) String en_message,
            @RequestParam(required = false) String date,
            @RequestParam(required = false) String phone,
            // `operaor` הוא האיות ב-Push; `operator` הוא האיות המתושאל.
            @RequestParam(required = false) String operaor,
            @RequestParam(required = false) String operator,
            @RequestParam(required = false) String shipment_id) {

        String carrier = operaor != null ? operaor : operator;
        queue.enqueue(external_id, status, en_message, date, phone, carrier, shipment_id);

        // אשרו מיד; התור עושה את העבודה.
        return ResponseEntity.ok().build();
    }
}

הערות על השדות ​

Push אינו מחליף תשאול לחלוטין ​

הודעת Push מספרת לכם על אירוע אחד, פעם אחת, ואם הכתובת שלכם הייתה מושבתת במהלך חלון הניסיונות, האירוע הזה נעלם מהערוץ. שמרו על סריקת dlrByDate תקופתית על היום האחרון כרשת ביטחון, ובצעו התאמה לפי external_id. Push נותן לכם השהיה קצרה; תשאול נותן לכם שלמות.

התאריכים מגיעים עם שניות ​

גוף Push נושא dd/mm/yy hh:mm:ss. רכיב אחד יותר מ-dd/mm/yy hh:mm שאתם שולחים בפרמטרים של בקשות. פרסר שנכתב בקפדנות לפי פורמט הבקשה ידחה אותם.

הערכים מקודדים ב-URL, כולל עברית ​

he_message=הגיע+ליעד מגיע מקודד באחוזים עם + במקום רווחים. כל פרסר טפסים סטנדרטי מטפל בזה; פיצול ידני על & ועל = לא יטפל.

לזהות ש-Push הפסיק ​

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