Tài liệu API eOTP
Gửi mã OTP cho người dùng của bạn qua Zalo hoặc SMS brandname. Máy chủ của bạn gọi otp/send để gửi mã, otp/verify để kiểm mã người dùng nhập, và otp/status khi cần biết tin đã tới đâu. Mọi lời gọi gửi kèm API key và Secret key của dự án.
Bắt đầu nhanh
- Tạo tài khoản và dự án
Đăng ký tại trang đăng ký (xác minh số điện thoại bằng một mã OTP), rồi nộp dự án: chọn loại (Android, iOS hoặc website), điền tên, đường dẫn cửa hàng hoặc tên miền và mô tả ngắn cách bạn dùng OTP. Hệ thống tự rà soát, thường trong vòng 2 phút; dự án đạt ở trạng thái
approved, gói FREE (100 OTP/tháng miễn phí). - Xác minh để lên DEVELOPER
Website dùng Popup xác thực (cách mặc định) hoặc nút hiện sẵn (tuỳ chọn); app gọi SDK kiểm giấy phép (xem Kích hoạt DEVELOPER). Đủ bằng chứng, dự án tự lên DEVELOPER (100,000 OTP/tháng). Bước này không bắt buộc để thử API.
- Lấy API key và Secret key
Trong trang dự án, mở tab Tích hợp. API key (
eo_live_…) luôn hiện; Secret key (eo_sec_…) chỉ hiện một lần khi tạo. Lưu cả 2 vào biến môi trường trên máy chủ:EASYOTP_API_KEY,EASYOTP_API_SECRET.
Thử ngay từ dòng lệnh: gửi mã, rồi kiểm mã bạn nhận được trên điện thoại.
# Gửi mã tới số Việt Nam
curl -X POST https://eotp.com.vn/api/v1/otp/send \
-H "X-Api-Key: $EASYOTP_API_KEY" \
-H "X-Api-Secret: $EASYOTP_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"phone":"0912345678","country_code":"84"}'
# → {"ok":true,"request_id":"3f9a1c0e8b7d4a26b5e0c9d18f2a7b64", ...}
# Kiểm mã người dùng nhập
curl -X POST https://eotp.com.vn/api/v1/otp/verify \
-H "X-Api-Key: $EASYOTP_API_KEY" \
-H "X-Api-Secret: $EASYOTP_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"request_id":"3f9a1c0e8b7d4a26b5e0c9d18f2a7b64","code":"123456"}'
# → {"ok":true,"verified":true}
Xác thực
Mỗi yêu cầu tới otp/send, otp/status, otp/verify và project gửi kèm 2 khoá của dự án:
- API key, dạng
eo_live_+ 16 ký tự: định danh dự án, luôn xem lại được ở tab Tích hợp. Gửi trong headerX-Api-Key. - Secret key, dạng
eo_sec_+ 32 ký tự: chỉ hiện một lần khi tạo, eOTP chỉ lưu bản băm. Gửi trong headerX-Api-Secret.
X-Api-Key: eo_live_xxxxxxxxxxxxxxxx
X-Api-Secret: eo_sec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Hoặc gộp vào một header: Authorization: Bearer <api_key>:<secret>.
| Tình huống | HTTP | error |
|---|---|---|
| Thiếu hoặc sai API key | 401 | invalid_api_key |
| API key đúng nhưng thiếu, sai Secret key hoặc secret đã bị tạo lại | 401 | invalid_api_secret |
| Khoá cũ dạng một khoá đã quá 30 ngày chuyển đổi | 401 | legacy_auth_expired |
| Một địa chỉ IP xác thực sai nhiều lần liên tiếp | 429 | auth_rate_limited |
| Dự án chưa duyệt, bị từ chối, tạm ngưng hoặc tài khoản bị khoá | 403 | project_not_active |
Chặn theo IP khi xác thực sai nhiều. Mỗi phản hồi 401 tính vào bộ đếm của IP gọi (IPv6 gộp theo dải /64). Sai nhiều lần liên tiếp thì mọi yêu cầu từ IP đó bị trả 429 auth_rate_limited trong một thời gian, kể cả khi khoá đúng (chờ theo retry_in, header Retry-After). Khi triển khai khoá mới, kiểm biến môi trường trước để máy chủ không tự khoá mình.
Chuyển đổi từ khoá cũ. Khoá cũ dạng một khoá (eo_live_ + 32 ký tự gửi một mình) vẫn dùng được tới 30 ngày sau khi bạn tạo secret, kèm header Deprecation: true và X-Eotp-Warning ghi ngày hết hạn. Quá hạn trả 401 legacy_auth_expired. Vào trang dự án, bấm Tạo secret mới để nhận cặp API key + Secret key rồi cập nhật máy chủ.
Chỉ gọi API từ máy chủ của bạn. Không đưa Secret key vào app Android/iOS, JavaScript trên trình duyệt hay kho mã công khai: ai có đủ 2 khoá đều gửi được OTP và tiêu hạn mức, số dư của bạn. API không mở CORS nên trình duyệt chặn lời gọi trực tiếp. API key một mình là định danh công khai, nhúng vào iframe hay SDK kiểm giấy phép đều an toàn.
Lộ Secret key? Vào tab Tích hợp và tạo secret mới. Secret cũ mất hiệu lực ngay lập tức. API key cũng tạo lại được ở cùng chỗ (nút Tạo API key mới): khoá cũ bị từ chối ngay, và vì đường dẫn /embed/<api_key> đi theo khoá nên bạn cập nhật header X-Api-Key, mã iframe nếu dùng nút hiện sẵn và SDK kiểm giấy phép trong app; Popup xác thực không cần sửa trang. Secret key giữ nguyên.
Quy ước chung
- URL gốc:
https://eotp.com.vn/api/v1. Chỉ nhận HTTPS. - Body gửi lên là JSON (
Content-Type: application/json), UTF-8. Phản hồi luôn là JSON. - Mọi phản hồi có trường
ok. Thành công:"ok": true, HTTP 200. Lỗi:"ok": false, mã lỗi máy đọc trongerrorvà lời nhắn trongmessage, kèm trường phụ tuỳ lỗi. - Xử lý theo
error, không dựa vàomessagevì lời nhắn có thể thay đổi. - Số tiền là số nguyên ở đơn vị nhỏ nhất của ví: VND tính bằng đồng, USD tính bằng cent. Trường
currencycho biết loại tiền. - Mốc thời gian là Unix (giây, UTC). Tháng và ngày của hạn mức tính theo giờ UTC.
- Header
X-Request-Id: nếu bạn gửi (tối đa 64 ký tự chữ, số,. _ : -), hệ thống trả lại đúng giá trị đó; nếu không, hệ thống tự tạo. Ghi giá trị này vào log để đối chiếu khi cần hỗ trợ.
{
"ok": false,
"error": "invalid_phone",
"message": "Số điện thoại chưa đúng.",
"reason": "too_short"
}
Gửi mã OTP
https://eotp.com.vn/api/v1/otp/sendTạo một mã OTP mới và gửi tới số điện thoại. Hệ thống tự sinh mã; bạn không cần và không bao giờ thấy mã.
Tham số (body JSON)
| Trường | Kiểu | Mô tả |
|---|---|---|
phonebắt buộc | string | Số dạng E.164 (+84912345678) hoặc số nội địa (0912345678) kèm country_code. Được phép có dấu cách, chấm, gạch. |
country_codetuỳ chọn | string | Mã nước không dấu +, ví dụ 84. Bỏ trống thì mặc định 84 (Việt Nam). Khi phone đã có dạng +… thì dùng mã trong số; country_code mâu thuẫn với mã đó trả invalid_phone. |
channeltuỳ chọn | string | zalo, sms, both hoặc zalo_then_sms (xem Kênh). Bỏ trống thì dùng kênh mặc định trong Cài đặt gửi của dự án. |
callback_urltuỳ chọn | string | URL trên máy chủ của bạn để eOTP POST kết quả gửi khi yêu cầu chốt sent hoặc failed (xem Callback). Phải là https://, cổng 443, tên miền phân giải ra IP công khai, tối đa 500 ký tự. Sai trả 400 invalid_callback_url. |
Thời gian sống, độ dài và kiểu mã đặt theo dự án, không truyền trong body: vào trang dự án ▸ Cài đặt gửi. Thời gian sống 60–900 giây (1–15 phút, mặc định 300); độ dài 4–10 ký tự (mặc định 6); kiểu mã digits (chỉ số 0–9) hoặc alnum (chữ + số, bảng A–Z bỏ O, I và số 2–9, dễ đọc trên điện thoại).
curl -X POST https://eotp.com.vn/api/v1/otp/send \
-H "X-Api-Key: $EASYOTP_API_KEY" \
-H "X-Api-Secret: $EASYOTP_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"phone":"+84912345678","channel":"zalo_then_sms","callback_url":"https://api.shop.vn/eotp/callback"}'
Phản hồi thành công (200)
Có 2 dạng, tuỳ chế độ vận hành của hệ thống. Code của bạn nên xử lý được cả hai: chỉ cần ok và request_id là đủ để đi tiếp tới verify.
- Hàng đợi (
"status": "queued"): eOTP nhận yêu cầu, giữ tiền và trả lời ngay; bộ gửi nền chuyển tin trong khoảngeta_msmili giây. Mã đã được tạo nên người dùng có thể nhập và bạn gọiverifybình thường. Muốn biết tin đã tới đâu, gọiotp/status. - Đồng bộ (không có
status): eOTP gửi xong mới trả lời, kèmsent_channels.
| Trường | Kiểu | Mô tả |
|---|---|---|
request_id | string | Mã yêu cầu, 32 ký tự hex. Lưu lại để gọi verify và status. |
status | string | Chỉ ở chế độ hàng đợi: queued. |
eta_ms | number | Chỉ ở chế độ hàng đợi: ước tính số mili giây tới khi tin được gửi đi. Tăng khi hệ thống đông, yêu cầu không bị từ chối vì đông. |
channel | string | Chế độ thực dùng. Có thể khác chế độ bạn chọn: gói FREE chỉ gửi một kênh nên both/zalo_then_sms chuyển về kênh mặc định của dự án (zalo hoặc sms, chọn ở Cài đặt gửi); nước chưa có Zalo thì chuyển về sms; ví chỉ đủ Zalo thì zalo_then_sms hạ về zalo. |
channel_note | string | Chỉ có khi kênh bị đổi theo nước người nhận: zalo_unavailable_in_country hoặc sms_unavailable_in_country. |
sent_channels | string[] | Chế độ đồng bộ: kênh đã gửi thành công, ["zalo"], ["sms"] hoặc ["zalo","sms"]. |
expires_in | number | Số giây mã còn hiệu lực (theo cài đặt dự án). |
code_type | string | digits hoặc alnum: kiểu mã người dùng sẽ nhận, dùng để chọn bàn phím số hay chữ cho ô nhập. |
cost, currency | number, string | Số tiền ví đang trừ cho lần gửi này, ở đơn vị nhỏ nhất của currency (VND hoặc USD). Bằng 0 ở gói FREE. Với zalo_then_sms gồm cả giá SMS dự phòng; phần này được hoàn khi Zalo tới máy. |
balance | number | Số dư ví sau khi trừ (khi có), cùng đơn vị với cost. |
cost_vnd, balance_vnd | number | Giữ cho tương thích khi ví VND. Ví USD dùng cost_usd_cents, balance_usd_cents. |
phone_e164 | string | Số đã chuẩn hoá dạng E.164, ví dụ +84912345678. Nên dùng làm khoá số điện thoại của người dùng. |
{
"ok": true,
"request_id": "3f9a1c0e8b7d4a26b5e0c9d18f2a7b64",
"status": "queued",
"eta_ms": 400,
"expires_in": 300,
"cost": 0,
"currency": "VND",
"code_type": "digits",
"phone_e164": "+84912345678"
}
{
"ok": true,
"request_id": "3f9a1c0e8b7d4a26b5e0c9d18f2a7b64",
"channel": "zalo_then_sms",
"sent_channels": ["zalo"],
"expires_in": 300,
"cost_vnd": 0,
"currency": "VND",
"cost": 0,
"phone_e164": "+84912345678"
}
Lỗi riêng của endpoint này
| error | HTTP | Khi nào | Trường thêm |
|---|---|---|---|
invalid_phone | 400 | Thiếu số, sai định dạng, quá ngắn/quá dài, hoặc country_code mâu thuẫn với mã trong số. | reason: empty, invalid, too_short, too_long |
country_not_supported | 400 | Mã nước chưa được mở gửi OTP. Danh sách nước ở GET /pricing. | country_code |
channel_not_supported_in_country | 400 | Nước của số nhận chưa có kênh nào, hoặc kênh dùng được cho nước đó đang tạm dừng. | country_code, country_iso, channels_enabled? |
invalid_channel | 400 | Kênh không thuộc 4 chế độ hoặc đang tạm dừng. | channels_enabled |
invalid_callback_url | 400 | callback_url không hợp lệ. Không trừ tiền, không gửi tin. | reason: bad_url, not_https, bad_port, private_host |
quota_exceeded | 429 | Tài khoản dùng hết hạn mức tháng. Hạn mức tính theo tài khoản: mọi dự án của bạn dùng chung một bể. | month_used, month_quota |
daily_quota_exceeded | 429 | Tài khoản dùng hết hạn mức ngày (FREE: tối đa 20 OTP/ngày; DEVELOPER: tối đa 10,000 OTP/ngày). Có header Retry-After. | daily_quota, today_used, resets_at, retry_after |
insufficient_balance | 402 | Gói trả phí: số dư ví không đủ cho tin này. | currency, balance, cost (+ balance_vnd, cost_vnd hoặc balance_usd_cents, cost_usd_cents) |
unavailable | 503 | Dịch vụ gửi tạm thời chưa sẵn sàng. | |
send_failed | 502 | Chế độ đồng bộ: nhà mạng/Zalo không nhận tin. Không trừ tiền. |
Không tự gọi lại send khi chưa nhận phản hồi: người dùng có thể nhận 2 mã. Đặt timeout từ 35 giây trở lên, vì ở chế độ đồng bộ khi Zalo không nhận, hệ thống gửi SMS dự phòng ngay trong cùng lời gọi.
Chế độ thử: dự án đã duyệt bật Chế độ thử ở tab API key & tích hợp (hoặc ở khối Nâng cấp lên DEVELOPER). Khi bật, gọi send tới số thử của eOTP (hiện ngay cạnh công tắc, mặc định +84 901 888 484) thì không gửi tin thật, không trừ ví và không tính vào hạn mức; phản hồi có "test_mode": true và "esms_code": "test". Gọi verify với mã 000000 là thành công. Các số khác vẫn gửi thật và trừ ví như thường.
Trạng thái gửi
https://eotp.com.vn/api/v1/otp/status?request_id=…Tra trạng thái gửi của một yêu cầu do chính dự án của khoá tạo. Dùng khi send trả "status": "queued" và bạn muốn báo người dùng "mã đã gửi" hoặc gợi ý đổi kênh khi gửi lỗi. Không cần đợi sent mới gọi verify.
curl "https://eotp.com.vn/api/v1/otp/status?request_id=3f9a1c0e8b7d4a26b5e0c9d18f2a7b64" \
-H "X-Api-Key: $EASYOTP_API_KEY" \
-H "X-Api-Secret: $EASYOTP_API_SECRET"
| status | Ý nghĩa |
|---|---|
queued | Đã nhận, đang chờ bộ gửi nền. |
sending | Bộ gửi đang chuyển tin cho nhà mạng/Zalo. |
sent | Nhà mạng/Zalo đã nhận tin. sent_at là lúc gửi. |
failed | Gửi không được; error cho biết lý do. Tiền đã giữ được hoàn lại. |
expired | Mã đã hết hạn. |
{
"ok": true,
"request_id": "3f9a1c0e8b7d4a26b5e0c9d18f2a7b64",
"status": "sent",
"channel": "sms",
"sent_at": 1794268800,
"error": null,
"attempts": 1
}
attempts là số lần bộ gửi đã thử. request_id sai dạng, không tồn tại hoặc của dự án khác trả 404 not_found. Nên tra cách nhau ít nhất 1 giây, dừng khi gặp sent, failed hoặc expired.
Kiểm mã OTP
https://eotp.com.vn/api/v1/otp/verifyKiểm mã người dùng nhập với request_id nhận từ send. Mỗi mã chỉ dùng được một lần. Chỉ kiểm được yêu cầu do chính dự án của khoá tạo; request_id của dự án khác trả not_found.
Tham số (body JSON)
| Trường | Kiểu | Mô tả |
|---|---|---|
request_idbắt buộc | string | Giá trị request_id trả về từ send. |
codebắt buộc | string | Mã người dùng nhập. Kiểu alnum so không phân biệt hoa/thường (ab7k = AB7K). |
phonetuỳ chọn | string | Số đã gửi mã (E.164 hoặc nội địa kèm country_code), chuẩn hoá như send. Gửi kèm mà khác số của yêu cầu thì trả not_found. |
country_codetuỳ chọn | string | Dùng cùng phone dạng nội địa, mặc định 84. |
curl -X POST https://eotp.com.vn/api/v1/otp/verify \
-H "X-Api-Key: $EASYOTP_API_KEY" \
-H "X-Api-Secret: $EASYOTP_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"request_id":"3f9a1c0e8b7d4a26b5e0c9d18f2a7b64","code":"123456"}'
{
"ok": true,
"verified": true
}
Lỗi riêng của endpoint này
| error | HTTP | Khi nào | Trường thêm |
|---|---|---|---|
wrong_code | 400 | Mã sai (hoặc thiếu code). Người dùng được nhập lại. | attempts_left |
expired | 410 | Mã đã hết hạn hoặc đã được dùng. | |
too_many_attempts | 429 | Nhập sai 5 lần, mã bị huỷ. Cần gửi mã mới. | |
not_found | 404 | request_id không tồn tại, thuộc dự án khác, hoặc phone không khớp. | |
rate_limited | 429 | Quá nhiều yêu cầu chưa xác thực (thiếu hoặc sai khoá); thử lại sau. Gọi bằng khoá + secret đúng không gặp lỗi này. Có header Retry-After. | retry_in |
{
"ok": false,
"error": "wrong_code",
"message": "Mã chưa đúng. Bạn còn 4 lần thử.",
"attempts_left": 4
}
Callback về máy chủ của bạn
Truyền callback_url khi gọi otp/send để eOTP tự báo kết quả gửi, thay vì tra otp/status nhiều lần. Khi yêu cầu chốt sent hoặc failed, eOTP gửi một lời gọi POST tới URL đó. Không truyền callback_url thì không có callback; tra otp/status vẫn dùng được như cũ.
Hoàn tiền, huỷ SMS dự phòng khi Zalo tới máy và báo cáo nhà mạng vẫn do eOTP tự xử lý; callback chỉ để bạn biết kết quả.
Yêu cầu eOTP gửi tới bạn
| Header | Giá trị |
|---|---|
Content-Type | application/json |
User-Agent | Bắt đầu bằng eOTP-Callback/1.0. |
X-Eotp-Request-Id | Bằng request_id trong thân, dùng để bỏ qua lời gọi trùng. |
X-Eotp-Timestamp | Mốc Unix (giây, UTC) lúc eOTP gửi lời gọi này. Mỗi lần gửi lại có mốc mới. |
X-Eotp-Signature | sha256= + HMAC-SHA256 (hex chữ thường) của chuỗi <X-Eotp-Timestamp>.<thân thô>, khoá là Callback secret của dự án. |
| Trường | Kiểu | Mô tả |
|---|---|---|
request_id | string | Mã yêu cầu trả về từ send. |
status | string | sent (nhà mạng/Zalo đã nhận tin) hoặc failed (tiền đã giữ được hoàn lại). |
channel | string | Kênh đã dùng, như ở otp/status. |
sent_at | number | null | Mốc Unix (giây, UTC) lúc gửi; null khi failed. |
error | string | null | Lý do khi failed. |
phone_e164 | string | Số nhận dạng E.164. |
POST /eotp/callback HTTP/1.1
Host: api.shop.vn
Content-Type: application/json
User-Agent: eOTP-Callback/1.0 (+https://eotp.com.vn)
X-Eotp-Request-Id: 3f9a1c0e8b7d4a26b5e0c9d18f2a7b64
X-Eotp-Timestamp: 1794268800
X-Eotp-Signature: sha256=097235b3686b78ffc336fcb92452539ce8b3f434c063044289e416fbff8c0cc7
{"request_id":"3f9a1c0e8b7d4a26b5e0c9d18f2a7b64","status":"sent","channel":"sms","sent_at":1794268800,"error":null,"phone_e164":"+84912345678"}
Xác minh chữ ký
Lấy Callback secret ở trang dự án ▸ tab Tích hợp. Ghép giá trị header X-Eotp-Timestamp, một dấu chấm và thân thô đúng từng byte (đọc trước khi parse JSON) thành timestamp + "." + body, tính HMAC-SHA256 bằng Callback secret, rồi so sha256= + kết quả với header X-Eotp-Signature bằng hàm so sánh hằng thời gian. Không khớp thì trả 401 và bỏ qua.
Chống phát lại: từ chối lời gọi khi X-Eotp-Timestamp thiếu, không phải số, hoặc lệch giờ máy chủ của bạn quá 300 giây (|now − timestamp| > 300). Đồng bộ giờ máy chủ bằng NTP để không từ chối nhầm.
const crypto = require('crypto');
app.post('/eotp/callback', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.get('X-Eotp-Timestamp') || '';
if (!/^\d+$/.test(ts) || Math.abs(Math.floor(Date.now() / 1000) - Number(ts)) > 300) return res.sendStatus(401);
const sig = 'sha256=' + crypto.createHmac('sha256', process.env.EOTP_CALLBACK_SECRET).update(ts + '.').update(req.body).digest('hex');
const got = Buffer.from(req.get('X-Eotp-Signature') || '');
if (got.length !== sig.length || !crypto.timingSafeEqual(got, Buffer.from(sig))) return res.sendStatus(401);
const data = JSON.parse(req.body); /* data.request_id, data.status */ res.sendStatus(200);
});
$raw = file_get_contents('php://input'); // thân thô, trước khi parse
$ts = $_SERVER['HTTP_X_EOTP_TIMESTAMP'] ?? '';
if (!ctype_digit($ts) || abs(time() - (int) $ts) > 300) { http_response_code(401); exit; }
$sig = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, getenv('EOTP_CALLBACK_SECRET'));
if (!hash_equals($sig, $_SERVER['HTTP_X_EOTP_SIGNATURE'] ?? '')) { http_response_code(401); exit; }
$data = json_decode($raw, true); // $data['request_id'], $data['status']
http_response_code(200);
import hmac, hashlib, os, time
from flask import Flask, request, abort
app = Flask(__name__)
@app.post('/eotp/callback')
def eotp_callback():
raw = request.get_data() # thân thô, trước khi parse
ts = request.headers.get('X-Eotp-Timestamp', '')
if not (ts.isascii() and ts.isdigit()) or abs(time.time() - int(ts)) > 300: abort(401)
sig = 'sha256=' + hmac.new(os.environ['EOTP_CALLBACK_SECRET'].encode(), ts.encode() + b'.' + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, request.headers.get('X-Eotp-Signature', '')): abort(401)
data = request.get_json() # data['request_id'], data['status']
return '', 200
Phản hồi và gửi lại
Trả bất kỳ mã 2xx trong 5 giây là eOTP coi như bạn đã nhận. Mã khác, quá 5 giây hoặc không kết nối được thì eOTP gửi lại theo lịch dưới. eOTP không theo chuyển hướng (3xx tính là lỗi).
| Lần gọi | Thời điểm | Timeout |
|---|---|---|
| 1 | Ngay khi yêu cầu chốt sent/failed | 5 giây |
| 2 | 30 giây sau lần 1 lỗi | 5 giây |
| 3 | 5 phút sau lần 2 lỗi | 5 giây |
Hết 3 lần mà chưa nhận 2xx thì dừng gửi. Kết quả vẫn tra được bằng GET /otp/status. Cùng một request_id có thể tới hơn một lần (mạng chập chờn); xử lý theo X-Eotp-Request-Id để không ghi trùng.
Giữ Callback secret ở máy chủ, như Secret key. Lộ secret thì người khác giả được callback; liên hệ hỗ trợ eOTP để cấp secret mới.
Popup xác thực
Cách tích hợp mặc định cho dự án website. Trang của bạn không hiện gì sẵn: người dùng nhập số điện thoại vào form của bạn như bình thường, sau đó bạn gọi popup eOTP. Popup hiện đè lên trang, người dùng nhập mã ngay trong popup; gửi lại mã, đếm ngược, báo mã sai và hết hạn do eOTP lo. Máy chủ của bạn chỉ cần gọi 2 endpoint.
- Máy chủ tạo phiên
Khi người dùng gửi số, máy chủ của bạn gọi
POST /popup/sessionbằng API key + Secret key. eOTP gửi mã tới số đó và trả vềsession. - Trang web mở popup
Trang nạp
https://eotp.com.vn/sdk/eotp-1.jsrồi gọieOTP.open({ session, … }). Người dùng nhập đúng mã thìonVerifiednhậnresult. - Máy chủ xác nhận kết quả
Trang gửi
resultlên máy chủ của bạn; máy chủ gọiPOST /popup/resultđể biết chắc số điện thoại đã xác minh rồi mới đăng nhập hoặc ghi nhận đơn.
Muốn đặt nút "Xác thực eOTP" hiện sẵn trên trang, người dùng bấm nút là mở đúng popup này, xem Bấm nút mở popup. Chọn cách tích hợp ở trang dự án ▸ tab API key & tích hợp; popup chỉ dùng cho dự án loại website.
Bước 1. Tạo phiên popup
https://eotp.com.vn/api/v1/popup/sessionGọi từ máy chủ, kèm header X-Api-Key và X-Api-Secret như otp/send. Mỗi lời gọi gửi một mã mới tới số điện thoại và tính như một lần gửi (hạn mức, ví).
| Trường | Kiểu | Mô tả |
|---|---|---|
phonebắt buộc | string | Số người dùng vừa nhập, dạng E.164 hoặc nội địa kèm country_code, như otp/send. |
country_codetuỳ chọn | string | Mã nước không dấu +, mặc định 84. |
channeltuỳ chọn | string | zalo, sms, both hoặc zalo_then_sms. Bỏ trống thì dùng kênh mặc định của dự án. |
langtuỳ chọn | string | Ngữ của popup: vi hoặc en. |
| Trường trả về | Kiểu | Mô tả |
|---|---|---|
session | string | Mã phiên dạng eops_…. Đưa xuống trang để gọi eOTP.open. Phiên ngắn hạn, chỉ dùng cho một lần xác minh. |
popup_url | string | Địa chỉ trang popup của phiên. SDK tự dùng, bạn không cần mở trực tiếp. |
request_id | string | Mã yêu cầu OTP đầu tiên, dùng để đối chiếu với otp/status hoặc nhật ký. |
expires_in | number | Số giây mã vừa gửi còn hiệu lực (theo cài đặt dự án). |
phone_masked | string | Số đã che, đúng như popup hiện cho người dùng. |
channel | string | Chế độ gửi thực dùng, như ở otp/send. |
max_resend | number | Số lần người dùng được bấm gửi lại trong popup (theo cài đặt dự án). |
{
"ok": true,
"session": "eops_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"popup_url": "https://eotp.com.vn/popup/eops_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"request_id": "3f9a1c0e8b7d4a26b5e0c9d18f2a7b64",
"expires_in": 300,
"phone_masked": "0912 *** 678",
"channel": "zalo_then_sms",
"max_resend": 3
}
Lỗi giống otp/send (invalid_phone, quota_exceeded, insufficient_balance…) và các lỗi xác thực. Lỗi có retry_in thì thử lại sau đúng khoảng đó. Thêm một lỗi riêng:
| error | HTTP | Khi nào |
|---|---|---|
popup_not_supported | 400 | Dự án không phải loại website (app Android/iOS dùng otp/send + otp/verify). |
curl -X POST https://eotp.com.vn/api/v1/popup/session \
-H "X-Api-Key: $EASYOTP_API_KEY" \
-H "X-Api-Secret: $EASYOTP_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"phone":"0912345678","country_code":"84","lang":"vi"}'
const EOTP = 'https://eotp.com.vn/api/v1';
const eotp = (path, body) => fetch(EOTP + path, {
method: 'POST',
headers: { 'X-Api-Key': process.env.EASYOTP_API_KEY, 'X-Api-Secret': process.env.EASYOTP_API_SECRET, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then((r) => r.json());
// Bước 1: trang gửi số lên, máy chủ trả session cho trang
app.post('/otp/start', express.json(), async (req, res) => {
const r = await eotp('/popup/session', { phone: req.body.phone, lang: 'vi' });
if (!r.ok) return res.status(400).json({ error: r.error });
res.json({ session: r.session });
});
// Bước 3: trang gửi result lên, máy chủ hỏi lại eOTP
app.post('/otp/done', express.json(), async (req, res) => {
const r = await eotp('/popup/result', { result: req.body.result });
if (!r.ok || !r.verified) return res.status(401).json({ error: 'not_verified' });
req.session.phone = r.phone_e164; // số đã xác minh
res.json({ ok: true });
});
function eotp(string $path, array $body): array {
$ch = curl_init('https://eotp.com.vn/api/v1' . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-Api-Key: ' . getenv('EASYOTP_API_KEY'), 'X-Api-Secret: ' . getenv('EASYOTP_API_SECRET'), 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($body),
]);
$out = json_decode((string) curl_exec($ch), true);
return is_array($out) ? $out : ['ok' => false];
}
// Bước 1
$r = eotp('/popup/session', ['phone' => $_POST['phone'], 'lang' => 'vi']);
echo json_encode($r['ok'] ? ['session' => $r['session']] : ['error' => $r['error'] ?? 'error']);
// Bước 3
$r = eotp('/popup/result', ['result' => $_POST['result']]);
if (!empty($r['ok']) && !empty($r['verified'])) { $_SESSION['phone'] = $r['phone_e164']; }
Bước 2. Mở popup trên trang
Nạp SDK một lần trên trang có form nhập số, rồi gọi eOTP.open khi máy chủ trả về session. SDK không cần thư viện ngoài và không chứa khoá nào.
<script src="https://eotp.com.vn/sdk/eotp-1.js"></script>
<script>
async function verifyPhone(phone) {
const { session } = await fetch('/otp/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phone }) }).then((r) => r.json());
eOTP.open({
session,
lang: 'vi',
onVerified: async ({ result }) => {
await fetch('/otp/done', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ result }) });
},
onClose: (reason) => { if (reason === 'change_phone') document.querySelector('#phone').focus(); },
onError: (error) => console.warn('eOTP', error),
});
}
</script>
Tham số của eOTP.open | Kiểu | Mô tả |
|---|---|---|
sessionbắt buộc | string | Giá trị session (eops_…) từ bước 1. |
langtuỳ chọn | string | vi hoặc en; bỏ trống thì dùng ngữ đã truyền ở bước 1. |
onVerifiedtuỳ chọn | function | Gọi khi người dùng nhập đúng mã. Tham số: { result, session }, result dạng eopr_…. |
onClosetuỳ chọn | function | Gọi khi người dùng đóng popup. Tham số reason: change_phone (bấm "Đổi số điện thoại", bạn đưa họ về ô nhập số) hoặc close (bấm ✕, phím Esc hoặc bấm ra ngoài). Sau khi xác minh xong, popup tự đóng mà không gọi onClose; bạn xử lý tiếp trong onVerified. |
onErrortuỳ chọn | function | Gọi khi popup không dùng được. Tham số là mã lỗi: invalid_session (giá trị session sai dạng), load_failed (không tải được popup, thường vì trang không chạy ở tên miền của dự án), session_expired hoặc session_closed (phiên đã hết hạn hoặc đã đóng). Tạo phiên mới rồi mở lại. |
eOTP.close() đóng popup đang mở. Gọi eOTP.open khi popup đang mở thì lời gọi sau bị bỏ qua. Khi popup đóng, trang của bạn cuộn được trở lại và con trỏ quay về phần tử đang chọn trước đó.
Bước 3. Xác nhận kết quả ở máy chủ
https://eotp.com.vn/api/v1/popup/resultGửi result nhận từ onVerified, kèm API key + Secret key. result chỉ dùng được với đúng dự án đã tạo phiên và còn hiệu lực trong thời gian ngắn; gọi lại trong thời gian đó vẫn trả cùng kết quả.
curl -X POST https://eotp.com.vn/api/v1/popup/result \
-H "X-Api-Key: $EASYOTP_API_KEY" \
-H "X-Api-Secret: $EASYOTP_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"result":"eopr_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}'
{
"ok": true,
"verified": true,
"phone_e164": "+84912345678",
"request_id": "3f9a1c0e8b7d4a26b5e0c9d18f2a7b64",
"verified_at": 1794268800
}
| error | HTTP | Khi nào |
|---|---|---|
invalid_result | 404 | result sai, đã hết hạn hoặc thuộc dự án khác. Coi như chưa xác minh. |
Luôn xác nhận ở máy chủ, không tin kết quả từ trình duyệt. onVerified chạy trên máy người dùng nên ai cũng gọi giả được. Chỉ coi số điện thoại là đã xác minh khi máy chủ của bạn gọi popup/result nhận "verified": true, và dùng phone_e164 trong phản hồi đó (không dùng số do trình duyệt gửi lên).
Hành vi của popup
- Popup hiện số đã che và kênh gửi. Người dùng nhập mã; mã sai thì popup báo và cho biết còn bao nhiêu lần thử, mã hết hạn thì popup chuyển sang nút gửi mã mới.
- Gửi lại mã: người dùng bấm gửi lại ngay trong popup, tối đa theo Số lần gửi lại trong popup của dự án (đặt ở trang dự án ▸ Cài đặt gửi, từ 0 đến 5). Mỗi lần gửi lại là một lần gửi thật: tính vào hạn mức và trừ ví như
otp/send. Gửi lại quá nhanh thì popup đếm ngược và mở nút khi được gửi. - Đổi số điện thoại: đóng popup,
onClosenhậnchange_phoneđể bạn cho người dùng nhập lại số rồi tạo phiên mới. Phiên cũ không dùng tiếp được. - Báo cáo lạm dụng: popup có ô để người dùng báo trang đang dùng eOTP để lừa đảo. Báo cáo gửi về đội ngũ eOTP xem xét; popup vẫn mở và người dùng vẫn nhập mã được.
Chế độ thử: dự án bật Chế độ thử (tab API key & tích hợp) và tạo phiên với số thử của eOTP thì không gửi tin thật, không trừ ví, không tính hạn mức; nhập 000000 trong popup là thành công, các bước còn lại chạy như thật. Số khác vẫn gửi thật. Lần xác minh ở chế độ thử không tính để mở DEVELOPER.
Yêu cầu về trang. Trang mở popup phải chạy HTTPS trên tên miền đã xác minh của dự án hoặc tên miền con của nó: popup chỉ nhúng được ở đó, trang ở tên miền khác sẽ không hiện popup và không nhận được result. Không đặt Referrer-Policy: no-referrer cho trang mở popup: eOTP dựa vào header Referer để ghi nhận popup chạy trên website của bạn (dùng cho Kích hoạt DEVELOPER). Không đưa API key hay Secret key xuống trình duyệt: trang chỉ cần session.
Thông tin dự án
https://eotp.com.vn/api/v1/projectXem trạng thái, gói, lượng OTP đã dùng và số dư ví của tài khoản sở hữu dự án. Hữu ích để theo dõi hạn mức hoặc kiểm khoá khi triển khai.
curl https://eotp.com.vn/api/v1/project \
-H "X-Api-Key: $EASYOTP_API_KEY" \
-H "X-Api-Secret: $EASYOTP_API_SECRET"
Trường trong project | Kiểu | Mô tả |
|---|---|---|
id, name | number, string | Mã và tên dự án. |
type | string | android, ios hoặc web. |
status | string | Luôn là approved khi gọi được (trạng thái khác trả 403). |
tier | number | Gói: 1 FREE, 2 DEVELOPER, 3 UNLIMITED, 4 BUSINESS. |
default_channel | string | Kênh dùng khi send không truyền channel. |
otp_ttl | number | Thời gian sống của mã (giây). |
month_used, month_quota | number, number | null | Đã dùng / hạn mức tháng (UTC); null là không giới hạn. |
today_used | number | Số OTP đã gửi hôm nay (UTC). |
currency | string | Ví đang dùng: VND hoặc USD. |
balance_minor | number | Tổng số dư = ví gốc + ví tặng, đơn vị nhỏ nhất (đồng hoặc cent). |
balance_main_minor, balance_bonus_minor | number | Số dư ví gốc và ví tặng. |
balance_vnd, balance_main_vnd, balance_bonus_vnd | number | Chỉ khi ví VND (giữ cho tương thích). Ví USD: balance_usd_cents, balance_main_usd_cents, balance_bonus_usd_cents. |
{
"ok": true,
"project": {
"id": 12,
"name": "Shop Hoa Mai",
"type": "web",
"status": "approved",
"tier": 2,
"default_channel": "zalo_then_sms",
"otp_ttl": 300,
"month_used": 37,
"month_quota": 10000,
"today_used": 4,
"currency": "VND",
"balance_minor": 120000,
"balance_main_minor": 100000,
"balance_bonus_minor": 20000,
"balance_vnd": 120000,
"balance_main_vnd": 100000,
"balance_bonus_vnd": 20000
}
}
Kích hoạt DEVELOPER
Website có hai cách tích hợp: Popup xác thực (mặc định) và Nút hiện sẵn. Nút hiện sẵn đặt nút "Xác thực eOTP" lên trang của bạn; người dùng bấm nút là popup xác thực mở, phiên vẫn do máy chủ của bạn tạo như kiểu popup (xem Bấm nút mở popup). Chọn cách ở trang dự án ▸ tab API key & tích hợp.
Dự án mới được duyệt ở gói FREE (Cấp 1). Để lên DEVELOPER (Cấp 2) không cần chờ duyệt tay: bạn chứng minh dự án đã tích hợp eOTP thật, hệ thống tự mở cấp.
- Website, popup xác thực: trong 30 ngày gần nhất có ít nhất một lần người dùng xác minh thành công trong popup mở từ đúng tên miền của dự án (mã gửi thật, không tính chế độ thử), và trang công khai của bạn có nạp
https://eotp.com.vn/sdk/eotp-1.js. Bot của eOTP tải trang, thấy SDK thì mở DEVELOPER. - Website, nút hiện sẵn: nhúng nút eOTP (iframe
/embed/<api_key>) lên trang thật chạy HTTPS trên tên miền đã khai báo hoặc tên miền con. Lượt tải iframe đúng tên miền chỉ ghi nhận ứng viên; bot của eOTP sau đó tải trang công khai của bạn, thấy đường dẫn/embed/<api_key>trên trang thì mở DEVELOPER. - Ứng dụng: thêm SDK kiểm license (
EotpLicense.ktcho Android,EotpLicense.swiftcho iOS) và gọicheck()một lần mỗi khi app mở. DEVELOPER tự mở khi đủ hai điều kiện: lượt kiểm license đúng app id từ ít nhất 2 mạng khác nhau (ví dụ Wi‑Fi rồi 4G; hai máy cùng một Wi‑Fi chỉ tính là một mạng), và ít nhất 1 mã OTP thật được máy chủ của bạn gửi và kiểm qua API (/otp/sendrồi/otp/verify). Mã gửi ở Chế độ thử không được tính.
Vào dự án ▸ tab API key & tích hợp, ở ô Cách tích hợp chọn Nút hiện sẵn, rồi chọn kiểu nút: Logo + chữ, Logo + chữ bo tròn, Chỉ chữ, Chỉ chữ bo tròn, Chỉ logo hoặc Viền; chọn giao diện tối hoặc sáng và cỡ 28, 32 hoặc 36 px. Phần xem trước và đoạn mã đổi theo lựa chọn của bạn; bấm Lưu để giữ lựa chọn cho dự án, rồi chép mã dán vào trang có ô nhập OTP. Mã đã có sẵn API key và đúng rộng × cao của nút. Trang phải chạy trên tên miền đã khai báo hoặc tên miền con của nó.
<iframe src="https://eotp.com.vn/embed/eo_live_xxxxxxxxxxxxxxxx?lang=vi&style=logo-text&theme=dark&size=32" width="153" height="32" style="border:0" loading="lazy" referrerpolicy="no-referrer-when-downgrade" title="Xác thực eOTP"></iframe>
Thuộc tính referrerpolicy giúp eOTP biết đúng trang đang nhúng nút để xác nhận tích hợp.
Tham số trên đường dẫn iframe (thiếu hoặc sai giá trị thì eOTP dùng lựa chọn bạn đã lưu cho dự án; chưa lưu thì dùng kiểu mặc định của eOTP):
| Tham số | Giá trị | Ghi chú |
|---|---|---|
lang | vi, en | Ngữ của chữ trên nút. Thiếu hoặc khác hai giá trị này thì dùng en. |
style | logo-text, logo-text-pill, text, text-pill, icon, outline | Lần lượt: Logo + chữ, Logo + chữ bo tròn, Chỉ chữ, Chỉ chữ bo tròn, Chỉ logo, Viền. |
theme | dark, light | dark: nền đen chữ trắng; light: nền trắng chữ đen. Kiểu Viền: dark viền và chữ đen (cho nền sáng), light viền và chữ trắng (cho nền tối). |
size | 28, 32, 36 | Chiều cao nút (px), cũng là height của iframe. |
Tự đổi tham số thì đổi luôn width và height của iframe cho khớp (nút luôn giãn kín khung):
| Kiểu | Rộng (px), lang=vi, cỡ 28 · 32 · 36 | Rộng (px), lang=en, cỡ 28 · 32 · 36 |
|---|---|---|
logo-text, logo-text-pill, outline | 140 · 153 · 159 | 151 · 166 · 172 |
text, text-pill | 81 · 89 · 91 | 62 · 69 · 70 |
icon | 28 · 32 · 36 | 28 · 32 · 36 |
height luôn bằng cỡ. Mã cũ 180 × 40 vẫn hoạt động (hiện nút theo lựa chọn đã lưu của dự án); bạn nên thay bằng mã mới để nút khít khung. Tạo API key mới ở tab API key & tích hợp thì đường dẫn /embed/<api_key> cũ không còn dùng được: bạn chép lại mã nhúng và thay trên website.
Referrer-Policy. eOTP nhận ra website của bạn qua header Referer mà trình duyệt gửi khi tải iframe. Nếu trang đặt Referrer-Policy: no-referrer (header hoặc thẻ <meta name="referrer">), lượt nhúng sẽ không được ghi nhận. Giữ nguyên thuộc tính referrerpolicy="no-referrer-when-downgrade" có sẵn trong mã iframe.
Bấm nút mở popup
Nạp SDK https://eotp.com.vn/sdk/eotp-1.js trên trang có nút và đăng ký eOTP.onButton: khi người dùng bấm nút eOTP, hàm của bạn chạy, gửi số điện thoại lên máy chủ của bạn để tạo phiên, rồi mở popup bằng eOTP.open. Mã mẫu đủ hai phần:
<!-- 1. Nút -->
<iframe src="https://eotp.com.vn/embed/eo_live_xxxxxxxxxxxxxxxx?lang=vi&style=logo-text&theme=dark&size=32" width="153" height="32" style="border:0" loading="lazy" referrerpolicy="no-referrer-when-downgrade" title="Xác thực eOTP"></iframe>
<!-- 2. Bấm nút → mở popup -->
<script src="https://eotp.com.vn/sdk/eotp-1.js"></script>
<script>
eOTP.onButton(function () {
var phone = document.querySelector('#phone').value; // số người dùng nhập trên trang của bạn
fetch('/your-server/eotp-session', { method: 'POST', body: new URLSearchParams({ phone: phone }) })
.then(function (r) { return r.json(); })
.then(function (d) {
eOTP.open({ session: d.session, onVerified: function (r) { /* gửi r.result về máy chủ để xác nhận */ } });
});
});
</script>
| Hàm | Mô tả |
|---|---|
eOTP.onButton(handler) | Đăng ký hàm handler chạy khi người dùng bấm nút eOTP trên trang (chuột, Enter hoặc Space). Gọi lại với hàm khác thì hàm mới thay hàm cũ; gọi eOTP.onButton(null) để gỡ (sau khi gỡ, bấm nút không làm gì cho tới khi bạn đăng ký lại). Đăng ký trước hay sau khi iframe nút tải xong đều được. |
Trang không đăng ký eOTP.onButton thì nút mở trang eOTP ở tab mới.
Đường dẫn /your-server/eotp-session là máy chủ của bạn: nhận số điện thoại, gọi POST /popup/session rồi trả session cho trang. Khi onVerified chạy, gửi r.result lên máy chủ để gọi POST /popup/result trước khi cho người dùng đi tiếp. Hai lệnh này y hệt kiểu popup: xem Popup xác thực (bước 1 và bước 3). Trang có nút phải chạy HTTPS trên tên miền đã xác minh của dự án hoặc tên miền con của nó.
Tải EotpLicense.kt (một file, không thư viện ngoài, cần quyền INTERNET), chép vào mã nguồn app (đổi dòng package cho hợp dự án). App id mặc định là packageName, phải khớp với app trên Google Play đã khai báo.
EotpLicense.check(context, "eo_live_xxxxxxxxxxxxxxxx") { r ->
if (r.licensed) Log.i("eOTP", "DEVELOPER: ${r.project}") else Log.w("eOTP", "license: ${r.error}")
}
Tải EotpLicense.swift (một file, iOS 13+, không thư viện ngoài), thêm vào project Xcode. App id mặc định là Bundle.main.bundleIdentifier, phải khớp với app trên App Store đã khai báo.
EotpLicense.check(apiKey: "eo_live_xxxxxxxxxxxxxxxx") { r in
print(r.licensed ? "DEVELOPER: \(r.project ?? "")" : "license: \(r.error ?? "")")
}
API key là định danh công khai, nhúng vào iframe hay app đều an toàn. Không bao giờ đưa Secret key vào app hay trang web: gửi và kiểm OTP vẫn đi qua máy chủ của bạn.
SDK gọi GET https://eotp.com.vn/api/v1/license?app=<package|bundle> kèm header X-Api-Key. Thành công trả về:
{
"ok": true,
"licensed": true,
"project": "PRJ-XXXXXX",
"tier": 2,
"unlocked_now": true,
"evidence": {
"pings_ok": 3,
"subnets": 2,
"otp_verified": 1,
"required": { "subnets": 2, "otp_verified": 1 }
}
}
unlocked_now: true nghĩa là dự án vừa được mở DEVELOPER ở chính lần gọi này. evidence cho biết bằng chứng đã có so với mức cần (required); otp_verified chỉ đếm mã thật, không đếm mã gửi ở Chế độ thử. SDK trả hai trường này trong Result: unlockedNow và evidence (pingsOk, subnets, otpVerified, requiredSubnets, requiredOtpVerified); lần gọi lỗi thì cả hai là null/nil. Chỉ gửi API key, không gửi Secret key từ app.
| error | HTTP | Ý nghĩa và nên làm |
|---|---|---|
missing_app | 400 | Thiếu tham số app. Truyền package (Android) hoặc bundle id (iOS). |
unknown_key | 404 | API key không tồn tại. Sao chép lại khoá ở tab API key & tích hợp của dự án. |
app_mismatch | 403 | App id không khớp với ứng dụng trên cửa hàng đã khai báo cho dự án. Kiểm applicationId / bundle id của bản phát hành. |
project_not_approved | 403 | Dự án chưa được duyệt hoặc đang tạm ngưng. Xem trạng thái ở trang dự án. |
store_unavailable | 503 | Chưa đọc được app id từ cửa hàng. Thử lại sau (header Retry-After). |
rate_limited | 429 | Gọi quá nhiều lần trong thời gian ngắn (theo IP hoặc tổng của dự án). Chỉ cần gọi một lần khi app khởi động. |
Kênh, giá và hạn mức
4 chế độ gửi
| channel | Cách gửi | Giá mỗi tin (ví VND · ví USD) |
|---|---|---|
zalo | Tin Zalo ZNS từ OA của ViHAT. Số không dùng Zalo sẽ không nhận được. | 1,550đ · $0.07 |
sms | SMS brandname. | 2,000đ · $0.09 |
both | Gửi cả Zalo và SMS cùng một mã. | Zalo + SMS |
zalo_then_sms | Gửi Zalo trước; Zalo không tới được thì tự gửi SMS cùng mã. Khuyến nghị. | Giữ Zalo + SMS; hoàn phần SMS khi Zalo tới máy |
Giá như nhau ở mọi gói trả phí; gói FREE không trừ ví. Lần gửi thất bại được hoàn tiền ngay, coi như không trừ. Ví chỉ đủ cho Zalo thì zalo_then_sms tự hạ về zalo. Gói FREE chỉ gửi một kênh: both và zalo_then_sms tự chuyển về kênh mặc định của dự án (zalo hoặc sms, chọn ở Cài đặt gửi), không báo lỗi.
Quốc gia người nhận
Mỗi nước có cờ kênh riêng (xem countries trong GET /pricing). Khi gửi, eOTP đọc mã nước của số nhận: nước chưa có Zalo thì gửi sms, nước chưa có SMS thì gửi zalo (phản hồi có channel_note); nước không có kênh nào trả channel_not_supported_in_country; nước chưa mở trả country_not_supported.
Các gói và hạn mức
Hạn mức tính theo TÀI KHOẢN: mọi dự án của bạn dùng chung một bể, theo gói cao nhất trong các dự án.
100 OTP/tháng · tối đa 20 OTP/ngày
Miễn phí, không trừ ví, một kênh. Có ngay khi dự án được duyệt.
100,000 OTP/tháng · tối đa 10,000 OTP/ngày
Trừ ví theo tin. Tự lên khi bạn kích hoạt DEVELOPER.
Không giới hạn
Trừ ví theo tin. Đội ngũ ViHAT mở sau khi làm việc với bạn (hotline +84 901 888 484).
Hạn mức riêng theo hợp đồng
Trừ ví theo tin. ViHAT đặt hạn mức tháng và ngày riêng cho tài khoản của bạn.
Tháng và ngày tính theo giờ UTC, làm mới lúc 00:00 UTC. Một OTP được tính hạn mức khi có ít nhất một lần gửi không thất bại. Hết hạn mức tháng, send trả quota_exceeded; hết hạn mức ngày trả daily_quota_exceeded (HTTP 429).
Giá và quốc gia
https://eotp.com.vn/api/v1/pricingBảng giá, hạn mức các gói, gói nạp và danh sách nước hỗ trợ. Công khai: không cần khoá, mở CORS (Access-Control-Allow-Origin: *) nên gọi được cả từ trình duyệt. Phản hồi được cache 300 giây (Cache-Control: public, max-age=300) và có ETag: gửi If-None-Match để nhận 304 khi chưa đổi.
curl https://eotp.com.vn/api/v1/pricing
{
"ok": true,
"currency": "VND",
"price_zalo_vnd": 1550,
"price_sms_vnd": 2000,
"tiers": {
"1": { "name": "FREE", "monthly_quota": 100, "daily_quota": 20 },
"2": { "name": "DEVELOPER", "monthly_quota": 100000, "daily_quota": 10000 },
"3": { "name": "UNLIMITED", "monthly_quota": null, "daily_quota": null }
},
"topup_packages": [{ "amount": 100000, "bonus_pct": 0 }],
"countries": [
{ "iso": "VN", "name_en": "Vietnam", "name_vi": "Việt Nam", "dial": "84", "sms": true, "zalo": true, "enabled": true }
],
"updated_at": 1794268800
}
monthly_quota/daily_quota bằng null là không giới hạn. Nước có enabled: false chưa nhận gửi OTP. dial là giá trị dùng cho country_code.
Ví và tiền tệ
- Mỗi loại tiền một ví riêng. Tài khoản có ví VND (tính bằng đồng) và ví USD (tính bằng cent). Chỉ một ví đang dùng (
currencytrongproject); ví kia được giữ nguyên, không quy đổi. Mọi số tiền trong API ở đơn vị nhỏ nhất của ví đang dùng. - Ví gốc và ví tặng. Mỗi ví gồm phần gốc (tiền bạn nạp) và phần tặng (khuyến mãi của gói nạp, mã khuyến mãi). Khi gửi OTP, eOTP trừ ví tặng trước, hết mới trừ ví gốc; hoàn tiền trả về đúng ví đã trừ.
- Trừ ngay khi nhận yêu cầu, hoàn khi thất bại. Tiền được trừ cùng lúc kiểm số dư, nên gọi song song không làm âm ví. Gửi thất bại được hoàn ngay;
zalo_then_smshoàn phần SMS khi Zalo tới máy. - Nạp tiền theo gói ở trang Ví của cổng developer. Chuyển khoản VietQR nạp vào ví VND, mã QR có hạn 30 phút. Gói nạp hiện hành có trong
topup_packagescủaGET /pricing. - Mã khuyến mãi nhập khi tạo yêu cầu nạp; mỗi tài khoản dùng mỗi mã một lần, tiền khuyến mãi vào ví tặng khi khoản nạp được xác nhận.
Gói FREE không trừ ví. Số dư không đủ cho một tin thì send trả 402 insufficient_balance kèm currency, balance, cost.
Mã lỗi
Toàn bộ mã lỗi của API v1. Cột "Ý nghĩa và nên làm" là cách xử lý khuyến nghị trên máy chủ của bạn.
| error | HTTP | Endpoint | Ý nghĩa và nên làm |
|---|---|---|---|
invalid_api_key | 401 | có khoá | Thiếu hoặc sai API key. Kiểm biến môi trường EASYOTP_API_KEY. |
invalid_api_secret | 401 | có khoá | API key đúng nhưng thiếu hoặc sai secret. Kiểm header X-Api-Secret; mất secret thì tạo secret mới ở trang dự án. |
legacy_auth_expired | 401 | có khoá | Khoá cũ một khoá đã quá 30 ngày chuyển đổi. Tạo secret mới rồi gửi đủ 2 header. |
auth_rate_limited | 429 | có khoá | Xác thực sai nhiều lần; thử lại sau. Sửa khoá rồi đợi retry_in giây. |
project_not_active | 403 | có khoá | Dự án chưa duyệt, bị từ chối, tạm ngưng hoặc tài khoản bị khoá. Có thêm status, tier. |
rate_limited | 429 | license, pricing, yêu cầu thiếu/sai khoá | Quá nhiều yêu cầu chưa xác thực; thử lại sau. Yêu cầu có khoá + secret đúng không gặp lỗi này. Đợi retry_in giây (header Retry-After). |
rate_limit_unavailable | 503 | tất cả | Hệ thống tạm lỗi. Thử lại sau vài giây. |
method_not_allowed | 405 | tất cả | Sai phương thức: send/verify dùng POST; status, project, license, pricing dùng GET. |
bad_request | 400 | send, verify | Body không phải JSON hợp lệ. |
invalid_callback_url | 400 | send | callback_url sai dạng (bad_url), không phải HTTPS (not_https), khác cổng 443 (bad_port) hoặc trỏ về IP nội bộ (private_host). Sửa URL theo reason rồi gửi lại. |
invalid_phone | 400 | send | Số không hợp lệ (reason cho biết lý do). Nhắc người dùng nhập lại; gửi dạng E.164 hoặc kèm country_code. |
country_not_supported | 400 | send | Nước của số chưa được mở (country_code). Báo người dùng dùng số của nước được hỗ trợ. |
channel_not_supported_in_country | 400 | send | Nước của số chưa có kênh nào, hoặc kênh duy nhất của nước đó đang tạm dừng. Có country_code, country_iso. |
invalid_channel | 400 | send | Kênh không hợp lệ hoặc tạm dừng. channels_enabled liệt kê kênh dùng được. |
quota_exceeded | 429 | send | Hết hạn mức tháng của bể tài khoản (month_used/month_quota). Lên gói cao hơn hoặc chờ sang tháng. |
daily_quota_exceeded | 429 | send | Hết hạn mức ngày của bể tài khoản (today_used/daily_quota). resets_at là mốc Unix làm mới, retry_after là số giây còn lại. |
insufficient_balance | 402 | send | Ví không đủ (balance < cost, đơn vị theo currency). Nạp thêm tiền. |
unavailable | 503 | send | Dịch vụ gửi tạm thời chưa sẵn sàng. Thử lại sau vài phút. |
send_failed | 502 | send | Không gửi được tới số này (ví dụ số không dùng Zalo khi chọn zalo). Thử sms hoặc zalo_then_sms. |
wrong_code | 400 | verify | Mã sai, còn attempts_left lần. |
expired | 410 | verify | Mã hết hạn hoặc đã dùng. Gửi mã mới. |
too_many_attempts | 429 | verify | Sai 5 lần, mã bị huỷ. Gửi mã mới. |
not_found | 404 | verify, status | request_id không đúng, thuộc dự án khác hoặc phone không khớp. |
unknown_key | 404 | license | API key không tồn tại. Sao chép lại ở tab Tích hợp. |
missing_app | 400 | license | Thiếu hoặc sai tham số app. |
app_mismatch | 403 | license | App id không khớp ứng dụng đã khai báo. |
project_not_approved | 403 | license | Dự án chưa duyệt hoặc đang tạm ngưng. |
store_unavailable | 503 | license | Chưa đọc được app id từ cửa hàng. Thử lại sau. |
server_error | 500 | tất cả | Lỗi hệ thống. Thử lại sau, gửi kèm X-Request-Id khi liên hệ hỗ trợ. |
Hạn mức gói và thông số mã
Mỗi gói có hạn mức OTP theo tháng và theo ngày, tính chung cho mọi dự án của tài khoản (xem Kênh, giá và hạn mức và GET /pricing). Vượt hạn mức, send trả HTTP 429:
| Mã lỗi | HTTP | Ý nghĩa | Cách xử lý |
|---|---|---|---|
quota_exceeded | 429 | Hết hạn mức tháng của gói (month_used/month_quota). | Nâng gói để gửi tiếp, hoặc chờ sang tháng (00:00 UTC ngày 1). |
daily_quota_exceeded | 429 | Hết hạn mức ngày của gói (today_used/daily_quota). Có header Retry-After. | Nâng gói, hoặc chờ tới resets_at (retry_after giây). |
Cần hạn mức cao hơn gói hiện có, gọi hotline +84 901 888 484.
| Thông số mã | Mức | Khi vượt |
|---|---|---|
| Nhập sai mã | 5 lần cho mỗi mã | 429 too_many_attempts, mã bị huỷ |
| Thời gian sống của mã | 60–900 giây theo cài đặt dự án, mặc định 300 | 410 expired |
| Độ dài mã | 4–10 ký tự theo cài đặt dự án | — |
Hàng đợi thay cho từ chối. Ở chế độ hàng đợi, lúc đông eOTP vẫn nhận yêu cầu send, chỉ tăng eta_ms; không có trần độ dài hàng đợi. Hạn mức gói ở trên vẫn áp dụng.
Ví dụ code
Mọi ví dụ chạy trên máy chủ, đọc khoá từ biến môi trường EASYOTP_API_KEY và EASYOTP_API_SECRET, gửi đủ 2 header X-Api-Key + X-Api-Secret. Mỗi ví dụ gửi mã rồi kiểm mã; chạy được với cả phản hồi hàng đợi lẫn đồng bộ của send.
Dùng để thử nhanh từ dòng lệnh.
# 1. Gửi mã
curl -X POST https://eotp.com.vn/api/v1/otp/send \
-H "X-Api-Key: $EASYOTP_API_KEY" \
-H "X-Api-Secret: $EASYOTP_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"phone":"0912345678","country_code":"84"}'
# 2. (Tuỳ chọn) Tra trạng thái gửi
curl "https://eotp.com.vn/api/v1/otp/status?request_id=3f9a1c0e8b7d4a26b5e0c9d18f2a7b64" \
-H "X-Api-Key: $EASYOTP_API_KEY" \
-H "X-Api-Secret: $EASYOTP_API_SECRET"
# 3. Kiểm mã (thay request_id và code)
curl -X POST https://eotp.com.vn/api/v1/otp/verify \
-H "X-Api-Key: $EASYOTP_API_KEY" \
-H "X-Api-Secret: $EASYOTP_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"request_id":"3f9a1c0e8b7d4a26b5e0c9d18f2a7b64","code":"123456"}'
# 4. Xem hạn mức và số dư
curl https://eotp.com.vn/api/v1/project \
-H "X-Api-Key: $EASYOTP_API_KEY" \
-H "X-Api-Secret: $EASYOTP_API_SECRET"
Node.js 18 trở lên (có sẵn fetch), dạng ES module.
const BASE = "https://eotp.com.vn/api/v1";
const API_KEY = process.env.EASYOTP_API_KEY;
const API_SECRET = process.env.EASYOTP_API_SECRET;
async function easyOtp(path, body) {
const res = await fetch(BASE + path, {
method: "POST",
headers: { "X-Api-Key": API_KEY, "X-Api-Secret": API_SECRET, "Content-Type": "application/json" },
body: JSON.stringify(body),
signal: AbortSignal.timeout(35000),
});
const data = await res.json();
return { status: res.status, ...data };
}
// 1. Gửi mã
const sent = await easyOtp("/otp/send", { phone: "0912345678" });
if (!sent.ok) {
throw new Error(`${sent.error}: ${sent.message}`);
}
// Lưu sent.request_id vào phiên của người dùng (session), không lưu mã.
// 2. Kiểm mã người dùng nhập
const check = await easyOtp("/otp/verify", {
request_id: sent.request_id,
code: "123456",
});
if (check.ok && check.verified) {
// Đăng nhập / xác nhận thành công
} else {
console.log(check.error, check.attempts_left);
}
PHP 7.4 trở lên, dùng tiện ích curl có sẵn.
<?php
function easy_otp_call(string $path, array $body): array
{
$ch = curl_init('https://eotp.com.vn/api/v1' . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'X-Api-Key: ' . getenv('EASYOTP_API_KEY'),
'X-Api-Secret: ' . getenv('EASYOTP_API_SECRET'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 35,
]);
$raw = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode((string) $raw, true);
if (!is_array($data)) {
$data = ['ok' => false, 'error' => 'network_error', 'message' => 'Không kết nối được eOTP.'];
}
$data['status'] = $status;
return $data;
}
// 1. Gửi mã
$sent = easy_otp_call('/otp/send', ['phone' => '0912345678']);
if (!$sent['ok']) {
exit($sent['error'] . ': ' . $sent['message']);
}
$_SESSION['otp_request_id'] = $sent['request_id'];
// 2. Kiểm mã người dùng nhập
$check = easy_otp_call('/otp/verify', [
'request_id' => $_SESSION['otp_request_id'],
'code' => (string) ($_POST['code'] ?? ''),
]);
if ($check['ok'] && $check['verified']) {
// Đăng nhập / xác nhận thành công
}
Python 3, thư viện requests (pip install requests).
import os
import requests
BASE = "https://eotp.com.vn/api/v1"
HEADERS = {
"X-Api-Key": os.environ["EASYOTP_API_KEY"],
"X-Api-Secret": os.environ["EASYOTP_API_SECRET"],
}
def easy_otp(path, body):
r = requests.post(BASE + path, json=body, headers=HEADERS, timeout=35)
data = r.json()
data["status"] = r.status_code
return data
# 1. Gửi mã
sent = easy_otp("/otp/send", {"phone": "0912345678"})
if not sent["ok"]:
raise RuntimeError(f"{sent['error']}: {sent['message']}")
request_id = sent["request_id"] # lưu vào phiên của người dùng
# 2. Kiểm mã người dùng nhập
check = easy_otp("/otp/verify", {"request_id": request_id, "code": "123456"})
if check["ok"] and check.get("verified"):
print("Xác minh thành công")
else:
print(check["error"], check.get("attempts_left"))
Kotlin trên máy chủ JVM (Spring, Ktor…), OkHttp 4 và org.json. Trong app Android, gọi máy chủ của bạn thay vì gọi thẳng eOTP.
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.util.concurrent.TimeUnit
private const val BASE = "https://eotp.com.vn/api/v1"
private val apiKey = System.getenv("EASYOTP_API_KEY")
private val apiSecret = System.getenv("EASYOTP_API_SECRET")
private val jsonType = "application/json".toMediaType()
private val client = OkHttpClient.Builder()
.callTimeout(35, TimeUnit.SECONDS)
.build()
fun easyOtp(path: String, body: JSONObject): JSONObject {
val request = Request.Builder()
.url(BASE + path)
.header("X-Api-Key", apiKey)
.header("X-Api-Secret", apiSecret)
.post(body.toString().toRequestBody(jsonType))
.build()
client.newCall(request).execute().use { res ->
return JSONObject(res.body!!.string()).put("status", res.code)
}
}
fun main() {
// 1. Gửi mã
val sent = easyOtp("/otp/send", JSONObject().put("phone", "0912345678"))
if (!sent.getBoolean("ok")) {
println(sent.getString("error") + ": " + sent.getString("message"))
return
}
val requestId = sent.getString("request_id") // lưu vào phiên của người dùng
// 2. Kiểm mã người dùng nhập
val check = easyOtp(
"/otp/verify",
JSONObject().put("request_id", requestId).put("code", "123456"),
)
println(if (check.optBoolean("verified")) "Xác minh thành công" else check.getString("error"))
}
Swift 5.7 trở lên với URLSession, chạy trên máy chủ (Vapor…) hoặc công cụ dòng lệnh. Trong app iOS, gọi máy chủ của bạn thay vì gọi thẳng eOTP.
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
let base = "https://eotp.com.vn/api/v1"
let apiKey = ProcessInfo.processInfo.environment["EASYOTP_API_KEY"] ?? ""
let apiSecret = ProcessInfo.processInfo.environment["EASYOTP_API_SECRET"] ?? ""
func easyOtp(_ path: String, _ body: [String: Any]) async throws -> [String: Any] {
var req = URLRequest(url: URL(string: base + path)!)
req.httpMethod = "POST"
req.timeoutInterval = 35
req.setValue(apiKey, forHTTPHeaderField: "X-Api-Key")
req.setValue(apiSecret, forHTTPHeaderField: "X-Api-Secret")
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, response) = try await URLSession.shared.data(for: req)
var json = (try JSONSerialization.jsonObject(with: data)) as? [String: Any] ?? [:]
json["status"] = (response as? HTTPURLResponse)?.statusCode ?? 0
return json
}
// 1. Gửi mã
let sent = try await easyOtp("/otp/send", ["phone": "0912345678"])
guard sent["ok"] as? Bool == true, let requestId = sent["request_id"] as? String else {
print(sent["error"] as? String ?? "", sent["message"] as? String ?? "")
exit(1)
}
// 2. Kiểm mã người dùng nhập
let check = try await easyOtp("/otp/verify", ["request_id": requestId, "code": "123456"])
print(check["verified"] as? Bool == true ? "Xác minh thành công" : (check["error"] as? String ?? ""))
Luồng khuyến nghị
- Người dùng nhập số điện thoại
Ứng dụng hoặc trang web gửi số lên máy chủ của bạn (không gọi thẳng eOTP).
- Máy chủ gọi
sendTrước khi gọi, máy chủ tự chặn lạm dụng theo IP/thiết bị của người dùng (ví dụ captcha, giới hạn số lần). Lưu
request_idvàphone_e164vào phiên của người dùng. Dùngcode_typeđể mở bàn phím số hay chữ cho ô nhập. - Người dùng nhập mã
Hiện ô nhập mã và đếm ngược theo
expires_in. Muốn báo "đã gửi" chính xác, traotp/status; gặpfailedthì gợi ý gửi lại qua kênh khác. Nút "Gửi lại" nên mở sau một khoảng chờ để người dùng kịp nhận tin. - Máy chủ gọi
verifyLấy
request_idtừ phiên (không tin giá trị do ứng dụng gửi lên), gửi kèm mã người dùng nhập. - Xử lý kết quả
verified: truethì đăng nhập/xác nhận và xoárequest_idkhỏi phiên.wrong_codecho nhập lại;expiredhoặctoo_many_attemptsthì gửi mã mới.
Trên website, bạn không cần tự dựng bước 3–5: dùng Popup xác thực, eOTP hiện ô nhập mã, gửi lại và báo lỗi, máy chủ của bạn chỉ gọi popup/session rồi popup/result.
Bảo mật
- Giữ Secret key trên máy chủ, trong biến môi trường hoặc kho bí mật. Không đưa vào app, JavaScript phía trình duyệt, ảnh chụp màn hình hay kho mã. API key một mình chỉ là định danh, nhúng iframe/SDK được.
- Không ghi mã OTP vào log. eOTP không bao giờ trả mã qua API; bạn cũng không ghi mã người dùng nhập vào log hay công cụ theo dõi lỗi.
- Không ghi Secret key vào log. Che khoá khi in cấu hình hoặc lỗi.
- Để thời gian sống ngắn. 5 phút là đủ cho đa số luồng; chỉnh 1–15 phút trong cài đặt dự án.
- Chặn lạm dụng ở phía bạn. Giới hạn của eOTP tính theo dự án và theo IP máy chủ của bạn, không theo người dùng cuối: bạn nên giới hạn thêm theo IP, thiết bị, tài khoản để kẻ xấu không dùng hết hạn mức.
- Đổi secret khi nghi lộ. Tạo secret mới trong tab Tích hợp, secret cũ mất hiệu lực ngay. Cập nhật máy chủ trước để không dính
auth_rate_limited.
Cần hỗ trợ tích hợp? Gọi hotline +84 901 888 484.
Thay đổi
11/11/2026
- Callback về máy chủ của bạn có thêm header
X-Eotp-Timestamp;X-Eotp-Signaturenay là HMAC-SHA256 củatimestamp + "." + thân thô(chữ ký chỉ trên thân không còn dùng). Bạn cập nhật đoạn kiểm theo Xác minh chữ ký và từ chối lời gọi lệch quá 300 giây.
10/11/2026
- Nút hiện sẵn gọi được popup: nạp
/sdk/eotp-1.jsvà đăng kýeOTP.onButton(handler), người dùng bấm nút eOTP là hàm của bạn chạy để tạo phiên và mởeOTP.open. Trang không đăng ký thì nút mở trang eOTP như cũ. Xem Bấm nút mở popup. - Thêm Popup xác thực cho website, cách tích hợp mặc định: máy chủ gọi
POST /popup/session, trang nạp/sdk/eotp-1.jsvà gọieOTP.open, máy chủ xác nhận bằngPOST /popup/result. Số lần gửi lại trong popup đặt ở Cài đặt gửi của dự án. Lỗi mớipopup_not_supported,invalid_result. - Nút nhúng
/embed/<api_key>có 6 kiểu, giao diện tối/sáng và cỡ 28/32/36: chọn ở tab API key & tích hợp của dự án, hoặc truyềnstyle,theme,sizetrên đường dẫn iframe. Mã cũ 180 × 40 vẫn hoạt động; bạn nên thay bằng mã mới để nút khít khung. otp/sendcó chế độ hàng đợi: trả ngay"status": "queued"vàeta_ms; lúc đông vẫn nhận yêu cầu, chỉ tăngeta_ms.- Thêm
GET /otp/statusđể tra trạng thái gửi (queued,sending,sent,failed,expired). otp/sendnhậncallback_url(tuỳ chọn): eOTP POST kết quả gửi về máy chủ của bạn, ký bằngX-Eotp-Signature, gửi lại tối đa 3 lần. Lỗi mớiinvalid_callback_url.
9/11/2026
- Xác thực bằng 2 khoá:
X-Api-Key+X-Api-Secret(hoặcBearer key:secret). Khoá cũ một khoá còn dùng 30 ngày kèm headerDeprecation, sau đó401 legacy_auth_expired. - Chặn IP tạm thời khi xác thực sai nhiều lần liên tiếp:
429 auth_rate_limited. - Số điện thoại nhiều nước:
phonedạng E.164 hoặc nội địa +country_code; phản hồi cóphone_e164. Lỗi mớicountry_not_supported,channel_not_supported_in_country; kênh đổi theo nước cóchannel_note. - Kiểu mã
alnum(chữ + số, so không phân biệt hoa/thường) và độ dài 4–10 đặt trong Cài đặt gửi của dự án. - Hạn mức tháng và ngày tính theo bể tài khoản (mọi dự án dùng chung); thêm
daily_quota_exceeded. Gói FREE gửi một kênh. - Ví theo loại tiền (VND, USD) và ví tặng: thêm
currency,cost,balanceởsendvàbalance_minorởproject; các trường*_vndgiữ cho tương thích. licensetrảevidence; DEVELOPER mở khi đủ bằng chứng tích hợp thật.