Skip to content

מעקב אחר מסירה (DLR)

שליחה מחזירה status: 0 כש-TextMe מקבלת את ההודעה. השאלה אם מכשיר אכן קיבל אותה היא שאלה נפרדת ואסינכרונית — ועליה עונה דוח מסירה, או DLR.

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

צעד 1 — לסמן כל יעד

תכונת ה-id על <phone> היא מפתח החיבור בין הדוחות של TextMe לרשומות שלכם. בחרו משהו משמעותי מהתחום שלכם:

xml
<destinations>
    <phone id="order-10052">5xxxxxxxx</phone>
    <phone id="order-10053">5xxxxxxxx</phone>
</destinations>
json
{
  "destinations": {
    "phone": [
      { "$": { "id": "order-10052" }, "_": "5xxxxxxxx" },
      { "$": { "id": "order-10053" }, "_": "5xxxxxxxx" }
    ]
  }
}

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

הפכו את המזהים לייחודיים וניתנים לפרסור

order-10052 טוב יותר מ-10052: הוא אומר לכם באיזו טבלה לחפש כשהדוח חוזר שבוע לאחר מכן. שימוש חוזר במזהה בשתי שליחות הופך את הדוח לדו-משמעי, ולכן כללו משהו שמשתנה — מספר ניסיון, חותמת זמן — אם אותה הזמנה יכולה לקבל שתי הודעות.

צעד 2 — לשלוף את הדוחות

שתי פעולות, תלוי מה אתם שואלים.

לפי מזהה חיצוני — dlr

"מה קרה להודעות הספציפיות האלה?"

xml
<?xml version="1.0" encoding="UTF-8"?>
<dlr>
    <user>
        <username>Leeroy</username>
    </user>
    <transactions>
        <external_id>some id 1</external_id>
        <external_id>some id 2</external_id>
    </transactions>
    <from>01/01/14 00:00</from>
    <to>01/01/14 23:59</to>
</dlr>
json
{
  "dlr": {
    "user": {
      "username": "Leeroy"
    },
    "transactions": {
      "external_id": [
        "some id 1",
        "some id 2"
      ]
    },
    "from": "01/01/14 00:00",
    "to": "01/01/14 23:59"
  }
}
bash
curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
  "dlr": {
    "user": {
      "username": "Leeroy"
    },
    "transactions": {
      "external_id": [
        "some id 1",
        "some id 2"
      ]
    },
    "from": "01/01/14 00:00",
    "to": "01/01/14 23:59"
  }
}'
js
// Node.js 18+ / דפדפנים — ללא תלויות
const response = await fetch('https://my.textme.co.il/api', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TEXTME_API_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    dlr: {
      user: {
        username: 'Leeroy',
      },
      transactions: {
        external_id: [
          'some id 1',
          'some id 2',
        ],
      },
      from: '01/01/14 00:00',
      to: '01/01/14 23:59',
    },
  }),
})

const result = await response.json()

// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
if (Number(result.status) !== 0) {
  throw new Error(`TextMe ${result.status}: ${result.message}`)
}

console.log(result)
php
<?php
// composer require guzzlehttp/guzzle

$client = new \GuzzleHttp\Client([
    'headers' => [
        'Authorization' => 'Bearer '.getenv('TEXTME_API_TOKEN'),
        'Accept' => 'application/json',
    ],
]);

$response = $client->post('https://my.textme.co.il/api', [
    'json' => [
        'dlr' => [
            'user' => [
                'username' => 'Leeroy',
            ],
            'transactions' => [
                'external_id' => [
                    'some id 1',
                    'some id 2',
                ],
            ],
            'from' => '01/01/14 00:00',
            'to' => '01/01/14 23:59',
        ],
    ],
]);

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

// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
if ((int) $result['status'] !== 0) {
    throw new RuntimeException("TextMe {$result['status']}: {$result['message']}");
}

print_r($result);
php
<?php

use Illuminate\Support\Facades\Http;

$result = Http::withToken(config('services.textme.token'))
    ->acceptJson()
    ->post('https://my.textme.co.il/api', [
        'dlr' => [
            'user' => [
                'username' => 'Leeroy',
            ],
            'transactions' => [
                'external_id' => [
                    'some id 1',
                    'some id 2',
                ],
            ],
            'from' => '01/01/14 00:00',
            'to' => '01/01/14 23:59',
        ],
    ])
    ->throw()
    ->json();

// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
throw_if((int) $result['status'] !== 0, RuntimeException::class,
    "TextMe {$result['status']}: {$result['message']}");

