WapFlow OTP API reference
Send and verify verification codes from your own WhatsApp number. All responses are JSON.
Base URL: https://wapflow.net/api/otp/v1
Authentication
Send the header Authorization: Bearer <key> with every request. You create keys on the WapFlow OTP page of your WapFlow panel; live keys start with wfo_live_ and test keys with wfo_test_. A key is shown only once, when it is created — store it safely and use it on your server only.
A successful response looks like { "success": true, ... }; an error looks like { "success": false, "error": { "code": "...", "message": "..." } }.
Send a code
phone (required) is the recipient's number. Optional: code_length (4–8), ttl (60–900 s), lang (e.g. tr, en) and app_name. Anything you leave out comes from your panel settings.
{
"phone": "905321234567",
"code_length": 6,
"ttl": 300,
"lang": "tr",
"app_name": "Acme"
}
{
"success": true,
"request_id": "req_...",
"phone": "905321234567",
"expires_at": "2026-11-12T10:05:00.000Z",
"status": "sent" // "sent" | "queued"
}
Verify a code
phone and code are required. With request_id that exact request is checked; without it, the latest unexpired code sent to that number with this key. After 5 wrong attempts the request is locked, and a verified code can't be reused.
{ "request_id": "req_...", "phone": "905321234567", "code": "123456" }
{ "success": true, "valid": true }
{ "success": true, "valid": false,
"reason": "invalid_code", // invalid_code | expired | too_many_attempts | already_verified | not_found
"attempts_left": 3 }
Check status
Returns the request status: queued, sent, delivered, read, failed, verified or expired.
{ "success": true, "request_id": "req_...", "status": "delivered",
"sent_at": "...", "delivered_at": "...", "verified_at": null }
Test the connection
Shows that the key is valid, your plan, the number of connected accounts and this month's quota usage (the WordPress plugin's "Test connection" button uses it).
{ "success": true, "plan": "otp-2",
"accounts": { "limit": 2, "connected": 1 },
"quota": { "monthly": 15000, "used": 42, "period": "2026-11" },
"ends_at": "2026-12-12T10:00:00.000Z", "mode": "live" }
Examples
These examples send a code and then verify what the user enters. Replace wfo_live_XXXX with your own key.
# 1) send
curl -X POST https://wapflow.net/api/otp/v1/send \
-H "Authorization: Bearer wfo_live_XXXX" \
-H "Content-Type: application/json" \
-d '{"phone":"905321234567","app_name":"Acme"}'
# 2) verify
curl -X POST https://wapflow.net/api/otp/v1/verify \
-H "Authorization: Bearer wfo_live_XXXX" \
-H "Content-Type: application/json" \
-d '{"request_id":"req_...","phone":"905321234567","code":"123456"}'<?php
function wapflow_otp(string $path, array $body): array {
$ch = curl_init('https://wapflow.net/api/otp/v1' . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WAPFLOW_OTP_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($body),
]);
$res = json_decode(curl_exec($ch), true) ?: [];
curl_close($ch);
return $res;
}
$sent = wapflow_otp('/send', ['phone' => '905321234567', 'app_name' => 'Acme']);
// ... kullanıcı kodu girince:
$check = wapflow_otp('/verify', ['request_id' => $sent['request_id'], 'phone' => '905321234567', 'code' => $_POST['code']]);
if (!empty($check['valid'])) { /* giriş başarılı */ }const BASE = 'https://wapflow.net/api/otp/v1';
async function otp(path, body) {
const res = await fetch(BASE + path, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.WAPFLOW_OTP_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
return res.json();
}
const sent = await otp('/send', { phone: '905321234567', app_name: 'Acme' });
// ... when the user submits the code:
const check = await otp('/verify', { request_id: sent.request_id, phone: '905321234567', code: '123456' });
if (check.valid) { /* signed in */ }import os
import requests
BASE = "https://wapflow.net/api/otp/v1"
HEADERS = {"Authorization": "Bearer " + os.environ["WAPFLOW_OTP_KEY"]}
def otp(path, body):
return requests.post(BASE + path, json=body, headers=HEADERS, timeout=15).json()
sent = otp("/send", {"phone": "905321234567", "app_name": "Acme"})
# ... when the user submits the code:
check = otp("/verify", {"request_id": sent["request_id"], "phone": "905321234567", "code": "123456"})
if check.get("valid"):
pass # signed inError codes
| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The request body or parameters are invalid. |
| 401 | invalid_key | The API key is invalid or has been revoked. |
| 402 | no_subscription | This account has no WapFlow OTP subscription. |
| 402 | subscription_expired | The subscription has expired; please renew it. |
| 403 | sales_disabled | The service is temporarily unavailable. |
| 422 | invalid_phone | The phone number is invalid. |
| 429 | rate_limited | Rate limit exceeded; retry after the retry_after seconds in the response. |
| 429 | quota_exceeded | This month's code quota is used up. |
| 429 | daily_limit_reached | The daily sending limit of the connected WhatsApp accounts has been reached; retry after the retry_after seconds in the response (next day 00:00, Istanbul time). No quota is used. |
| 503 | no_account_connected | There is no connected, enabled WhatsApp account. |
| 503 | send_failed | The message could not be sent; try again shortly. |
Rate limits
- Per number: 1 code per 60 seconds, 5 per hour and 10 per day.
- Per API key: 60 sends per minute.
- Per IP address: 120 requests per minute (verification included).
- Verification: 10 attempts per minute per number, and at most 5 wrong attempts per code.
- The monthly quota depends on your plan; when it is used up,
quota_exceededis returned. Test keys use no quota.
Account warm-up and daily limit
- Newly connected WhatsApp numbers warm up to reduce ban risk: 30 codes per day for the first 3 days, 100 per day on days 4–7, then 500 per day per account (default values).
- An account that reaches its daily limit is skipped and codes go out from your other connected accounts. If all are full,
429 daily_limit_reachedis returned; the counter resets at midnight Istanbul time. - For more volume, choose a package with several accounts. Test keys are not affected by these limits.
Test keys
Keys starting with wfo_test_ send no WhatsApp message and use no quota; the code is returned in the test_code field. Sending and rate-limit rules are the same as for live keys, and an active subscription is still required. To go live, just swap the key.
{ "success": true, "request_id": "req_...", "status": "sent", "test_code": "482913" }
Phone number format
- The number is reduced to digits (+, spaces and dashes are dropped) and must be 10–15 digits long.
- Include the country code (e.g.
905321234567). An 11-digit number starting with 0 is treated as Turkish and gets 90 prepended. - An invalid number returns
422 invalid_phone.
Security notes
- Codes are generated with a cryptographically secure random generator and are never stored in plain text.
- Never use the API key in a browser, a mobile app or public code; call the API from your server.
- Always check the code on the server with
/verify. - WapFlow OTP is not the official WhatsApp API and does not guarantee delivery; keep a backup channel for critical actions.
WordPress plugin
- Download wapflow-otp.zip.
- In WordPress admin go to Plugins → Add New → Upload Plugin, upload the zip and activate it.
- In the plugin settings, paste the API key you created in the WapFlow panel.
- Use the "Test connection" button to check the key and your plan.
- Try it with a test key first, then switch to the live key.