Cascade Cascade

API

Документация API

Интеграция OTP через WhatsApp

Документация для подключения вашего приложения к сервису Cascade.

Базовый URL: https://otp.kztusdt.kz/api


Содержание

  1. Общая схема
  2. Получение API-токена
  3. Авторизация
  4. Формат номера телефона
  5. Отправка OTP
  6. Короткие ссылки
  7. Проверка OTP
  8. Коды ответов и ошибки
  9. Ограничения
  10. Примеры интеграции
  11. Рекомендации
  12. Исходящие webhooks

Общая схема

┌─────────────────┐     POST /otp/send      ┌──────────────────┐
│  Ваше приложение │ ──────────────────────► │  otp.kztusdt.kz  │
│  (сайт, API)     │                         │  Laravel API     │
└────────┬────────┘                         └────────┬─────────┘
         │                                           │
         │  Пользователь вводит код                  │ WhatsApp
         │                                           ▼
         │                                 ┌──────────────────┐
         │     POST /otp/verify            │  Номер клиента   │
         └───────────────────────────────► └──────────────────┘

Типичный сценарий:

  1. Пользователь вводит номер телефона в вашем приложении.
  2. Ваш бэкенд вызывает POST /api/otp/send.
  3. Клиент получает код в WhatsApp на указанный номер.
  4. Пользователь вводит код у вас в форме.
  5. Ваш бэкенд вызывает POST /api/otp/verify.
  6. При успехе — подтверждаете аккаунт / вход / операцию.

Получение API-токена

  1. Войдите в кабинет компании: https://otp.kztusdt.kz/cabinet
  2. Откройте раздел API-ключи
  3. Нажмите Создать
  4. Укажите название (например, Мой сайт)
  5. Скопируйте ключ — его можно снова посмотреть в списке (кнопка «Показать»)

Токен храните только на сервере (переменные окружения, 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 не подключён Показать «Сервис временно недоступен», уведомить админа
Подождите перед повторной отправкой Показать кнопку «Отправить снова» с таймером
Неверный или просроченный код Предложить ввести код заново или запросить новый
Неверный формат номера Подсветить поле телефона

Проверка перед продакшеном

  1. В админке подключён WhatsApp (WhatsApp → Подключить).
  2. Создан и активен API-токен.
  3. Тестовый send + verify на реальный номер с WhatsApp.
  4. В История 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 попыток.


Поддержка

По вопросам подключения, оплаты и работы сервиса: