Skip to content

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:

WhereWhat it is
TokenAuthorization: Bearer … headerYour credential. Always the reseller's.
user.usernameRequest bodyThe 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
<?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>
json
{
  "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
<?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>
json
{
  "addSub": {
    "user": {
      "username": "xxxxxx"
    },
    "userDetails": {
      "name": "israel israeli",
      "username": "israelisraeli",
      "password": "israelisraeli",
      "source": "055xxxxxxx",
      "amount": "70000",
      "otpPhone": "5xxxxxxxx"
    }
  }
}
bash
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"
    }
  }
}'
js
// Node.js 18+ / browsers — no dependencies
const response = await fetch('https://my.textme.co.il/api', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TEXTME_API_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    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
<?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
<?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);
python
# pip install httpx
import os

import httpx

response = httpx.post(
    "https://my.textme.co.il/api",
    headers={"Authorization": f"Bearer {os.environ['TEXTME_API_TOKEN']}"},
    json={
        "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)
go
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
// 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());
    }
}
csharp
// .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);
ruby
require "net/http"
require "json"

uri = URI("https://my.textme.co.il/api")

request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('TEXTME_API_TOKEN')}"
request["Content-Type"] = "application/json"
request.body = JSON.dump({
  "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
rust
// [dependencies]
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
use serde_json::{json, Value};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let result: Value = reqwest::Client::new()
        .post("https://my.textme.co.il/api")
        .bearer_auth(std::env::var("TEXTME_API_TOKEN")?)
        .json(&json!({
          "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
<?xml version="1.0" encoding="UTF-8"?>
<addSub>
    <status>0</status>
    <message>The user was created successfully</message>
</addSub>
json
{
  "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.

StatusMeaning
990The amount exceeds the credit you hold.
991The amount is not all digits.
992The default source is outside the 11-character limit.
993Password length rejected.
994That username already exists — pick another.
995Username length rejected.
996Display 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:

js
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
<?xml version="1.0" encoding="UTF-8"?>
<updateAmountSub>
    <user>
        <username>xxxxxx</username>
    </user>
    <userDetails>
        <username>username1</username>
        <amount>70000</amount>
    </userDetails>
</updateAmountSub>
json
{
  "updateAmountSub": {
    "user": {
      "username": "xxxxxx"
    },
    "userDetails": {
      "username": "username1",
      "amount": "70000"
    }
  }
}
bash
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"
    }
  }
}'
js
// Node.js 18+ / browsers — no dependencies
const response = await fetch('https://my.textme.co.il/api', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TEXTME_API_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    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
<?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
<?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);
python
# pip install httpx
import os

import httpx

response = httpx.post(
    "https://my.textme.co.il/api",
    headers={"Authorization": f"Bearer {os.environ['TEXTME_API_TOKEN']}"},
    json={
        "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)
go
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
// 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());
    }
}
csharp
// .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);
ruby
require "net/http"
require "json"

uri = URI("https://my.textme.co.il/api")

request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('TEXTME_API_TOKEN')}"
request["Content-Type"] = "application/json"
request.body = JSON.dump({
  "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
rust
// [dependencies]
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
use serde_json::{json, Value};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let result: Value = reqwest::Client::new()
        .post("https://my.textme.co.il/api")
        .bearer_auth(std::env::var("TEXTME_API_TOKEN")?)
        .json(&json!({
          "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
<?xml version="1.0" encoding="UTF-8"?>
<updateAmountSub>
    <status>0</status>
    <message>Wallet successfully updated</message>
</updateAmountSub>
json
{
  "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:

js
// 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
<?xml version="1.0" encoding="UTF-8"?>
<getBlanceSubs>
    <user>
        <username>xxxxxx</username>
    </user>
</getBlanceSubs>
json
{
  "getBlanceSubs": {
    "user": {
      "username": "xxxxxx"
    }
  }
}
bash
curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
  "getBlanceSubs": {
    "user": {
      "username": "xxxxxx"
    }
  }
}'
js
// Node.js 18+ / browsers — no dependencies
const response = await fetch('https://my.textme.co.il/api', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TEXTME_API_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    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
<?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
<?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);
python
# pip install httpx
import os

import httpx

response = httpx.post(
    "https://my.textme.co.il/api",
    headers={"Authorization": f"Bearer {os.environ['TEXTME_API_TOKEN']}"},
    json={
        "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)
go
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
// 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());
    }
}
csharp
// .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);
ruby
require "net/http"
require "json"

uri = URI("https://my.textme.co.il/api")

request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('TEXTME_API_TOKEN')}"
request["Content-Type"] = "application/json"
request.body = JSON.dump({
  "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
rust
// [dependencies]
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
use serde_json::{json, Value};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let result: Value = reqwest::Client::new()
        .post("https://my.textme.co.il/api")
        .bearer_auth(std::env::var("TEXTME_API_TOKEN")?)
        .json(&json!({
          "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
<?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>
json
{
  "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:

js
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 })
}
python
# 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
<?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
<?xml version="1.0" encoding="UTF-8"?>
<getVerifiedPhones>
    <user>
        <username>username</username>
    </user>
    <is_subs>1</is_subs>
</getVerifiedPhones>
json
{
  "getVerifiedPhones": {
    "user": {
      "username": "username"
    },
    "is_subs": "1"
  }
}
bash
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"
  }
}'
js
// Node.js 18+ / browsers — no dependencies
const response = await fetch('https://my.textme.co.il/api', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TEXTME_API_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    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
<?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
<?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);
python
# pip install httpx
import os

import httpx

response = httpx.post(
    "https://my.textme.co.il/api",
    headers={"Authorization": f"Bearer {os.environ['TEXTME_API_TOKEN']}"},
    json={
        "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)
go
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
// 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());
    }
}
csharp
// .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);
ruby
require "net/http"
require "json"

uri = URI("https://my.textme.co.il/api")

request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('TEXTME_API_TOKEN')}"
request["Content-Type"] = "application/json"
request.body = JSON.dump({
  "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
rust
// [dependencies]
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
use serde_json::{json, Value};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let result: Value = reqwest::Client::new()
        .post("https://my.textme.co.il/api")
        .bearer_auth(std::env::var("TEXTME_API_TOKEN")?)
        .json(&json!({
          "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
<?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>
json
{
  "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

WantReality
Reclaim credit from a sub-accountNo operation. Credit moves one way.
Delete or suspend a sub-accountConsole only.
Change a sub-account's passwordConsole only.
List your sub-accountsClosest thing is getBlanceSubs, which returns name and sms_user_id but not usernames.
Read one sub-account's balanceRead 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

  1. Reserve the username in your own registry, so 994 cannot surprise you.
  2. Generate the password; never log it.
  3. addSub with a modest opening amount.
  4. Store the returned account against sms_user_id from a getBlanceSubs sweep.
  5. Have them verify a sender, then confirm with getVerifiedPhones and is_subs: 1.
  6. Send a test message with user.username set to theirs and confirm it draws on their balance.
  7. Add them to your low-balance sweep.

Next