Sending and growing
Send login codes on WhatsApp (OTP)
Customers log in to your website with a code on WhatsApp instead of a password or an SMS. Your website makes two calls: one sends the code, one checks what the customer typed. Part of the Pro plan.
Step 1: What you need
The Pro plan and a connected WhatsApp number. Open Settings → WhatsApp OTP: Sendmint creates your OTP message (an Authentication template) and sends it to WhatsApp for approval, usually within minutes. When it says Approved by WhatsApp, you can send codes.
Each code is a WhatsApp Authentication message: Meta charges ₹0.115 + GST per code from your payment method in Meta (how to add it). Sendmint adds nothing.
Settings → WhatsApp OTP: the OTP message, approved Step 2: Create an API key
In Settings → WhatsApp OTP, click New key, give it a name (for example “Main website”) and copy it. The key is shown only once. Keep it on your server, in a setting or an environment variable, never in a page or in JavaScript that runs in the browser. If a key gets out, click Revoke and create a new one.
New key: copy it now, it's shown once Step 3: Send a code
When the customer types their number, your server calls POST https://sendmint.in/api/v1/otp/send with the number and your key. Numbers without a country code get +91. Sendmint makes a 6-digit code and sends it on WhatsApp, and answers with a requestId (keep it on your server, for example in the session) and expiresIn: the code works for 5 minutes.
A new code for the same number can be sent after 30 seconds; it replaces the old one.
cURL
# 1. Send the code curl -X POST https://sendmint.in/api/v1/otp/send \ -H "Authorization: Bearer $SENDMINT_KEY" -H "Content-Type: application/json" \ -d '{"phone": "+919876543210"}' # → {"requestId": "…", "expiresIn": 300} # 2. Check the code the customer typed curl -X POST https://sendmint.in/api/v1/otp/check \ -H "Authorization: Bearer $SENDMINT_KEY" -H "Content-Type: application/json" \ -d '{"requestId": "…", "code": "482913"}' # → {"valid": true}Send: the number in, a requestId out Step 4: Check the code
When the customer types the code, your server calls POST https://sendmint.in/api/v1/otp/check with the requestId and the code. The answer is {"valid": true} once for the right code. Otherwise valid is false and reason says why: wrong_code, expired, too_many_tries (after 5 wrong codes: send a new one), already_used or not_found.
PHP
// 1. Send the code $res = wp_remote_post("https://sendmint.in/api/v1/otp/send", [ "headers" => ["Authorization" => "Bearer " . SENDMINT_KEY, "Content-Type" => "application/json"], "body" => json_encode(["phone" => $phone]), ]); $requestId = json_decode(wp_remote_retrieve_body($res))->requestId; // 2. Check the code the customer typed $res = wp_remote_post("https://sendmint.in/api/v1/otp/check", [ "headers" => ["Authorization" => "Bearer " . SENDMINT_KEY, "Content-Type" => "application/json"], "body" => json_encode(["requestId" => $requestId, "code" => $code]), ]); $valid = json_decode(wp_remote_retrieve_body($res))->valid; // true or falseJavaScript
// 1. Send the code let res = await fetch("https://sendmint.in/api/v1/otp/send", { method: "POST", headers: { Authorization: `Bearer ${process.env.SENDMINT_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ phone }), }); const { requestId } = await res.json(); // 2. Check the code the customer typed res = await fetch("https://sendmint.in/api/v1/otp/check", { method: "POST", headers: { Authorization: `Bearer ${process.env.SENDMINT_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ requestId, code }), }); const { valid } = await res.json(); // true or falseCheck: valid is true or false Step 5: WordPress: a login form with [sendmint_otp_login]
Put your key in wp-config.php as shown in the first line, then paste the rest into your theme's functions.php or a code snippets plugin. Add the shortcode [sendmint_otp_login] to any page: it shows a number field, sends the code and logs in the WordPress user whose profile phone (the “phone” or WooCommerce “billing_phone” field) matches. Your theme may show the form differently.
PHP (functions.php)
<?php // wp-config.php: define('SENDMINT_KEY', 'sm_live_…'); (never in a page or in JavaScript) function sendmint_otp_call($path, $body) { $res = wp_remote_post("https://sendmint.in/api/v1/otp/" . $path, [ "headers" => ["Authorization" => "Bearer " . SENDMINT_KEY, "Content-Type" => "application/json"], "body" => wp_json_encode($body), "timeout" => 15, ]); if (is_wp_error($res)) return [0, null]; return [wp_remote_retrieve_response_code($res), json_decode(wp_remote_retrieve_body($res))]; } function sendmint_otp_find_user($phone) { foreach (["phone", "billing_phone"] as $key) { $users = get_users(["meta_key" => $key, "meta_value" => $phone, "number" => 1]); if ($users) return $users[0]; } return null; } add_shortcode("sendmint_otp_login", function () { if (is_user_logged_in()) return "<p>You are logged in.</p>"; $message = ""; $step = "phone"; if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["sendmint_otp"]) && wp_verify_nonce($_POST["sendmint_otp"], "sendmint_otp")) { $phone = sanitize_text_field(wp_unslash($_POST["phone"] ?? "")); if (isset($_POST["code"])) { $code = preg_replace("/\D/", "", wp_unslash($_POST["code"])); $requestId = get_transient("sendmint_otp_" . md5($phone)); [$status, $data] = sendmint_otp_call("check", ["requestId" => $requestId, "code" => $code]); $user = ($status === 200 && $data && $data->valid) ? sendmint_otp_find_user($phone) : null; if ($user) { delete_transient("sendmint_otp_" . md5($phone)); wp_set_auth_cookie($user->ID, true); wp_safe_redirect(home_url()); exit; } $message = "That code is not right, or it has expired."; $step = "code"; } else { [$status, $data] = sendmint_otp_call("send", ["phone" => $phone]); if ($status === 200 && $data) { set_transient("sendmint_otp_" . md5($phone), $data->requestId, 5 * MINUTE_IN_SECONDS); $message = "We sent a code to your WhatsApp."; $step = "code"; } else { $message = ($data && isset($data->error)) ? $data->error : "We couldn't send a code. Please try again."; } } } ob_start(); ?> <form method="post" class="sendmint-otp-login"> <?php wp_nonce_field("sendmint_otp", "sendmint_otp"); ?> <?php if ($message) echo "<p>" . esc_html($message) . "</p>"; ?> <p><label>WhatsApp number<br> <input type="tel" name="phone" required value="<?php echo esc_attr($_POST["phone"] ?? ""); ?>"></label></p> <?php if ($step === "code") { ?> <p><label>Code<br><input type="text" name="code" inputmode="numeric" autocomplete="one-time-code" maxlength="6" required></label></p> <p><button type="submit">Log in</button></p> <?php } else { ?> <p><button type="submit">Send code on WhatsApp</button></p> <?php } ?> </form> <?php return ob_get_clean(); });The [sendmint_otp_login] form on a WordPress page Step 6: Node.js: two routes for your login page
An Express example with sessions: one route sends the code, one checks it. The requestId stays in the session, never in the page. Put your key in the SENDMINT_KEY environment variable.
JavaScript (Node.js)
// npm install express express-session // SENDMINT_KEY=sm_live_… SESSION_SECRET=… node server.js import express from "express"; import session from "express-session"; const app = express(); app.use(express.json()); app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false })); async function sendmint(path, body) { const res = await fetch(`https://sendmint.in/api/v1/otp/${path}`, { method: "POST", headers: { Authorization: `Bearer ${process.env.SENDMINT_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify(body), }); return { status: res.status, data: await res.json() }; } // Your login page posts the number here. The requestId stays in the session, never in the page. app.post("/login/send-code", async (req, res) => { const { status, data } = await sendmint("send", { phone: req.body.phone }); if (status !== 200) return res.status(status).json({ error: data.error }); req.session.otpRequestId = data.requestId; req.session.otpPhone = req.body.phone; res.json({ sent: true, expiresIn: data.expiresIn }); }); app.post("/login/check-code", async (req, res) => { const { data } = await sendmint("check", { requestId: req.session.otpRequestId, code: req.body.code }); if (!data.valid) return res.status(401).json({ error: "That code is not right, or it has expired." }); delete req.session.otpRequestId; // Log the customer in here: find your user by req.session.otpPhone. res.json({ loggedIn: true }); }); app.listen(3000);Node.js: send-code and check-code routes
Common problems
401: “The API key is missing, wrong or revoked”
Send the header Authorization: Bearer followed by your key, from your server. Check that the key wasn't revoked in Settings → WhatsApp OTP.
403: the plan or a paused account
WhatsApp OTP is part of the Pro plan, and a paused account can't send codes. See Settings → Plan and billing.
409: “Your OTP message is not approved by WhatsApp yet”
Open Settings → WhatsApp OTP and check the status of the OTP message. If your number is not connected, connect it first (how).
429: too many codes
A new code for the same number only after 30 seconds (the answer says how long: retryAfter), and a few codes per number an hour. Show the customer a “Send again” button that waits.
502: WhatsApp did not accept the code
The number may not be on WhatsApp, or Meta refused the message (for example a missing payment method in Meta). Ask the customer to check the number.
Still stuck? Book a free setup call and we'll connect it to your website with you.
Book a free setup callWas this helpful? ·