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.
POST https://my.textme.co.il/apisms Send a message
Parameters
| Name | Type | Description | Required |
|---|---|---|---|
sms | object | Root element. | ✔️ |
user | object | Contains the user element. | ✔️ |
username | string | The username of the account by which you are recognized in the system. | ✔️ |
source | string | The number or name the message appears to come from. Maximum 11 characters, digits and English letters only, no +. Must be a verified sender. | ✔️ |
destinations | object | Contains every destination. May hold multiple phone and multiple cl_id elements. | ✔️ |
phone | string | A destination number, formatted 5xxxxxxx or 05xxxxxxx. | ✔️ |
id | string | Attribute 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_id | string | Send to a whole contact list by its id. Repeatable. | ➖ |
message | string | The message body. Maximum 1005 characters. | ✔️ |
tag | string | Prefix 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_dynamic | string | Set 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. | ➖ |
timing | string | Schedule the send, formatted dd/mm/yy hh:mm. Sent immediately when absent. | ➖ |
add_unsubscribe | int | Append an opt-out mechanism: 3 for a removal link, 2 for reply-to-remove. Any other value means no. | ➖ |
temp_bl | string | Skip destinations that already received a message in the last n days, where n is 1–14. | ➖ |
includes_international | int | 1 to allow non-Israeli destinations, 0 otherwise. | ➖ |
campaign_name | string | Name shown on the system reports, and the handle for cancelling by name. Maximum 50 characters. | ➖ |
links | object | Contains the links to shorten. May hold multiple link elements. | ➖ |
link | string | A URL to shorten. Must carry an id attribute; write [link-id] in the message body where the short link should appear. | ➖ |
Request example
<?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>{
"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"
}
]
}
}
}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"
}
]
}
}
}'// 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
// 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
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);# 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)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 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());
}
}// .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);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// [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 version="1.0" encoding="UTF-8"?>
<sms>
<status>0</status>
<message>SMS will be sent</message>
<shipment_id>xxxxxxx</shipment_id>
</sms>{
"status": 0,
"message": "SMS will be sent",
"shipment_id": "XXXXXXX"
}| Field | Type | Description |
|---|---|---|
status | int | 0 — the message was accepted for sending. |
message | string | SMS will be sent. |
shipment_id | string | Identifies 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
| Status | When |
|---|---|
2 | A required element is missing; message names it. |
4 | Not enough credit. |
5 | Outside the account's permitted sending hours. |
7 | Several distinct messages were passed to sms; use bulk. |
8 | Every destination is on the blocklist. |
9 | A destination is too short or too long. |
515 | source is not a verified sender. |
714 | temp_bl is not a whole number of days from 1 to 14. |
715 | Every destination was filtered out by temp_bl. |
980 | A link value is not a usable URL. |
981 | The short link could not be generated. |
986 | add_unsubscribe was neither 2 nor 3. |
988 | A cl_id does not exist. |
989 | The 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.
Short links
Each link needs an id, and the message body refers to it as [link-id]:
<message>Your order has shipped: [link-a1]</message>
<links>
<link id="a1">https://www.example.com/orders/10052/tracking</link>
</links>{
"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:
| Value | Effect |
|---|---|
3 | Adds a removal link. |
2 | Adds reply-to-remove instructions. |
| anything else | Nothing 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.

