Skip to content

Send SMS

Sends one message body to any number of destinations — individual phone numbers, whole contact lists, or both. When each recipient needs different text, use bulk instead. To speak the message down the line rather than text it, attach a tts block.

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

sms Send a message

Parameters

NameTypeDescriptionRequired
smsobjectRoot element.✔️
userobjectContains the user element.✔️
usernamestringThe username of the account by which you are recognized in the system.✔️
sourcestringThe number or name the message appears to come from. Maximum 11 characters, digits and English letters only, no +. Must be a verified sender.✔️
destinationsobjectContains every destination. May hold multiple phone and multiple cl_id elements.✔️
phonestringA destination number, formatted 5xxxxxxx or 05xxxxxxx.✔️
idstringAttribute of phone. Your own external id for this destination, so you can look up its delivery report later. Omit it, or leave it blank, if you do not need reports.
cl_idstringSend to a whole contact list by its id. Repeatable.
messagestringThe message body. Maximum 1005 characters.✔️
tagstringPrefix glued to the front of the message — intended for Google's automatic SMS verification. <tag>#</tag> with <message>Hello world</message> is delivered as <#>Hello world.
add_dynamicstringSet to 1 to substitute the contact list's dynamic fields into the message. Any other value means no. Requires exactly one cl_id and no individual phone elements.
timingstringSchedule the send, formatted dd/mm/yy hh:mm. Sent immediately when absent.
add_unsubscribeintAppend an opt-out mechanism: 3 for a removal link, 2 for reply-to-remove. Any other value means no.
temp_blstringSkip destinations that already received a message in the last n days, where n is 1–14.
includes_internationalint1 to allow non-Israeli destinations, 0 otherwise.
campaign_namestringName shown on the system reports, and the handle for cancelling by name. Maximum 50 characters.
linksobjectContains the links to shorten. May hold multiple link elements.
linkstringA URL to shorten. Must carry an id attribute; write [link-id] in the message body where the short link should appear.

Request example

xml
<?xml version="1.0" encoding="UTF-8"?>
<sms>
    <user>
        <username>Leeroy</username>
    </user>
    <source>DemoAPI</source>
    <destinations>
        <cl_id>21518</cl_id>
        <cl_id>21500</cl_id>
        <phone id="external id1">5xxxxxxxx</phone>
        <phone id="external id2">5xxxxxxxx</phone>
        <phone>5xxxxxxxx</phone>
        <phone id="">5xxxxxxxx</phone>
    </destinations>
    <tag>#</tag>
    <message>This is a sample message. [link-a1]</message>
    <add_dynamic>0</add_dynamic>
    <timing>30/03/14 10:10</timing>
    <add_unsubscribe>0</add_unsubscribe>
    <temp_bl>2</temp_bl>
    <includes_international>1</includes_international>
    <campaign_name>Sample Campaign</campaign_name>
    <links>
        <link id="a1">https://www.example.com/path/to/resource</link>
    </links>
</sms>
json
{
  "sms": {
    "user": {
      "username": "Leeroy"
    },
    "source": "DemoAPI",
    "destinations": {
      "cl_id": [
        "21518",
        "21500"
      ],
      "phone": [
        {
          "$": {
            "id": "external id1"
          },
          "_": "5xxxxxxxx"
        },
        {
          "$": {
            "id": "external id2"
          },
          "_": "5xxxxxxxx"
        },
        {
          "_": "5xxxxxxxx"
        },
        {
          "$": {
            "id": ""
          },
          "_": "5xxxxxxxx"
        }
      ]
    },
    "tag": "#",
    "message": "This is a sample message. [link-a1]",
    "add_dynamic": "0",
    "timing": "30/03/14 10:10",
    "add_unsubscribe": "0",
    "temp_bl": "2",
    "includes_international": "1",
    "campaign_name": "Sample Campaign",
    "links": {
      "link": [
        {
          "$": {
            "id": "a1"
          },
          "_": "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 '{
  "sms": {
    "user": {
      "username": "Leeroy"
    },
    "source": "DemoAPI",
    "destinations": {
      "cl_id": [
        "21518",
        "21500"
      ],
      "phone": [
        {
          "$": {
            "id": "external id1"
          },
          "_": "5xxxxxxxx"
        },
        {
          "$": {
            "id": "external id2"
          },
          "_": "5xxxxxxxx"
        },
        {
          "_": "5xxxxxxxx"
        },
        {
          "$": {
            "id": ""
          },
          "_": "5xxxxxxxx"
        }
      ]
    },
    "tag": "#",
    "message": "This is a sample message. [link-a1]",
    "add_dynamic": "0",
    "timing": "30/03/14 10:10",
    "add_unsubscribe": "0",
    "temp_bl": "2",
    "includes_international": "1",
    "campaign_name": "Sample Campaign",
    "links": {
      "link": [
        {
          "$": {
            "id": "a1"
          },
          "_": "https://www.example.com/path/to/resource"
        }
      ]
    }
  }
}'
js
// Node.js 18+ / browsers — 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({
    sms: {
      user: {
        username: 'Leeroy',
      },
      source: 'DemoAPI',
      destinations: {
        cl_id: [
          '21518',
          '21500',
        ],
        phone: [
          {
            $: {
              id: 'external id1',
            },
            _: '5xxxxxxxx',
          },
          {
            $: {
              id: 'external id2',
            },
            _: '5xxxxxxxx',
          },
          {
            _: '5xxxxxxxx',
          },
          {
            $: {
              id: '',
            },
            _: '5xxxxxxxx',
          },
        ],
      },
      tag: '#',
      message: 'This is a sample message. [link-a1]',
      add_dynamic: '0',
      timing: '30/03/14 10:10',
      add_unsubscribe: '0',
      temp_bl: '2',
      includes_international: '1',
      campaign_name: 'Sample Campaign',
      links: {
        link: [
          {
            $: {
              id: 'a1',
            },
            _: 'https://www.example.com/path/to/resource',
          },
        ],
      },
    },
  }),
})

const result = await response.json()

// Errors arrive as HTTP 200 too — the payload status is what counts
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' => [
        'sms' => [
            'user' => [
                'username' => 'Leeroy',
            ],
            'source' => 'DemoAPI',
            'destinations' => [
                'cl_id' => [
                    '21518',
                    '21500',
                ],
                'phone' => [
                    [
                        '$' => [
                            'id' => 'external id1',
                        ],
                        '_' => '5xxxxxxxx',
                    ],
                    [
                        '$' => [
                            'id' => 'external id2',
                        ],
                        '_' => '5xxxxxxxx',
                    ],
                    [
                        '_' => '5xxxxxxxx',
                    ],
                    [
                        '$' => [
                            'id' => '',
                        ],
                        '_' => '5xxxxxxxx',
                    ],
                ],
            ],
            'tag' => '#',
            'message' => 'This is a sample message. [link-a1]',
            'add_dynamic' => '0',
            'timing' => '30/03/14 10:10',
            'add_unsubscribe' => '0',
            'temp_bl' => '2',
            'includes_international' => '1',
            'campaign_name' => 'Sample Campaign',
            'links' => [
                'link' => [
                    [
                        '$' => [
                            'id' => 'a1',
                        ],
                        '_' => 'https://www.example.com/path/to/resource',
                    ],
                ],
            ],
        ],
    ],
]);

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

// Errors arrive as HTTP 200 too — the payload status is what counts
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', [
        'sms' => [
            'user' => [
                'username' => 'Leeroy',
            ],
            'source' => 'DemoAPI',
            'destinations' => [
                'cl_id' => [
                    '21518',
                    '21500',
                ],
                'phone' => [
                    [
                        '$' => [
                            'id' => 'external id1',
                        ],
                        '_' => '5xxxxxxxx',
                    ],
                    [
                        '$' => [
                            'id' => 'external id2',
                        ],
                        '_' => '5xxxxxxxx',
                    ],
                    [
                        '_' => '5xxxxxxxx',
                    ],
                    [
                        '$' => [
                            'id' => '',
                        ],
                        '_' => '5xxxxxxxx',
                    ],
                ],
            ],
            'tag' => '#',
            'message' => 'This is a sample message. [link-a1]',
            'add_dynamic' => '0',
            'timing' => '30/03/14 10:10',
            'add_unsubscribe' => '0',
            'temp_bl' => '2',
            'includes_international' => '1',
            'campaign_name' => 'Sample Campaign',
            'links' => [
                'link' => [
                    [
                        '$' => [
                            'id' => 'a1',
                        ],
                        '_' => 'https://www.example.com/path/to/resource',
                    ],
                ],
            ],
        ],
    ])
    ->throw()
    ->json();

// Errors arrive as HTTP 200 too — the payload status is what counts
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={
        "sms": {
            "user": {
                "username": "Leeroy",
            },
            "source": "DemoAPI",
            "destinations": {
                "cl_id": [
                    "21518",
                    "21500",
                ],
                "phone": [
                    {
                        "$": {
                            "id": "external id1",
                        },
                        "_": "5xxxxxxxx",
                    },
                    {
                        "$": {
                            "id": "external id2",
                        },
                        "_": "5xxxxxxxx",
                    },
                    {
                        "_": "5xxxxxxxx",
                    },
                    {
                        "$": {
                            "id": "",
                        },
                        "_": "5xxxxxxxx",
                    },
                ],
            },
            "tag": "#",
            "message": "This is a sample message. [link-a1]",
            "add_dynamic": "0",
            "timing": "30/03/14 10:10",
            "add_unsubscribe": "0",
            "temp_bl": "2",
            "includes_international": "1",
            "campaign_name": "Sample Campaign",
            "links": {
                "link": [
                    {
                        "$": {
                            "id": "a1",
                        },
                        "_": "https://www.example.com/path/to/resource",
                    },
                ],
            },
        },
    },
)
response.raise_for_status()
result = response.json()

