A booking system where anyone can type any phone number is a fake-booking machine. Drivers get dispatched to numbers that do not answer, and the business bleeds. So the limo plugin verifies phones the way apps do, a one-time code sent by SMS, typed back by the customer. Building OTP taught me one security principle above all others, and this post is honest about where the check must live.
The plugin has a dedicated table for codes, created at activation, because a code is a record with a lifespan, the phone it belongs to, the code itself, when it expires, whether it was used. Generating one is straightforward:
public function generate_otp($phone) {
global $wpdb;
$code = str_pad(wp_rand(0, 999999), 6, '0', STR_PAD_LEFT);
$expires = gmdate('Y-m-d H:i:s', time() + 5 * MINUTE_IN_SECONDS);
$wpdb->insert($this->table_otp, array(
'phone' => sanitize_text_field($phone),
'code' => $code,
'expires_at' => $expires,
'used' => 0,
));
// hand $code to the SMS sender
}
Details that matter: wp_rand instead of PHP’s plain rand, WordPress’s version draws from a stronger source, and for security codes predictability is the whole game. str_pad keeps codes like 004312 six digits instead of collapsing to 4312. And every code carries an expiry, five minutes, because a code that lives forever is a code waiting to be stolen.
Now the principle. The customer types the code into a form, and JavaScript could compare it right there in the browser. It must not, ever, and here is why in one sentence, everything in the browser belongs to the user. Anyone can open developer tools, read the JavaScript, skip the check, and submit the form as verified. Client-side verification is decoration. The real check happens in PHP, where the visitor cannot reach:
public function verify_otp($phone, $code) {
global $wpdb;
$row = $wpdb->get_row($wpdb->prepare(
'SELECT id FROM ' . $this->table_otp . '
WHERE phone = %s AND code = %s AND used = 0
AND expires_at > UTC_TIMESTAMP()',
$phone, $code
));
if (!$row) { return false; }
$wpdb->update($this->table_otp, array('used' => 1), array('id' => $row->id));
return true;
}
The query demands all four truths at once, right phone, right code, not yet used, not yet expired, through $wpdb->prepare so the input cannot inject SQL. And the moment a code passes, it is marked used, one code, one verification, no replays. The booking only proceeds when this server-side function says yes, whatever the browser claimed.
A few things people ask me about this
Why can I not verify the OTP in JavaScript? Because the visitor controls the browser. They can read your script, skip the comparison, and submit anyway. JavaScript may give instant feedback, but PHP must make the decision.
Why do OTP codes expire? A code is a temporary secret. Five minutes bounds the window in which an intercepted or guessed code is worth anything, and marking codes used prevents replaying one that worked.
Next
Verified customers were ready to pay, which meant card payments, Stripe, and the healthy fear of touching people’s money. That weight, and the payment intent flow, is the next post.

