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.
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.
// 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
// 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.
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
}
/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.
app_id + API key thậtChỉ 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.
Base URL: https://sms.lentrang1.com. Mọi request
POST /api/v1/..., JSON, kèm header
X-API-Key: <api_key>.
{ "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 }
| Status | Khi nào |
|---|---|
| 400 | phone/app_id sai định dạng hoặc thiếu |
| 401 | API key sai hoặc app_id không tồn tại/đã bị thu hồi |
| 429 | Vượt rate limit — body kèm retry_after (giây) |
| 503 | Không còn node SIM nào online lúc đó — hiếm gặp, retry sau vài phút |
{ "phone": "0987654321", "otp": "123456", "app_id": "ten-project-cua-ban" }
Response 200:
{ "verified": true }
| Status | Khi nào |
|---|---|
| 400 | thiếu field / otp không phải đúng 6 số |
| 401 | API key sai hoặc app_id không tồn tại/đã bị thu hồi |
| 410 | OTP 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ã" |
| 422 | OTP sai — body kèm attempts_left, hiện cho user biết còn mấy lần thử |
| 423 | Sai 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).
Áp dụng theo app_id của bạn, không dùng chung với
project khác:
app_id: 500 lần gửi / ngày.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.
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).
sendOtp thật
trong CI — mock lớp gọi HTTP ra ngoài (fetch/curl)
ở test, không mock nội bộ SMS Center.app_id — hỏi Zerox nếu cần
biết số lượng đã dùng trong tháng.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):
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).