# Errors arrive as HTTP 200 too — the payload status is what counts
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{
		"sms": map[string]any{
			"user": map[string]any{
				"username": "Leeroy",
			},
			"source": "DemoAPI",
			"destinations": map[string]any{
				"cl_id": []any{
					"21518",
					"21500",
				},
				"phone": []any{
					map[string]any{
						"$": map[string]any{
							"id": "external id1",
						},
						"_": "5xxxxxxxx",
					},
					map[string]any{
						"$": map[string]any{
							"id": "external id2",
						},
						"_": "5xxxxxxxx",
					},
					map[string]any{
						"_": "5xxxxxxxx",
					},
					map[string]any{
						"$": map[string]any{
							"id": "",
						},
						"_": "5xxxxxxxx",
					},
				},
			},
			"tag": "#",
			"message": "This is a sample message. [link-a1]",
			"add_dynamic": "0",
			"timing": "30/03/14 10:10",
			"add_unsubscribe": "0",
			"temp_bl": "2",
			"includes_international": "1",
			"campaign_name": "Sample Campaign",
			"links": map[string]any{
				"link": []any{
					map[string]any{
						"$": map[string]any{
							"id": "a1",
						},
						"_": "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)
	}

	// Errors arrive as HTTP 200 too — 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 17+ — 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 TextMeSendSms {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "sms": {
                "user": {
                  "username": "Leeroy"
                },
                "source": "DemoAPI",
                "destinations": {
                  "cl_id": [
                    "21518",
                    "21500"
                  ],
                  "phone": [
                    {
                      "$": {
                        "id": "external id1"
                      },
                      "_": "5xxxxxxxx"
                    },
                    {
                      "$": {
                        "id": "external id2"
                      },
                      "_": "5xxxxxxxx"
                    },
                    {
                      "_": "5xxxxxxxx"
                    },
                    {
                      "$": {
                        "id": ""
                      },
                      "_": "5xxxxxxxx"
                    }
                  ]
                },
                "tag": "#",
                "message": "This is a sample message. [link-a1]",
                "add_dynamic": "0",
                "timing": "30/03/14 10:10",
                "add_unsubscribe": "0",
                "temp_bl": "2",
                "includes_international": "1",
                "campaign_name": "Sample Campaign",
                "links": {
                  "link": [
                    {
                      "$": {
                        "id": "a1"
                      },
                      "_": "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());

        // Errors arrive as HTTP 200 too — the payload status is what counts
        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 = """
    {
      "sms": {
        "user": {
          "username": "Leeroy"
        },
        "source": "DemoAPI",
        "destinations": {
          "cl_id": [
            "21518",
            "21500"
          ],
          "phone": [
            {
              "$": {
                "id": "external id1"
              },
              "_": "5xxxxxxxx"
            },
            {
              "$": {
                "id": "external id2"
              },
              "_": "5xxxxxxxx"
            },
            {
              "_": "5xxxxxxxx"
            },
            {
              "$": {
                "id": ""
              },
              "_": "5xxxxxxxx"
            }
          ]
        },
        "tag": "#",
        "message": "This is a sample message. [link-a1]",
        "add_dynamic": "0",
        "timing": "30/03/14 10:10",
        "add_unsubscribe": "0",
        "temp_bl": "2",
        "includes_international": "1",
        "campaign_name": "Sample Campaign",
        "links": {
          "link": [
            {
              "$": {
                "id": "a1"
              },
              "_": "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();

// Errors arrive as HTTP 200 too — 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
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({
  "sms" => {
    "user" => {
      "username" => "Leeroy",
    },
    "source" => "DemoAPI",
    "destinations" => {
      "cl_id" => [
        "21518",
        "21500",
      ],
      "phone" => [
        {
          "$" => {
            "id" => "external id1",
          },
          "_" => "5xxxxxxxx",
        },
        {
          "$" => {
            "id" => "external id2",
          },
          "_" => "5xxxxxxxx",
        },
        {
          "_" => "5xxxxxxxx",
        },
        {
          "$" => {
            "id" => "",
          },
          "_" => "5xxxxxxxx",
        },
      ],
    },
    "tag" => "#",
    "message" => "This is a sample message. [link-a1]",
    "add_dynamic" => "0",
    "timing" => "30/03/14 10:10",
    "add_unsubscribe" => "0",
    "temp_bl" => "2",
    "includes_international" => "1",
    "campaign_name" => "Sample Campaign",
    "links" => {
      "link" => [
        {
          "$" => {
            "id" => "a1",
          },
          "_" => "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)

# Errors arrive as HTTP 200 too — the payload status is what counts
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!({
          "sms": {
            "user": {
              "username": "Leeroy"
            },
            "source": "DemoAPI",
            "destinations": {
              "cl_id": [
                "21518",
                "21500"
              ],
              "phone": [
                {
                  "$": {
                    "id": "external id1"
                  },
                  "_": "5xxxxxxxx"
                },
                {
                  "$": {
                    "id": "external id2"
                  },
                  "_": "5xxxxxxxx"
                },
                {
                  "_": "5xxxxxxxx"
                },
                {
                  "$": {
                    "id": ""
                  },
                  "_": "5xxxxxxxx"
                }
              ]
            },
            "tag": "#",
            "message": "This is a sample message. [link-a1]",
            "add_dynamic": "0",
            "timing": "30/03/14 10:10",
            "add_unsubscribe": "0",
            "temp_bl": "2",
            "includes_international": "1",
            "campaign_name": "Sample Campaign",
            "links": {
              "link": [
                {
                  "$": {
                    "id": "a1"
                  },
                  "_": "https://www.example.com/path/to/resource"
                }
              ]
            }
          }
        }))
        .send()
        .await?
        .json()
        .await?;

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

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

Response

xml
<?xml version="1.0" encoding="UTF-8"?>
<sms>
    <status>0</status>
    <message>SMS will be sent</message>
    <shipment_id>xxxxxxx</shipment_id>
</sms>
json
{
  "status": 0,
  "message": "SMS will be sent",
  "shipment_id": "XXXXXXX"
}
FieldTypeDescription
statusint0 — the message was accepted for sending.
messagestringSMS will be sent.
shipment_idstringIdentifies the campaign this call created. Keep it: it is what delivery reports refer back to.

Accepted is not delivered

status: 0 means TextMe took the message, not that a handset received it. Delivery is reported separately and asynchronously — see Track delivery.

Errors

StatusWhen
2A required element is missing; message names it.
4Not enough credit.
5Outside the account's permitted sending hours.
7Several distinct messages were passed to sms; use bulk.
8Every destination is on the blocklist.
9A destination is too short or too long.
515source is not a verified sender.
714temp_bl is not a whole number of days from 1 to 14.
715Every destination was filtered out by temp_bl.
980A link value is not a usable URL.
981The short link could not be generated.
986add_unsubscribe was neither 2 nor 3.
988A cl_id does not exist.
989The message is empty or over 1005 characters, or campaign_name is over 50.

The complete list is in Status codes.

Field notes

Mixing phones and contact lists

destinations accepts any combination of phone and cl_id. One call can reach a hand-built list of numbers and two stored lists at once, and the message is sent once per unique destination.

The one restriction is add_dynamic: personalising from a list requires exactly one cl_id and no loose phone elements, because the merge fields come from that list's rows.

External ids and delivery reports

The id attribute on <phone> is yours to choose — an order number, a user id, a UUID. It is the key you later hand to dlr to ask what happened to that specific message, and it is echoed back in every pushed report.

Destinations sent without an id still deliver; you just cannot query them individually afterwards. Since the id costs nothing, set one on every destination you might want to reconcile — which, for transactional traffic, is all of them.

Each link needs an id, and the message body refers to it as [link-id]:

xml
<message>Your order has shipped: [link-a1]</message>
<links>
    <link id="a1">https://www.example.com/orders/10052/tracking</link>
</links>
json
{
  "message": "Your order has shipped: [link-a1]",
  "links": {
    "link": [
      { "$": { "id": "a1" }, "_": "https://www.example.com/orders/10052/tracking" }
    ]
  }
}

Shortening keeps long tracking URLs from eating the character budget, and makes clicks measurable. A malformed URL fails the whole send with status 980.

Recency filtering with temp_bl

temp_bl suppresses destinations that already heard from you within the last n days (1–14). It is applied per destination at send time, so a single value protects an audience assembled from several lists.

If it filters everyone out, the call fails with 715 rather than sending nothing quietly.

Opt-out footers

add_unsubscribe appends the removal mechanism the message needs to carry:

ValueEffect
3Adds a removal link.
2Adds reply-to-remove instructions.
anything elseNothing is added.

Opt-outs land on the account's blocklist, and are then excluded from future sends automatically. See Opt-out & compliance.

Scheduling

timing takes dd/mm/yy hh:mm and defers the send. A scheduled campaign can be called off right up until it goes out — with cancel, using either the shipment_id you got back or the campaign_name you set.