SMS Center
Dành cho dev tích hợp

Tích hợp OTP vào app của bạn

SMS Center gửi mã OTP xác thực số điện thoại qua SIM vật lý thật (không phải Brandname). Làm theo đúng các bước dưới đây là tự tích hợp được — không cần hiểu bên trong SMS Center vận hành thế nào. Hiện chỉ hỗ trợ đúng 1 nghiệp vụ: gửi mã OTP 6 số và xác thực số điện thoại — xem mục Lộ trình ở cuối trang nếu bạn cần loại tin nhắn khác.

1Copy SDK vào project

Không có package npm/composer để cài — tải thẳng 1 file vào project, chỉnh theo convention riêng của bạn nếu cần. Bạn có thể bắt đầu code ngay với app_id tạm bất kỳ (vd tên project của bạn) — chưa cần liên hệ xin gì ở bước này.

Node.js / TypeScript (Next.js, Express...) — tải smsCenter.ts
// Client cho SMS Center (https://sms.lentrang1.com) — xem hướng dẫn tích
// hợp đầy đủ tại https://sms.lentrang1.com/tich-hop (cách xin app_id + API
// key và quy tắc tích hợp bắt buộc: verify OTP đúng 1 lần duy nhất, atomic
// với hành động thật).
//
// Copy file này thẳng vào project của bạn (vd src/lib/smsCenter.ts), rồi
// khai 3 biến môi trường:
//   SMS_CENTER_BASE_URL=https://sms.lentrang1.com
//   SMS_CENTER_APP_ID=<app_id được cấp>
//   SMS_CENTER_API_KEY=<API key được cấp, chỉ hiện 1 lần lúc tạo>

const PHONE_RE = /^0[35789]\d{8}$/;
const OTP_RE = /^[0-9]{6}$/;

export function isValidPhone(phone: string): boolean {
  return PHONE_RE.test(phone);
}

export function isValidOtpFormat(otp: string): boolean {
  return OTP_RE.test(otp);
}

function smsCenterConfig() {
  const baseUrl = process.env.SMS_CENTER_BASE_URL;
  const appId = process.env.SMS_CENTER_APP_ID;
  const apiKey = process.env.SMS_CENTER_API_KEY;
  if (!baseUrl || !appId || !apiKey) {
    throw new Error('Thiếu cấu hình SMS_CENTER_BASE_URL/SMS_CENTER_APP_ID/SMS_CENTER_API_KEY trong .env');
  }
  return {
    baseUrl,
    appId,
    headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
  };
}

export interface SmsCenterResult<T = Record<string, unknown>> {
  ok: boolean;
  status: number;
  data: T;
}

export type SendOtpData = {
  job_id?: string;
  expires_in?: number;
  retry_after?: number;
  error?: string;
};

export type VerifyOtpData = {
  verified?: boolean;
  attempts_left?: number;
  retry_after?: number;
  error?: string;
};

/** POST /api/v1/otp/send — gửi mã OTP 6 số tới `phone` (định dạng 0xxxxxxxxx). */
export async function sendOtp(phone: string): Promise<SmsCenterResult<SendOtpData>> {
  const { baseUrl, appId, headers } = smsCenterConfig();
  const res = await fetch(`${baseUrl}/api/v1/otp/send`, {
    method: 'POST',
    headers,
    body: JSON.stringify({ phone, app_id: appId }),
  });
  const data = await res.json().catch(() => ({}));
  return { ok: res.ok, status: res.status, data };
}

/**
 * POST /api/v1/otp/verify — kiểm tra mã OTP.
 *
 * QUAN TRỌNG: verify đúng sẽ xoá ngay OTP phía server (chống replay), nên
 * gọi hàm này NGAY TẠI bước thực hiện hành động thật (tạo đơn, tạo tài
 * khoản, đổi mật khẩu...) — verify xong mới ghi dữ liệu, atomic trong cùng
 * 1 request. Không tách "verify riêng rồi mới hành động" ở 2 bước khác
 * nhau, vì verify lần 2 sẽ luôn fail dù mã đúng.
 */
export async function verifyOtp(phone: string, otp: string): Promise<SmsCenterResult<VerifyOtpData>> {
  const { baseUrl, appId, headers } = smsCenterConfig();
  const res = await fetch(`${baseUrl}/api/v1/otp/verify`, {
    method: 'POST',
    headers,
    body: JSON.stringify({ phone, otp, app_id: appId }),
  });
  const data = await res.json().catch(() => ({}));
  return { ok: res.ok, status: res.status, data };
}
PHP (Laravel và tương đương) — tải SmsCenter.php
<?php

