curl --request POST \
--url https://api.otpbay.com/v1/verifications \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"to": "+14155552671",
"metadata": {
"user_id": "usr_1042"
}
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({to: '+14155552671', metadata: {user_id: 'usr_1042'}})
};
fetch('https://api.otpbay.com/v1/verifications', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.otpbay.com/v1/verifications"
payload = {
"to": "+14155552671",
"metadata": { "user_id": "usr_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/verifications",
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' => '+14155552671',
'metadata' => [
'user_id' => 'usr_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/verifications"
payload := strings.NewReader("{\n \"to\": \"+14155552671\",\n \"metadata\": {\n \"user_id\": \"usr_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/verifications")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"to\": \"+14155552671\",\n \"metadata\": {\n \"user_id\": \"usr_1042\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.otpbay.com/v1/verifications")
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\": \"+14155552671\",\n \"metadata\": {\n \"user_id\": \"usr_1042\"\n }\n}"
response = http.request(request)
puts response.read_bodyusing RestSharp;
var options = new RestClientOptions("https://api.otpbay.com/v1/verifications");
var client = new RestClient(options);
var request = new RestRequest("");
request.AddHeader("Authorization", "Bearer <token>");
request.AddJsonBody("{\n \"to\": \"+14155552671\",\n \"metadata\": {\n \"user_id\": \"usr_1042\"\n }\n}", false);
var response = await client.PostAsync(request);
Console.WriteLine("{0}", response.Content);
{
"sid": "VE6650c3a1b2c3d4e5f6a7b8c9",
"status": "pending",
"to": "+14155552671",
"channel": "telegram",
"fallback_reason": null,
"attempts": [
{
"channel": "telegram",
"sid": "TG6650c3a1b2c3d4e5f6a7b8d0",
"status": "sent",
"cost": "0.02",
"date_created": "2026-09-26T10:15:02.114Z"
}
],
"checks_left": 5,
"fee": "0.02",
"cost": "0.04",
"date_created": "2026-09-26T10:15:01.873Z",
"expires_at": "2026-09-26T10:25:01.873Z",
"approved_at": null,
"metadata": {
"user_id": "usr_1042"
}
}{
"error": {
"code": "INVALID_API_KEY",
"message": "Invalid API key."
}
}{
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Insufficient balance for this SMS destination."
}
}{
"error": {
"code": "VERIFY_DISABLED",
"message": "Verify is turned off for this project."
}
}{
"error": {
"code": "INVALID_REQUEST_BODY",
"message": "to: Must be a valid international phone number (E.164)"
}
}{
"error": {
"code": "QUEUE_UNAVAILABLE",
"message": "SMS saved but queue is unavailable — try again later or check Redis."
}
}Start a verification
Generates a code and sends it to to on the channel set in your project’s Verify settings. When Telegram is the channel and SMS fallback is on, OTPBay switches to SMS if Telegram can’t deliver or doesn’t deliver within the fallback wait. Store the returned sid and pass it to Check a code when the user enters the code.
curl --request POST \
--url https://api.otpbay.com/v1/verifications \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"to": "+14155552671",
"metadata": {
"user_id": "usr_1042"
}
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({to: '+14155552671', metadata: {user_id: 'usr_1042'}})
};
fetch('https://api.otpbay.com/v1/verifications', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.otpbay.com/v1/verifications"
payload = {
"to": "+14155552671",
"metadata": { "user_id": "usr_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/verifications",
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' => '+14155552671',
'metadata' => [
'user_id' => 'usr_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/verifications"
payload := strings.NewReader("{\n \"to\": \"+14155552671\",\n \"metadata\": {\n \"user_id\": \"usr_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/verifications")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"to\": \"+14155552671\",\n \"metadata\": {\n \"user_id\": \"usr_1042\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.otpbay.com/v1/verifications")
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\": \"+14155552671\",\n \"metadata\": {\n \"user_id\": \"usr_1042\"\n }\n}"
response = http.request(request)
puts response.read_bodyusing RestSharp;
var options = new RestClientOptions("https://api.otpbay.com/v1/verifications");
var client = new RestClient(options);
var request = new RestRequest("");
request.AddHeader("Authorization", "Bearer <token>");
request.AddJsonBody("{\n \"to\": \"+14155552671\",\n \"metadata\": {\n \"user_id\": \"usr_1042\"\n }\n}", false);
var response = await client.PostAsync(request);
Console.WriteLine("{0}", response.Content);
{
"sid": "VE6650c3a1b2c3d4e5f6a7b8c9",
"status": "pending",
"to": "+14155552671",
"channel": "telegram",
"fallback_reason": null,
"attempts": [
{
"channel": "telegram",
"sid": "TG6650c3a1b2c3d4e5f6a7b8d0",
"status": "sent",
"cost": "0.02",
"date_created": "2026-09-26T10:15:02.114Z"
}
],
"checks_left": 5,
"fee": "0.02",
"cost": "0.04",
"date_created": "2026-09-26T10:15:01.873Z",
"expires_at": "2026-09-26T10:25:01.873Z",
"approved_at": null,
"metadata": {
"user_id": "usr_1042"
}
}{
"error": {
"code": "INVALID_API_KEY",
"message": "Invalid API key."
}
}{
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Insufficient balance for this SMS destination."
}
}{
"error": {
"code": "VERIFY_DISABLED",
"message": "Verify is turned off for this project."
}
}{
"error": {
"code": "INVALID_REQUEST_BODY",
"message": "to: Must be a valid international phone number (E.164)"
}
}{
"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"
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 verification was created and the first code was sent.
Unique verification ID.
^VE[a-f0-9]{24}$"VE6650c3a1b2c3d4e5f6a7b8c9"
pending until the user enters the right code (approved), the code expires (expired), or the code can't be sent or too many wrong codes are entered (failed).
pending, approved, expired, failed Destination phone number in international format. OTPBay normalizes it to E.164, so +1 (415) 555-2671 becomes +14155552671.
"+14155552671"
Channel the code was last sent on.
telegram, sms Why the code was re-sent by SMS, or null if it wasn't.
telegram_failed, timeout, null Every send of the code, oldest first.
Hide child attributes
Hide child attributes
Channel this attempt was sent on.
telegram, sms sid of the underlying message: TG… for Telegram, SM… for SMS.
"TG6650c3a1b2c3d4e5f6a7b8d0"
Delivery status of the message. Telegram: sending, sent, delivered, read, expired, revoked, failed. SMS: queued, processing, sent, failed, enqueue_failed.
"sent"
Amount in US dollars as a decimal string.
^\d+\.\d{2}$"0.02"
Wrong codes the user can still enter before the verification fails.
5
Amount in US dollars as a decimal string.
^\d+\.\d{2}$"0.02"
Verify fee plus what the channel sends currently cost. Refunded sends count as 0.00.
^\d+\.\d{2}$"0.04"
When the code stops being accepted.
When the right code was entered.
Why the verification failed. Only present when status is failed.
"Too many wrong codes."