---
url: https://textme-docs.matat.io/use-cases/delivery-reports.md
description: >-
  Tagging destinations with your own external ids, pulling reports by id or by
  date window, reading DLR statuses, and moving from polling to push.
---

# Track delivery (DLR)

A send returns `status: 0` when TextMe accepts the message. Whether a handset actually received it is a separate, asynchronous question, answered by a **delivery report**, or DLR.

This page covers the whole loop: tagging messages so they can be found, pulling reports, interpreting the statuses, and moving from polling to push.

## Step 1, Tag every destination

The `id` attribute on `<phone>` is the join key between TextMe's reports and your own records. Choose something meaningful from your domain:

::: code-group

```xml [XML]
<destinations>
    <phone id="order-10052">5xxxxxxxx</phone>
    <phone id="order-10053">5xxxxxxxx</phone>
</destinations>
```

```json [JSON]
{
  "destinations": {
    "phone": [
      { "$": { "id": "order-10052" }, "_": "5xxxxxxxx" },
      { "$": { "id": "order-10053" }, "_": "5xxxxxxxx" }
    ]
  }
}
```

:::

Messages sent without an id are still delivered. You simply cannot ask about them individually afterwards. Since the id costs nothing, set one on every destination you might ever need to explain.

::: tip Make ids unique and parseable
`order-10052` beats `10052`: it tells you which of your tables to look in when the report comes back a week later. Reusing an id across two sends makes the report ambiguous, so include something that varies (an attempt number, a timestamp) if the same order can be messaged twice.
:::

## Step 2, Pull the reports

Two operations, depending on what you are asking.

### By external id, `dlr`

"What happened to these specific messages?"

::: code-group

```xml [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 [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]
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 [JavaScript]
// Node.js 18+ or any modern browser. No dependencies
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()

// Errors arrive as HTTP 200 too, so the payload status is what counts
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' => [
        '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);

// Errors arrive as HTTP 200 too, so the payload status is what counts
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', [
        '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();

// Errors arrive as HTTP 200 too, so the payload status is what counts
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={
        "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()

# Errors arrive as HTTP 200 too, so the payload status is what counts
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{
		"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)
	}

	// Errors arrive as HTTP 200 too, so the payload status is what counts
	if result.Status.String() != "0" {
		panic(fmt.Sprintf("TextMe %s: %s", result.Status, result.Message))
	}

	fmt.Println(result.Message)
}
```

```java [Java]
// Java 17+ using java.net.http. No dependencies (parse with 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());

        // Errors arrive as HTTP 200 too, so the payload status is what counts
        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 = """
    {
      "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();

// Errors arrive as HTTP 200 too, so the payload status is what counts
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({
  "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)

# Errors arrive as HTTP 200 too, so the payload status is what counts
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!({
          "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?;

    // Errors arrive as HTTP 200 too, so the payload status is what counts
    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"?>
<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 [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"
    }
  ]
}
```

:::

::: warning Two limits

* The window between `from` and `to` may not exceed **one week**.
* At most **1,000** ids per request, chunk larger sets.
  :::

### By date, `dlrByDate`

"How did yesterday go?" No ids needed; send the literal string `null` in `external_id`.

::: code-group

```xml [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 [JSON]
{
  "dlrByDate": {
    "user": {
      "username": "Leeroy"
    },
    "transactions": {
      "external_id": "null"
    },
    "from": "04/09/22 00:00",
    "to": "04/09/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 '{
  "dlrByDate": {
    "user": {
      "username": "Leeroy"
    },
    "transactions": {
      "external_id": "null"
    },
    "from": "04/09/22 00:00",
    "to": "04/09/22 23:59"
  }
}'
```

```js [JavaScript]
// Node.js 18+ or any modern browser. No dependencies
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()

// Errors arrive as HTTP 200 too, so the payload status is what counts
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' => [
        '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);

// Errors arrive as HTTP 200 too, so the payload status is what counts
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', [
        'dlrByDate' => [
            'user' => [
                'username' => 'Leeroy',
            ],
            'transactions' => [
                'external_id' => 'null',
            ],
            'from' => '04/09/22 00:00',
            'to' => '04/09/22 23:59',
        ],
    ])
    ->throw()
    ->json();

// Errors arrive as HTTP 200 too, so the payload status is what counts
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={
        "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()

# Errors arrive as HTTP 200 too, so the payload status is what counts
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{
		"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)
	}

	// Errors arrive as HTTP 200 too, so the payload status is what counts
	if result.Status.String() != "0" {
		panic(fmt.Sprintf("TextMe %s: %s", result.Status, result.Message))
	}

	fmt.Println(result.Message)
}
```

```java [Java]
// Java 17+ using java.net.http. No dependencies (parse with 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());

        // Errors arrive as HTTP 200 too, so the payload status is what counts
        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 = """
    {
      "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();

// Errors arrive as HTTP 200 too, so the payload status is what counts
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({
  "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)

# Errors arrive as HTTP 200 too, so the payload status is what counts
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!({
          "dlrByDate": {
            "user": {
              "username": "Leeroy"
            },
            "transactions": {
              "external_id": "null"
            },
            "from": "04/09/22 00:00",
            "to": "04/09/22 23:59"
          }
        }))
        .send()
        .await?
        .json()
        .await?;

    // Errors arrive as HTTP 200 too, so the payload status is what counts
    if result["status"] != 0 {
        return Err(format!("TextMe {}: {}", result["status"], result["message"]).into());
    }

    println!("{result}");
    Ok(())
}
```

