WappSync Phone Verify - API Docs
Phone verification where your user sends a code to us via WhatsApp - no OTP typing, one tap, fully verified.
Contents
How It Works
You request a code
Your backend calls our API with the user's phone number. We return a unique code and the WhatsApp number to send it to.
User sends it via WhatsApp
Your user taps a WhatsApp link - the code is pre-filled. No typing, no copy-paste. They just hit send.
Verified instantly
We receive the message, see which WhatsApp number sent it, and your poll returns auth=1 with the certified phone. Done!
Get Your Credentials
Sign up at wappsync.com/mybusiness to receive:
| Credential | Description |
|---|---|
businessid | Your numeric business ID |
businesskey | Your secret key - never share this, never put it in client-side code |
businesskey is used for HMAC signing and must never appear in browser JavaScript, HTML source, or mobile app code.Two Verification Modes
Phone Verify runs in two modes, chosen per request. In both, your user sends the code to us via WhatsApp - the only difference is which phone number gets certified.
confirm_phone (default) | verify_sender | |
|---|---|---|
| You send | a phone number N | nothing (no number) |
| What gets certified | that the caller controls N specifically | whichever number actually sends the code back |
| Typical use | login / recovery on an on-file number, anti-SIM-swap | fresh signup - "give me a real, proven phone for this client" |
| Code delivery | WhatsApp-pushed to N and shown in your UI (delivery: pushed_to_phone) | only via you (delivery: relay_by_business) - you hand the code to your client |
| Poll result | sender_matches_intended tells you whether the sender is N | phoneEnc is the certified number; sender_matches_intended is omitted |
confirm_phone proves control of a specific declared number; verify_sender proves control of some number and reports which one. If your flow needs "the number on this account really belongs to the user," stay in confirm_phone with on_mismatch: reject.Corrected phone (on_mismatch): a common case is the client declaring A but sending the code from B (typo / dual-SIM). In confirm_phone, reject (the default) treats that as a failed attempt; accept_sender certifies the actual sender B instead (sender_matches_intended: false, phoneEnc = B), and your backend decides whether to accept the corrected number B or re-request a fresh OTP for A.
Request a Reverse OTP
Call this from your backend when you need to verify a user:
POSThttps://wappsync.com/phone-verify/api/getReverseOTP
Request Body (JSON)
| Field | Type | Description |
|---|---|---|
businessid | int | Your business ID |
businessref | string | A unique reference you generate for this auth request (e.g., bin2hex(random_bytes(4))) |
phone | string | The user's phone number in international format (+40755123456). Required in confirm_phone mode; omit/empty for verify_sender. |
mode | string | Optional - confirm_phone (default when a phone is supplied) or verify_sender (default when phone is omitted) |
on_mismatch | string | Optional (confirm_phone only) - reject (default) or accept_sender (certify the actual sender if the code arrives from a different number) |
ts | int | Current Unix timestamp |
hmac | string | HMAC-SHA256 signature |
HMAC Signing
Sign this string with your businesskey:
{businessid}|{businessref}|{phone}|{ts}
In verify_sender mode there is no phone, so the signed string is {businessid}|{businessref}||{ts} (empty middle slot).
Example:
$hmac = hash_hmac('sha256', "{$businessid}|{$businessref}|{$phone}|{$ts}", $businesskey);
Response
| Field | Type | Description |
|---|---|---|
status | string | "ok" or "error" |
reverseotp_display | string | The code your user sends, e.g. "2FA-828161" |
waphone | string | The WhatsApp number your user sends the code TO |
logincode | string | Internal code to use when polling |
in_ts | int | Timestamp for the polling request |
out_ts | int | Response timestamp |
otp_valid_minutes | int | How long the code is valid (default 10) |
mode | string | The active mode: confirm_phone or verify_sender |
delivery | string | pushed_to_phone (confirm_phone: the code is also WhatsApp-pushed to the declared phone) or relay_by_business (verify_sender: you must deliver the code to your client yourself) |
hmac | string | HMAC of the response - verify with your businesskey |
Verify the response HMAC against {businessid}|{businessref}|{logincode}|{reverseotp_display}|{waphone}|{out_ts}.
What to show your user
Display the code and phone number. Use a whatsapp://send link so the user can send it in one tap:
<a href="whatsapp://send?phone=40725296320&text=2FA-828161">
💬 Send Code via WhatsApp
</a>
Poll for Verification
Poll every 2-3 seconds until the user is verified:
POSThttps://wappsync.com/phone-verify/api/getReverseOTPStatus
Request Body (JSON)
| Field | Type | Description |
|---|---|---|
businessid | int | Your business ID |
businessref | string | The same reference from the previous call |
logincode | string | The logincode from the getReverseOTP response |
in_ts | int | The original ts from the getReverseOTP request |
hmac | string | HMAC-SHA256 of {businessid}|{businessref}|{logincode}|{in_ts} |
Response
| Field | Type | Meaning |
|---|---|---|
used | 0 | ⏳ Still waiting - poll again |
used | 1 | ✅ Just verified - the first poll to see this consumes it |
used | 2 | 🔒 Already consumed by a previous poll |
auth | 1 | User is verified - log them in! |
phoneEnc | string | AES-256-CBC encrypted certified phone (only when auth=1) - the sender when a code was received (identical to your declared number under strict confirm_phone) |
tries | int | Failed attempts counted (wrong code or wrong phone) |
expired | bool | true when too many failed attempts (tries ≥ 4) - request a new OTP |
mode | string | The mode of this request: confirm_phone or verify_sender |
sender_matches_intended | bool | confirm_phone only, once a code was received - true = the sender is the number you asked to verify; false = a corrected sender was certified. Omitted in verify_sender. |
out_ts | int | Response timestamp |
hmac | string | HMAC - verify against {businessid}|{businessref}|{logincode}|{used}|{phoneEnc}|{out_ts} |
When auth === 1, a valid code was received. Decrypt phoneEnc to get the certified phone (check sender_matches_intended in confirm_phone mode), then log the user in.
Decrypt the Phone Number
When auth=1, phoneEnc contains the certified phone (the sender that proved itself) encrypted with AES-256-CBC using your businesskey:
function decryptPhoneAES($encryptedData, $businesskey) {
$method = 'AES-256-CBC';
$key = hash('sha256', $businesskey, true);
$enc = str_replace(['-', '_'], ['+', '/'], $encryptedData);
$enc = str_pad($enc, strlen($enc) % 4 === 0
? strlen($enc) : strlen($enc) + 4 - strlen($enc) % 4,
'=', STR_PAD_RIGHT);
$decoded = base64_decode($enc);
$iv = substr($decoded, 0, 16);
$ciphertext = substr($decoded, 16);
return openssl_decrypt($ciphertext, $method, $key, OPENSSL_RAW_DATA, $iv);
}
$phone = decryptPhoneAES($response['phoneEnc'], $businesskey);
// Store this phone - it's the user's verified identity
Frontend Example
The only UI you need - show after your backend calls getReverseOTP:
<div style="text-align:center; max-width:400px; margin:auto;">
<p>Send this exact code via WhatsApp from your phone:</p>
<div style="font-size:28px; font-weight:bold; color:#25D366;
letter-spacing:3px;">
2FA-828161
</div>
<p>to <strong>+40 725 296 320</strong></p>
<a href="whatsapp://send?phone=40725296320&text=2FA-828161"
style="display:inline-block; padding:14px 30px; background:#25D366;
color:white; font-size:18px; font-weight:bold; border-radius:8px;
text-decoration:none; margin-top:12px;">
💬 Send via WhatsApp
</a>
<p style="color:#888; font-size:13px; margin-top:12px;">
⏱ Code expires in 10 minutes - no typing required!
</p>
</div>
Complete PHP Example
Copy-paste ready. Replace YOUR_BUSINESS_ID and 'your-secret-key' with the values from wappsync.com/mybusiness.
<?php
$businessid = YOUR_BUSINESS_ID; // from wappsync.com/mybusiness
$businesskey = 'your-secret-key'; // from wappsync.com/mybusiness - keep server-side
// ── Step 1: Request reverse OTP ──
$businessref = bin2hex(random_bytes(4));
$phone = $_POST['phone']; // user's phone from your form
$ts = time();
$hmac = hash_hmac('sha256',
"{$businessid}|{$businessref}|{$phone}|{$ts}",
$businesskey
);
$response = callAPI('https://wappsync.com/phone-verify/api/getReverseOTP', [
'businessid' => $businessid,
'businessref' => $businessref,
'phone' => $phone,
'ts' => $ts,
'hmac' => $hmac,
]);
$data = json_decode($response, true);
$code = $data['reverseotp_display']; // "2FA-828161"
$waphone = $data['waphone']; // "+40725296320"
$logincode = $data['logincode'];
$in_ts = $data['in_ts'];
// ── Show the code to your user (render frontend example above) ──
// ── Step 2: Poll until verified ──
$maxAttempts = 40; // ~2 minutes at 3s intervals
$attempt = 0;
do {
sleep(3);
$attempt++;
$hmac = hash_hmac('sha256',
"{$businessid}|{$businessref}|{$logincode}|{$in_ts}",
$businesskey
);
$response = callAPI(
'https://wappsync.com/phone-verify/api/getReverseOTPStatus',
[
'businessid' => $businessid,
'businessref' => $businessref,
'logincode' => $logincode,
'in_ts' => $in_ts,
'hmac' => $hmac,
]
);
$data = json_decode($response, true);
} while (($data['auth'] ?? 0) !== 1 && $attempt < $maxAttempts);
if (($data['auth'] ?? 0) === 1) {
// ✅ Verified - log the user in!
$phone = decryptPhoneAES($data['phoneEnc'], $businesskey);
$_SESSION['user_phone'] = $phone;
header('Location: /dashboard');
exit;
} else {
echo "Verification timed out. Please try again.";
}
// ── Helper ──
function callAPI($url, $payload) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
verify_sender: send phone => '' (and optionally mode => 'verify_sender') - no WhatsApp push happens, so relay reverseotp_display to your client yourself. On auth=1, phoneEnc decrypts to the certified sender number; there is no sender_matches_intended to check.
accept_sender: for confirm_phone, add 'on_mismatch' => 'accept_sender'. If auth=1 and sender_matches_intended is false, the certified number differs from the one you asked for - accept it as the corrected number, or request a fresh OTP for the number you actually need.
Error Handling
The API returns "status": "error" with a human-readable message:
| HTTP | message | When |
|---|---|---|
| 400 | Missing required parameters | You forgot a required field |
| 400 | A phone is required in confirm_phone mode | confirm_phone requested without a phone |
| 400 | Invalid mode / Invalid on_mismatch | Unknown enum value |
| 400 | The phone number is invalid or not a mobile number. | Bad phone format |
| 403 | Unknown or invalid business ID | Wrong businessid |
| 403 | Invalid HMAC | Signature doesn't match - check your signing string |
| 409 | This request was already used. | Duplicate businessref+in_ts |
Security
| 🔐 HMAC-signed | Every request and response is verified with your businesskey |
| 📱 Mode-aware phone matching | confirm_phone verifies the sender matches the number you requested; verify_sender / accept_sender certify only the actual sender - never a number that did not speak |
| 🎯 Unique live codes | No two outstanding codes share a value, so an inbound code maps deterministically to one request |
| 🔂 Single-use | Codes go 0→1→2, never reusable |
| ⏱ Time-limited | Codes expire after 10 minutes, max 4 delivery attempts |
| 🔒 No client secrets | Your businesskey stays on your server; never in browser code |
| 🛡 AES-256 encrypted | Phone numbers in responses are encrypted with your businesskey |
Try It Live
🎮 Demo Login Flow - a working demo showing the complete user experience.
🧪 API Testing Tool - requires your own businessid and businesskey from wappsync.com/mybusiness.