Tài liệu API Free Passive OTP
Passive OTP đảo chiều SMS OTP: người dùng của bạn tự nhắn một tin chứa mã tới số chung, hệ thống xác nhận SĐT gửi đúng là SĐT cần xác thực. Không cần gửi tin đi, không lo tin bị chặn hay đến chậm.
Bắt đầu nhanh
- Đăng ký bằng Google, xác thực SĐT và nhận 600.000đ (2.000 lượt) dùng trong 6 tháng.
- Lấy API key, syntax và số chung ở dashboard → Tích hợp. Khai báo webhook URL nếu muốn nhận kết quả đẩy về.
- Server của bạn gọi
POST /otp/requestsvới SĐT người dùng; app hiển thị “SoạnSYNTAX MÃgửisố chung”. - Nhận
otp.verifiedqua webhook, hoặc hỏiGET /otp/requests/{id}.
Base URL: http://localhost:3000/api/v1. Mọi response là JSON, thời gian theo ISO 8601 UTC, tiền là số nguyên VNĐ.
Xác thực
Gửi API key trong header Authorization: Bearer pk_live_…. Key chỉ dùng ở server: không đặt trong app di động, trình duyệt hay repo. Mỗi tài khoản có tối đa 3 key đang hoạt động; thu hồi key ở dashboard → Cài đặt.
Hai cách tích hợp
| Cách | Khi nào dùng | Tính phí |
|---|---|---|
| Có request | Bạn biết trước SĐT cần xác thực. Gọi API tạo request, người dùng nhắn SYNTAX MÃ, nhận otp.verified. | 300đ khi tạo request; tin nhắn khớp request không tính thêm |
| Chỉ syntax | Bạn chỉ cần biết “SĐT này vừa nhắn đúng syntax”. App tự hiển thị nội dung; mỗi tin đến tạo sự kiện message.received. | 300đ mỗi tin đến hợp lệ |
Mã khớp khi đứng đầu phần sau syntax; chữ phía sau mã được bỏ qua (SHOP 482913 cam on vẫn khớp, SHOP X482913 thì không). Hạn được tính theo thời điểm nhà mạng nhận tin.
Tạo request OTP
POST /otp/requests — trừ 300đ khi tạo thành công. Request cũ còn chờ của cùng SĐT chuyển superseded (không hoàn tiền).
| Trường | Bắt buộc | Mô tả |
|---|---|---|
phone | Có | SĐT di động Việt Nam: 0…, 84… hoặc +84… |
code | Không | 4–10 chữ/số; bỏ trống thì hệ thống sinh 6 số. Không phân biệt hoa thường |
expires_in | Không | Giây, 60–600, mặc định 180 |
max_attempts | Không | Số lần nhắn sai tối đa, 1–5, mặc định 3 |
reference | Không | Mã tham chiếu của bạn (≤ 100 ký tự), trả lại trong webhook |
curl -X POST http://localhost:3000/api/v1/otp/requests \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1001" \
-d '{"phone":"0912345678","reference":"order-1001"}'201 Created
{
"id": "otp_01JABCXYZ",
"status": "pending",
"phone": "84912345678",
"sms_to": "<số chung>",
"sms_content": "SHOPABC 482913",
"sms_uri": "sms:<số chung>?&body=SHOPABC%20482913",
"expires_at": "2026-10-07T03:03:00Z",
"max_attempts": 3,
"charged": 300,
"balance": 599700,
"reference": "order-1001"
}Trên điện thoại, mở sms_uri để ứng dụng tin nhắn điền sẵn nội dung và số nhận.
Xem request
GET /otp/requests/{id} trả các trường như trên cộng attempts, verified_at, created_at và message (tin đã khớp: {id, content, received_at}).
| status | Nghĩa |
|---|---|
pending | Đang chờ người dùng nhắn |
verified | Đã nhận đúng mã, đúng SĐT, còn hạn |
failed | Nhắn sai quá max_attempts lần |
expired | Hết hạn mà chưa xác thực |
superseded | Bị thay bởi request mới cùng SĐT |
curl http://localhost:3000/api/v1/otp/requests/otp_01JABCXYZ \
-H "Authorization: Bearer $API_KEY"Danh sách, tin đến, tài khoản
| Endpoint | Mô tả |
|---|---|
GET /otp/requests?phone=&status=&from=&to=&cursor=&limit= | Danh sách request, mới nhất trước |
GET /messages?phone=&from=&to=&cursor=&limit= | Tin đến bắt đầu bằng syntax của bạn |
GET /messages/{id} | Một tin đến: phone, content, syntax, payload, status, received_at, request_id, charged |
GET /account | balance, unit_price, remaining_requests, syntax, shared_number, bonus_expires_at |
Phân trang: limit mặc định 20, tối đa 100; dùng next_cursor của trang trước làm cursor. Response: {"data": [...], "next_cursor": "..." | null}.
Webhook
Hệ thống gửi POST JSON tới webhook URL (bắt buộc https://) với các header X-Event-Id, X-Event-Type, X-Timestamp, X-Signature. Trả HTTP 2xx trong 5 giây là thành công; nếu không, hệ thống thử lại sau 10 giây, 1 phút, 5 phút (tổng 4 lần). Webhook lỗi không hoàn tiền; kết quả vẫn lấy được qua API, và bạn có thể gửi lại từ dashboard.
| type | Khi nào |
|---|---|
otp.verified | Request được xác thực |
otp.failed | Nhắn sai quá số lần |
otp.expired | Request hết hạn |
message.received | Tin chỉ syntax (cách 2) |
ping | Nút “Gửi thử” trên dashboard |
{
"id": "evt_01JABD...",
"type": "otp.verified",
"created_at": "2026-10-07T03:01:12Z",
"data": {
"request_id": "otp_01JABCXYZ",
"reference": "order-1001",
"phone": "84912345678",
"status": "verified",
"attempts": 1,
"message_id": "msg_01JABD...",
"content": "SHOPABC 482913",
"received_at": "2026-10-07T03:01:10Z"
}
}message.received có data gồm message_id, phone, syntax, payload, content, received_at; otp.expired có request_id, reference, phone, status, attempts, expires_at; ping có data rỗng. Dùng id (= X-Event-Id) để bỏ qua sự kiện đã xử lý.
Kiểm tra chữ ký
X-Signature: sha256=<hex HMAC-SHA256(webhook_secret, X-Timestamp + "." + body)>. Tính trên body nguyên bản (trước khi parse JSON), so sánh bằng phép so thời gian hằng, và từ chối nếu X-Timestamp lệch quá 5 phút. Webhook secret xem ở dashboard → Tích hợp.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: chuỗi body nguyên bản (chưa parse JSON)
export function verifyWebhook(rawBody, headers, secret) {
const ts = headers["x-timestamp"];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // lệch quá 5 phút
const expected = "sha256=" + createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
const given = String(headers["x-signature"] ?? "");
return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}Mã lỗi
Dạng chung: {"error": {"code": "insufficient_balance", "message": "...", "request_id": "req_..."}}. Gửi kèm request_id khi cần hỗ trợ.
| HTTP | code | Khi nào |
|---|---|---|
| 400 | invalid_phone | SĐT không phải di động Việt Nam |
| 400 | validation_error | Tham số sai kiểu hoặc ngoài giới hạn |
| 401 | invalid_api_key | Thiếu, sai hoặc đã thu hồi |
| 402 | insufficient_balance | Số dư dưới đơn giá |
| 403 | tenant_suspended | Tài khoản bị khoá |
| 403 | tenant_migrated | Tài khoản đã chuyển sang gói trả phí trên hệ thống chính |
| 404 | not_found | Không có hoặc không thuộc tài khoản của bạn |
| 409 | idempotency_conflict | Cùng Idempotency-Key nhưng khác nội dung (hoặc đang chạy) |
| 429 | rate_limited | Vượt giới hạn; xem header Retry-After |
| 500 | internal_error | Lỗi hệ thống |
Giới hạn và idempotency
- 10 request/giây cho mỗi API key.
- Mỗi SĐT: 1 request mỗi 30 giây và 20 request mỗi 24 giờ.
- Header
Idempotency-Key(≤ 100 ký tự) choPOST: gửi lại cùng key và cùng body trong 24 giờ sẽ nhận đúng response cũ (headerIdempotent-Replayed: true) và không bị trừ tiền lần hai.
Câu hỏi thường gặp
- Người dùng có mất phí nhắn tin không?
- Có, cước một tin nhắn thường theo gói của họ. Bù lại họ không phải chờ tin OTP đến.
- Người dùng nhắn sai mã thì sao?
- Request tăng
attempts; đạtmax_attemptsthìfailedvà bạn nhậnotp.failed. Tạo request mới để thử lại. - Tin nhắn đến sau khi request hết hạn?
- Không còn request để khớp nên được tính như tin chỉ syntax (300đ) và gửi
message.received. - Hết tiền thì sao?
- API trả
402, tin chỉ syntax bị chặn (vẫn xem được trong log). Liên hệ để dùng gói trả phí. - Đổi syntax được không?
- 1 lần mỗi 30 ngày ở dashboard → Cài đặt. Syntax cũ vẫn về tài khoản bạn trong 30 ngày.