logger()->info('TextMe', $result);
python
# pip install httpx
import os

import httpx

response = httpx.post(
    "https://my.textme.co.il/api",
    headers={"Authorization": f"Bearer {os.environ['TEXTME_API_TOKEN']}"},
    json={
        "dlr": {
            "user": {
                "username": "Leeroy",
            },
            "transactions": {
                "external_id": [
                    "some id 1",
                    "some id 2",
                ],
            },
            "from": "01/01/14 00:00",
            "to": "01/01/14 23:59",
        },
    },
)
response.raise_for_status()
result = response.json()

# גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
if int(result["status"]) != 0:
    raise RuntimeError(f"TextMe {result['status']}: {result['message']}")

print(result)
go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

func main() {
	payload, _ := json.Marshal(map[string]any{
		"dlr": map[string]any{
			"user": map[string]any{
				"username": "Leeroy",
			},
			"transactions": map[string]any{
				"external_id": []any{
					"some id 1",
					"some id 2",
				},
			},
			"from": "01/01/14 00:00",
			"to": "01/01/14 23:59",
		},
	})

	req, _ := http.NewRequest("POST", "https://my.textme.co.il/api", bytes.NewReader(payload))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("TEXTME_API_TOKEN"))
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	var result struct {
		Status  json.Number `json:"status"`
		Message string      `json:"message"`
	}
	if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
		panic(err)
	}

	// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
	if result.Status.String() != "0" {
		panic(fmt.Sprintf("TextMe %s: %s", result.Status, result.Message))
	}

	fmt.Println(result.Message)
}
java
// Java 17+ — java.net.http, ללא תלויות (פענוח עם Jackson/Gson)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class TextMeDlr {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "dlr": {
                "user": {
                  "username": "Leeroy"
                },
                "transactions": {
                  "external_id": [
                    "some id 1",
                    "some id 2"
                  ]
                },
                "from": "01/01/14 00:00",
                "to": "01/01/14 23:59"
              }
            }
            """;

        HttpRequest request = HttpRequest.newBuilder(URI.create("https://my.textme.co.il/api"))
            .header("Authorization", "Bearer " + System.getenv("TEXTME_API_TOKEN"))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        // גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
        System.out.println(response.body());
    }
}
csharp
// .NET 8+ — System.Net.Http
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

var payload = """
    {
      "dlr": {
        "user": {
          "username": "Leeroy"
        },
        "transactions": {
          "external_id": [
            "some id 1",
            "some id 2"
          ]
        },
        "from": "01/01/14 00:00",
        "to": "01/01/14 23:59"
      }
    }
    """;

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("TEXTME_API_TOKEN"));

var response = await http.PostAsync("https://my.textme.co.il/api",
    new StringContent(payload, Encoding.UTF8, "application/json"));

var result = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement;
var status = result.GetProperty("status").ToString();

// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
if (status != "0")
{
    var message = result.GetProperty("message").ToString();
    throw new Exception($"TextMe {status}: {message}");
}

Console.WriteLine(result);
ruby
require "net/http"
require "json"

uri = URI("https://my.textme.co.il/api")

request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('TEXTME_API_TOKEN')}"
request["Content-Type"] = "application/json"
request.body = JSON.dump({
  "dlr" => {
    "user" => {
      "username" => "Leeroy",
    },
    "transactions" => {
      "external_id" => [
        "some id 1",
        "some id 2",
      ],
    },
    "from" => "01/01/14 00:00",
    "to" => "01/01/14 23:59",
  },
})

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end

result = JSON.parse(response.body)

# גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
raise "TextMe #{result['status']}: #{result['message']}" unless result["status"].to_i.zero?

pp result
rust
// [dependencies]
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
use serde_json::{json, Value};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let result: Value = reqwest::Client::new()
        .post("https://my.textme.co.il/api")
        .bearer_auth(std::env::var("TEXTME_API_TOKEN")?)
        .json(&json!({
          "dlr": {
            "user": {
              "username": "Leeroy"
            },
            "transactions": {
              "external_id": [
                "some id 1",
                "some id 2"
              ]
            },
            "from": "01/01/14 00:00",
            "to": "01/01/14 23:59"
          }
        }))
        .send()
        .await?
        .json()
        .await?;

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

    println!("{result}");
    Ok(())
}
xml
<?xml version="1.0" encoding="UTF-8"?>
<dlr>
    <status>0</status>
    <message></message>
    <transactions>
        <transaction>
            <external_id>1391438285</external_id>
            <status>102</status>
            <he_message>הגיע ליעד</he_message>
            <en_message>Delivered</en_message>
            <date>03/02/14 16:38</date>
            <shipment_id>12345678</shipment_id>
        </transaction>
    </transactions>
</dlr>
json
{
  "status": 0,
  "message": "all is well!",
  "transactions": [
    {
      "external_id": "1391438285",
      "source": "Test",
      "phone": "5XXXXXXXX",
      "status": "102",
      "message_he": "הגיע ליעד",
      "en_message": "Delivered",
      "shipment_id": "XXXXXXX",
      "date": "02/05/23 10:22",
      "operator": "Telzar"
    }
  ]
}

שתי מגבלות

  • החלון בין from ל-to לא יעלה על שבוע אחד.
  • עד 1,000 מזהים לכל בקשה — פצלו קבוצות גדולות יותר.

לפי תאריך — dlrByDate

"איך עבר אתמול?" בלי צורך במזהים; שלחו את המחרוזת null בשדה external_id.

xml
<?xml version="1.0" encoding="UTF-8"?>
<dlrByDate>
    <user>
        <username>Leeroy</username>
    </user>
    <transactions>
        <external_id>null</external_id>
    </transactions>
    <from>04/09/22 00:00</from>
    <to>04/09/22 23:59</to>
</dlrByDate>
json
{
  "dlrByDate": {
    "user": {
      "username": "Leeroy"
    },
    "transactions": {
      "external_id": "null"
    },
    "from": "04/09/22 00:00",
    "to": "04/09/22 23:59"
  }
}
bash
curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
  "dlrByDate": {
    "user": {
      "username": "Leeroy"
    },
    "transactions": {
      "external_id": "null"
    },
    "from": "04/09/22 00:00",
    "to": "04/09/22 23:59"
  }
}'
js
// Node.js 18+ / דפדפנים — ללא תלויות
const response = await fetch('https://my.textme.co.il/api', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TEXTME_API_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    dlrByDate: {
      user: {
        username: 'Leeroy',
      },
      transactions: {
        external_id: 'null',
      },
      from: '04/09/22 00:00',
      to: '04/09/22 23:59',
    },
  }),
})

const result = await response.json()

// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
if (Number(result.status) !== 0) {
  throw new Error(`TextMe ${result.status}: ${result.message}`)
}

console.log(result)
php
<?php
// composer require guzzlehttp/guzzle

$client = new \GuzzleHttp\Client([
    'headers' => [
        'Authorization' => 'Bearer '.getenv('TEXTME_API_TOKEN'),
        'Accept' => 'application/json',
    ],
]);

$response = $client->post('https://my.textme.co.il/api', [
    'json' => [
        'dlrByDate' => [
            'user' => [
                'username' => 'Leeroy',
            ],
            'transactions' => [
                'external_id' => 'null',
            ],
            'from' => '04/09/22 00:00',
            'to' => '04/09/22 23:59',
        ],
    ],
]);

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

// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
if ((int) $result['status'] !== 0) {
    throw new RuntimeException("TextMe {$result['status']}: {$result['message']}");
}

print_r($result);
php
<?php

use Illuminate\Support\Facades\Http;

$result = Http::withToken(config('services.textme.token'))
    ->acceptJson()
    ->post('https://my.textme.co.il/api', [
        'dlrByDate' => [
            'user' => [
                'username' => 'Leeroy',
            ],
            'transactions' => [
                'external_id' => 'null',
            ],
            'from' => '04/09/22 00:00',
            'to' => '04/09/22 23:59',
        ],
    ])
    ->throw()
    ->json();

// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
throw_if((int) $result['status'] !== 0, RuntimeException::class,
    "TextMe {$result['status']}: {$result['message']}");

logger()->info('TextMe', $result);
python
# pip install httpx
import os

import httpx

response = httpx.post(
    "https://my.textme.co.il/api",
    headers={"Authorization": f"Bearer {os.environ['TEXTME_API_TOKEN']}"},
    json={
        "dlrByDate": {
            "user": {
                "username": "Leeroy",
            },
            "transactions": {
                "external_id": "null",
            },
            "from": "04/09/22 00:00",
            "to": "04/09/22 23:59",
        },
    },
)
response.raise_for_status()
result = response.json()

# גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
if int(result["status"]) != 0:
    raise RuntimeError(f"TextMe {result['status']}: {result['message']}")

print(result)
go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

func main() {
	payload, _ := json.Marshal(map[string]any{
		"dlrByDate": map[string]any{
			"user": map[string]any{
				"username": "Leeroy",
			},
			"transactions": map[string]any{
				"external_id": "null",
			},
			"from": "04/09/22 00:00",
			"to": "04/09/22 23:59",
		},
	})

	req, _ := http.NewRequest("POST", "https://my.textme.co.il/api", bytes.NewReader(payload))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("TEXTME_API_TOKEN"))
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	var result struct {
		Status  json.Number `json:"status"`
		Message string      `json:"message"`
	}
	if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
		panic(err)
	}

	// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
	if result.Status.String() != "0" {
		panic(fmt.Sprintf("TextMe %s: %s", result.Status, result.Message))
	}

	fmt.Println(result.Message)
}
java
// Java 17+ — java.net.http, ללא תלויות (פענוח עם Jackson/Gson)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class TextMeDlrByDate {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "dlrByDate": {
                "user": {
                  "username": "Leeroy"
                },
                "transactions": {
                  "external_id": "null"
                },
                "from": "04/09/22 00:00",
                "to": "04/09/22 23:59"
              }
            }
            """;

        HttpRequest request = HttpRequest.newBuilder(URI.create("https://my.textme.co.il/api"))
            .header("Authorization", "Bearer " + System.getenv("TEXTME_API_TOKEN"))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        // גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
        System.out.println(response.body());
    }
}
csharp
// .NET 8+ — System.Net.Http
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

