חשבונות משנה למשווקים
חשבון שמשווק תעבורת TextMe יכול ליצור חשבונות משנה, לממן אותם מהיתרה שלו, ולפעול בשמם. שלוש פעולות מכסות את מחזור החיים, ושדה אחד בכל פעולה אחרת מבצע את הפעולה בשם.
אם החשבון שלכם אינו משווק, הפעולות האלה מחזירות סטטוס 511 ואפשר לדלג על העמוד.
מודל שני שמות המשתמש
כל בקשה נושאת שתי זהויות:
| איפה | מה זה | |
|---|---|---|
| טוקן | כותרת Authorization: Bearer … | פרט הגישה שלכם. תמיד של המשווק. |
user.username | גוף הבקשה | החשבון שהפעולה מתבצעת עבורו. |
בתעבורה שלכם שניהם אותו חשבון. כדי לשלוח בשם חשבון משנה, השאירו את הטוקן שלכם והגדירו את user.username לשלהם:
<?xml version="1.0" encoding="UTF-8"?>
<sms>
<user>
<username>israelisraeli</username> <!-- חשבון המשנה -->
</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."
}
}הקרדיט נגרע מהארנק של חשבון המשנה, השליחה מופיעה בדוחות שלהם, והשולחים המאומתים שלהם חלים. username שאינו אחד משלכם נכשל בסטטוס 503.
החלפה בין השניים היא הטעות הקלאסית
user.username שמוגדר לחשבון שלכם בזמן שהכוונה היא לפעול עבור חשבון משנה יצליח בשמחה — ויגרע את הקרדיט שלכם וירשום את השליחה תחת הדוחות שלכם. שום דבר לא נכשל; הכסף פשוט יוצא מהכיס הלא נכון. שווה בדיקת יחידה.
צעד 1 — יצירת חשבון משנה
<?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+ / דפדפנים — ללא תלויות
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()
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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);
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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();
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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()
# גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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)
}
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
if result.Status.String() != "0" {
panic(fmt.Sprintf("TextMe %s: %s", result.Status, result.Message))
}
fmt.Println(result.Message)
}// Java 17+ — java.net.http, ללא תלויות (פענוח עם 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());
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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();
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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)
# גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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?;
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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 הוא הקרדיט הפותח, שנגרע מהיתרה שלכם. otpPhone הוא אופציונלי ומאפשר אימות OTP למשתמש החדש.
| Status | משמעות |
|---|---|
990 | הסכום עובר את הקרדיט שיש לכם. |
991 | הסכום אינו כולו ספרות. |
992 | ה-source המוגדר כברירת מחדל מחוץ למגבלת 11 התווים. |
993 | אורך הסיסמה נדחה. |
994 | שם המשתמש הזה כבר קיים — בחרו אחר. |
995 | אורך שם המשתמש נדחה. |
996 | אורך שם התצוגה נדחה. |
הסיסמה עוברת בגוף הבקשה כטקסט גלוי
TLS מגן עליה בתעבורה, אבל היא תגיע לכל לוג בקשות שאתם שומרים. הסתירו את userDetails.password לפני תיעוד, וגנרטו את הערך במקום לתת לאדם לבחור אותו:
const password = crypto.randomBytes(18).toString('base64url')בדקו כפילות 994 לפני היצירה
אין פעולה שבודקת "האם שם המשתמש הזה קיים", וכשל ביצירה לא מספר לכם דבר נוסף. נהלו רישום משלכם של שמות המשתמש שהנפקתם, כך שהתנגשות תיתפס לפני הקריאה ולא אחריה.
צעד 2 — טעינת הארנק
<?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+ / דפדפנים — ללא תלויות
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()
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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);
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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();
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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()
# גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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)
}
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
if result.Status.String() != "0" {
panic(fmt.Sprintf("TextMe %s: %s", result.Status, result.Message))
}
fmt.Println(result.Message)
}// Java 17+ — java.net.http, ללא תלויות (פענוח עם 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());
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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();
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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)
# גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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?;
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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 מעביר קרדיטים של הודעות. amount_int מעביר כסף במקום — שלחו אחד מהם, מעולם לא את שניהם.
טעינות אינן אידמפוטנטיות
אין מזהה בקשה ואין סילוק כפילויות. שתי קריאות מעניקות את הקרדיט פעמיים, ואין פעולת API שמחזירה אותו.
אם טעינה נכשלת בטיים-אאוט, אל תנסו אותה שוב. קראו את היתרה עם getBlanceSubs והחליטו לפי המספר בפועל:
// טיים-אאוט לא מספר לכם דבר. קראו את המצב, ואז החליטו.
const before = await subAccountBalance('username1')
try {
await textme(topUp('username1', 5000))
} catch (timeout) {
const after = await subAccountBalance('username1')
// נסו שוב רק אם ניתן להראות שהקרדיט לא נכנס.
if (after === before) await textme(topUp('username1', 5000))
}צעד 3 — קריאת כל היתרות
קריאה אחת מחזירה את היתרה של כל חשבון משנה. שימו לב לשגיאת הכתיב — אלמנט השורש הוא באמת 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+ / דפדפנים — ללא תלויות
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()
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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);
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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();
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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()
# גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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)
}
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
if result.Status.String() != "0" {
panic(fmt.Sprintf("TextMe %s: %s", result.Status, result.Message))
}
fmt.Println(result.Message)
}// Java 17+ — java.net.http, ללא תלויות (פענוח עם 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());
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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();
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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)
# גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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?;
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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, לא getBalanceSubs
איות נכון מחזיר סטטוס 997, Not a valid command sent. שימו הערה בקוד לידו כדי שאף אחד לא "יתקן" את שגיאת הכתיב.
לרשומות אין username
הן נושאות name ו-sms_user_id, ולא את שם המשתמש להתחברות שהעברתם ל-addSub. כדי למפות יתרות לרשומות שלכם, שמרו את sms_user_id בזמן יצירת החשבון — או שמרו על name ייחודי ומשמעותי.
סריקת יתרה נמוכה, שזה מה שהפעולה הזו משמשת לו בדרך כלל:
const { balances } = await textme({
// כן, "Blance" — זה האיות בפרוטוקול.
getBlanceSubs: { user: { username: 'reseller' } },
})
const low = balances.filter((b) => Number(b.amount) < 500)
for (const account of low) {
// אין `username` בתשובה — sms_user_id הוא המפתח היציב.
await alerts.lowBalance({ id: account.sms_user_id, name: account.name, left: account.amount })
}# כן, "Blance" — זה האיות בפרוטוקול.
balances = textme({"getBlanceSubs": {"user": {"username": "reseller"}}})["balances"]
low = [b for b in balances if int(b["amount"]) < 500]
for account in low:
# אין `username` בתשובה — sms_user_id הוא המפתח היציב.
alerts.low_balance(id=account["sms_user_id"], name=account["name"], left=account["amount"])<?php
// כן, "Blance" — זה האיות בפרוטוקול.
$balances = textme(['getBlanceSubs' => ['user' => ['username' => 'reseller']]])['balances'];
$low = array_filter($balances, fn ($b) => (int) $b['amount'] < 500);
foreach ($low as $account) {
// אין `username` בתשובה — sms_user_id הוא המפתח היציב.
$alerts->lowBalance($account['sms_user_id'], $account['name'], $account['amount']);
}ניהול שולחים בין חשבונות משנה
חשבונות משנה מאמתים את השולחים שלהם. is_subs: 1 ב-getVerifiedPhones נותן לכם מבט אחד על כולם, כאשר USER_ID מזהה את הבעלים של כל שורה:
<?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+ / דפדפנים — ללא תלויות
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()
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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);
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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();
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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()
# גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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)
}
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
if result.Status.String() != "0" {
panic(fmt.Sprintf("TextMe %s: %s", result.Status, result.Message))
}
fmt.Println(result.Message)
}// Java 17+ — java.net.http, ללא תלויות (פענוח עם 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());
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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();
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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)
# גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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?;
// גם שגיאה חוזרת כ-HTTP 200 — הסטטוס שבגוף התשובה הוא הקובע
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"
}
]
}שימושי כבדיקת הצטרפות: חשבון משנה בלי שולח מאומת אינו יכול לשלוח דבר, ויחזיר רק סטטוס 515 עד שמישהו ישים לב.
מה שה-API אינו מכסה
| מה שרוצים | המציאות |
|---|---|
| להחזיר קרדיט מחשבון משנה | אין פעולה. הקרדיט זורם בכיוון אחד. |
| למחוק או להשעות חשבון משנה | ממשק הניהול בלבד. |
| לשנות סיסמה של חשבון משנה | ממשק הניהול בלבד. |
| לקבל רשימה של חשבונות המשנה שלכם | הדבר הקרוב ביותר הוא getBlanceSubs, שמחזירה name ו-sms_user_id אבל לא שמות משתמש. |
| לקרוא יתרה של חשבון משנה אחד | קראו את כולן וסננו, או קראו ל-balance עם user.username שלהם. |
מכיוון שאין פעולת רשימה שמחזירה שמות משתמש, נהלו רישום משלכם — שם משתמש, sms_user_id, ומה העברתם. שחזור שלו מה-API בהמשך אינו אפשרי.
צ'ק-ליסט הצטרפות
- שמרו את שם המשתמש ברישום שלכם, כך ש-
994לא יפתיע. - גנרטו את הסיסמה; אף פעם לא לתעד אותה.
addSubעםamountפותח מתון.- שמרו את החשבון שהוחזר מול
sms_user_idמסריקתgetBlanceSubs. - בקשו מהם לאמת שולח, ואז ודאו עם
getVerifiedPhonesו-is_subs: 1. - שלחו הודעת בדיקה עם
user.usernameשלהם וודאו שהיא נגרעת מהיתרה שלהם. - הוסיפו אותם לסריקת היתרה הנמוכה שלכם.
הצעד הבא
- נקודת הקצה של מנויים — תיעוד הפעולות המלא
- מספרי שולח מאומתים — מה כל חשבון משנה צריך לעשות לפני שליחה
- אימות — מודל שתי הזהויות בפירוט

