Never put API keys in browsers, mobile apps, or public repos.
Send OTPs in one request
Authenticate with your private key, post to /secure-api/v1/send, and deliver WhatsApp or Telegram verification messages. Same endpoint — pick the channel.
https://otpchat.com
-
1
Get your key
Sign up, open Dashboard → API keys, copy
oc_live_…. -
2
Choose a channel
whatsapportelegram+ phone (or TelegramchatId). -
3
Choose how you send
Platform plan from Wallet, or your own WhatsApp line on My numbers (then pass
customNumberId).
Prefer E.164 without +, e.g. 9647XXXXXXXXX.
First Telegram phone send returns a deep link; after the user shares their phone with the bot, later OTPs deliver instantly.
Authentication
Every request needs your private API key as a Bearer token.
Authorization: Bearer oc_live_xxxxxxxxxxxxxxxx
Rotate the key from the dashboard if it was ever exposed. Treat it like a password.
/secure-api/v1/send
Send message
Deliver an OTP or free-text message on WhatsApp or Telegram.
| Field | Type | Required | Description |
|---|---|---|---|
phoneNumber |
string | yes* | E.164 phone digits, e.g. 9647XXXXXXXXX. Aliases: phone, recipient |
chatId |
string | no* | Telegram only. Private chat id or group/supergroup id (-100…). Prefer this over putting chat ids in phoneNumber |
channel |
string | no | whatsapp (default), telegram, or sms |
text |
string | yes* | Full message body. Alias: message |
verificationCode |
string | no | If set and text omitted → “Your verification code is {code}” |
provider |
string | no | Alias for channel (compat) |
customNumberId |
string | no | WhatsApp only. Your custom line id (uwn_…) from My numbers / GET /secure-api/custom-wa. Alias: senderNumberId. Omit to use your default active/trial line (or the shared pool on a platform plan). |
* Provide phoneNumber or chatId, and either text/message or verificationCode.
Success · 200
{
"success": true,
"message": "sent",
"data": {
"logId": "msg_…",
"channel": "whatsapp",
"costIqd": 0,
"trial": false,
"customNumberId": "uwn_…",
"customRoute": true
}
}
Channels
Pick a channel per request. Disabled channels return a clear error.
Default route. Send with phoneNumber + verificationCode (or text). OTPChat routes delivery automatically when WhatsApp is available.
Telegram
Real-person OTP via phone link to @otpchatai_bot, or direct send to a known chatId / group. Same API key and subscription.
SMS
Listed for pricing. Returns CHANNEL_DISABLED until enabled.
Telegram OTP
Telegram bots cannot cold-message a phone. OTPChat verifies the person once, then delivers codes to their linked chat.
Flow · phone verification
- Send —
POST /secure-api/v1/sendwithchannel: "telegram"andphoneNumber. - If not linked — HTTP
202+TELEGRAM_LINK_REQUIREDand adeepLink(https://t.me/…). - Recipient — opens the link → Start → taps Share phone number (must match the OTP phone).
- OTP delivers — pending code is sent automatically; later sends go straight through (HTTP
200).
Link required · 202
{
"success": false,
"message": "Telegram identity not linked yet. Open the bot link, tap Start, then share your phone number to receive the OTP.",
"code": "TELEGRAM_LINK_REQUIRED",
"data": {
"logId": "msg_…",
"channel": "telegram",
"phone": "9647XXXXXXXXX",
"pendingId": "tgpend_…",
"expiresAt": "2026-07-17T01:34:07.063Z",
"botUsername": "otpchatai_bot",
"deepLink": "https://t.me/otpchatai_bot?start=…",
"deepLinkApp": "tg://resolve?domain=otpchatai_bot&start=…",
"openUrl": "https://otpchat.com/telegram/open?bot=otpchatai_bot&start=…",
"mobileOpen": {
"preferred": "_system",
"fallback": "_blank"
},
"instructions": [
"Open deep link / openUrl",
"Tap Start",
"Share phone number",
"OTP arrives automatically"
]
}
}
Mobile / Cordova: open data.deepLink or data.openUrl with cordova.InAppBrowser.open(url, '_system') (fallback _blank) so the OS can launch the Telegram app. Do not keep t.me inside an in-app WebView.
Send to a group
- Add
@otpchatai_botto the group. - In the group, send
/chatid— the bot replies with an id like-1001234567890. - Call send with
chatId+channel: "telegram".
curl -X POST "https://otpchat.com/secure-api/v1/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"chatId": "-1001234567890",
"channel": "telegram",
"verificationCode": "552211"
}'
/secure-api/v1/telegram/status
Telegram link status
Check whether a phone is linked, or poll a pending OTP after TELEGRAM_LINK_REQUIRED.
| Query | Type | Description |
|---|---|---|
phoneNumber |
string | E.164 digits — returns linked, chatId, phone |
pendingId |
string | From a prior TELEGRAM_LINK_REQUIRED response — returns pending row status |
curl "https://otpchat.com/secure-api/v1/telegram/status?phoneNumber=9647XXXXXXXXX" \
-H "Authorization: Bearer YOUR_API_KEY"
# {
# "success": true,
# "data": {
# "linked": true,
# "chatId": "176500130",
# "phone": "9647XXXXXXXXX"
# }
# }
curl "https://otpchat.com/secure-api/v1/telegram/status?pendingId=tgpend_…" \
-H "Authorization: Bearer YOUR_API_KEY"
# data.status: "waiting" | "delivered" | …
Custom WhatsApp numbers
Send OTPs from your own business WhatsApp line. No platform subscription required — each number has a 3‑day trial, then a yearly activation fee.
- Two products — Platform monthly plans (shared pool + Telegram) or custom WhatsApp lines on My numbers. You can use both.
- Flow — Add phone → OTPChat creates an upstream sender → scan QR on the connect page → trial (3 days) → pay yearly → admin activates.
- Yearly price — $149 USD / line (USD wallet or Stripe) or 230,000 IQD / line (IQD wallet or Wayl).
- Phone uniqueness — A number already linked on another account is rejected.
- Status —
lastStatusis the live WhatsApp link:connectedordisconnected(phone can be registered while still disconnected until you scan QR).
Auth
- Management APIs below require a logged-in dashboard session cookie (same as the website).
- Send API uses your private
oc_live_Bearer key — pass optionalcustomNumberId.
Management endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /secure-api/custom-wa | List your lines + pricing |
POST | /secure-api/custom-wa | Create a line (provisions upstream key + binds phone) |
POST | /secure-api/custom-wa/:id/refresh | Refresh live connect status |
GET | /secure-api/custom-wa/:id/connect | Connect session (status + QR proxy URL) |
GET | /secure-api/custom-wa/:id/connect-qr | PNG QR image (same live QR as upstream status) |
POST | /secure-api/custom-wa/:id/pay-yearly | Pay yearly activation (wallet / Stripe / Wayl) |
/secure-api/custom-wa
Session cookie required.
curl -X GET "https://otpchat.com/secure-api/custom-wa" \
-H "Cookie: connect.sid=YOUR_SESSION"
{
"success": true,
"data": {
"numbers": [
{
"id": "uwn_…",
"label": "Baghdad store",
"phone": "9647718593918",
"whatsappName": "",
"status": "connecting",
"lastStatus": "disconnected",
"trialEndsAt": "2026-07-23T…",
"paidUntil": null,
"canConnect": true,
"canPay": true,
"isActive": false
}
],
"pricing": {
"yearlyUsd": 149,
"yearlyIqd": 230000,
"trialDays": 3
}
}
}
/secure-api/custom-wa
| Field | Type | Required | Description |
|---|---|---|---|
phone | string | yes | E.164 digits without +, e.g. 9647XXXXXXXXX |
label | string | no | Display name (max 80 chars) |
curl -X POST "https://otpchat.com/secure-api/custom-wa" \
-H "Content-Type: application/json" \
-H "Cookie: connect.sid=YOUR_SESSION" \
-d '{
"label": "Baghdad store",
"phone": "9647XXXXXXXXX"
}'
On success, open the connect UI: /dashboard/custom-numbers/{id}/connect and scan the QR until lastStatus is connected.
/secure-api/custom-wa/:id/connect
{
"success": true,
"data": {
"connected": false,
"dbStatus": "disconnected",
"phone": "+9647XXXXXXXXX",
"whatsappName": "",
"instructions": "Phone is registered but WhatsApp is disconnected…",
"qrProxyUrl": "/secure-api/custom-wa/uwn_…/connect-qr",
"number": { "id": "uwn_…", "status": "connecting", "lastStatus": "disconnected" }
}
}
GET …/connect-qr returns image/png (no-store). Refresh it every ~25s while waiting for scan.
/secure-api/custom-wa/:id/refresh
Re-checks upstream and updates status / lastStatus. Use until lastStatus === "connected".
/secure-api/custom-wa/:id/pay-yearly
| Field | Type | Required | Description |
|---|---|---|---|
currency | string | no | USD (default) or IQD |
provider | string | no | wallet, stripe (USD), or wayl (IQD). Default: wallet for USD, Wayl for IQD. |
curl -X POST "https://otpchat.com/secure-api/custom-wa/uwn_…/pay-yearly" \
-H "Content-Type: application/json" \
-H "Cookie: connect.sid=YOUR_SESSION" \
-d '{
"currency": "IQD",
"provider": "wallet"
}'
Wallet payment moves the line to pending_approval. Card/Wayl may return data.redirectUrl. After admin approve, status becomes active with paidUntil ≈ +1 year.
Line status values
| status | Meaning |
|---|---|
setup / connecting | Created — waiting for WhatsApp QR link |
trial | Connected during the 3‑day trial window |
trial_expired | Trial ended — pay yearly to continue |
pending_approval | Payment received — awaiting OTPChat review |
active | Approved yearly line (check paidUntil) |
disconnected | Was active/trial but WhatsApp link dropped — re-scan QR |
rejected | Payment rejected by admin |
Send via a custom line
curl -X POST "https://otpchat.com/secure-api/v1/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phoneNumber": "9647XXXXXXXXX",
"channel": "whatsapp",
"verificationCode": "543646",
"customNumberId": "uwn_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}'
Omit customNumberId to use your default active/trial custom line. Users on a platform plan without custom lines keep using the shared WhatsApp pool. Access is allowed with an active platform trial/plan or an active/trial custom WhatsApp line.
Errors
Handle these codes so your app can retry, open Telegram, or prompt a subscription renewal.
| HTTP | code | Meaning |
|---|---|---|
401 | — | Missing or invalid API key |
202 | TELEGRAM_LINK_REQUIRED | Open data.deepLink; user must Start + share phone |
400 | TELEGRAM_SEND_FAILED | Bot could not deliver (bad chat id, blocked, etc.) |
400 | CHANNEL_DISABLED | Channel off or SMS coming soon |
400 | BAD_CHANNEL | Unknown channel value |
400 | BAD_RECIPIENT | Missing phone / chat id |
402 | TRIAL_API_LIMIT | Trial API OTP limit reached |
402 | SUBSCRIPTION_REQUIRED | No active platform trial/plan and no active/trial custom WhatsApp line |
400 | — | Custom WA create/pay errors (e.g. phone already linked, insufficient wallet, QR not ready) |
Billing rules
Trial usage plus monthly unlimited OTP subscriptions. WhatsApp and Telegram share the same plan.
- Platform trial — 3 days, 5 dashboard tests, and 50 API OTP messages on the shared pool.
- Platform plans — monthly/yearly unlimited on shared WhatsApp + Telegram (Wallet).
- Custom WhatsApp lines — separate 3‑day trial per number, then $149 / 230,000 IQD per year (My numbers). Admin activates after payment.
- Active access — successful sends are logged with zero message cost while a platform plan or custom line trial/active period covers the send.
- Renewal — renew platform plans from Wallet; renew each custom line from My numbers → Pay yearly.
Examples
WhatsApp and Telegram side by side — swap the key and recipient, ship.
curl -X POST "https://otpchat.com/secure-api/v1/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phoneNumber": "9647XXXXXXXXX",
"channel": "whatsapp",
"verificationCode": "543646"
}'
curl -X POST "https://otpchat.com/secure-api/v1/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phoneNumber": "9647XXXXXXXXX",
"channel": "whatsapp",
"verificationCode": "543646",
"customNumberId": "uwn_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}'
curl -X POST "https://otpchat.com/secure-api/v1/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phoneNumber": "9647XXXXXXXXX",
"channel": "telegram",
"verificationCode": "882910"
}'
const res = await fetch('https://otpchat.com/secure-api/v1/send', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + process.env.OTPCHAT_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
phoneNumber: '9647XXXXXXXXX',
channel: 'whatsapp',
verificationCode: '543646'
})
});
const json = await res.json();
if (!json.success) throw new Error(json.message);
const res = await fetch('https://otpchat.com/secure-api/v1/send', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + process.env.OTPCHAT_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
phoneNumber: '9647XXXXXXXXX',
channel: 'telegram',
verificationCode: '882910'
})
});
const json = await res.json();
if (res.status === 202 || json.code === 'TELEGRAM_LINK_REQUIRED') {
// Show deepLink to the user (or open in Cordova _system)
const url = json.data.deepLink; // https://t.me/otpchatai_bot?start=…
console.log('Open Telegram:', url);
// Optional: poll until linked
// GET /secure-api/v1/telegram/status?pendingId=…
} else if (!json.success) {
throw new Error(json.message);
}
$payload = json_encode([
'phoneNumber' => '9647XXXXXXXXX',
'channel' => 'whatsapp',
'verificationCode' => '543646'
]);
$ch = curl_init('https://otpchat.com/secure-api/v1/send');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('OTPCHAT_API_KEY'),
'Content-Type: application/json'
],
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true
]);
$response = curl_exec($ch);
curl_close($ch);
$payload = json_encode([
'phoneNumber' => '9647XXXXXXXXX',
'channel' => 'telegram',
'verificationCode' => '882910'
]);
$ch = curl_init('https://otpchat.com/secure-api/v1/send');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('OTPCHAT_API_KEY'),
'Content-Type: application/json'
],
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true
]);
$response = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($response, true);
if ($http === 202 || ($data['code'] ?? '') === 'TELEGRAM_LINK_REQUIRED') {
$deepLink = $data['data']['deepLink']; // open https://t.me/...
}
// After TELEGRAM_LINK_REQUIRED:
const url = data.deepLink || data.openUrl;
// Prefer system browser → Telegram app
cordova.InAppBrowser.open(url, '_system');
// Fallback
// cordova.InAppBrowser.open(url, '_blank');
// OTPChat helper (if loading oc-app.js)
// ocOpenTelegramDeepLink(url);
# 1) Add @otpchatai_bot to your group
# 2) In the group send: /chatid
# 3) Use the returned id
curl -X POST "https://otpchat.com/secure-api/v1/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"chatId": "-1001234567890",
"channel": "telegram",
"text": "Team OTP: 552211"
}'
/secure-api/health
Health check
Ping the API without an API key.
GET https://otpchat.com/secure-api/health
{ "success": true, "service": "otpchat", "port": 3018 }