curl --request POST \
--url https://api.otpbay.com/v1/messages/sms \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"to": "+447700900123",
"from": "Acme",
"body": "Your Acme order 1042 has shipped.",
"callback": "https://example.com/webhooks/otpbay",
"metadata": {
"order_id": "1042"
}
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
to: '+447700900123',
from: 'Acme',
body: JSON.stringify('Your Acme order 1042 has shipped.'),
callback: 'https://example.com/webhooks/otpbay',
metadata: {order_id: '1042'}
})
};
fetch('https://api.otpbay.com/v1/messages/sms', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.otpbay.com/v1/messages/sms"
payload = {
"to": "+447700900123",
"from": "Acme",
"body": "Your Acme order 1042 has shipped.",
"callback": "https://example.com/webhooks/otpbay",
"metadata": { "order_id": "1042" }
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.otpbay.com/v1/messages/sms",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'to' => '+447700900123',
'from' => 'Acme',
'body' => 'Your Acme order 1042 has shipped.',
'callback' => 'https://example.com/webhooks/otpbay',
'metadata' => [
'order_id' => '1042'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.otpbay.com/v1/messages/sms"
payload := strings.NewReader("{\n \"to\": \"+447700900123\",\n \"from\": \"Acme\",\n \"body\": \"Your Acme order 1042 has shipped.\",\n \"callback\": \"https://example.com/webhooks/otpbay\",\n \"metadata\": {\n \"order_id\": \"1042\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.otpbay.com/v1/messages/sms")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"to\": \"+447700900123\",\n \"from\": \"Acme\",\n \"body\": \"Your Acme order 1042 has shipped.\",\n \"callback\": \"https://example.com/webhooks/otpbay\",\n \"metadata\": {\n \"order_id\": \"1042\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.otpbay.com/v1/messages/sms")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"to\": \"+447700900123\",\n \"from\": \"Acme\",\n \"body\": \"Your Acme order 1042 has shipped.\",\n \"callback\": \"https://example.com/webhooks/otpbay\",\n \"metadata\": {\n \"order_id\": \"1042\"\n }\n}"
response = http.request(request)
puts response.read_bodyusing RestSharp;
var options = new RestClientOptions("https://api.otpbay.com/v1/messages/sms");
var client = new RestClient(options);
var request = new RestRequest("");
request.AddHeader("Authorization", "Bearer <token>");
request.AddJsonBody("{\n \"to\": \"+447700900123\",\n \"from\": \"Acme\",\n \"body\": \"Your Acme order 1042 has shipped.\",\n \"callback\": \"https://example.com/webhooks/otpbay\",\n \"metadata\": {\n \"order_id\": \"1042\"\n }\n}", false);
var response = await client.PostAsync(request);
Console.WriteLine("{0}", response.Content);
{
"sid": "SM6650c4b7c2d3e4f5a6b7c8d9",
"status": "queued",
"to": "+447700900123",
"from": "Acme",
"body": "Your Acme order 1042 has shipped.",
"sms_segments": 1,
"cost": "0.04",
"metadata": {
"order_id": "1042"
},
"date_created": "2026-09-26T10:20:11.302Z",
"processed_at": null
}{
"error": {
"code": "INVALID_JSON",
"message": "Invalid JSON body"
}
}{
"error": {
"code": "INVALID_API_KEY",
"message": "Invalid API key."
}
}{
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Insufficient balance for this SMS destination."
}
}{
"error": {
"code": "CHANNEL_DISABLED",
"message": "SMS is turned off for this project."
}
}{
"error": {
"code": "SENDER_ID_NOT_APPROVED",
"message": "\"Acme\" is not an approved sender ID for this project."
}
}{
"error": {
"code": "QUEUE_UNAVAILABLE",
"message": "SMS saved but queue is unavailable — try again later or check Redis."
}
}Send an SMS
Queues one SMS for delivery. The price is quoted from the destination network and the number of segments, and your balance is charged when the message is handed to the carrier. The response returns right away with status queued; follow the message with Get an SMS or a callback URL.
curl --request POST \
--url https://api.otpbay.com/v1/messages/sms \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"to": "+447700900123",
"from": "Acme",
"body": "Your Acme order 1042 has shipped.",
"callback": "https://example.com/webhooks/otpbay",
"metadata": {
"order_id": "1042"
}
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
to: '+447700900123',
from: 'Acme',
body: JSON.stringify('Your Acme order 1042 has shipped.'),
callback: 'https://example.com/webhooks/otpbay',
metadata: {order_id: '1042'}
})
};
fetch('https://api.otpbay.com/v1/messages/sms', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.otpbay.com/v1/messages/sms"
payload = {
"to": "+447700900123",
"from": "Acme",
"body": "Your Acme order 1042 has shipped.",
"callback": "https://example.com/webhooks/otpbay",
"metadata": { "order_id": "1042" }
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.otpbay.com/v1/messages/sms",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'to' => '+447700900123',
'from' => 'Acme',
'body' => 'Your Acme order 1042 has shipped.',
'callback' => 'https://example.com/webhooks/otpbay',
'metadata' => [
'order_id' => '1042'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.otpbay.com/v1/messages/sms"
payload := strings.NewReader("{\n \"to\": \"+447700900123\",\n \"from\": \"Acme\",\n \"body\": \"Your Acme order 1042 has shipped.\",\n \"callback\": \"https://example.com/webhooks/otpbay\",\n \"metadata\": {\n \"order_id\": \"1042\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.otpbay.com/v1/messages/sms")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"to\": \"+447700900123\",\n \"from\": \"Acme\",\n \"body\": \"Your Acme order 1042 has shipped.\",\n \"callback\": \"https://example.com/webhooks/otpbay\",\n \"metadata\": {\n \"order_id\": \"1042\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.otpbay.com/v1/messages/sms")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"to\": \"+447700900123\",\n \"from\": \"Acme\",\n \"body\": \"Your Acme order 1042 has shipped.\",\n \"callback\": \"https://example.com/webhooks/otpbay\",\n \"metadata\": {\n \"order_id\": \"1042\"\n }\n}"
response = http.request(request)
puts response.read_bodyusing RestSharp;
var options = new RestClientOptions("https://api.otpbay.com/v1/messages/sms");
var client = new RestClient(options);
var request = new RestRequest("");
request.AddHeader("Authorization", "Bearer <token>");
request.AddJsonBody("{\n \"to\": \"+447700900123\",\n \"from\": \"Acme\",\n \"body\": \"Your Acme order 1042 has shipped.\",\n \"callback\": \"https://example.com/webhooks/otpbay\",\n \"metadata\": {\n \"order_id\": \"1042\"\n }\n}", false);
var response = await client.PostAsync(request);
Console.WriteLine("{0}", response.Content);
{
"sid": "SM6650c4b7c2d3e4f5a6b7c8d9",
"status": "queued",
"to": "+447700900123",
"from": "Acme",
"body": "Your Acme order 1042 has shipped.",
"sms_segments": 1,
"cost": "0.04",
"metadata": {
"order_id": "1042"
},
"date_created": "2026-09-26T10:20:11.302Z",
"processed_at": null
}{
"error": {
"code": "INVALID_JSON",
"message": "Invalid JSON body"
}
}{
"error": {
"code": "INVALID_API_KEY",
"message": "Invalid API key."
}
}{
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Insufficient balance for this SMS destination."
}
}{
"error": {
"code": "CHANNEL_DISABLED",
"message": "SMS is turned off for this project."
}
}{
"error": {
"code": "SENDER_ID_NOT_APPROVED",
"message": "\"Acme\" is not an approved sender ID for this project."
}
}{
"error": {
"code": "QUEUE_UNAVAILABLE",
"message": "SMS saved but queue is unavailable — try again later or check Redis."
}
}Authorizations
Your project API key, sent as Authorization: Bearer otp_live_.... Create keys on the API Keys page of the dashboard.
Body
Destination phone number in international format. OTPBay normalizes it to E.164, so +1 (415) 555-2671 becomes +14155552671.
"+14155552671"
Message text. Long messages are split into segments and billed per segment: 160 characters (153 per part) for plain text, 70 (67 per part) if the text contains any non-ASCII character such as emoji or accented letters.
10240"Your Acme order 1042 has shipped."
One of your project's approved sender IDs: 3–11 letters and digits with at least one letter. Defaults to the SMS default sender ID from project settings.
3 - 11^(?=.*[A-Za-z])[A-Za-z0-9]{3,11}$"Acme"
Public http or https URL that receives status webhooks. Localhost, private and link-local addresses are rejected. Defaults to the channel's default callback from project settings.
2048"https://example.com/webhooks/otpbay"
Response
The SMS was saved and queued.
Unique SMS ID.
^SM[a-f0-9]{24}$"SM6650c4b7c2d3e4f5a6b7c8d9"
queued → processing → sent when the carrier accepts the message, or failed after retries. enqueue_failed means the message was saved but never queued.
queued, processing, sent, failed, enqueue_failed Destination phone number in international format. OTPBay normalizes it to E.164, so +1 (415) 555-2671 becomes +14155552671.
"+14155552671"
Sender ID the message was sent from.
"Acme"
"Your Acme order 1042 has shipped."
Number of billed segments.
1
Price of the message (per-segment price × segments).
^\d+\.\d{2}$"0.04"
When the carrier accepted the message.
Latest delivery error. Only present when there is one.