:::

Same response shape, same one-week ceiling. There is no page cursor: **the window is the pagination**. Walking a month means four or five sequential, non-overlapping requests.

::: tip A wider window over SOAP
The [`getDlrReport`](../reference/soap.md#getdlrreport) SOAP method allows a **30-day** range reaching a year back. The one thing that interface does better than this one.
:::

## Step 3, Interpret the status

Each transaction carries a `status` from the [DLR vocabulary](../reference/dlr-statuses.md). This is *not* the request status scale. `0` and `102` both mean delivered here, while the top-level `0` on the same response only means the report was produced.

Rather than switching on every code, sort them into the four outcomes that drive behaviour:

::: code-group

```js [JavaScript]
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' // never retry this number
  return 'failed'
}

for (const t of report.transactions) {
  // The Hebrew field is `message_he` in JSON and `he_message` in XML.
  await orders.recordDelivery(t.external_id, outcome(t.status), t.en_message, t.date)
}
```

```python [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"  # never retry this number
    return "failed"


for t in report["transactions"]:
    # The Hebrew field is `message_he` in JSON and `he_message` in XML.
    orders.record_delivery(t["external_id"], outcome(t["status"]), t["en_message"], t["date"])
```

```php [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', // never retry
        default => 'failed',
    };
}

foreach ($report['transactions'] as $t) {
    // The Hebrew field is `message_he` in JSON and `he_message` in XML.
    $orders->recordDelivery($t['external_id'], outcome($t['status']), $t['en_message'], $t['date']);
}
```

:::

::: warning Do not treat `-1` as a failure
`-1` means *sent, no delivery confirmation returned*. The message usually did arrive; the carrier simply did not say so. Resending on `-1` charges you twice to deliver one message. Keep it in its own bucket and let it age out.
:::

::: tip `blocked` statuses are permanent
`15` (kosher handset), `17` (blocked for marketing) and `201` (blocked on request) will not change on a retry. Write them back to your own contact record: `15` means route to [voice](../endpoints/tts.md) next time, `201` means stop messaging that person entirely.
:::

## Step 4, Stop polling

Once the loop works, register a [push URL](../endpoints/push.md) and TextMe POSTs each report as it arrives. Same fields, form-encoded, no polling.

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

::: danger Push is unauthenticated and gives up quietly
No token, no signature. Treat the URL as a secret and make handling idempotent.

Worse, a non-`200` response starts a retry cycle that **stops permanently** after several failures, with no notification. Acknowledge first, process afterwards, and alert on the *absence* of reports rather than waiting to be told.
:::

Full receiver examples in eight languages are on the [Push API](../endpoints/push.md) page.

## Step 5. Keep a reconciliation sweep

Push gives you latency; polling gives you completeness. If your endpoint is down through the retry window, those reports are gone from the feed for good.

Production integrations run both: push for immediacy, plus a periodic `dlrByDate` sweep over the last day that fills any gaps, matched on `external_id`.

::: code-group

```js [JavaScript]
// Nightly backstop: anything push missed, reconciled by external id.
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 ?? []) {
  // Idempotent: a report already recorded by push is a no-op.
  await orders.recordDelivery(t.external_id, outcome(t.status), t.en_message, t.date)
}
```

```python [Python]
# Nightly backstop: anything push missed, reconciled by external id.
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", []):
    # Idempotent: a report already recorded by push is a no-op.
    orders.record_delivery(t["external_id"], outcome(t["status"]), t["en_message"], t["date"])
```

:::

## Field naming traps

Three inconsistencies that cost debugging time:

| Field | Polled XML | Polled JSON | Pushed |
|---|---|---|---|
| Hebrew status text | `he_message` | `message_he` | `he_message` |
| Carrier | `operator` | `operator` | `operaor` |
| Date precision | `dd/mm/yy hh:mm` | `dd/mm/yy hh:mm` | `dd/mm/yy hh:mm:ss` |

A parser written against one source will silently return nothing from another. Read both spellings and accept both date shapes.

## Recovering from a timed-out send

There is no idempotency key, so a send that times out at the transport level leaves you genuinely unsure whether it went out, and resending risks delivering twice.

The answer is to read rather than write. If every destination carried an id, query `dlr` for that id over the last hour: a transaction means the message exists, so do not resend. No transaction after a few minutes means it never landed, and resending is safe.

That recovery path only exists if the id was set at send time, which is the real argument for making it a habit.

## Next

* **[Incoming SMS & push](./incoming-and-push.md)**: the other direction: replies and inbound messages
* **[Opt-out & compliance](./opt-out-and-compliance.md)**: what to do with `201` and blocked statuses
* **[DLR statuses](../reference/dlr-statuses.md)**: the complete vocabulary
