Reseller sub-accounts
An account that resells TextMe capacity can create sub-accounts, fund them from its own credit, and act on their behalf. Three operations cover the lifecycle, and one field on every other operation does the acting.
If your account does not resell, these return status 511 and you can skip this page.
The two-username model
Every request carries two identities:
| Where | What it is | |
|---|---|---|
| Token | Authorization: Bearer … header | Your credential. Always the reseller's. |
user.username | Request body | The account the operation acts for. |
For your own traffic they are the same account. To send on a sub-account's behalf, keep your token and set user.username to theirs:
<?xml version="1.0" encoding="UTF-8"?>
<sms>
<user>
<username>israelisraeli</username> <!-- the sub-account -->
</user>
<source>055xxxxxxx</source>
<destinations>
<phone id="order-10052">5xxxxxxxx</phone>
</destinations>
<message>Your order is on its way.</message>
</sms>{
"sms": {
"user": { "username": "israelisraeli" },
"source": "055xxxxxxx",
"destinations": {
"phone": { "$": { "id": "order-10052" }, "_": "5xxxxxxxx" }
},
"message": "Your order is on its way."
}
}The credit comes from the sub-account's wallet, the send appears in their reports, and their verified senders apply. A username that is not one of yours fails with status 503.
Reversing the two is the classic mistake
user.username set to your own account while intending to act for a sub-account will happily succeed — spending your credit and filing the send under your reports. Nothing fails; the money just comes out of the wrong pocket. Worth a unit test.
Step 1 — Create a sub-account
<?xml version="1.0" encoding="UTF-8"?>
<addSub>
<user>
<username>xxxxxx</username>
</user>
<userDetails>
<name>israel israeli</name>
<username>israelisraeli</username>
<password>israelisraeli</password>
<source>055xxxxxxx</source>
<amount>70000</amount>
<otpPhone>5xxxxxxxx</otpPhone>
</userDetails>
</addSub>{
"addSub": {
"user": {
"username": "xxxxxx"
},
"userDetails": {
"name": "israel israeli",
"username": "israelisraeli",
"password": "israelisraeli",
"source": "055xxxxxxx",
"amount": "70000",
"otpPhone": "5xxxxxxxx"
}
}
}curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"addSub": {
"user": {
"username": "xxxxxx"
},
"userDetails": {
"name": "israel israeli",
"username": "israelisraeli",
"password": "israelisraeli",
"source": "055xxxxxxx",
"amount": "70000",
"otpPhone": "5xxxxxxxx"
}
}
}'// 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({
addSub: {
user: {
username: 'xxxxxx',
},
userDetails: {
name: 'israel israeli',
username: 'israelisraeli',
password: 'israelisraeli',
source: '055xxxxxxx',
amount: '70000',
otpPhone: '5xxxxxxxx',
},
},
}),
})
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' => [
'addSub' => [
'user' => [
'username' => 'xxxxxx',
],
'userDetails' => [
'name' => 'israel israeli',
'username' => 'israelisraeli',
'password' => 'israelisraeli',
'source' => '055xxxxxxx',
'amount' => '70000',
'otpPhone' => '5xxxxxxxx',
],
],
],
]);
$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', [
'addSub' => [
'user' => [
'username' => 'xxxxxx',
],
'userDetails' => [
'name' => 'israel israeli',
'username' => 'israelisraeli',
'password' => 'israelisraeli',
'source' => '055xxxxxxx',
'amount' => '70000',
'otpPhone' => '5xxxxxxxx',
],
],
])
->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={
"addSub": {
"user": {
"username": "xxxxxx",
},
"userDetails": {
"name": "israel israeli",
"username": "israelisraeli",
"password": "israelisraeli",
"source": "055xxxxxxx",
"amount": "70000",
"otpPhone": "5xxxxxxxx",
},
},
},
)
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{
"addSub": map[string]any{
"user": map[string]any{
"username": "xxxxxx",
},
"userDetails": map[string]any{
"name": "israel israeli",
"username": "israelisraeli",
"password": "israelisraeli",
"source": "055xxxxxxx",
"amount": "70000",
"otpPhone": "5xxxxxxxx",
},
},
})
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 TextMeSubscriberAdd {
public static void main(String[] args) throws Exception {
String body = """
{
"addSub": {
"user": {
"username": "xxxxxx"
},
"userDetails": {
"name": "israel israeli",
"username": "israelisraeli",
"password": "israelisraeli",
"source": "055xxxxxxx",
"amount": "70000",
"otpPhone": "5xxxxxxxx"
}
}
}
""";
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 = """
{
"addSub": {
"user": {
"username": "xxxxxx"
},
"userDetails": {
"name": "israel israeli",
"username": "israelisraeli",
"password": "israelisraeli",
"source": "055xxxxxxx",
"amount": "70000",
"otpPhone": "5xxxxxxxx"
}
}
}
""";
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({
"addSub" => {
"user" => {
"username" => "xxxxxx",
},
"userDetails" => {
"name" => "israel israeli",
"username" => "israelisraeli",
"password" => "israelisraeli",
"source" => "055xxxxxxx",
"amount" => "70000",
"otpPhone" => "5xxxxxxxx",
},
},
})
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!({
"addSub": {
"user": {
"username": "xxxxxx"
},
"userDetails": {
"name": "israel israeli",
"username": "israelisraeli",
"password": "israelisraeli",
"source": "055xxxxxxx",
"amount": "70000",
"otpPhone": "5xxxxxxxx"
}
}
}))
.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"?>
<addSub>
<status>0</status>
<message>The user was created successfully</message>
</addSub>{
"status": 0,
"message": "The user was created successfully"
}userDetails.amount is the opening credit, taken from your balance. otpPhone is optional and enables OTP authentication for the new user.
| Status | Meaning |
|---|---|
990 | The amount exceeds the credit you hold. |
991 | The amount is not all digits. |
992 | The default source is outside the 11-character limit. |
993 | Password length rejected. |
994 | That username already exists — pick another. |
995 | Username length rejected. |
996 | Display name length rejected. |
The password goes over the wire in plaintext
TLS protects it in transit, but it will land in any request log you keep. Redact userDetails.password before logging, and generate the value rather than letting a human choose one:
const password = crypto.randomBytes(18).toString('base64url')Check for 994 before creating
There is no "does this username exist" operation, and a failed create tells you nothing else. Keep your own registry of the usernames you have issued, so a collision is caught before the call rather than after.
Step 2 — Fund the wallet
<?xml version="1.0" encoding="UTF-8"?>
<updateAmountSub>
<user>
<username>xxxxxx</username>
</user>
<userDetails>
<username>username1</username>
<amount>70000</amount>
</userDetails>
</updateAmountSub>{
"updateAmountSub": {
"user": {
"username": "xxxxxx"
},
"userDetails": {
"username": "username1",
"amount": "70000"
}
}
}curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"updateAmountSub": {
"user": {
"username": "xxxxxx"
},
"userDetails": {
"username": "username1",
"amount": "70000"
}
}
}'// 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({
updateAmountSub: {
user: {
username: 'xxxxxx',
},
userDetails: {
username: 'username1',
amount: '70000',
},
},
}),
})
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' => [
'updateAmountSub' => [
'user' => [
'username' => 'xxxxxx',
],
'userDetails' => [
'username' => 'username1',
'amount' => '70000',
],
],
],
]);
$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', [
'updateAmountSub' => [
'user' => [
'username' => 'xxxxxx',
],
'userDetails' => [
'username' => 'username1',
'amount' => '70000',
],
],
])
->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={
"updateAmountSub": {
"user": {
"username": "xxxxxx",
},
"userDetails": {
"username": "username1",
"amount": "70000",
},
},
},
)
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{
"updateAmountSub": map[string]any{
"user": map[string]any{
"username": "xxxxxx",
},
"userDetails": map[string]any{
"username": "username1",
"amount": "70000",
},
},
})
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 TextMeSubscriberWallet {
public static void main(String[] args) throws Exception {
String body = """
{
"updateAmountSub": {
"user": {
"username": "xxxxxx"
},
"userDetails": {
"username": "username1",
"amount": "70000"
}
}
}
""";
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 = """
{
"updateAmountSub": {
"user": {
"username": "xxxxxx"
},
"userDetails": {
"username": "username1",
"amount": "70000"
}
}
}
""";
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({
"updateAmountSub" => {
"user" => {
"username" => "xxxxxx",
},
"userDetails" => {
"username" => "username1",
"amount" => "70000",
},
},
})
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!({
"updateAmountSub": {
"user": {
"username": "xxxxxx"
},
"userDetails": {
"username": "username1",
"amount": "70000"
}
}
}))
.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"?>
<updateAmountSub>
<status>0</status>
<message>Wallet successfully updated</message>
</updateAmountSub>{
"status": 0,
"message": "Wallet successfully updated"
}amount moves message credits. amount_int moves money instead — send one or the other, never both.
Top-ups are not idempotent
There is no request id and no deduplication. Two calls grant the credit twice, and there is no API operation to take it back.
If a top-up times out, do not retry it. Read the balance with getBlanceSubs and decide from the actual number:
// A timeout tells you nothing. Read the state, then decide.
const before = await subAccountBalance('username1')
try {
await textme(topUp('username1', 5000))
} catch (timeout) {
const after = await subAccountBalance('username1')
// Only retry if the credit demonstrably did not land.
if (after === before) await textme(topUp('username1', 5000))
}Step 3 — Read every balance
One call returns every sub-account's balance. Note the misspelling — the root element really is getBlanceSubs:
<?xml version="1.0" encoding="UTF-8"?>
<getBlanceSubs>
<user>
<username>xxxxxx</username>
</user>
</getBlanceSubs>{
"getBlanceSubs": {
"user": {
"username": "xxxxxx"
}
}
}curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"getBlanceSubs": {
"user": {
"username": "xxxxxx"
}
}
}'// 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({
getBlanceSubs: {
user: {
username: 'xxxxxx',
},
},
}),
})
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' => [
'getBlanceSubs' => [
'user' => [
'username' => 'xxxxxx',
],
],
],
]);
$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', [
'getBlanceSubs' => [
'user' => [
'username' => 'xxxxxx',
],
],
])
->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={
"getBlanceSubs": {
"user": {
"username": "xxxxxx",
},
},
},
)
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{
"getBlanceSubs": map[string]any{
"user": map[string]any{
"username": "xxxxxx",
},
},
})
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 TextMeSubscriberBalances {
public static void main(String[] args) throws Exception {
String body = """
{
"getBlanceSubs": {
"user": {
"username": "xxxxxx"
}
}
}
""";
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 = """
{
"getBlanceSubs": {
"user": {
"username": "xxxxxx"
}
}
}
""";
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({
"getBlanceSubs" => {
"user" => {
"username" => "xxxxxx",
},
},
})
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!({
"getBlanceSubs": {
"user": {
"username": "xxxxxx"
}
}
}))
.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"?>
<getBlanceSubs>
<status>0</status>
<message></message>
<balances>
<balance>
<amount>80</amount>
<sms_user_id>xxx</sms_user_id>
<name>name1</name>
</balance>
<balance>
<amount>50</amount>
<sms_user_id>xxx</sms_user_id>
<name>name2</name>
</balance>
</balances>
</getBlanceSubs>{
"status": 0,
"message": "",
"balances": [
{
"amount": "80",
"sms_user_id": "XXXX",
"name": "name1"
},
{
"amount": "50",
"sms_user_id": "XXXX",
"name": "name2"
}
]
}getBlanceSubs, not getBalanceSubs
Spelling it correctly returns status 997, Not a valid command sent. Put a comment next to it in your code so nobody helpfully fixes the typo.
Entries have no username
They carry name and sms_user_id, not the login username you passed to addSub. To map balances back to your own records, store sms_user_id when you create the account — or keep name unique and meaningful.
A low-balance sweep, which is what this operation is usually for:
const { balances } = await textme({
// Yes, "Blance" — that is the wire spelling.
getBlanceSubs: { user: { username: 'reseller' } },
})
const low = balances.filter((b) => Number(b.amount) < 500)
for (const account of low) {
// No `username` in the response — sms_user_id is the stable key.
await alerts.lowBalance({ id: account.sms_user_id, name: account.name, left: account.amount })
}# Yes, "Blance" — that is the wire spelling.
balances = textme({"getBlanceSubs": {"user": {"username": "reseller"}}})["balances"]
low = [b for b in balances if int(b["amount"]) < 500]
for account in low:
# No `username` in the response — sms_user_id is the stable key.
alerts.low_balance(id=account["sms_user_id"], name=account["name"], left=account["amount"])<?php
// Yes, "Blance" — that is the wire spelling.
$balances = textme(['getBlanceSubs' => ['user' => ['username' => 'reseller']]])['balances'];
$low = array_filter($balances, fn ($b) => (int) $b['amount'] < 500);
foreach ($low as $account) {
// No `username` in the response — sms_user_id is the stable key.
$alerts->lowBalance($account['sms_user_id'], $account['name'], $account['amount']);
}Managing senders across sub-accounts
Sub-accounts verify their own senders. is_subs: 1 on getVerifiedPhones gives you one view across all of them, with USER_ID identifying the owner of each row:
<?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"
}
]
}Useful as an onboarding check: a sub-account with no verified sender cannot send anything, and will produce nothing but status 515 until somebody notices.
What the API does not cover
| Want | Reality |
|---|---|
| Reclaim credit from a sub-account | No operation. Credit moves one way. |
| Delete or suspend a sub-account | Console only. |
| Change a sub-account's password | Console only. |
| List your sub-accounts | Closest thing is getBlanceSubs, which returns name and sms_user_id but not usernames. |
| Read one sub-account's balance | Read them all and filter, or call balance with user.username set to theirs. |
Because there is no listing operation that returns usernames, keep your own registry — username, sms_user_id, and what you granted. Rebuilding it from the API afterwards is not possible.
Onboarding checklist
- Reserve the username in your own registry, so
994cannot surprise you. - Generate the password; never log it.
addSubwith a modest openingamount.- Store the returned account against
sms_user_idfrom agetBlanceSubssweep. - Have them verify a sender, then confirm with
getVerifiedPhonesandis_subs: 1. - Send a test message with
user.usernameset to theirs and confirm it draws on their balance. - Add them to your low-balance sweep.
Next
- Subscribers endpoint — the full operation reference
- Verified senders — what each sub-account has to do before sending
- Authentication — the two-identity model in detail

