---
url: https://textme-docs.matat.io/guide/introduction.md
description: >-
  What the API is, the single POST /api endpoint, the test endpoint, and the
  conventions for phone numbers, dates, senders and status codes.
---

# Introduction

The TextMe API sends SMS and voice messages, manages the contact lists and blocklists behind them, schedules and cancels campaigns, and reports on what was delivered. It is a small API with an unusual shape: **one URL, and the root element of the body chooses the operation**.

## Base URL

```http
POST https://my.textme.co.il/api
```

There is no per-operation path. `POST /api` with a `<sms>` root sends a message; the same URL with a `<dlr>` root pulls delivery reports. The [endpoint reference](../endpoints/overview.md) is organised by that root element.

A second URL validates without sending:

```http
POST https://my.textme.co.il/api/test
```

It answers exactly like the live endpoint but performs no action: nothing is sent, nothing is charged, no list is modified. Use it while you are still shaping a payload.

A [SOAP interface](../reference/soap.md) covering four operations is also available at `https://my.textme.co.il/soap?wsdl`, and a [push interface](../endpoints/push.md) delivers reports to a URL you own.

## Anatomy of a call

Every request carries three things: a bearer token in the header, a `user.username` in the body, and a root element naming the operation.

::: code-group

```xml [XML]
POST /api HTTP/1.1
Host: my.textme.co.il
Authorization: Bearer {api_token}
Content-Type: application/xml

<?xml version="1.0" encoding="UTF-8"?>
<sms>                              <!-- the operation -->
    <user>
        <username>Leeroy</username>    <!-- the account acting -->
    </user>
    <source>DemoAPI</source>
    <destinations>
        <phone>5xxxxxxxx</phone>
    </destinations>
    <message>Hello</message>
</sms>
```

```json [JSON]
POST /api HTTP/1.1
Host: my.textme.co.il
Authorization: Bearer {api_token}
Content-Type: application/json

{
  "sms": {
    "user": { "username": "Leeroy" },
    "source": "DemoAPI",
    "destinations": { "phone": "5xxxxxxxx" },
    "message": "Hello"
  }
}
```

:::

The token proves who is calling; the `username` says which account the call acts for. They are usually the same account. They differ when a reseller acts on behalf of a [sub-account](../use-cases/reseller-subscribers.md).

## Conventions

| Convention | Rule |
|---|---|
| Encoding | UTF-8 throughout. Hebrew message bodies and Hebrew response messages are both normal. |
| Phone numbers | `5xxxxxxxx` or `05xxxxxxx`. Israeli landlines are written the same way (`3xxxxxxx`). |
| Dates | `dd/mm/yy hh:mm`. Two-digit year, 24-hour clock. Some report fields answer with seconds appended. |
| Sender (`source`) | Up to 11 characters, letters and digits only, no `+`. Must be a [verified sender](../endpoints/verified-senders.md). |
| Message length | Up to 1005 characters. |
| Booleans | Sent as `1` / `0` strings, not `true` / `false`. |
| Success | `status` `0`. Anything else is a failure. See [status codes](../reference/status-codes.md). |

::: warning HTTP status is not the API status
A rejected request still comes back as `HTTP 200`. The `status` field inside the body is the one that tells you what happened, which is why every example on this site checks it before doing anything else.
:::

## Authentication in one line

Send `Authorization: Bearer {api_token}` on every request. Tokens are created in the TextMe console and can then be rotated through the API; up to five stay valid at once. Full detail is in [Authentication](./authentication.md).

## Quick start

**1. Confirm your token works.** Ask for the account's current token. A cheap call that fails loudly if the credentials are wrong.

::: code-group

```xml [XML]
<?xml version="1.0" encoding="UTF-8"?>
<getApiToken>
    <user>
        <username>admin_username</username>
    </user>
    <username>username_for_token</username>
    <action>current</action>
</getApiToken>
```

```json [JSON]
{
  "getApiToken": {
    "user": {
      "username": "admin_username"
    },
    "username": "username_for_token",
    "action": "current"
  }
}
```

