Интеграция OTP через WhatsApp
Документация для подключения вашего приложения к сервису Cascade.
Базовый URL: https://otp.kztusdt.kz/api
Содержание
- Общая схема
- Получение API-токена
- Авторизация
- Формат номера телефона
- Отправка OTP
- Короткие ссылки
- Проверка OTP
- Коды ответов и ошибки
- Ограничения
- Примеры интеграции
- Рекомендации
- Исходящие webhooks
Общая схема
┌─────────────────┐ POST /otp/send ┌──────────────────┐
│ Ваше приложение │ ──────────────────────► │ otp.kztusdt.kz │
│ (сайт, API) │ │ Laravel API │
└────────┬────────┘ └────────┬─────────┘
│ │
│ Пользователь вводит код │ WhatsApp
│ ▼
│ ┌──────────────────┐
│ POST /otp/verify │ Номер клиента │
└───────────────────────────────► └──────────────────┘
Типичный сценарий:
- Пользователь вводит номер телефона в вашем приложении.
- Ваш бэкенд вызывает
POST /api/otp/send. - Клиент получает код в WhatsApp на указанный номер.
- Пользователь вводит код у вас в форме.
- Ваш бэкенд вызывает
POST /api/otp/verify. - При успехе — подтверждаете аккаунт / вход / операцию.
Получение API-токена
- Войдите в кабинет компании: https://otp.kztusdt.kz/cabinet
- Откройте раздел API-ключи
- Нажмите Создать
- Укажите название (например,
Мой сайт) - Скопируйте ключ — его можно снова посмотреть в списке (кнопка «Показать»)
Токен храните только на сервере (переменные окружения, secrets). Не вставляйте в фронтенд, мобильное приложение или публичный репозиторий.
Авторизация
Все запросы к OTP API требуют заголовок:
Authorization: Bearer <ВАШ_API_ТОКЕН>
Content-Type: application/json
Accept: application/json
При неверном или отсутствующем токене сервер вернёт 401 Unauthorized:
{
"success": false,
"message": "Invalid API token"
}
Формат номера телефона
Поле phone — строка, только цифры или с символами форматирования (они будут удалены).
| Ввод | Нормализованный вид |
|---|---|
+7 700 123 45 67 |
77001234567 |
87001234567 |
77001234567 |
77001234567 |
77001234567 |
Правила:
- Длина после нормализации: 10–15 цифр
- Номер с
8в начале (11 цифр, Казахстан) автоматически заменяется на7…
Отправка OTP
Запрос
POST /api/otp/send
Тело (JSON):
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
phone |
string | да | Номер телефона получателя |
purpose |
string | нет | Назначение кода (по умолчанию verification) |
channel |
string | нет | whatsapp, telegram, sms или auto. Если не указан — берётся канал по умолчанию из кабинета (Откуда отправлять). auto равномерно распределяет нагрузку между номерами WhatsApp и Telegram; если Telegram не отправил — повтор через WhatsApp. Канал, выключенный в настройках компании, вернёт ошибку. |
link |
string | нет | Полный http/https URL — будет укорочен и добавлен в то же сообщение |
link_expires_in |
integer | нет | Срок жизни короткой ссылки в секундах (60–2592000) |
Пример:
{
"phone": "77001234567",
"purpose": "registration",
"link": "https://example.com/confirm/abc"
}
Поле purpose позволяет разделять коды для разных сценариев (регистрация, вход, сброс пароля). При проверке нужно передать тот же purpose.
Если передан link, сервис создаёт короткую ссылку вида https://otp.kztusdt.kz/s/Ab3xK9 и подставляет её в текст сообщения вместе с кодом. Стоимость одной доставки: WhatsApp/Telegram = 1 токен, SMS = 16 токенов (код+ссылка вместе не удваивают стоимость).
Успешный ответ — 200 OK
{
"success": true,
"message": "OTP отправлен через WhatsApp",
"expires_in": 300,
"short_url": "https://otp.kztusdt.kz/s/Ab3xK9"
}
| Поле | Описание |
|---|---|
expires_in |
Время жизни кода в секундах (по умолчанию 300 = 5 минут) |
short_url |
Короткая ссылка (только если передан link) |
Ошибки — 422 Unprocessable Entity
{
"success": false,
"message": "Неверный формат номера телефона"
}
{
"success": false,
"message": "WhatsApp не подключён. Обратитесь к администратору."
}
{
"success": false,
"message": "Подождите перед повторной отправкой OTP"
}
{
"success": false,
"message": "Не удалось отправить OTP через WhatsApp"
}
Короткие ссылки
Короткие ссылки работают на том же домене: https://otp.kztusdt.kz/s/{slug} → 302 на целевой URL. Клики считаются. Неактивные и просроченные ссылки отдают 404.
Управление — в кабинете компании (Короткие ссылки): создание, копирование, клики, срок, вкл/выкл.
Укоротить без отправки — бесплатно
POST /api/links/shorten
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
url |
string | да | Целевой http/https URL |
alias |
string | нет | Свой slug (3–32: буквы, цифры, -, _) |
title |
string | нет | Название для кабинета |
expires_in |
integer | нет | Срок жизни в секундах |
{
"url": "https://example.com/very/long/path",
"alias": "promo-may"
}
{
"success": true,
"short_url": "https://otp.kztusdt.kz/s/promo-may",
"slug": "promo-may",
"expires_at": null
}
Отправить ссылку без кода — 1 токен
POST /api/links/send
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
phone |
string | да | Номер получателя |
url |
string | да | Целевой http/https URL |
channel |
string | нет | whatsapp, telegram, sms или auto (должен быть включён / допустим в настройках компании) |
text |
string | нет | Текст с плейсхолдером :link (иначе шаблон по умолчанию) |
expires_in |
integer | нет | Срок жизни короткой ссылки |
{
"phone": "77001234567",
"url": "https://example.com/promo",
"text": "Ваша скидка: :link"
}
{
"success": true,
"message": "Ссылка отправлена через WhatsApp",
"short_url": "https://otp.kztusdt.kz/s/Ab3xK9",
"slug": "Ab3xK9"
}
Биллинг: WhatsApp и Telegram списывают 1 токен за доставку, SMS — 16 токенов. В режиме auto сначала выбирается наименее загруженный номер WhatsApp/Telegram; SMS используется только если мессенджеры недоступны (и SMS включён, баланс ≥ 16). Укорачивание без отправки бесплатно.
Проверка OTP
Запрос
POST /api/otp/verify
Тело (JSON):
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
phone |
string | да | Тот же номер, что при отправке |
code |
string | да | Код из WhatsApp |
purpose |
string | нет | То же значение, что при send (по умолчанию verification) |
Пример:
{
"phone": "77001234567",
"code": "482910",
"purpose": "registration"
}
Успешный ответ — 200 OK
{
"success": true,
"message": "Номер подтверждён"
}
После успешной проверки код одноразовый — повторное использование того же кода вернёт ошибку.
Ошибка — 422 Unprocessable Entity
{
"success": false,
"message": "Неверный или просроченный код"
}
Коды ответов и ошибки
| HTTP | Значение |
|---|---|
200 |
Успех (success: true) |
401 |
Нет или неверный API-токен |
422 |
Ошибка бизнес-логики (success: false) |
422 |
Ошибка валидации полей (Laravel validation) |
Валидация (пример):
{
"message": "The phone field is required.",
"errors": {
"phone": ["The phone field is required."]
}
}
Всегда проверяйте поле success в теле ответа, а не только HTTP-статус.
Ограничения
| Параметр | Значение по умолчанию |
|---|---|
| Длина кода | 6 цифр |
| Срок действия | 300 сек (5 мин) |
| Повторная отправка на тот же номер | не чаще 1 раза в 60 сек |
Сообщение в WhatsApp (шаблон):
Ваш код подтверждения: 123456. Действителен 5 мин. Не сообщайте код никому.
Примеры интеграции
cURL
# Отправить OTP
curl -X POST "https://otp.kztusdt.kz/api/otp/send" \
-H "Authorization: Bearer ВАШ_API_ТОКЕН" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"phone":"77001234567","purpose":"registration"}'
# Проверить код
curl -X POST "https://otp.kztusdt.kz/api/otp/verify" \
-H "Authorization: Bearer ВАШ_API_ТОКЕН" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"phone":"77001234567","code":"482910","purpose":"registration"}'
PHP (Laravel / Guzzle)
<?php
declare(strict_types=1);
use Illuminate\Support\Facades\Http;
final class OtpClient
{
public function __construct(
private readonly string $baseUrl = 'https://otp.kztusdt.kz/api',
private readonly string $token = '', // из env('OTP_API_TOKEN')
) {}
public function send(string $phone, string $purpose = 'verification'): array
{
$response = Http::withToken($this->token)
->acceptJson()
->post("{$this->baseUrl}/otp/send", [
'phone' => $phone,
'purpose' => $purpose,
]);
return $response->json();
}
public function verify(string $phone, string $code, string $purpose = 'verification'): array
{
$response = Http::withToken($this->token)
->acceptJson()
->post("{$this->baseUrl}/otp/verify", [
'phone' => $phone,
'code' => $code,
'purpose' => $purpose,
]);
return $response->json();
}
}
// Использование
$otp = new OtpClient(token: config('services.otp.token'));
$result = $otp->send('77001234567', 'registration');
if (! ($result['success'] ?? false)) {
throw new RuntimeException($result['message'] ?? 'OTP send failed');
}
$check = $otp->verify('77001234567', '482910', 'registration');
if ($check['success'] ?? false) {
// номер подтверждён — создайте сессию / активируйте пользователя
}
JavaScript (Node.js / fetch)
const BASE_URL = 'https://otp.kztusdt.kz/api';
const API_TOKEN = process.env.OTP_API_TOKEN;
async function sendOtp(phone, purpose = 'verification') {
const res = await fetch(`${BASE_URL}/otp/send`, {
method: 'POST',
headers: {
Authorization: `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({ phone, purpose }),
});
return res.json();
}
async function verifyOtp(phone, code, purpose = 'verification') {
const res = await fetch(`${BASE_URL}/otp/verify`, {
method: 'POST',
headers: {
Authorization: `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({ phone, code, purpose }),
});
return res.json();
}
// Пример
const sent = await sendOtp('77001234567', 'login');
if (!sent.success) {
console.error(sent.message);
}
const verified = await verifyOtp('77001234567', '123456', 'login');
if (verified.success) {
console.log('OK');
}
Python
import os
import requests
BASE_URL = "https://otp.kztusdt.kz/api"
TOKEN = os.environ["OTP_API_TOKEN"]
headers = {
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
}
def send_otp(phone: str, purpose: str = "verification") -> dict:
r = requests.post(
f"{BASE_URL}/otp/send",
json={"phone": phone, "purpose": purpose},
headers=headers,
timeout=30,
)
return r.json()
def verify_otp(phone: str, code: str, purpose: str = "verification") -> dict:
r = requests.post(
f"{BASE_URL}/otp/verify",
json={"phone": phone, "code": code, "purpose": purpose},
headers=headers,
timeout=30,
)
return r.json()
Рекомендации
Безопасность
- Вызывайте API только с бэкенда, не из браузера напрямую.
- Не логируйте API-токен и OTP-коды в открытом виде.
- На своей стороне ограничьте число попыток ввода кода (например, 5 попыток → блокировка на 15 минут).
UX
- Показывайте таймер до повторной отправки (минимум 60 сек).
- Сообщайте пользователю, что код придёт в WhatsApp, не в SMS.
- У номера должен быть установлен WhatsApp.
Разные сценарии (purpose)
| purpose | Когда использовать |
|---|---|
registration |
Регистрация нового пользователя |
login |
Вход по номеру |
password_reset |
Сброс пароля |
verification |
Общая верификация (по умолчанию) |
Для одного номера одновременно может быть несколько активных кодов с разными purpose.
Обработка ошибок на вашей стороне
| Сообщение API | Действие в приложении |
|---|---|
WhatsApp не подключён |
Показать «Сервис временно недоступен», уведомить админа |
Подождите перед повторной отправкой |
Показать кнопку «Отправить снова» с таймером |
Неверный или просроченный код |
Предложить ввести код заново или запросить новый |
Неверный формат номера |
Подсветить поле телефона |
Проверка перед продакшеном
- В админке подключён WhatsApp (WhatsApp → Подключить).
- Создан и активен API-токен.
- Тестовый
send+verifyна реальный номер с WhatsApp. - В История OTP видна запись об отправке.
Исходящие webhooks
В кабинете (Интеграция → Webhooks) можно указать URL, на который сервис будет отправлять события:
| Событие | Когда |
|---|---|
otp.sent |
Код успешно отправлен |
otp.verified |
Код подтверждён |
otp.failed |
Ошибка отправки / списания |
Запрос: POST с JSON-телом. Заголовки:
Content-Type: application/json
X-Webhook-Event: otp.sent
X-Webhook-Signature: sha256=<hmac_sha256_hex_of_raw_body>
Подпись: HMAC-SHA256 от сырого тела запроса с секретом webhook (показывается один раз при создании / ротации).
Пример проверки (PHP):
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
hash_equals($expected, $request->header('X-Webhook-Signature'));
Пример payload:
{
"id": "evt_01h…",
"event": "otp.sent",
"created_at": "2026-07-21T10:00:00+00:00",
"company_id": 1,
"data": {
"otp_log_id": 42,
"phone": "77001234567",
"purpose": "verification",
"status": "sent",
"api_token_id": 3,
"session_name": "wa-1",
"expires_at": "2026-07-21T10:05:00+00:00",
"verified_at": null,
"error_message": null
}
}
Доставка асинхронная (очередь). Нужен php artisan queue:work. Повтор до 3 попыток.
Поддержка
По вопросам подключения, оплаты и работы сервиса:
- Email: support@otp.kztusdt.kz
- Телефон: +7 (777) 123-45-67