// Client cho SMS Center (https://sms.lentrang1.com) — xem hướng dẫn tích
// hợp đầy đủ tại https://sms.lentrang1.com/tich-hop (cách xin app_id + API
// key và quy tắc tích hợp bắt buộc: verify OTP đúng 1 lần duy nhất, atomic
// với hành động thật).
//
// Copy file này vào project (vd app/Services/SmsCenter.php), rồi khai 3
// biến trong .env:
//   SMS_CENTER_BASE_URL=https://sms.lentrang1.com
//   SMS_CENTER_APP_ID=<app_id được cấp>
//   SMS_CENTER_API_KEY=<API key được cấp, chỉ hiện 1 lần lúc tạo>
//
// Không phụ thuộc Laravel — chỉ dùng cURL thuần, dùng được ở bất kỳ PHP
// project nào (đổi getenv() sang config()/env() helper của framework nếu cần).

final class SmsCenter
{
    private const PHONE_RE = '/^0[35789]\d{8}$/';
    private const OTP_RE = '/^[0-9]{6}$/';

    public static function isValidPhone(string $phone): bool
    {
        return preg_match(self::PHONE_RE, $phone) === 1;
    }

    public static function isValidOtpFormat(string $otp): bool
    {
        return preg_match(self::OTP_RE, $otp) === 1;
    }

    /** POST /api/v1/otp/send — gửi mã OTP 6 số tới $phone (định dạng 0xxxxxxxxx). */
    public static function sendOtp(string $phone): array
    {
        return self::call('/api/v1/otp/send', ['phone' => $phone]);
    }

    /**
     * POST /api/v1/otp/verify — kiểm tra mã OTP.
     *
     * QUAN TRỌNG: verify đúng sẽ xoá ngay OTP phía server (chống replay),
     * nên gọi hàm này NGAY TẠI bước thực hiện hành động thật (tạo đơn, tạo
     * tài khoản, đổi mật khẩu...) — verify xong mới ghi dữ liệu, atomic
     * trong cùng 1 request. Không tách "verify riêng rồi mới hành động" ở
     * 2 bước khác nhau, vì verify lần 2 sẽ luôn fail dù mã đúng.
     */
    public static function verifyOtp(string $phone, string $otp): array
    {
        return self::call('/api/v1/otp/verify', ['phone' => $phone, 'otp' => $otp]);
    }

    /** @return array{ok: bool, status: int, data: array} */
    private static function call(string $path, array $body): array
    {
        $baseUrl = getenv('SMS_CENTER_BASE_URL');
        $appId = getenv('SMS_CENTER_APP_ID');
        $apiKey = getenv('SMS_CENTER_API_KEY');
        if (!$baseUrl || !$appId || !$apiKey) {
            throw new RuntimeException('Thiếu cấu hình SMS_CENTER_BASE_URL/SMS_CENTER_APP_ID/SMS_CENTER_API_KEY trong .env');
        }

        $body['app_id'] = $appId;

        $ch = curl_init(rtrim($baseUrl, '/') . $path);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 10,
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
                'X-API-Key: ' . $apiKey,
            ],
            CURLOPT_POSTFIELDS => json_encode($body, JSON_UNESCAPED_UNICODE),
        ]);
        $raw = curl_exec($ch);
        if ($raw === false) {
            $err = curl_error($ch);
            curl_close($ch);
            throw new RuntimeException('SMS Center không phản hồi: ' . $err);
        }
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        $data = json_decode($raw, true) ?? [];

        return ['ok' => $status >= 200 && $status < 300, 'status' => $status, 'data' => $data];
    }
}

Stack khác? Gọi thẳng REST API bằng HTTP client bất kỳ — xem mục Tham chiếu API bên dưới.

2Viết luồng gửi/xác thực OTP

Dùng 2 hàm sendOtp/verifyOtp (hoặc tương đương ở SDK PHP) trong code của bạn — tạm để biến môi trường SMS_CENTER_APP_ID là 1 chuỗi bất kỳ (vd ten-project-cua-ban) để code chạy/build được, chưa cần API key thật ở bước này — gọi thử sẽ nhận lỗi 401, không sao, mục tiêu bước này là viết xong luồng UI + xử lý lỗi.

import { sendOtp, verifyOtp } from '@/lib/smsCenter';

// gửi OTP
const res = await sendOtp('0987654321');
if (!res.ok) {
  // map res.status / res.data.error sang thông báo cho user — xem bảng lỗi bên dưới
}