```bash [cURL]
curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
  "getApiToken": {
    "user": {
      "username": "admin_username"
    },
    "username": "username_for_token",
    "action": "current"
  }
}'
```

```js [JavaScript]
// Node.js 18+ or any modern browser. No dependencies
const response = await fetch('https://my.textme.co.il/api', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TEXTME_API_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    getApiToken: {
      user: {
        username: 'admin_username',
      },
      username: 'username_for_token',
      action: 'current',
    },
  }),
})

const result = await response.json()

// Errors arrive as HTTP 200 too, so the payload status is what counts
if (Number(result.status) !== 0) {
  throw new Error(`TextMe ${result.status}: ${result.message}`)
}

console.log(result)
```

```php [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' => [
        'getApiToken' => [
            'user' => [
                'username' => 'admin_username',
            ],
            'username' => 'username_for_token',
            'action' => 'current',
        ],
    ],
]);

$result = json_decode($response->getBody()->getContents(), true);

// Errors arrive as HTTP 200 too, so the payload status is what counts
if ((int) $result['status'] !== 0) {
    throw new RuntimeException("TextMe {$result['status']}: {$result['message']}");
}

print_r($result);
```

```php [Laravel]
<?php

use Illuminate\Support\Facades\Http;

$result = Http::withToken(config('services.textme.token'))
    ->acceptJson()
    ->post('https://my.textme.co.il/api', [
        'getApiToken' => [
            'user' => [
                'username' => 'admin_username',
            ],
            'username' => 'username_for_token',
            'action' => 'current',
        ],
    ])
    ->throw()
    ->json();

// Errors arrive as HTTP 200 too, so the payload status is what counts
throw_if((int) $result['status'] !== 0, RuntimeException::class,
    "TextMe {$result['status']}: {$result['message']}");

logger()->info('TextMe', $result);
```

```python [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={
        "getApiToken": {
            "user": {
                "username": "admin_username",
            },
            "username": "username_for_token",
            "action": "current",
        },
    },
)
response.raise_for_status()
result = response.json()

# Errors arrive as HTTP 200 too, so the payload status is what counts
if int(result["status"]) != 0:
    raise RuntimeError(f"TextMe {result['status']}: {result['message']}")

print(result)
```

```go [Go]
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

func main() {
	payload, _ := json.Marshal(map[string]any{
		"getApiToken": map[string]any{
			"user": map[string]any{
				"username": "admin_username",
			},
			"username": "username_for_token",
			"action": "current",
		},
	})

	req, _ := http.NewRequest("POST", "https://my.textme.co.il/api", bytes.NewReader(payload))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("TEXTME_API_TOKEN"))
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	var result struct {
		Status  json.Number `json:"status"`
		Message string      `json:"message"`
	}
	if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
		panic(err)
	}

	// Errors arrive as HTTP 200 too, so 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]
// Java 17+ using java.net.http. No dependencies (parse with Jackson/Gson)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class TextMeTokenCurrent {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "getApiToken": {
                "user": {
                  "username": "admin_username"
                },
                "username": "username_for_token",
                "action": "current"
              }
            }
            """;

        HttpRequest request = HttpRequest.newBuilder(URI.create("https://my.textme.co.il/api"))
            .header("Authorization", "Bearer " + System.getenv("TEXTME_API_TOKEN"))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        // Errors arrive as HTTP 200 too, so the payload status is what counts
        System.out.println(response.body());
    }
}
```

```csharp [C#]
// .NET 8+ using System.Net.Http
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

var payload = """
    {
      "getApiToken": {
        "user": {
          "username": "admin_username"
        },
        "username": "username_for_token",
        "action": "current"
      }
    }
    """;

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("TEXTME_API_TOKEN"));

var response = await http.PostAsync("https://my.textme.co.il/api",
    new StringContent(payload, Encoding.UTF8, "application/json"));

var result = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement;
var status = result.GetProperty("status").ToString();

// Errors arrive as HTTP 200 too, so 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 [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({
  "getApiToken" => {
    "user" => {
      "username" => "admin_username",
    },
    "username" => "username_for_token",
    "action" => "current",
  },
})

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end

result = JSON.parse(response.body)

# Errors arrive as HTTP 200 too, so the payload status is what counts
raise "TextMe #{result['status']}: #{result['message']}" unless result["status"].to_i.zero?

pp result
```

