Send your first SMS
From nothing to a delivered message, in five steps. Budget about fifteen minutes — most of which is the two things that need a human: authorising your IP address and verifying a sender.
Before you start
You need three things, and two of them cannot be created through the API.
| What | Where it comes from | |
|---|---|---|
| 1 | A TextMe account username | Your account manager. |
| 2 | An authorised IP address | The console — Settings → My Account → Enable Authorized IP Address Check. Mandatory: the API refuses calls from an account with the check on and no addresses registered. |
| 3 | An API token | The console, the first time — Settings → API Token Management → Create New Token. Shown once, so save it. |
Store the token in the environment rather than in code. Every example on this site reads TEXTME_API_TOKEN:
export TEXTME_API_TOKEN='0zrjDjZ26Dll5dqdTBmuwwtQg8'TLS 1.2 or higher is required
Older TLS versions are refused outright. Most current runtimes negotiate 1.2+ by default; a very old PHP or Java install may not.
Step 1 — Prove the credentials work
Before sending anything, confirm that the token, the username and the IP allowlist all agree. Reading back the account's current token touches nothing and fails loudly if any of the three is wrong:
<?xml version="1.0" encoding="UTF-8"?>
<getApiToken>
<user>
<username>admin_username</username>
</user>
<username>username_for_token</username>
<action>current</action>
</getApiToken>{
"getApiToken": {
"user": {
"username": "admin_username"
},
"username": "username_for_token",
"action": "current"
}
}curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"getApiToken": {
"user": {
"username": "admin_username"
},
"username": "username_for_token",
"action": "current"
}
}'// 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({
getApiToken: {
user: {
username: 'admin_username',
},
username: 'username_for_token',
action: 'current',
},
}),
})
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' => [
'getApiToken' => [
'user' => [
'username' => 'admin_username',
],
'username' => 'username_for_token',
'action' => 'current',
],
],
]);
$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', [
'getApiToken' => [
'user' => [
'username' => 'admin_username',
],
'username' => 'username_for_token',
'action' => 'current',
],
])
->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={
"getApiToken": {
"user": {
"username": "admin_username",
},
"username": "username_for_token",
"action": "current",
},
},
)
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{
"getApiToken": map[string]any{
"user": map[string]any{
"username": "admin_username",
},
"username": "username_for_token",
"action": "current",
},
})
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 TextMeTokenCurrent {
public static void main(String[] args) throws Exception {
String body = """
{
"getApiToken": {
"user": {
"username": "admin_username"
},
"username": "username_for_token",
"action": "current"
}
}
""";
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 = """
{
"getApiToken": {
"user": {
"username": "admin_username"
},
"username": "username_for_token",
"action": "current"
}
}
""";
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({
"getApiToken" => {
"user" => {
"username" => "admin_username",
},
"username" => "username_for_token",
"action" => "current",
},
})
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!({
"getApiToken": {
"user": {
"username": "admin_username"
},
"username": "username_for_token",
"action": "current"
}
}))
.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(())
}A healthy account answers with the token and its expiry:
<?xml version="1.0" encoding="utf-8"?>
<getApiToken>
<status>0</status>
<message>0zrjDjZ26Dll5dqdTBmuwwtQg8</message>
<expiration_date>07/06/2025 09:17:34</expiration_date>
</getApiToken>{
"status": 0,
"message": "0zrjDjZ26Dll5dqdTBmuwwtQg8",
"expiration_date": "07/06/2025 09:17:34"
}If instead you get:
| Status | What it means |
|---|---|
3 | The token is not recognised — check for a copy-paste truncation. |
10 | The token expired. Create a new one in the console. |
11 | The token belongs to a different username. |
504 | The account has never had a token — create the first one in the console. |
| nothing, connection refused | The calling IP is not on the allowlist, or TLS negotiation failed. |
Step 2 — Check you have a verified sender
The source on a message — the name recipients see it come from — must be verified first. Ask which ones already are:
<?xml version="1.0" encoding="UTF-8"?>
<getVerifiedPhones>
<user>
<username>username</username>
</user>
<is_subs>1</is_subs>
</getVerifiedPhones>{
"getVerifiedPhones": {
"user": {
"username": "username"
},
"is_subs": "1"
}
}curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"getVerifiedPhones": {
"user": {
"username": "username"
},
"is_subs": "1"
}
}'// 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({
getVerifiedPhones: {
user: {
username: 'username',
},
is_subs: '1',
},
}),
})
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' => [
'getVerifiedPhones' => [
'user' => [
'username' => 'username',
],
'is_subs' => '1',
],
],
]);
$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', [
'getVerifiedPhones' => [
'user' => [
'username' => 'username',
],
'is_subs' => '1',
],
])
->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={
"getVerifiedPhones": {
"user": {
"username": "username",
},
"is_subs": "1",
},
},
)
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{
"getVerifiedPhones": map[string]any{
"user": map[string]any{
"username": "username",
},
"is_subs": "1",
},
})
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 TextMeVerifiedPhones {
public static void main(String[] args) throws Exception {
String body = """
{
"getVerifiedPhones": {
"user": {
"username": "username"
},
"is_subs": "1"
}
}
""";
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 = """
{
"getVerifiedPhones": {
"user": {
"username": "username"
},
"is_subs": "1"
}
}
""";
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({
"getVerifiedPhones" => {
"user" => {
"username" => "username",
},
"is_subs" => "1",
},
})
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!({
"getVerifiedPhones": {
"user": {
"username": "username"
},
"is_subs": "1"
}
}))
.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(())
}<?xml version="1.0" encoding="UTF-8"?>
<getVerifiedPhones>
<status>0</status>
<message></message>
<verified_phones>
<verified_phone>
<PHONE>xxxxxxxxxx</PHONE>
<USER_ID>xxx</USER_ID>
<ASSIGNMENT_DATE>03/12/14 12:33</ASSIGNMENT_DATE>
<LAST_UPDATE>03/12/14 12:33</LAST_UPDATE>
</verified_phone>
</verified_phones>
</getVerifiedPhones>{
"status": 0,
"message": "",
"verified_phones": [
{
"PHONE": "xxxxxxxxxx",
"USER_ID": "XXXX",
"ASSIGNMENT_DATE": "03/12/14 12:33",
"LAST_UPDATE": "03/12/14 12:33"
}
]
}If the list is empty, submit a number with verify_phone. That sends a verification link by SMS to a mobile, or asks you to call a given number for a landline — a human step, so start it now rather than at deploy time.
Sender names
A source can also be a short alphanumeric name such as DemoAPI — up to 11 characters, letters and digits only. Those are arranged with TextMe directly rather than through verify_phone, because there is no line to send a link to.
Step 3 — Validate the payload without sending
Point the same call at https://my.textme.co.il/api/test and it is parsed, permission-checked and answered exactly as the real endpoint would — but nothing is sent and nothing is charged.
This is the cheapest way to iterate on a payload. When the test endpoint returns status: 0, the shape is right.
What the test endpoint does not tell you
It confirms the request is well-formed and permitted. It does not confirm the number is reachable, and it does not produce a shipment_id you can report on later.
Step 4 — Send
Swap the URL back to /api and send for real:
<?xml version="1.0" encoding="UTF-8"?>
<sms>
<user>
<username>Leeroy</username>
</user>
<source>DemoAPI</source>
<destinations>
<phone>5xxxxxxxx</phone>
</destinations>
<message>This is a sample message</message>
</sms>{
"sms": {
"user": {
"username": "Leeroy"
},
"source": "DemoAPI",
"destinations": {
"phone": "5xxxxxxxx"
},
"message": "This is a sample message"
}
}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": {
"phone": "5xxxxxxxx"
},
"message": "This is a sample message"
}
}'// 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: {
phone: '5xxxxxxxx',
},
message: 'This is a sample message',
},
}),
})
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' => [
'phone' => '5xxxxxxxx',
],
'message' => 'This is a sample message',
],
],
]);
$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' => [
'phone' => '5xxxxxxxx',
],
'message' => 'This is a sample message',
],
])
->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": {
"phone": "5xxxxxxxx",
},
"message": "This is a sample message",
},
},
)
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{
"phone": "5xxxxxxxx",
},
"message": "This is a sample message",
},
})
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 TextMeSendMinimal {
public static void main(String[] args) throws Exception {
String body = """
{
"sms": {
"user": {
"username": "Leeroy"
},
"source": "DemoAPI",
"destinations": {
"phone": "5xxxxxxxx"
},
"message": "This is a sample message"
}
}
""";
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": {
"phone": "5xxxxxxxx"
},
"message": "This is a sample message"
}
}
""";
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" => {
"phone" => "5xxxxxxxx",
},
"message" => "This is a sample message",
},
})
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": {
"phone": "5xxxxxxxx"
},
"message": "This is a sample message"
}
}))
.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(())
}<?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"
}status: 0 means TextMe accepted the message. shipment_id names the campaign it created.
Accepted is not delivered — and there are no safe retries
A 0 means the message was queued, not that a handset received it. And if the call times out at the transport level, you do not know whether it went out: retrying sends a second message, and both are delivered and billed. There is no idempotency key.
The way out is Step 5: set an id on every destination and ask what happened rather than resending.
Step 5 — Find out what happened
Add an id attribute to each destination and you can look that exact message up afterwards. Use something from your own domain — an order number, a user id:
<destinations>
<phone id="order-10052">5xxxxxxxx</phone>
</destinations>{
"destinations": {
"phone": { "$": { "id": "order-10052" }, "_": "5xxxxxxxx" }
}
}Then ask for its delivery report. Reports are asynchronous — the carrier answers in seconds or minutes, sometimes longer — so query over a window rather than immediately:
<?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>{
"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"
}
}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"
}
}'// 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({
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 — 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' => [
'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 — 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', [
'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 — 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={
"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 — 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{
"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 — 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 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 — 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 = """
{
"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 — 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({
"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 — 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!({
"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 — the payload status is what counts
if result["status"] != 0 {
return Err(format!("TextMe {}: {}", result["status"], result["message"]).into());
}
println!("{result}");
Ok(())
}<?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>{
"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"
}
]
}Each transaction carries a status from the DLR vocabulary, which is a different scale from the request status above it. 102 and 0 both mean delivered.
Stop polling
Once this works, register a push URL and TextMe will POST each report to you as it arrives, instead of you asking. That is the shape most production integrations end up with — see Track delivery.
The full sequence
console → authorise IP, create first token
getApiToken → credentials verified (action: current)
getVerifiedPhones → sender available
/api/test → payload accepted, nothing sent
/api sms → status 0, shipment_id (id set per destination)
dlr → status 102, en_message "Delivered"What can go wrong
| Status | Meaning | Fix |
|---|---|---|
2 | A required field is missing | The message names it. |
4 | Not enough credit | Check with balance. |
5 | Outside permitted sending hours | Retry inside your account's window. |
8 | Every destination is blocklisted | Not a bug — the audience opted out. See Opt-out & compliance. |
9 | A destination is malformed | Format numbers as 5xxxxxxxx or 05xxxxxxx. |
515 | The sender is not verified | Step 2. |
989 | The body is empty or over 1005 characters | Shorten it. |
The full list is in Status codes.
Next
- Different text per recipient → Bulk & personalisation
- Store your audience with TextMe → Work with contact lists
- Reconcile deliveries at scale → Track delivery
- Handle replies and opt-outs → Incoming SMS & push

