---
url: https://textme-docs.matat.io/endpoints/tts.md
description: >-
  sms with a tts block: speak the message as a phone call instead of texting it,
  reaching filtered handsets and landlines.
---

# Voice (TTS)

Turns a written message into a spoken phone call. This is the [`sms`](./send.md) operation with a `tts` block attached: same root element, same destinations, same everything, plus instructions for how to say it.

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

Its main use is reaching handsets that cannot receive regular SMS. Kosher (filtered) phones are widespread in Israel and report [DLR status `15`](../reference/dlr-statuses.md#blocked-or-refused) when texted; `tts.type` `2` routes those numbers to a voice call automatically while everyone else still gets an SMS.

## `sms` Send a voice message

### Parameters

Everything [`sms`](./send.md#parameters) accepts, plus:

| Name | Type | Description | Required |
|---|---|---|---|
| `tts` | object | Contains all settings for sending calls. Its presence is what turns the send into a voice send. | ✔️ |
| `tts.type` | int | Which channel each destination gets. See the table below. | ✔️ |
| `tts.rate` | int | Speech speed, from `-10` to `10`. `0` is normal. | ➖ |
| `tts.repeat` | int | How many times the message plays during the call. Default is twice. | ➖ |
| `tts.voice` | string | `ymMale` or `ymFemale`. Default is `ymMale`. | ➖ |

And the fields it shares with a text send:

| Name | Type | Description | Required |
|---|---|---|---|
| `user.username` | string | The username of the account by which you are recognized in the system. | ✔️ |
| `source` | string | Sender for the SMS half of the send. Maximum 11 characters. **Calls always originate from a system number**: `source` does not apply to them. | ✔️ |
| `destinations` | object | Contains every destination. May hold multiple `phone` and `cl_id` elements. | ✔️ |
| `phone` | string | A destination number, formatted `5xxxxxxx` or `05xxxxxxx`. | ✔️ |
| `message` | string | The text that will be spoken, and texted where `type` calls for it. Maximum 1005 characters. | ✔️ |

### `tts.type`

| Value | Behaviour |
|---|---|
| `2` | Send a regular message to a regular phone, and a voice call to a Kosher phone. |
| `3` | Send both a regular message and a voice call to all numbers. |
| `4` | Send a voice call only to all numbers. |

`2` is the one you usually want: it costs a call only where a text would have failed. `3` doubles up deliberately. Useful for urgent notifications. `4` skips SMS entirely.

### Request example

::: code-group

```xml [XML]
<?xml version="1.0" encoding="UTF-8"?>
<sms>
    <user>
        <username>Leeroy</username>
    </user>
    <source>DemoAPI</source>
    <destinations>
        <cl_id>21518</cl_id>
        <cl_id>21500</cl_id>
        <phone id="external id1">5xxxxxxxx</phone>
        <phone id="external id2">5xxxxxxxx</phone>
        <phone>5xxxxxxxx</phone>
        <phone id="">5xxxxxxxx</phone>
    </destinations>
    <message>This is a sample tts message</message>
    <tts>
        <type>4</type>
        <rate>-1</rate>
        <repeat>1</repeat>
        <voice>ymMale</voice>
    </tts>
</sms>
```

```json [JSON]
{
  "sms": {
    "user": {
      "username": "Leeroy"
    },
    "source": "DemoAPI",
    "destinations": {
      "cl_id": [
        "21518",
        "21500"
      ],
      "phone": [
        {
          "$": {
            "id": "external id1"
          },
          "_": "5xxxxxxxx"
        },
        {
          "$": {
            "id": "external id2"
          },
          "_": "5xxxxxxxx"
        },
        {
          "_": "5xxxxxxxx"
        },
        {
          "$": {
            "id": ""
          },
          "_": "5xxxxxxxx"
        }
      ]
    },
    "message": "This is a sample tts message",
    "tts": {
      "type": 4,
      "rate": -1,
      "repeat": 1,
      "voice": "ymMale"
    }
  }
}
```

```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": {
      "cl_id": [
        "21518",
        "21500"
      ],
      "phone": [
        {
          "$": {
            "id": "external id1"
          },
          "_": "5xxxxxxxx"
        },
        {
          "$": {
            "id": "external id2"
          },
          "_": "5xxxxxxxx"
        },
        {
          "_": "5xxxxxxxx"
        },
        {
          "$": {
            "id": ""
          },
          "_": "5xxxxxxxx"
        }
      ]
    },
    "message": "This is a sample tts message",
    "tts": {
      "type": 4,
      "rate": -1,
      "repeat": 1,
      "voice": "ymMale"
    }
  }
}'
```

```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: {
        cl_id: [
          '21518',
          '21500',
        ],
        phone: [
          {
            $: {
              id: 'external id1',
            },
            _: '5xxxxxxxx',
          },
          {
            $: {
              id: 'external id2',
            },
            _: '5xxxxxxxx',
          },
          {
            _: '5xxxxxxxx',
          },
          {
            $: {
              id: '',
            },
            _: '5xxxxxxxx',
          },
        ],
      },
      message: 'This is a sample tts message',
      tts: {
        type: 4,
        rate: -1,
        repeat: 1,
        voice: 'ymMale',
      },
    },
  }),
})

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' => [
                'cl_id' => [
                    '21518',
                    '21500',
                ],
                'phone' => [
                    [
                        '$' => [
                            'id' => 'external id1',
                        ],
                        '_' => '5xxxxxxxx',
                    ],
                    [
                        '$' => [
                            'id' => 'external id2',
                        ],
                        '_' => '5xxxxxxxx',
                    ],
                    [
                        '_' => '5xxxxxxxx',
                    ],
                    [
                        '$' => [
                            'id' => '',
                        ],
                        '_' => '5xxxxxxxx',
                    ],
                ],
            ],
            'message' => 'This is a sample tts message',
            'tts' => [
                'type' => 4,
                'rate' => -1,
                'repeat' => 1,
                'voice' => 'ymMale',
            ],
        ],
    ],
]);

$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' => [
                'cl_id' => [
                    '21518',
                    '21500',
                ],
                'phone' => [
                    [
                        '$' => [
                            'id' => 'external id1',
                        ],
                        '_' => '5xxxxxxxx',
                    ],
                    [
                        '$' => [
                            'id' => 'external id2',
                        ],
                        '_' => '5xxxxxxxx',
                    ],
                    [
                        '_' => '5xxxxxxxx',
                    ],
                    [
                        '$' => [
                            'id' => '',
                        ],
                        '_' => '5xxxxxxxx',
                    ],
                ],
            ],
            'message' => 'This is a sample tts message',
            'tts' => [
                'type' => 4,
                'rate' => -1,
                'repeat' => 1,
                'voice' => 'ymMale',
            ],
        ],
    ])
    ->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": {
                "cl_id": [
                    "21518",
                    "21500",
                ],
                "phone": [
                    {
                        "$": {
                            "id": "external id1",
                        },
                        "_": "5xxxxxxxx",
                    },
                    {
                        "$": {
                            "id": "external id2",
                        },
                        "_": "5xxxxxxxx",
                    },
                    {
                        "_": "5xxxxxxxx",
                    },
                    {
                        "$": {
                            "id": "",
                        },
                        "_": "5xxxxxxxx",
                    },
                ],
            },
            "message": "This is a sample tts message",
            "tts": {
                "type": 4,
                "rate": -1,
                "repeat": 1,
                "voice": "ymMale",
            },
        },
    },
)
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{
				"cl_id": []any{
					"21518",
					"21500",
				},
				"phone": []any{
					map[string]any{
						"$": map[string]any{
							"id": "external id1",
						},
						"_": "5xxxxxxxx",
					},
					map[string]any{
						"$": map[string]any{
							"id": "external id2",
						},
						"_": "5xxxxxxxx",
					},
					map[string]any{
						"_": "5xxxxxxxx",
					},
					map[string]any{
						"$": map[string]any{
							"id": "",
						},
						"_": "5xxxxxxxx",
					},
				},
			},
			"message": "This is a sample tts message",
			"tts": map[string]any{
				"type": 4,
				"rate": -1,
				"repeat": 1,
				"voice": "ymMale",
			},
		},
	})

	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 TextMeSendTts {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "sms": {
                "user": {
                  "username": "Leeroy"
                },
                "source": "DemoAPI",
                "destinations": {
                  "cl_id": [
                    "21518",
                    "21500"
                  ],
                  "phone": [
                    {
                      "$": {
                        "id": "external id1"
                      },
                      "_": "5xxxxxxxx"
                    },
                    {
                      "$": {
                        "id": "external id2"
                      },
                      "_": "5xxxxxxxx"
                    },
                    {
                      "_": "5xxxxxxxx"
                    },
                    {
                      "$": {
                        "id": ""
                      },
                      "_": "5xxxxxxxx"
                    }
                  ]
                },
                "message": "This is a sample tts message",
                "tts": {
                  "type": 4,
                  "rate": -1,
                  "repeat": 1,
                  "voice": "ymMale"
                }
              }
            }
            """;

        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": {
          "cl_id": [
            "21518",
            "21500"
          ],
          "phone": [
            {
              "$": {
                "id": "external id1"
              },
              "_": "5xxxxxxxx"
            },
            {
              "$": {
                "id": "external id2"
              },
              "_": "5xxxxxxxx"
            },
            {
              "_": "5xxxxxxxx"
            },
            {
              "$": {
                "id": ""
              },
              "_": "5xxxxxxxx"
            }
          ]
        },
        "message": "This is a sample tts message",
        "tts": {
          "type": 4,
          "rate": -1,
          "repeat": 1,
          "voice": "ymMale"
        }
      }
    }
    """;

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" => {
      "cl_id" => [
        "21518",
        "21500",
      ],
      "phone" => [
        {
          "$" => {
            "id" => "external id1",
          },
          "_" => "5xxxxxxxx",
        },
        {
          "$" => {
            "id" => "external id2",
          },
          "_" => "5xxxxxxxx",
        },
        {
          "_" => "5xxxxxxxx",
        },
        {
          "$" => {
            "id" => "",
          },
          "_" => "5xxxxxxxx",
        },
      ],
    },
    "message" => "This is a sample tts message",
    "tts" => {
      "type" => 4,
      "rate" => -1,
      "repeat" => 1,
      "voice" => "ymMale",
    },
  },
})

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": {
              "cl_id": [
                "21518",
                "21500"
              ],
              "phone": [
                {
                  "$": {
                    "id": "external id1"
                  },
                  "_": "5xxxxxxxx"
                },
                {
                  "$": {
                    "id": "external id2"
                  },
                  "_": "5xxxxxxxx"
                },
                {
                  "_": "5xxxxxxxx"
                },
                {
                  "$": {
                    "id": ""
                  },
                  "_": "5xxxxxxxx"
                }
              ]
            },
            "message": "This is a sample tts message",
            "tts": {
              "type": 4,
              "rate": -1,
              "repeat": 1,
              "voice": "ymMale"
            }
          }
        }))
        .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(())
}
```

:::

### Response

::: 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"
}
```

:::

Identical to a text send: `status` `0`, and a `shipment_id` naming the campaign.

### Errors

Same set as [`sms`](./send.md#errors). Voice-specific rejections surface through the ordinary validation codes. `2` for a missing `tts.type`, `989` for a body outside the length limit.

## Field notes

### The caller ID is not yours

However `source` is set, a TTS call arrives from a TextMe system number. Recipients cannot call back on it, and it will not match a number you have [verified](./verified-senders.md). Say who is calling in the first sentence of the message. The audio is all the identification the recipient gets.

### Writing for the ear

The message is read aloud verbatim, so text that scans well on a screen often does not out loud:

* Spell out what matters. `ORD-10052` is read as a string of characters; "order one zero zero five two" is clearer.
* Drop URLs and `[link-…]` placeholders. Nobody can type a URL they heard, and the short-link machinery is meaningless in audio.
* Keep it short. `repeat` defaults to twice, so a long message makes for a long call.
* Use `rate` sparingly. Negative values slow the delivery, which helps with numbers and unfamiliar names.

### Cost and reporting

A voice call and an SMS are billed separately, so `type` `3` costs both. [Balance](./balance.md) reports the account's credit; [delivery reports](./reports.md) cover the SMS half of the send in the usual way.

::: tip Discovering which numbers need voice
You do not have to know in advance. Send normally, then read the [delivery reports](../use-cases/delivery-reports.md): destinations that come back `15` are filtered handsets. Store that fact against the contact and use `type` `4` for them next time, or just use `type` `2` for the whole audience and let the routing sort it out.
:::