// verify NGAY TẠI bước thực hiện hành động thật — xem quy tắc bên dưới
const verify = await verifyOtp('0987654321', '123456');
if (verify.ok && verify.data.verified) {
  // tạo tài khoản / tạo đơn hàng / đổi mật khẩu... ở NGAY ĐÂY
}
Quy tắc bắt buộc — verify đúng 1 lần duy nhất: /otp/verify đúng sẽ xoá ngay OTP đó phía server (chống replay). KHÔNG thiết kế luồng "verify riêng để mở khoá form → bước sau mới tạo đơn/tạo tài khoản" — verify lần 2 sẽ luôn fail (410) dù mã đúng. Gọi verifyOtp() NGAY TẠI chính request thực hiện hành động thật, verify xong mới ghi dữ liệu, atomic trong cùng 1 lần gọi. Không tin cờ "đã verify" gửi từ client.

Pattern UI gợi ý: ô nhập SĐT → nút "Gửi mã" → hiện thêm ô nhập OTP 6 số → nút hành động chính (VD "Đặt hàng", "Đăng ký") gửi kèm cả OTP trong cùng payload, server verify + thực hiện hành động cùng lúc.

3Khi code đã xong — liên hệ xin app_id + API key thật

Chỉ liên hệ ở bước này — sau khi đã viết xong luồng tích hợp và sẵn sàng test/đưa vào dùng thật, nhắn cho người quản lý SMS Center (Zerox) kèm:

Bạn sẽ nhận lại đúng 1 lần (API key không xem lại được sau đó, mất thì báo lại để sinh mã mới cho đúng app_id đó):

app_id:  ten-project-cua-ban
api_key: <chuỗi ngẫu nhiên, CHỈ hiện đúng 1 lần lúc tạo — lưu lại ngay>

Điền vào .env của project — cả môi trường dev lẫn production (thiếu ở production là lỗi hay gặp nhất, vì code vẫn chạy được ở dev với giá trị tạm):

SMS_CENTER_BASE_URL=https://sms.lentrang1.com
SMS_CENTER_APP_ID=ten-project-cua-ban
SMS_CENTER_API_KEY=<api_key được cấp>

SMS_CENTER_API_KEY là secret — không commit vào git, không log ra console, không trả về client.

4Tham chiếu API

Base URL: https://sms.lentrang1.com. Mọi request POST /api/v1/..., JSON, kèm header X-API-Key: <api_key>.

POST /api/v1/otp/send

{ "phone": "0987654321", "app_id": "ten-project-cua-ban" }

phone: bắt buộc, dạng nội địa 0xxxxxxxxx (đầu số di động VN 3/5/7/8/9, 10 số) — chuẩn hoá ở phía app bạn trước khi gọi.

Response 200:

{ "job_id": "b7e6f6b0-...", "expires_in": 120 }
StatusKhi nào
400phone/app_id sai định dạng hoặc thiếu
401API key sai hoặc app_id không tồn tại/đã bị thu hồi
429Vượt rate limit — body kèm retry_after (giây)
503Không còn node SIM nào online lúc đó — hiếm gặp, retry sau vài phút

POST /api/v1/otp/verify

{ "phone": "0987654321", "otp": "123456", "app_id": "ten-project-cua-ban" }

Response 200:

{ "verified": true }
StatusKhi nào
400thiếu field / otp không phải đúng 6 số
401API key sai hoặc app_id không tồn tại/đã bị thu hồi
410OTP hết hạn (quá 120s) hoặc chưa từng gửi cho số này — báo user bấm "Gửi lại mã"
422OTP sai — body kèm attempts_left, hiện cho user biết còn mấy lần thử
423Sai quá 5 lần liên tiếp, khoá 15 phút — body kèm retry_after (giây)

Verify đúng → OTP bị xoá ngay lập tức, không verify lại lần 2 được (xem quy tắc kiến trúc ở Bước 2).

5Rate limit

Áp dụng theo app_id của bạn, không dùng chung với project khác:

Cần khối lượng lớn hơn thì báo khi liên hệ ở Bước 3 để chỉnh riêng cho app_id của bạn.

6Lưu ý khi test

Không có môi trường sandbox — mọi POST /otp/send gửi SMS thật qua SIM vật lý (tốn tiền thật, tốn quota SIM dùng chung với các project khác).

7Lộ trình — sắp tới

Các nghiệp vụ dưới đây chưa có API — nếu project bạn cần, liên hệ để ưu tiên phát triển thay vì tự ghép tạm qua /otp/send (nội dung tin nhắn OTP hiện cố định, không nhận nội dung tự do):

?Hỗ trợ

Vấn đề về app_id/API key, tăng rate limit, lỗi tích hợp không map được vào bảng lỗi ở trên: liên hệ trực tiếp người quản lý SMS Center (Zerox).