Коды подтверждения (OTP) в WhatsApp вместо SMS
Код подтверждения при регистрации, входе или смене телефона обычно отправляют по SMS. WhatsApp — удобная альтернатива: пользователь видит код в привычном приложении, а вы платите за номер, а не за каждое сообщение. Но у схемы есть честные ограничения, и о них лучше знать до запуска.
Ниже — как устроена отправка кодов через wapi.kz, где она подходит, где нет, и пример сервера на Node.js с хешированием и лимитами.
Как это устроено
wapi.kz только доставляет сообщение. Код генерирует и проверяет ваш сервер — так безопаснее и проще.
Шаг 1. Сгенерируйте код
6 цифр из криптографически стойкого генератора — в Node.js это crypto.randomInt, в Python — модуль secrets. Не используйте Math.random и не делайте код из времени или номера телефона.
Шаг 2. Сохраните хеш и срок жизни
Храните не сам код, а его хеш с секретом (HMAC), время истечения — 5 минут — и счётчик попыток. Утечка базы тогда не выдаст действующие коды.
Шаг 3. Проверьте номер и отправьте
Перед отправкой спросите POST /contacts/check, есть ли у номера WhatsApp. Есть — отправляйте через POST /messages/send. Нет или отправка не прошла — уходите на резервный канал, обычно SMS.
Шаг 4. Проверьте введённый код
Посчитайте хеш введённого кода и сравните с сохранённым за постоянное время — в Node.js это crypto.timingSafeEqual. После 5 неверных попыток код аннулируйте, после успешной проверки — удаляйте: код одноразовый.
Шаг 5. Ограничьте частоту запросов
Не больше одного кода в минуту и трёх за 15 минут на номер, не больше десятка в час с одного IP. Без этого форма входа превращается в инструмент для «SMS-бомбинга» — о нём ниже.
Текст сообщения
482913 — код для входа в «Ваш сервис». Никому его не сообщайте. Код действует 5 минут.Ставьте код в начало: он виден прямо в уведомлении, открывать чат не нужно. Назовите сервис, предупредите «никому не сообщайте», укажите срок. Ссылок не добавляйте: сообщение с кодом и ссылкой похоже на фишинг.
Плюсы и честные минусы
Плюсы:
- Цена. Номер стоит 9 900 ₸ в месяц, сообщения не тарифицируются. Например, 3 000 кодов в месяц стоят те же 9 900 ₸ (цены).
- Доставка видна. Вебхук
message.ackскажет, доставлено и прочитано ли сообщение. - Привычно. Пользователь получает код там же, где переписывается каждый день.
Минусы, о которых молчать нельзя:
- WhatsApp есть не у всех, а телефон пользователя может быть не в сети. Резервный канал — SMS — нужен всегда.
- Почти каждый код — первое сообщение новому человеку. Такие сообщения ограничены: в первые 3 дня после подключения номера — 20 новых контактов в сутки, на уровне «Хороший» — 150, и не чаще одного раза в 5 секунд (лимиты).
- Коды выглядят как рассылка. Если один и тот же текст за час ушёл 25+ новым людям или за час появилось 40+ новых контактов, первые сообщения новым людям приостанавливаются на 24 часа. Коды различаются только цифрами, и защита считает такие тексты одинаковыми — пик регистраций после рекламы упрётся в неё быстро.
- На коды не отвечают. Индекс доверия учитывает, отвечают ли люди на ваши первые сообщения и кто начинает диалоги. Поток кодов незнакомым людям его снижает, а вместе с ним падают лимиты.
- Риск блокировки. Большой поток сообщений незнакомым людям сильнее всего повышает риск блокировки номера. Гарантий «без блокировок» нет ни у кого.
- Коды видны на телефоне номера и хранятся в журнале сообщений без срока. Хранение текстов можно выключить в настройках аккаунта — оно выключится для всех сообщений.
Вывод: коды в WhatsApp через подключённый номер подходят для небольшого потока — десятки новых пользователей в день плюс повторные входы тех, кому вы уже писали. Для больших объёмов нужен официальный WhatsApp Business API с шаблоном категории «аутентификация» — раскидывать коды по нескольким номерам ради обхода лимитов не стоит, это лишь умножает риск блокировки: дольше подключение, Meta берёт плату за каждое доставленное шаблонное сообщение, зато при соблюдении правил риск блокировки ниже. Сравнение — в гайде официальный или неофициальный API.
Обратная схема: пользователь пишет первым
Если поток растёт, переверните схему. Сайт показывает короткий код и ссылку вида https://wa.me/77010000000?text=482913 — по ней открывается WhatsApp с готовым текстом на ваш номер. Пользователь нажимает «отправить», ваш вебхук получает событие message с номером отправителя и текстом, сервер сверяет код и подтверждает этот телефон.
Диалог начинает сам пользователь, поэтому лимит новых контактов не расходуется, а индекс доверия не страдает. Минус — лишний шаг для пользователя. Если в событии нет телефона, например номер скрыт, отправьте такого пользователя на обычную схему.
Пример на Node.js
Express-сервер с двумя адресами: /otp/send и /otp/verify. Коды и счётчики здесь в памяти; в продакшене храните их в Redis или базе.
// otp.mjs — npm i express; OTP_SECRET и WAPI_KEY — в переменных окружения; node otp.mjs
import express from "express";
import crypto from "node:crypto";
const app = express();
app.use(express.json());
app.set("trust proxy", 1); // за прокси: req.ip — адрес клиента
const WAPI = "https://wapi.kz/api/v1";
const TTL = 5 * 60_000, MAX_ATTEMPTS = 5;
const codes = new Map(); // телефон → { hash, expires, attempts }
const hits = new Map(); // ключ лимита → время запросов
const hmac = (phone, code) => crypto.createHmac("sha256", process.env.OTP_SECRET).update(`${phone}:${code}`).digest();
const digits = (v) => String(v ?? "").replace(/\D/g, "");
function limited(key, max, windowMs) {
const now = Date.now();
const list = (hits.get(key) ?? []).filter((t) => now - t < windowMs);
list.push(now);
hits.set(key, list);
return list.length > max;
}
async function wapi(path, body) {
const r = await fetch(WAPI + path, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.WAPI_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
signal: AbortSignal.timeout(15_000),
});
return { status: r.status, data: await r.json().catch(() => ({})) };
}
async function sendSms(phone, text) {
throw new Error("Подключите резервный SMS-канал"); // здесь — ваш SMS-провайдер
}
app.post("/otp/send", async (req, res) => {
const phone = digits(req.body?.phone);
if (!/^7\d{10}$/.test(phone)) return res.status(400).json({ error: "Номер в формате 7XXXXXXXXXX" });
if (limited(`ip:${req.ip}`, 10, 3_600_000) || limited(`phone:${phone}`, 3, 900_000) || limited(`gap:${phone}`, 1, 60_000)) {
return res.status(429).json({ error: "Слишком много запросов, попробуйте позже" });
}
const code = String(crypto.randomInt(0, 1_000_000)).padStart(6, "0");
codes.set(phone, { hash: hmac(phone, code), expires: Date.now() + TTL, attempts: 0 });
const text = `${code} — код для входа в «Ваш сервис». Никому его не сообщайте. Код действует 5 минут.`;
try {
const check = await wapi("/contacts/check", { phone });
if (check.data.exists) {
const sent = await wapi("/messages/send", { to: phone, text });
if (sent.status === 200) return res.json({ ok: true, channel: "whatsapp" });
console.warn("wapi:", sent.data.code); // код ошибки, не OTP: new_contacts_limit, spam_detected…
}
await sendSms(phone, text);
res.json({ ok: true, channel: "sms" });
} catch {
res.status(503).json({ error: "Не удалось отправить код, попробуйте позже" });
}
});
app.post("/otp/verify", (req, res) => {
const phone = digits(req.body?.phone);
const code = String(req.body?.code ?? "");
const rec = codes.get(phone);
if (!rec || rec.expires < Date.now()) {
codes.delete(phone);
return res.status(400).json({ error: "Код устарел, запросите новый" });
}
if (++rec.attempts > MAX_ATTEMPTS) {
codes.delete(phone);
return res.status(429).json({ error: "Слишком много попыток, запросите новый код" });
}
if (!/^\d{6}$/.test(code) || !crypto.timingSafeEqual(rec.hash, hmac(phone, code))) {
return res.status(400).json({ error: "Неверный код" });
}
codes.delete(phone); // код одноразовый
res.json({ ok: true }); // дальше — ваша сессия или токен
});
app.listen(3000);Защита от перебора и SMS-бомбинга
Перебор. У 6-значного кода миллион вариантов. Пять попыток на код и не больше трёх кодов за 15 минут на номер делают угадывание практически безнадёжным. Новый код заменяет старый — подбирать его заново придётся с нуля.
SMS-бомбинг. Злоумышленник вводит в форму чужой номер снова и снова: жертва получает поток сообщений, вы тратите деньги на SMS, а номер WhatsApp — лимиты и индекс доверия. Защищают лимиты на номер и IP, капча при подозрительной активности и общий суточный потолок кодов.
Гигиена. Не пишите коды в логи, держите OTP_SECRET и API-ключ на сервере — никогда в браузере или мобильном приложении. Кто получит ключ, сможет писать с вашего номера. Если ключ утёк, перевыпустите его в кабинете.
Частые вопросы
wapi.kz сам генерирует и проверяет коды?
Нет. Сервис доставляет сообщение, а код создаёт и проверяет ваш сервер — как в примере выше. Так код не покидает вашу систему иначе как в тексте сообщения.
Сколько стоит отправка кода?
Отдельной платы за сообщения нет: 9 900 ₸ в месяц за номер, оплата с баланса по дням. Ограничивают не деньги, а лимиты номера на первые сообщения новым людям.
Что, если у пользователя нет WhatsApp?
/contacts/check вернёт exists: false — отправляйте код по SMS. Резервный канал нужен в любом случае: телефон пользователя может быть не в сети.
Не заблокируют ли номер из-за кодов?
Риск есть, и он растёт с потоком кодов незнакомым людям. Лимиты сервиса его снижают, но не убирают. Для больших объёмов честнее официальный API с шаблоном аутентификации — подробнее в гайде как снизить риск блокировки.
Как попробовать?
Зарегистрируйтесь, подключите номер по QR-коду и отправьте код себе. Бесплатного теста нет, но пополнение начинается от 1 000 ₸, а номер списывается по дням — 330 ₸ в сутки.
Подключите WhatsApp к своим системам
9 900 ₸ в месяц за номер, без платы за сообщения. Сервер в Казахстане, оплата Kaspi или счётом на ТОО/ИП. Подключение — около 5 минут.