Overview
The MTD API WhatsApp integration lets you:
- Link a WhatsApp number to a session using a pairing code (no QR scan needed)
- Check if a session is active before sending
- Send any text message to any WhatsApp number in a supported country
The typical flow is:
1. Pair your WhatsApp number once → get a session_id
2. Check the session is connected
3. Send messages programmatically forever (until you log out of that device)
Prerequisites
- MTD API account → api.motadev.xyz
- Your API key from the Dashboard
- A WhatsApp account (any number — personal or business)
- Credits in your account (1/50 credit per send/pair operation)
Supported Countries
Before you send anything, know the supported country codes. Fetch them live:
GET https://api.motadev.xyz/api/whatsapp/countries
This endpoint requires no authentication — call it freely.
Current supported countries:
| Code | Country | Dial Code | Local digits | Example format |
|---|---|---|---|---|
ZA or SA |
South Africa | +27 | 9 | 0821234567 |
NG |
Nigeria | +234 | 10 | 08012345678 |
IN |
India | +91 | 10 | 09876543210 |
PH |
Philippines | +63 | 10 | 09171234567 |
US or CA |
United States/Canada | +1 | 10 | 2125551234 |
The API handles the formatting. You pass the number as the user typed it — with or without a leading zero, with or without + — and the API normalises it to E.164 format (27821234567).
Required Headers
All WhatsApp endpoints (except /countries) require:
X-API-KEY: mtd_key1234567890ABCD
User-Agent: MyApp/1.0
Referer: https://myapp.com
Step 1 — Pair Your WhatsApp Number
This links a WhatsApp number to a session_id of your choosing. You only need to do this once per number. The session persists until you manually log out.
Endpoint
POST /api/whatsapp/pair
Content-Type: application/json
Request Body
{
"session_id": "my-business-account",
"phone_number": "0821234567",
"country": "ZA"
}
| Field | Required | Description |
|---|---|---|
session_id |
✅ | Any string you choose. This is how you reference this WhatsApp account in future API calls. |
phone_number |
✅ | The WhatsApp number to link. Accepts any common format. |
country |
✅ | Country code from /api/whatsapp/countries. |
Response
{
"success": true,
"message": "Pairing code issued. Enter it in WhatsApp → Linked Devices → Link with Phone Number.",
"pairing_code": "ABCD-1234",
"session_id": "my-business-account",
"phone_number": "27821234567",
"credits_remaining": 49
}
What to do with the pairing code
- Open WhatsApp on the phone you're linking
- Go to Settings → Linked Devices → Link a Device
- Tap "Link with phone number instead"
- Enter the
pairing_codefrom the response (e.g.ABCD-1234) - Done — your session is now active
You never need to touch that phone again. All future messages go through the API using the
session_id.
Step 2 — Verify the Session is Active
Before sending production messages, confirm the session connected successfully.
Endpoint
GET /api/whatsapp/status/{session_id}
Response (connected)
{
"success": true,
"session_id": "my-business-account",
"connected": true,
"status": "active"
}
Response (not connected)
{
"success": true,
"session_id": "my-business-account",
"connected": false,
"status": "not_connected"
}
If the status is not_connected, repeat the pairing step — the code may have expired or was entered incorrectly.
Step 3 — Send a Message
Endpoint
POST /api/whatsapp/send
Content-Type: application/json
Request Body
{
"session_id": "my-business-account",
"to": "0731234567",
"country": "ZA",
"message": "Your OTP is 482910. It expires in 5 minutes."
}
| Field | Required | Description |
|---|---|---|
session_id |
✅ | The session you paired in Step 1 |
to |
✅ | Recipient's WhatsApp number |
country |
✅ | Recipient's country code |
message |
✅ | The text to send |
Response
{
"success": true,
"message": "Message sent successfully.",
"to": "27731234567",
"session_id": "my-business-account",
"credits_remaining": 48
}
Code Examples
Example 1 — OTP Verification System (Python)
A complete OTP flow: generate a code, send it via WhatsApp, verify it in your app.
import requests
import random
import string
import time
API_KEY = "mtd_key1234567890ABCD"
SESSION_ID = "my-business-account"
BASE_URL = "https://api.motadev.xyz"
HEADERS = {
"X-API-KEY": API_KEY,
"User-Agent": "OTPService/1.0",
"Referer": "https://myapp.com",
"Content-Type": "application/json",
}
# In-memory OTP store (use Redis/DB in production)
_otp_store = {}
def generate_otp(length=6) -> str:
return "".join(random.choices(string.digits, k=length))
def send_whatsapp_otp(phone: str, country: str) -> dict:
"""Generate and send an OTP to a WhatsApp number."""
otp = generate_otp()
expires_at = time.time() + 300 # 5 minutes
message = (
f"🔐 *MTD App Verification*\n\n"
f"Your one-time code is: *{otp}*\n\n"
f"This code expires in 5 minutes.\n"
f"Do not share this code with anyone."
)
response = requests.post(
f"{BASE_URL}/api/whatsapp/send",
json={
"session_id": SESSION_ID,
"to": phone,
"country": country,
"message": message,
},
headers=HEADERS,
timeout=30,
)
data = response.json()
if data.get("success"):
# Store OTP keyed by the normalised number the API returned
normalised = data["to"]
_otp_store[normalised] = {"otp": otp, "expires_at": expires_at}
print(f"[OTP] Sent to {normalised}. Credits left: {data['credits_remaining']}")
return {"success": True, "to": normalised}
else:
print(f"[OTP] Failed: {data.get('message')}")
return {"success": False, "error": data.get("message")}
def verify_otp(phone_e164: str, submitted_code: str) -> bool:
"""Verify a submitted OTP code."""
record = _otp_store.get(phone_e164)
if not record:
return False
if time.time() > record["expires_at"]:
del _otp_store[phone_e164]
return False
if record["otp"] == submitted_code:
del _otp_store[phone_e164]
return True
return False
# --- Usage ---
result = send_whatsapp_otp("0821234567", "ZA")
if result["success"]:
# Simulate user entering the OTP
user_input = input("Enter the OTP you received: ")
if verify_otp(result["to"], user_input):
print("✅ OTP verified. User authenticated.")
else:
print("❌ Invalid or expired OTP.")
Example 2 — Order Notification + Status Check (Python)
Send a delivery notification and confirm the session is alive first.
import requests
API_KEY = "mtd_key1234567890ABCD"
SESSION_ID = "standby-store"
BASE_URL = "https://api.motadev.xyz"
HEADERS = {
"X-API-KEY": API_KEY,
"User-Agent": "StandbyStore/1.0",
"Referer": "https://standby.co.za",
"Content-Type": "application/json",
}
def is_session_alive(session_id: str) -> bool:
resp = requests.get(
f"{BASE_URL}/api/whatsapp/status/{session_id}",
headers=HEADERS,
timeout=15,
)
data = resp.json()
return data.get("connected", False)
def notify_order(customer_phone: str, country: str, order: dict) -> bool:
if not is_session_alive(SESSION_ID):
print("[WhatsApp] Session not connected. Message not sent.")
return False
message = (
f"Hey {order['name']}! 👋\n\n"
f"✅ Your *Standby* order #{order['id']} has been confirmed.\n\n"
f"📦 Items: {order['items']}\n"
f"💳 Total: R{order['total']:.2f}\n"
f"🚚 Estimated delivery: {order['eta']}\n\n"
f"Track your order: https://standby.co.za/track/{order['id']}\n\n"
f"Reply *HELP* if you need assistance."
)
resp = requests.post(
f"{BASE_URL}/api/whatsapp/send",
json={
"session_id": SESSION_ID,
"to": customer_phone,
"country": country,
"message": message,
},
headers=HEADERS,
timeout=30,
)
data = resp.json()
if data.get("success"):
print(f"[WhatsApp] Notification sent to {data['to']}")
return True
else:
print(f"[WhatsApp] Send failed: {data.get('message')}")
return False
# --- Usage ---
order = {
"id": "ORD-20240601-0042",
"name": "Sipho",
"items": "Black Oversized Hoodie x1, Cargo Pants x2",
"total": 849.00,
"eta": "2–3 business days",
}
notify_order("0738881234", "ZA", order)
Node.js — Pair + Send in One Script
const axios = require("axios");
const API_KEY = "mtd_key1234567890ABCD";
const BASE_URL = "https://api.motadev.xyz";
const headers = {
"X-API-KEY": API_KEY,
"User-Agent": "MyApp/1.0",
"Referer": "https://myapp.com",
"Content-Type": "application/json",
};
// ── Step 1: Pair a WhatsApp number ──────────────────────────
async function pairNumber(sessionId, phoneNumber, country) {
const res = await axios.post(
`${BASE_URL}/api/whatsapp/pair`,
{ session_id: sessionId, phone_number: phoneNumber, country },
{ headers, timeout: 30000 }
);
return res.data;
}
// ── Step 2: Check session status ────────────────────────────
async function checkStatus(sessionId) {
const res = await axios.get(
`${BASE_URL}/api/whatsapp/status/${sessionId}`,
{ headers, timeout: 15000 }
);
return res.data.connected;
}
// ── Step 3: Send a message ──────────────────────────────────
async function sendMessage(sessionId, to, country, message) {
const res = await axios.post(
`${BASE_URL}/api/whatsapp/send`,
{ session_id: sessionId, to, country, message },
{ headers, timeout: 30000 }
);
return res.data;
}
// ── Example 1: Full pairing flow ────────────────────────────
(async () => {
console.log("Pairing WhatsApp number...");
const pair = await pairNumber("my-app-session", "0821234567", "ZA");
if (pair.success) {
console.log(`Got pairing code: ${pair.pairing_code}`);
console.log("Enter this code in WhatsApp → Linked Devices → Link with Phone Number");
console.log("Waiting 30 seconds for you to enter the code...");
await new Promise(r => setTimeout(r, 30000));
const connected = await checkStatus("my-app-session");
console.log("Connected:", connected);
}
})();
// ── Example 2: Send a verification link ─────────────────────
(async () => {
const SESSION = "my-app-session";
const connected = await checkStatus(SESSION);
if (!connected) {
console.error("Session not active. Run pairing first.");
return;
}
const verifyToken = "abc123xyz";
const result = await sendMessage(
SESSION,
"0761234567",
"ZA",
`Welcome to MyApp! 🎉\n\nClick the link below to verify your account:\n\nhttps://myapp.com/verify?token=${verifyToken}\n\nThis link expires in 24 hours.`
);
if (result.success) {
console.log(`Verification link sent to ${result.to}`);
console.log(`Credits remaining: ${result.credits_remaining}`);
} else {
console.error("Failed:", result.message);
}
})();
Rust — Send OTP
use reqwest::blocking::Client;
use serde::{Deserialize, Serialize};
use serde_json::json;
const API_KEY: &str = "mtd_key1234567890ABCD";
const BASE_URL: &str = "https://api.motadev.xyz";
const SESSION_ID: &str = "rust-app-session";
#[derive(Debug, Deserialize)]
struct WhatsAppResponse {
success: bool,
message: String,
to: Option<String>,
pairing_code: Option<String>,
credits_remaining: Option<i64>,
connected: Option<bool>,
}
fn build_headers() -> reqwest::header::HeaderMap {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("X-API-KEY", API_KEY.parse().unwrap());
headers.insert("User-Agent", "RustApp/1.0".parse().unwrap());
headers.insert("Referer", "https://myrustapp.com".parse().unwrap());
headers.insert("Content-Type", "application/json".parse().unwrap());
headers
}
fn send_otp(client: &Client, phone: &str, country: &str, otp: &str) -> Result<WhatsAppResponse, Box<dyn std::error::Error>> {
let message = format!(
"🔐 Your verification code is: *{}*\n\nExpires in 5 minutes. Do not share this code.",
otp
);
let body = json!({
"session_id": SESSION_ID,
"to": phone,
"country": country,
"message": message,
});
let response = client
.post(format!("{}/api/whatsapp/send", BASE_URL))
.headers(build_headers())
.json(&body)
.send()?;
let data: WhatsAppResponse = response.json()?;
Ok(data)
}
fn check_status(client: &Client) -> Result<bool, Box<dyn std::error::Error>> {
let response = client
.get(format!("{}/api/whatsapp/status/{}", BASE_URL, SESSION_ID))
.headers(build_headers())
.send()?;
let data: WhatsAppResponse = response.json()?;
Ok(data.connected.unwrap_or(false))
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// Check the session is live before sending
match check_status(&client) {
Ok(true) => println!("Session active ✅"),
Ok(false) => {
eprintln!("Session not connected. Pair your number first.");
return Ok(());
}
Err(e) => {
eprintln!("Status check failed: {}", e);
return Ok(());
}
}
// Send OTP
let otp = "847291";
let result = send_otp(&client, "0821234567", "ZA", otp)?;
if result.success {
println!("OTP sent to {}", result.to.unwrap_or_default());
println!("Credits remaining: {}", result.credits_remaining.unwrap_or(0));
} else {
eprintln!("Failed: {}", result.message);
}
Ok(())
}
Common Message Templates
Copy and adapt these for your use case. WhatsApp renders *bold* and _italic_.
OTP:
🔐 *Verification Code*
Your code is: *{otp}*
Valid for 5 minutes. Never share this with anyone.
Order confirmation:
✅ Order *#{order_id}* confirmed!
Items: {items}
Total: R{total}
Delivery: {eta}
Track: {tracking_url}
Appointment reminder:
📅 *Reminder*
You have an appointment tomorrow at *{time}* with {business}.
Reply *CONFIRM* to confirm or *CANCEL* to cancel.
Password reset link:
🔑 *Password Reset*
Someone requested a password reset for your {app_name} account.
Reset here: {reset_url}
This link expires in 15 minutes. If you didn't request this, ignore this message.
Reconnecting a Dropped Session
If WhatsApp logs out (phone restart, inactive session, etc.), call the connect endpoint to restore the session without re-pairing:
POST /api/whatsapp/connect
Content-Type: application/json
{
"session_id": "my-business-account"
}
If that doesn't work after a few seconds, do a full re-pair.
Rate Limits & Costs
| Endpoint | Cost | Rate limit |
|---|---|---|
POST /api/whatsapp/pair |
1 credit | 5 requests / 5 min |
GET /api/whatsapp/status/{id} |
0 credits | 20 requests / min |
POST /api/whatsapp/send |
1 credit | 30 requests / min |
POST /api/whatsapp/connect |
0 credits | 10 requests / min |
GET /api/whatsapp/countries |
0 credits | Unlimited |
Troubleshooting
"Unsupported country code" — Use one of the codes from /api/whatsapp/countries. "SA" is accepted as an alias for "ZA".
"Phone number must be X digits" — Strip the country code and leading zero before worrying — the API does that automatically. Just pass the number as the user typed it and let the API normalise it.
Session connects but messages don't deliver — Make sure the recipient's number is on WhatsApp. The API sends to the number, not a verified business profile, so there's no delivery receipt in the response — messages either reach the device or silently fail if the number isn't on WhatsApp.
402 Insufficient credits — Top up at api.motadev.xyz/billing/buy. R99 gets you 500 credits — that's 500 messages.
Pairing code expired — Codes expire quickly (within ~120 seconds of generation). Call /pair again to get a fresh one and enter it immediately.
0 Comments
Join the conversation
No comments yet. Be the first!