var payload = """
    {
      "dlrByDate": {
        "user": {
          "username": "Leeroy"
        },
        "transactions": {
          "external_id": "null"
        },
        "from": "04/09/22 00:00",
        "to": "04/09/22 23:59"
      }
    }
    """;

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("TEXTME_API_TOKEN"));

var response = await http.PostAsync("https://my.textme.co.il/api",
    new StringContent(payload, Encoding.UTF8, "application/json"));

var result = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement;
var status = result.GetProperty("status").ToString();

// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
if (status != "0")
{
    var message = result.GetProperty("message").ToString();
    throw new Exception($"TextMe {status}: {message}");
}

Console.WriteLine(result);
ruby
require "net/http"
require "json"

uri = URI("https://my.textme.co.il/api")

request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('TEXTME_API_TOKEN')}"
request["Content-Type"] = "application/json"
request.body = JSON.dump({
  "dlrByDate" => {
    "user" => {
      "username" => "Leeroy",
    },
    "transactions" => {
      "external_id" => "null",
    },
    "from" => "04/09/22 00:00",
    "to" => "04/09/22 23:59",
  },
})

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end

result = JSON.parse(response.body)

# גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
raise "TextMe #{result['status']}: #{result['message']}" unless result["status"].to_i.zero?

pp result
rust
// [dependencies]
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
use serde_json::{json, Value};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let result: Value = reqwest::Client::new()
        .post("https://my.textme.co.il/api")
        .bearer_auth(std::env::var("TEXTME_API_TOKEN")?)
        .json(&json!({
          "dlrByDate": {
            "user": {
              "username": "Leeroy"
            },
            "transactions": {
              "external_id": "null"
            },
            "from": "04/09/22 00:00",
            "to": "04/09/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(())
}

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

חלון רחב יותר דרך SOAP

מתודת ה-SOAP getDlrReport מאפשרת טווח של 30 יום שמגיע עד שנה אחורה — הדבר היחיד שהממשק הזה עושה טוב יותר מזה.

צעד 3 — לפרש את הסטטוס

כל רשומה נושאת status מאוצר המילים של סטטוסי מסירה. זה לא סולם סטטוס הבקשה — כאן גם 0 וגם 102 אומרים "נמסר", בעוד ה-0 ברמה העליונה של אותה תשובה אומר רק שהדוח הופק.

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

js
const DELIVERED = new Set(['0', '102'])
const PENDING = new Set(['-1', '2'])
const BLOCKED = new Set(['15', '16', '17', '18', '201'])

function outcome(status) {
  const code = String(status)

  if (DELIVERED.has(code)) return 'delivered'
  if (PENDING.has(code)) return 'unconfirmed'
  if (BLOCKED.has(code)) return 'blocked' // אף פעם לא לנסות שוב את המספר הזה
  return 'failed'
}

for (const t of report.transactions) {
  // השדה בעברית הוא `message_he` ב-JSON ו-`he_message` ב-XML.
  await orders.recordDelivery(t.external_id, outcome(t.status), t.en_message, t.date)
}
python
DELIVERED = {"0", "102"}
PENDING = {"-1", "2"}
BLOCKED = {"15", "16", "17", "18", "201"}


def outcome(status):
    code = str(status)

    if code in DELIVERED:
        return "delivered"
    if code in PENDING:
        return "unconfirmed"
    if code in BLOCKED:
        return "blocked"  # אף פעם לא לנסות שוב את המספר הזה
    return "failed"


for t in report["transactions"]:
    # השדה בעברית הוא `message_he` ב-JSON ו-`he_message` ב-XML.
    orders.record_delivery(t["external_id"], outcome(t["status"]), t["en_message"], t["date"])
php
<?php

const DELIVERED = ['0', '102'];
const PENDING = ['-1', '2'];
const BLOCKED = ['15', '16', '17', '18', '201'];

function outcome(string|int $status): string
{
    $code = (string) $status;

    return match (true) {
        in_array($code, DELIVERED, true) => 'delivered',
        in_array($code, PENDING, true) => 'unconfirmed',
        in_array($code, BLOCKED, true) => 'blocked', // אף פעם לא לנסות שוב
        default => 'failed',
    };
}

foreach ($report['transactions'] as $t) {
    // השדה בעברית הוא `message_he` ב-JSON ו-`he_message` ב-XML.
    $orders->recordDelivery($t['external_id'], outcome($t['status']), $t['en_message'], $t['date']);
}

אל תתייחסו ל--1 ככשל

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

סטטוסי blocked הם קבועים

15 (מכשיר כשר), 17 (חסום להודעות פרסומיות) ו-201 (נחסם לפי בקשה) לא ישתנו בניסיון חוזר. כתבו אותם בחזרה לרשומת איש הקשר שלכם: 15 אומר לנתב לקול בפעם הבאה, 201 אומר להפסיק לשלוח לאדם הזה לחלוטין.

צעד 4 — להפסיק לתשאל

כשהמחזור עובד, רשמו כתובת Push ו-TextMe תשלח כל דוח ב-POST ברגע שהוא נוצר — אותם שדות, בפורמט form-encoded, בלי תשאול.

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

external_id=order-10052&status=102&en_message=Delivered&date=01/04/26 16:05:05&phone=9725xxxxxxxx&operaor=Telzar&shipment_id=xxxxxxxxx

Push אינו מאומת ומתייאש בשקט

אין טוקן, אין חתימה — התייחסו לכתובת כאל סוד והפכו את הטיפול לאידמפוטנטי.

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

דוגמאות מקבל מלאות בשמונה שפות נמצאות בעמוד Push API.

צעד 5 — לשמור על סריקת התאמה

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

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

js
// רשת ביטחון לילית: כל מה ש-Push פספס, בהתאמה לפי מזהה חיצוני.
const report = await textme({
  dlrByDate: {
    user: { username: 'Leeroy' },
    transactions: { external_id: 'null' },
    from: format(yesterday, 'dd/MM/yy 00:00'),
    to: format(yesterday, 'dd/MM/yy 23:59'),
  },
})

for (const t of report.transactions ?? []) {
  // אידמפוטנטי: דוח שכבר נרשם דרך Push לא עושה כלום.
  await orders.recordDelivery(t.external_id, outcome(t.status), t.en_message, t.date)
}
python
# רשת ביטחון לילית: כל מה ש-Push פספס, בהתאמה לפי מזהה חיצוני.
report = textme({
    "dlrByDate": {
        "user": {"username": "Leeroy"},
        "transactions": {"external_id": "null"},
        "from": yesterday.strftime("%d/%m/%y 00:00"),
        "to": yesterday.strftime("%d/%m/%y 23:59"),
    }
})

for t in report.get("transactions", []):
    # אידמפוטנטי: דוח שכבר נרשם דרך Push לא עושה כלום.
    orders.record_delivery(t["external_id"], outcome(t["status"]), t["en_message"], t["date"])

מלכודות בשמות השדות

שלוש אי-עקביות שגוזלות זמן ניפוי באגים:

שדהXML מתושאלJSON מתושאלב-Push
מלל הסטטוס בעבריתhe_messagemessage_hehe_message
מפעילoperatoroperatoroperaor
דיוק התאריךdd/mm/yy hh:mmdd/mm/yy hh:mmdd/mm/yy hh:mm:ss

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

התאוששות משליחה שנכשלה בטיים-אאוט

אין מנגנון idempotency key, ולכן שליחה שנכשלת בטיים-אאוט בשכבת התעבורה משאירה אותכם באמת לא בטוחים אם היא יצאה — ושליחה חוזרת מסתכנת במסירה כפולה.

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

מסלול ההתאוששות הזה קיים רק אם המזהה הוגדר בזמן השליחה — וזה הנימוק האמיתי להפוך את זה להרגל.

הצעד הבא