```rust [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!({
          "getApiToken": {
            "user": {
              "username": "admin_username"
            },
            "username": "username_for_token",
            "action": "current"
          }
        }))
        .send()
        .await?
        .json()
        .await?;

    // Errors arrive as HTTP 200 too, so the payload status is what counts
    if result["status"] != 0 {
        return Err(format!("TextMe {}: {}", result["status"], result["message"]).into());
    }

    println!("{result}");
    Ok(())
}
```

:::

**2. Send a message.**

::: code-group

```xml [XML]
<?xml version="1.0" encoding="UTF-8"?>
<sms>
    <user>
        <username>Leeroy</username>
    </user>
    <source>DemoAPI</source>
    <destinations>
        <phone>5xxxxxxxx</phone>
    </destinations>
    <message>This is a sample message</message>
</sms>
```

```json [JSON]
{
  "sms": {
    "user": {
      "username": "Leeroy"
    },
    "source": "DemoAPI",
    "destinations": {
      "phone": "5xxxxxxxx"
    },
    "message": "This is a sample message"
  }
}
```

```bash [cURL]
curl --location 'https://my.textme.co.il/api' \
--header "Authorization: Bearer $TEXTME_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
  "sms": {
    "user": {
      "username": "Leeroy"
    },
    "source": "DemoAPI",
    "destinations": {
      "phone": "5xxxxxxxx"
    },
    "message": "This is a sample message"
  }
}'
```

```js [JavaScript]
// Node.js 18+ or any modern browser. No dependencies
const response = await fetch('https://my.textme.co.il/api', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TEXTME_API_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    sms: {
      user: {
        username: 'Leeroy',
      },
      source: 'DemoAPI',
      destinations: {
        phone: '5xxxxxxxx',
      },
      message: 'This is a sample message',
    },
  }),
})

const result = await response.json()

// Errors arrive as HTTP 200 too, so the payload status is what counts
if (Number(result.status) !== 0) {
  throw new Error(`TextMe ${result.status}: ${result.message}`)
}

console.log(result)
```

```php [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' => [
        'sms' => [
            'user' => [
                'username' => 'Leeroy',
            ],
            'source' => 'DemoAPI',
            'destinations' => [
                'phone' => '5xxxxxxxx',
            ],
            'message' => 'This is a sample message',
        ],
    ],
]);

$result = json_decode($response->getBody()->getContents(), true);

// Errors arrive as HTTP 200 too, so the payload status is what counts
if ((int) $result['status'] !== 0) {
    throw new RuntimeException("TextMe {$result['status']}: {$result['message']}");
}

print_r($result);
```

```php [Laravel]
<?php

use Illuminate\Support\Facades\Http;

$result = Http::withToken(config('services.textme.token'))
    ->acceptJson()
    ->post('https://my.textme.co.il/api', [
        'sms' => [
            'user' => [
                'username' => 'Leeroy',
            ],
            'source' => 'DemoAPI',
            'destinations' => [
                'phone' => '5xxxxxxxx',
            ],
            'message' => 'This is a sample message',
        ],
    ])
    ->throw()
    ->json();

// Errors arrive as HTTP 200 too, so the payload status is what counts
throw_if((int) $result['status'] !== 0, RuntimeException::class,
    "TextMe {$result['status']}: {$result['message']}");

logger()->info('TextMe', $result);
```

```python [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={
        "sms": {
            "user": {
                "username": "Leeroy",
            },
            "source": "DemoAPI",
            "destinations": {
                "phone": "5xxxxxxxx",
            },
            "message": "This is a sample message",
        },
    },
)
response.raise_for_status()
result = response.json()

# Errors arrive as HTTP 200 too, so the payload status is what counts
if int(result["status"]) != 0:
    raise RuntimeError(f"TextMe {result['status']}: {result['message']}")

print(result)
```

