WappSync
A

WappSync Phone Verify - API Docs

Phone verification where your user sends a code to us via WhatsApp - no OTP typing, one tap, fully verified.

How It Works

1

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.

2

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.

3

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:

CredentialDescription
businessidYour numeric business ID
businesskeyYour secret key - never share this, never put it in client-side code
Important: All API calls must come from your backend. The 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 senda phone number Nnothing (no number)
What gets certifiedthat the caller controls N specificallywhichever number actually sends the code back
Typical uselogin / recovery on an on-file number, anti-SIM-swapfresh signup - "give me a real, proven phone for this client"
Code deliveryWhatsApp-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 resultsender_matches_intended tells you whether the sender is NphoneEnc is the certified number; sender_matches_intended is omitted
Security: 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)

FieldTypeDescription
businessidintYour business ID
businessrefstringA unique reference you generate for this auth request (e.g., bin2hex(random_bytes(4)))
phonestringThe user's phone number in international format (+40755123456). Required in confirm_phone mode; omit/empty for verify_sender.
modestringOptional - confirm_phone (default when a phone is supplied) or verify_sender (default when phone is omitted)
on_mismatchstringOptional (confirm_phone only) - reject (default) or accept_sender (certify the actual sender if the code arrives from a different number)
tsintCurrent Unix timestamp
hmacstringHMAC-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

FieldTypeDescription
statusstring"ok" or "error"
reverseotp_displaystringThe code your user sends, e.g. "2FA-828161"
waphonestringThe WhatsApp number your user sends the code TO
logincodestringInternal code to use when polling
in_tsintTimestamp for the polling request
out_tsintResponse timestamp
otp_valid_minutesintHow long the code is valid (default 10)
modestringThe active mode: confirm_phone or verify_sender
deliverystringpushed_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)
hmacstringHMAC 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)

FieldTypeDescription
businessidintYour business ID
businessrefstringThe same reference from the previous call
logincodestringThe logincode from the getReverseOTP response
in_tsintThe original ts from the getReverseOTP request
hmacstringHMAC-SHA256 of {businessid}|{businessref}|{logincode}|{in_ts}

Response

FieldTypeMeaning
used0⏳ Still waiting - poll again
used1✅ Just verified - the first poll to see this consumes it
used2🔒 Already consumed by a previous poll
auth1User is verified - log them in!
phoneEncstringAES-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)
triesintFailed attempts counted (wrong code or wrong phone)
expiredbooltrue when too many failed attempts (tries ≥ 4) - request a new OTP
modestringThe mode of this request: confirm_phone or verify_sender
sender_matches_intendedboolconfirm_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_tsintResponse timestamp
hmacstringHMAC - 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:

HTTPmessageWhen
400Missing required parametersYou forgot a required field
400A phone is required in confirm_phone modeconfirm_phone requested without a phone
400Invalid mode / Invalid on_mismatchUnknown enum value
400The phone number is invalid or not a mobile number.Bad phone format
403Unknown or invalid business IDWrong businessid
403Invalid HMACSignature doesn't match - check your signing string
409This request was already used.Duplicate businessref+in_ts

Security

🔐 HMAC-signedEvery request and response is verified with your businesskey
📱 Mode-aware phone matchingconfirm_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 codesNo two outstanding codes share a value, so an inbound code maps deterministically to one request
🔂 Single-useCodes go 0→1→2, never reusable
⏱ Time-limitedCodes expire after 10 minutes, max 4 delivery attempts
🔒 No client secretsYour businesskey stays on your server; never in browser code
🛡 AES-256 encryptedPhone 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.