MRF SMS Developer API
Sell phone numbers and OTP verifications on your own website using the MRF SMS backend. Your users buy numbers through your site, MRF SMS handles the provider, delivery, and refunds.
Introduction
The MRF SMS API lets you offer phone-number rental and OTP verification to your customers without operating your own SMS infrastructure. You send a request, MRF SMS returns a real phone number, and you poll for the OTP when it arrives.
This documentation is aimed at developers integrating MRF SMS into their own website, app, bot, or software. Everything you need โ endpoints, examples, ready-to-copy code โ is on this page.
What you can do with this API
- Sell verification numbers for 25+ services including WhatsApp, Facebook, Instagram, Telegram, TikTok, Google, Amazon, and more.
- Access 170+ countries with per-country pricing tiers.
- Automatically receive OTP codes in your app.
- Cancel and refund orders if the OTP doesn't arrive.
- Charge your own margin on top of MRF SMS prices.
How it works
- Your customer picks a service (e.g. WhatsApp) and country on your website.
- Your backend calls
POST /api/v1/orderswith your API key. - MRF SMS purchases a real number from a provider and returns it.
- Your site displays the number to the customer.
- Your backend polls
GET /api/v1/orders/:idevery few seconds. - When the OTP arrives, you display it to your customer.
- If it never arrives, you call cancel and the money is refunded.
Quick Start (2 Minutes)
Copy this into your terminal, replace YOUR_API_KEY, and you'll have a working phone number in seconds.
curl -X POST "https://your-domain.com/api/v1/orders" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"service":"whatsapp","countryId":187}'
You'll get a response like this:
{
"orderId": "API-A1B2C3D4E5F6G7H8",
"phoneNumber": "+12025551234",
"status": "pending",
"price": 100,
"expiresAt": "2025-01-01T12:25:00.000Z"
}
Then poll the order for the OTP:
curl "https://your-domain.com/api/v1/orders/API-A1B2C3D4E5F6G7H8" \
-H "Authorization: Bearer YOUR_API_KEY"
Authentication
Every request must include your API key as a Bearer token in the Authorization header.
Authorization: Bearer mrf_your_api_key_here
- Never expose it in browser JavaScript, mobile apps, or public code repositories.
- Always call the MRF SMS API from your backend server, never directly from the browser.
- If a key is leaked, click Regenerate Key in your dashboard immediately.
- Anyone with your key can spend your balance.
Testing your key
curl "https://your-domain.com/api/v1/balance" \
-H "Authorization: Bearer YOUR_API_KEY"
A successful response returns your current balance. A 401 means the key is invalid or missing.
Rate Limits
Every API key is limited to 60 requests per minute. If you exceed the limit, the API returns HTTP 429 Too Many Requests.
Available Services
Below are all services you can order numbers for. Use the serviceType value in your API requests.
GET /api/v1/services/:service/countries to see live availability.
Countries & Country IDs
Every country has a numeric countryId that you must include when creating an order. Get the full list via the countries endpoint.
Popular country IDs
| Country | Code | countryId |
|---|---|---|
| ๐บ๐ธ USA | +1 | 187 |
| ๐ฌ๐ง United Kingdom | +44 | 16 |
| ๐ฎ๐ณ India | +91 | 22 |
| ๐ต๐ฐ Pakistan | +92 | 66 |
| ๐ฎ๐ฉ Indonesia | +62 | 6 |
| ๐ท๐บ Russia | +7 | 0 |
| ๐จ๐ณ China | +86 | 3 |
| ๐ง๐ท Brazil | +55 | 73 |
| ๐ฉ๐ช Germany | +49 | 43 |
| ๐น๐ท Turkey | +90 | 62 |
To see every country + live pricing for a service, use:
GET /api/v1/services/whatsapp/countries
Pricing & Tiers
Prices are always in PKR (Pakistani Rupee) and set on the server side. You cannot pass a custom price.
Tiers (Bronze / Silver / Gold)
Some countries offer multiple tiers for the same service. Higher tiers usually have better delivery rates but cost more. If you don't specify a tier, the cheapest available provider is used automatically.
| Field | Meaning |
|---|---|
tierNumber | 1 = Bronze (cheapest), 2 = Silver, 3 = Gold, etc. |
rank | Human-readable tier name. |
price | Cost in PKR for that tier. |
API Reference
Full list of all endpoints with request and response examples.
List every service you can order numbers for.
Response
{
"services": [
{
"serviceType": "whatsapp",
"serviceName": "WhatsApp Number",
"canOrder": true,
"countryCount": 85
},
{
"serviceType": "facebook",
"serviceName": "Facebook Number",
"canOrder": true,
"countryCount": 78
}
]
}
List countries available for a service, with base prices.
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
service | string | Optional | Service type. Defaults to whatsapp. |
Response
{
"service": "whatsapp",
"countries": [
{
"name": "USA",
"code": "+1",
"countryId": 187,
"flag": "๐บ๐ธ",
"price": 120
}
]
}
List countries for a service with all available tiers/ranks.
Response
{
"service": "whatsapp",
"countries": [
{
"name": "USA",
"code": "+1",
"countryId": 187,
"flag": "๐บ๐ธ",
"price": 120,
"tiers": [
{ "tierNumber": 1, "price": 100, "rank": "Bronze" },
{ "tierNumber": 2, "price": 150, "rank": "Silver" }
]
}
]
}
Check your API account balance.
Response
{
"balance": 5000,
"currency": "PKR"
}
Request a new phone number. This is the main endpoint.
Body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
service | string | Required | Service type from the services endpoint. |
countryId | number | Required | Country ID from the countries endpoint. |
tierNumber | number | Optional | Specific tier. If omitted, cheapest available is used. |
providerId | string | Optional | Force a specific provider (rarely needed). |
Response 201
{
"orderId": "API-A1B2C3D4E5F6G7H8",
"service": "whatsapp",
"country": "USA",
"countryId": 187,
"price": 100,
"phoneNumber": "+12025551234",
"status": "pending",
"expiresAt": "2025-01-01T12:25:00.000Z"
}
409 error โ no money is spent.
List your recent orders (most recent first).
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
limit | number | Optional | Default 50, max 200. |
offset | number | Optional | For pagination. Default 0. |
Poll this endpoint every 3โ5 seconds to check for the OTP.
Response
{
"orderId": "API-A1B2C3D4E5F6G7H8",
"service": "whatsapp",
"country": "USA",
"countryId": 187,
"price": 100,
"status": "active",
"phoneNumber": "+12025551234",
"otpCode": "123456",
"createdAt": "2025-01-01T12:00:00.000Z",
"completedAt": null
}
The otpCode field is populated as soon as the SMS arrives. Status becomes active.
Cancel an order and refund it back to your balance.
Response
{
"orderId": "API-A1B2C3D4E5F6G7H8",
"status": "cancelled",
"refunded": true,
"refundAmount": 100,
"message": "Order cancelled & refunded."
}
- You must wait ~17 seconds after order creation before cancelling.
- You cannot cancel once the OTP has arrived.
- A refund may be blocked if the provider reports the number was already used elsewhere.
Full Integration Tutorial
Complete guide for adding MRF SMS to your own website โ from zero to working number sales.
-
Get your API key
Log in to MRF SMS โ open the 3-dots menu โ click ๐ Developer API. Copy the key from the amber banner.
-
Store the key in your backend
Put your API key in an environment variable โ never hardcode it and never expose it to the browser. Example:
MRF_API_KEY=mrf_... -
Build a country selector on your site
Fetch
/api/v1/services/:service/countriesfrom your backend to show country options + prices to your customer. Cache the response for 5โ10 minutes to reduce API calls. -
Handle "Buy" button click
When your customer clicks buy, call
POST /api/v1/ordersfrom your backend. Add your margin to the price. Show your customer the phone number. -
Poll for the OTP
Every 3โ5 seconds, call
GET /api/v1/orders/:orderId. WhenotpCodeis not null, show it to the customer. -
Handle timeout / cancel
If no OTP arrives after ~5 minutes, call
POST /api/v1/orders/:orderId/cancel. You'll get an automatic refund. -
Track your balance
Call
/api/v1/balancedaily. Top up your MRF SMS balance from the dashboard when it runs low.
Drop-in HTML Widget
The fastest way to try this API on your own site. This is a complete, working HTML page โ save it as test.html, replace the two placeholders, and open it in your browser.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>MRF SMS Test</title>
<style>
body { font-family: system-ui; max-width: 500px; margin: 40px auto; padding: 20px; }
button { padding: 12px 24px; background: #10b981; color: white; border: none;
border-radius: 8px; font-weight: 700; cursor: pointer; font-size: 15px; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
.box { padding: 16px; background: #f1f5f9; border-radius: 8px; margin: 12px 0; }
.otp { font-size: 32px; font-weight: 900; color: #10b981; letter-spacing: 4px; }
</style>
</head>
<body>
<h1>Buy a WhatsApp Number</h1>
<select id="country">
<option value="187">๐บ๐ธ USA</option>
<option value="16">๐ฌ๐ง UK</option>
<option value="22">๐ฎ๐ณ India</option>
<option value="66">๐ต๐ฐ Pakistan</option>
</select>
<button id="buy">Buy Number</button>
<div id="result"></div>
<script>
const API_BASE = 'https://your-domain.com/api/v1';
const API_KEY = 'YOUR_API_KEY_HERE'; // โ replace me
async function api(path, opts = {}) {
const r = await fetch(API_BASE + path, {
...opts,
headers: {
'Authorization': 'Bearer ' + API_KEY,
'Content-Type': 'application/json',
...(opts.headers || {})
}
});
return r.json();
}
document.getElementById('buy').onclick = async () => {
const btn = document.getElementById('buy');
const out = document.getElementById('result');
btn.disabled = true;
out.innerHTML = 'Buying number...';
const order = await api('/orders', {
method: 'POST',
body: JSON.stringify({
service: 'whatsapp',
countryId: Number(document.getElementById('country').value)
})
});
if (order.error) { out.innerHTML = 'โ ' + order.error; btn.disabled = false; return; }
out.innerHTML = `
<div class="box">
<strong>Number:</strong> <code>${order.phoneNumber}</code><br>
<strong>Order:</strong> ${order.orderId}<br>
<strong>Status:</strong> <span id="s">${order.status}</span>
</div>
<div class="box">Waiting for OTP…</div>
`;
for (let i = 0; i < 60; i++) {
await new Promise(r => setTimeout(r, 4000));
const check = await api('/orders/' + order.orderId);
document.getElementById('s').textContent = check.status;
if (check.otpCode) {
out.innerHTML += '<div class="box">OTP: <div class="otp">' + check.otpCode + '</div></div>';
break;
}
if (['expired','cancelled'].includes(check.status)) break;
}
btn.disabled = false;
};
</script>
</body>
</html>
Code Examples
Backend integration examples in the most common languages. All examples do the same thing: buy a number โ poll for OTP โ cancel if none arrives.
const API_BASE = 'https://your-domain.com/api/v1';
const API_KEY = process.env.MRF_API_KEY;
const headers = {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
};
async function buyAndPoll(service, countryId) {
// 1. Create order
const order = await fetch(`${API_BASE}/orders`, {
method: 'POST',
headers,
body: JSON.stringify({ service, countryId })
}).then(r => r.json());
if (order.error) throw new Error(order.error);
console.log('Got number:', order.phoneNumber);
// 2. Poll for OTP (max ~4 minutes)
for (let i = 0; i < 50; i++) {
await new Promise(r => setTimeout(r, 5000));
const status = await fetch(`${API_BASE}/orders/${order.orderId}`, { headers })
.then(r => r.json());
if (status.otpCode) {
console.log('OTP received:', status.otpCode);
return { order, otp: status.otpCode };
}
if (['expired', 'cancelled'].includes(status.status)) break;
}
// 3. Cancel if no OTP
await fetch(`${API_BASE}/orders/${order.orderId}/cancel`, {
method: 'POST', headers
});
throw new Error('OTP did not arrive โ order cancelled and refunded.');
}
// Usage
buyAndPoll('whatsapp', 187)
.then(({ order, otp }) => console.log('Success:', otp))
.catch(err => console.error('Failed:', err.message));
<?php
$API_BASE = 'https://your-domain.com/api/v1';
$API_KEY = getenv('MRF_API_KEY');
function api($method, $path, $body = null) {
global $API_BASE, $API_KEY;
$ch = curl_init($API_BASE . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $API_KEY",
"Content-Type: application/json"
]);
if ($body) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
return $response;
}
function buyAndPoll($service, $countryId) {
// 1. Create order
$order = api('POST', '/orders', ['service' => $service, 'countryId' => $countryId]);
if (!empty($order['error'])) throw new Exception($order['error']);
echo "Number: " . $order['phoneNumber'] . "\n";
// 2. Poll for OTP
for ($i = 0; $i < 50; $i++) {
sleep(5);
$status = api('GET', '/orders/' . $order['orderId']);
if (!empty($status['otpCode'])) {
echo "OTP: " . $status['otpCode'] . "\n";
return ['order' => $order, 'otp' => $status['otpCode']];
}
if (in_array($status['status'], ['expired', 'cancelled'])) break;
}
// 3. Cancel if timeout
api('POST', '/orders/' . $order['orderId'] . '/cancel');
throw new Exception('OTP did not arrive โ order cancelled and refunded.');
}
buyAndPoll('whatsapp', 187);
?>
import os, time, requests
API_BASE = 'https://your-domain.com/api/v1'
API_KEY = os.environ['MRF_API_KEY']
HEADERS = {'Authorization': f'Bearer {API_KEY}', 'Content-Type': 'application/json'}
def buy_and_poll(service, country_id):
# 1. Create order
order = requests.post(f'{API_BASE}/orders',
json={'service': service, 'countryId': country_id},
headers=HEADERS
).json()
if 'error' in order:
raise Exception(order['error'])
print('Number:', order['phoneNumber'])
# 2. Poll for OTP
for _ in range(50):
time.sleep(5)
status = requests.get(f"{API_BASE}/orders/{order['orderId']}",
headers=HEADERS).json()
if status.get('otpCode'):
print('OTP:', status['otpCode'])
return order, status['otpCode']
if status['status'] in ('expired', 'cancelled'):
break
# 3. Cancel on timeout
requests.post(f"{API_BASE}/orders/{order['orderId']}/cancel", headers=HEADERS)
raise Exception('OTP did not arrive โ order cancelled and refunded.')
buy_and_poll('whatsapp', 187)
# Check balance
curl "https://your-domain.com/api/v1/balance" \
-H "Authorization: Bearer YOUR_API_KEY"
# List services
curl "https://your-domain.com/api/v1/services" \
-H "Authorization: Bearer YOUR_API_KEY"
# List countries for WhatsApp
curl "https://your-domain.com/api/v1/countries?service=whatsapp" \
-H "Authorization: Bearer YOUR_API_KEY"
# Buy a WhatsApp number for USA
curl -X POST "https://your-domain.com/api/v1/orders" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"service":"whatsapp","countryId":187}'
# Check for OTP
curl "https://your-domain.com/api/v1/orders/API-A1B2C3D4E5F6G7H8" \
-H "Authorization: Bearer YOUR_API_KEY"
# Cancel and refund
curl -X POST "https://your-domain.com/api/v1/orders/API-A1B2C3D4E5F6G7H8/cancel" \
-H "Authorization: Bearer YOUR_API_KEY"
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
const apiBase = "https://your-domain.com/api/v1"
var apiKey = os.Getenv("MRF_API_KEY")
func apiCall(method, path string, body map[string]any) (map[string]any, error) {
var buf io.Reader
if body != nil {
b, _ := json.Marshal(body)
buf = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, apiBase+path, buf)
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer resp.Body.Close()
var out map[string]any
json.NewDecoder(resp.Body).Decode(&out)
return out, nil
}
func main() {
order, _ := apiCall("POST", "/orders", map[string]any{
"service": "whatsapp", "countryId": 187,
})
fmt.Println("Number:", order["phoneNumber"])
orderID := order["orderId"].(string)
for i := 0; i < 50; i++ {
time.Sleep(5 * time.Second)
s, _ := apiCall("GET", "/orders/"+orderID, nil)
if otp, ok := s["otpCode"]; ok && otp != nil {
fmt.Println("OTP:", otp)
return
}
}
apiCall("POST", "/orders/"+orderID+"/cancel", nil)
}
Common Patterns
Adding your margin
MRF SMS charges you the API price (say 100 PKR). You can charge your customer more (say 150 PKR). Store the margin as a config value in your backend:
const MARGIN_PERCENT = 50; // 50% markup
const apiPrice = country.price;
const yourPrice = Math.ceil(apiPrice * (1 + MARGIN_PERCENT / 100));
Caching the countries list
Countries and prices don't change every second. Cache the response for 5โ10 minutes to avoid hitting the rate limit:
let cache = null; let cacheTime = 0;
async function getCountries(service) {
if (cache && Date.now() - cacheTime < 600000) return cache;
cache = await api(`/services/${service}/countries`);
cacheTime = Date.now();
return cache;
}
Handling rate limits (429)
If you get a 429, wait 60 seconds and retry. Or better โ throttle your requests up front.
async function safeCall(fn) {
try { return await fn(); }
catch (err) {
if (err.status === 429) {
await new Promise(r => setTimeout(r, 60000));
return fn();
}
throw err;
}
}
Auto-refund on failed orders
Give the customer a refund on your side automatically when the API cancels the order:
const cancel = await api(`/orders/${orderId}/cancel`, { method: 'POST' });
if (cancel.refunded) {
await refundCustomer(customerId, orderPrice);
}
Order Statuses
| Status | Meaning |
|---|---|
pending | Number was purchased, waiting for the OTP SMS to arrive. |
active | OTP arrived. Read otpCode for the code. |
completed | Order fully completed. |
cancelled | You cancelled the order (may or may not be refunded). |
expired | No OTP arrived within the timeout. Auto-refunded. |
Error Codes
All errors return JSON: { "error": "message" }
| Code | Meaning | What to do |
|---|---|---|
| 400 | Bad request | Check your request body / parameters. |
| 401 | Missing / invalid API key | Check the Authorization header. |
| 403 | Account suspended | Contact MRF SMS support. |
| 404 | Not found | Order ID or resource doesn't exist. |
| 409 | No numbers available OR price blocked | Try again shortly, or use a different tier. |
| 429 | Rate limit exceeded | Wait 60s and retry. |
| 500 | Server error | Retry with exponential backoff. |
FAQ
How much does the API cost?
The API itself is free to use. You pay per number you buy โ the price depends on the service and country. See your dashboard for live prices, or call GET /api/v1/services/whatsapp/countries.
Do I need to add money before using the API?
Yes. Top up your MRF SMS wallet from your dashboard first. Each API order deducts from that balance.
Can I charge my customers more than the API price?
Yes โ the difference is your profit. MRF SMS only charges you the base API price.
What if the OTP doesn't arrive?
Call the cancel endpoint. If eligible, you'll get an automatic refund. If the number expires by itself (no cancel), it's still auto-refunded.
Why did I get a 409 error saying price blocked?
MRF SMS's strict price firewall blocked the purchase because the provider cost was higher than the selling price. No money was spent. Try again in a moment or pick a different tier.
How long is my API key valid?
Forever, until you regenerate it. If it leaks, regenerate immediately from the dashboard.
Can I use one API key on multiple websites?
Yes, but they share the same balance and rate limit. If you need separation, generate a separate account per site.
Is there a sandbox / test mode?
Not currently. Every API order buys a real number. Start with a small balance while integrating.
Can my customers use the OTP for account verification on WhatsApp / Facebook / etc?
Yes. Every number is a real, working phone number from the provider. Your customer receives the OTP just like any regular verification.
How do I regenerate my API key?
Dashboard โ 3-dots menu โ ๐ Developer API โ click Regenerate Key. Your old key stops working instantly.