```go [Go]
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

func main() {
	payload, _ := json.Marshal(map[string]any{
		"sms": map[string]any{
			"user": map[string]any{
				"username": "Leeroy",
			},
			"source": "DemoAPI",
			"destinations": map[string]any{
				"phone": "5xxxxxxxx",
			},
			"message": "This is a sample message",
		},
	})

	req, _ := http.NewRequest("POST", "https://my.textme.co.il/api", bytes.NewReader(payload))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("TEXTME_API_TOKEN"))
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	var result struct {
		Status  json.Number `json:"status"`
		Message string      `json:"message"`
	}
	if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
		panic(err)
	}

	// Errors arrive as HTTP 200 too, so 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]
// Java 17+ using java.net.http. No dependencies (parse with Jackson/Gson)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class TextMeSendMinimal {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "sms": {
                "user": {
                  "username": "Leeroy"
                },
                "source": "DemoAPI",
                "destinations": {
                  "phone": "5xxxxxxxx"
                },
                "message": "This is a sample message"
              }
            }
            """;

        HttpRequest request = HttpRequest.newBuilder(URI.create("https://my.textme.co.il/api"))
            .header("Authorization", "Bearer " + System.getenv("TEXTME_API_TOKEN"))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        // Errors arrive as HTTP 200 too, so the payload status is what counts
        System.out.println(response.body());
    }
}
```

```csharp [C#]
// .NET 8+ using System.Net.Http
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

var payload = """
    {
      "sms": {
        "user": {
          "username": "Leeroy"
        },
        "source": "DemoAPI",
        "destinations": {
          "phone": "5xxxxxxxx"
        },
        "message": "This is a sample message"
      }
    }
    """;

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("TEXTME_API_TOKEN"));

var response = await http.PostAsync("https://my.textme.co.il/api",
    new StringContent(payload, Encoding.UTF8, "application/json"));

var result = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement;
var status = result.GetProperty("status").ToString();

// Errors arrive as HTTP 200 too, so 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 [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({
  "sms" => {
    "user" => {
      "username" => "Leeroy",
    },
    "source" => "DemoAPI",
    "destinations" => {
      "phone" => "5xxxxxxxx",
    },
    "message" => "This is a sample message",
  },
})

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end

result = JSON.parse(response.body)

# Errors arrive as HTTP 200 too, so the payload status is what counts
raise "TextMe #{result['status']}: #{result['message']}" unless result["status"].to_i.zero?

pp result
```

```rust [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!({
          "sms": {
            "user": {
              "username": "Leeroy"
            },
            "source": "DemoAPI",
            "destinations": {
              "phone": "5xxxxxxxx"
            },
            "message": "This is a sample message"
          }
        }))
        .send()
        .await?
        .json()
        .await?;

    // Errors arrive as HTTP 200 too, so the payload status is what counts
    if result["status"] != 0 {
        return Err(format!("TextMe {}: {}", result["status"], result["message"]).into());
    }

    println!("{result}");
    Ok(())
}
```

:::

A `status` of `0` means the message was accepted and `shipment_id` names the campaign it created:

::: code-group

```xml [XML]
<?xml version="1.0" encoding="UTF-8"?>
<sms>
    <status>0</status>
    <message>SMS will be sent</message>
    <shipment_id>xxxxxxx</shipment_id>
</sms>
```

```json [JSON]
{
  "status": 0,
  "message": "SMS will be sent",
  "shipment_id": "XXXXXXX"
}
```

:::

## Finding your way around

* **[Use cases](../use-cases/send-your-first-sms.md)**: end-to-end walkthroughs: first message, bulk with personalisation, delivery reconciliation, opt-out handling.
* **[Endpoints](../endpoints/overview.md)**: one page per operation family, with every parameter and both payload encodings.
* **[Reference](../reference/status-codes.md)**: the status code tables, the delivery-status vocabulary, and the SOAP contract.
