Tài liệu API VietOTP
Phần mềm của bạn thuê số, nhận mã và huỷ đơn qua API, trừ tiền vào cùng số dư với tài khoản trên web.
Bắt đầu trong 3 bước
Mọi yêu cầu gửi tới cùng một địa chỉ và trả về JSON.
Địa chỉ gốc của API:
- Đăng nhập, vào Tài khoản, chọn Khoá API rồi bấm Tạo khoá API. Khoá chỉ hiện một lần, bạn lưu lại ngay.
- Gửi khoá trong header Authorization: Bearer <khoá> ở mọi yêu cầu.
- Nạp tiền trên web. API trừ tiền vào số dư đó, đúng giá đang hiển thị, không phí ẩn.
https://vietotp.com/api/public/v1Khoá API và bảo mật
Khoá thay cho tên đăng nhập và mật khẩu, ai có khoá đều thuê số được bằng số dư của bạn.
- Chỉ dùng khoá trên máy chủ của bạn. Không đặt khoá trong mã chạy trên trình duyệt, ứng dụng phát hành cho người khác hay kho mã công khai.
- Mỗi tài khoản có tối đa 10 khoá. Bạn thu hồi khoá bất cứ lúc nào trong trang Khoá API, khoá bị thu hồi ngừng hoạt động ngay.
- Đổi mật khẩu hoặc đặt lại mật khẩu sẽ thu hồi mọi khoá. Sau đó bạn tạo khoá mới.
- VietOTP chỉ lưu dạng băm của khoá, nên không xem lại hay gửi lại được khoá cũ.
Authorization: Bearer vo_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxĐịnh dạng trả về
Thành công có ok là true, lỗi có ok là false kèm mã lỗi và lời nhắn tiếng Việt.
Mã trạng thái HTTP phản ánh kết quả: 200 hoặc 201 là thành công, 4xx là yêu cầu chưa hợp lệ hoặc bị từ chối, 5xx là phía VietOTP chưa xử lý được. Tiền tính bằng đồng, số nguyên. Thời gian theo chuẩn ISO 8601, múi giờ UTC.
Dữ liệu gửi lên là JSON với Content-Type: application/json. Trường không có trong tài liệu sẽ bị từ chối với mã INVALID_INPUT.
{
"ok": true,
"data": { "username": "shopcuaban", "balance": 150000, "currency": "VND" }
}{
"ok": false,
"error": { "code": "INSUFFICIENT_BALANCE", "message": "Số dư không đủ, vui lòng nạp thêm tiền." }
}Xem số dư
GET/me
Trả về tên đăng nhập và số dư hiện tại.
curl https://vietotp.com/api/public/v1/me \
-H "Authorization: Bearer $VIETOTP_KEY"Danh sách dịch vụ và giá
GET/services
Thêm ?kind=OTP, ?kind=SMS hoặc ?kind=ZALO để lọc theo loại số. Mỗi dịch vụ có giá, trạng thái còn bán và danh sách nhà mạng chọn được. Danh sách và giá được làm mới khoảng mỗi phút.
Một ứng dụng có thể có nhiều máy chủ. Mỗi máy chủ là một dòng riêng với giá riêng, và trường service của dòng đó là giá trị bạn gửi khi thuê số.
| Trường | Ý nghĩa |
|---|---|
kind | OTP, SMS hoặc ZALO |
service | Mã dịch vụ để thuê số, ví dụ shopee |
name, server | Tên ứng dụng và máy chủ, ví dụ Shopee, Máy chủ 2 |
price | Giá mỗi đơn, tính bằng đồng |
available | false là đang tạm ngừng bán |
networks | Nhà mạng chọn được; rỗng là không chọn được nhà mạng |
send | SMS và Zalo: nơi nhận, nội dung chọn sẵn và số tin tối đa |
{
"kind": "OTP",
"service": "shopee",
"name": "Shopee",
"server": "Máy chủ 1",
"price": 3700,
"available": true,
"networks": [],
"send": null
}Thuê số
POST/orders
Trừ đúng giá đang bán vào số dư và trả về đơn kèm số điện thoại, mã HTTP 201. Gửi maxPrice để không bị trừ nhiều hơn mức bạn chấp nhận: nếu giá đã tăng, đơn không được tạo và bạn nhận mã PRICE_CHANGED kèm giá mới.
Một lần thuê thường trả lời trong vài giây, đôi khi lâu hơn. Bạn nên đặt thời gian chờ của phần mềm từ 130 giây. Nếu mất kết nối trước khi có trả lời, bạn gọi GET /orders/active để xem đơn đã được tạo chưa rồi mới thuê lại, tránh thuê trùng hai số.
| Trường | Bắt buộc | Ý nghĩa |
|---|---|---|
kind | Có | OTP, SMS hoặc ZALO |
service | Có | Trường service lấy từ danh sách dịch vụ |
network | Không | Một nhà mạng trong networks; bỏ trống là ngẫu nhiên |
maxPrice | Không | Giá cao nhất bạn chấp nhận, tính bằng đồng |
curl -X POST https://vietotp.com/api/public/v1/orders \
-H "Authorization: Bearer $VIETOTP_KEY" \
-H "Content-Type: application/json" \
-d '{"kind":"OTP","service":"shopee","maxPrice":4000}'Lấy mã OTP
GET/orders/{code}
code là mã đơn trả về khi thuê, ví dụ VO7K2M9QXA. Bạn gọi lại mỗi 3 đến 5 giây cho tới khi status khác WAITING. Khi mã về, trường otp có mã và smsText có nguyên văn tin nhắn.
Muốn theo dõi nhiều đơn cùng lúc, bạn gọi GET /orders/active một lần thay vì gọi từng đơn.
| status | Ý nghĩa |
|---|---|
CREATING | Đang tạo đơn, chưa có số |
WAITING | Đã có số, đang chờ mã |
SUCCESS | Đã nhận mã hoặc đã gửi tin, đơn được tính tiền |
CANCELLED | Đã huỷ, tiền đã hoàn vào số dư |
EXPIRED | Hết giờ không về mã, tiền đã hoàn vào số dư |
FAILED | Không tạo được đơn, tiền đã hoàn vào số dư |
{
"ok": true,
"data": {
"order": {
"code": "VO7K2M9QXA",
"kind": "OTP",
"serviceName": "Shopee – Máy chủ 1",
"phone": "0912345678",
"status": "SUCCESS",
"price": 3700,
"otp": "482913",
"smsText": "Ma xac minh cua ban la 482913",
"refunded": false,
"expiresAt": "2026-11-08T03:15:00.000Z",
"canCancel": false
},
"balance": 146300
}
}Danh sách đơn
GET/orders
GET /orders/active trả về các đơn đang chờ và đơn vừa kết thúc trong 30 phút qua, tối đa 200 đơn.
GET /orders trả về lịch sử, mới nhất trước. Tham số: page (từ 1), pageSize (10, 20, 50 hoặc 100), kind và status để lọc.
curl "https://vietotp.com/api/public/v1/orders?status=SUCCESS&pageSize=50" \
-H "Authorization: Bearer $VIETOTP_KEY"Huỷ đơn và hoàn tiền
POST/orders/{code}/cancel
Huỷ đơn chưa có mã để nhận lại tiền ngay. Đơn cần chạy ít nhất 30 giây mới huỷ được; trường cancelableAt cho biết lúc huỷ được. Không huỷ thì khi hết giờ mà chưa có mã, đơn tự hoàn tiền 100%.
Thuê số SMS huỷ được trong 3 phút đầu và khi chưa gửi tin nào. Đơn đã có mã thì không huỷ và không hoàn tiền.
Thuê lại số cũ
POST/orders/{code}/rerent
Thuê lại đúng số của một đơn đã kết thúc, khi số đó còn trống và chưa ai thuê sau bạn. Trường canRerent và rerentPrice trên đơn cho biết có thuê lại được không và giá bao nhiêu. Có thể gửi maxPrice như khi thuê số.
Gửi tin kích hoạt (SMS, Zalo)
POST/orders/{code}/send
Gửi tin từ số đã thuê. Trường send trên đơn cho biết nơi nhận được phép (receivers), nội dung chọn sẵn (contentOptions) và số tin tối đa.
- to: nơi nhận. Dịch vụ có danh sách nơi nhận thì to phải là một số trong danh sách đó (bỏ trống là số đầu tiên). Dịch vụ không có danh sách thì to là một số di động Việt Nam.
- text: nội dung tin. Dịch vụ có nội dung chọn sẵn thì text phải là một trong số đó. Tối đa 160 ký tự gồm chữ, số và dấu câu thông thường, không chứa đường dẫn web.
- Thuê số Zalo gửi được một tin mỗi đơn. Thuê số SMS gửi được vài tin mỗi đơn, đã gửi tin thì đơn không còn hoàn tiền.
curl -X POST https://vietotp.com/api/public/v1/orders/VO7K2M9QXA/send \
-H "Authorization: Bearer $VIETOTP_KEY" \
-H "Content-Type: application/json" \
-d '{"to":"8085","text":"DK"}'Nghe mã gửi bằng cuộc gọi
GET/orders/{code}/audio
Khi ứng dụng đọc mã qua cuộc gọi, đơn có hasAudio là true và audioUrl trỏ tới bản ghi âm. Gọi đường dẫn đó kèm khoá API để tải về.
Giới hạn yêu cầu
Đây là giới hạn kỹ thuật để một phần mềm chạy lỗi không làm chậm cả hệ thống.
Vượt giới hạn, bạn nhận HTTP 429 kèm header Retry-After cho biết số giây cần chờ. Khi một máy chủ đang nhận nhiều đơn cùng lúc, yêu cầu thuê có thể phải chờ vài giây, hoặc trả về RATE_LIMITED để bạn thử lại sau.
| Loại | Giới hạn |
|---|---|
Mỗi khoá API | 600 yêu cầu mỗi phút |
Mỗi địa chỉ IP | 1.200 yêu cầu mỗi phút |
Thuê số, thuê lại | 300 lần mỗi phút mỗi tài khoản |
Huỷ đơn | 300 lần mỗi phút mỗi tài khoản |
Gửi tin | 120 lần mỗi phút mỗi tài khoản |
Gửi sai khoá | 20 lần trong 5 phút mỗi IP, sau đó IP chờ 5 phút |
Mã lỗi thường gặp
Mỗi lỗi có lời nhắn tiếng Việt trong error.message, bạn có thể hiện thẳng cho người dùng.
Mã lỗi khác có thể xuất hiện; bạn đọc error.message để biết nguyên nhân. Lỗi 5xx không trừ tiền, hoặc đã hoàn tiền nếu đơn đã bị trừ.
| Mã | HTTP | Ý nghĩa và cách xử lý |
|---|---|---|
UNAUTHORIZED | 401 | Thiếu header Authorization hoặc khoá sai định dạng |
INVALID_KEY | 401 | Khoá không đúng hoặc đã bị thu hồi |
INVALID_INPUT | 400 | Dữ liệu gửi lên chưa hợp lệ, xem error.message |
INSUFFICIENT_BALANCE | 402 | Số dư không đủ, nạp thêm tiền trên web |
PRICE_CHANGED | 409 | Giá đã cao hơn maxPrice; error.newPrice là giá mới |
SERVICE_UNAVAILABLE | 404, 409 | Dịch vụ không có hoặc đang tạm ngừng |
OUT_OF_STOCK | 409 | Tạm hết số, thử máy chủ hoặc nhà mạng khác |
NETWORK_INVALID | 400 | Nhà mạng không có trong networks của dịch vụ |
NOT_FOUND | 404 | Không có đơn này trong tài khoản của bạn |
CANCEL_TOO_EARLY | 409 | Đơn chưa đủ 30 giây, đợi rồi huỷ lại |
ORDER_ENDED | 409 | Đơn đã kết thúc, không thao tác thêm được |
INVALID_CONTENT | 400 | Nội dung tin không được phép |
ORDER_UNCERTAIN | 503 | Chưa xác định được kết quả, tiền đã hoàn; thuê lại nếu cần |
RATE_LIMITED | 429 | Gửi quá nhanh, chờ theo Retry-After |
OTP_PAUSED | 503 | Tạm dừng thuê sim OTP, thử lại sau ít phút |
MAINTENANCE | 503 | VietOTP đang bảo trì |
API_DISABLED | 503 | API đang tạm ngừng, web vẫn dùng được |
Ví dụ đầy đủ
Thuê một số Shopee, chờ mã tối đa 5 phút, huỷ nếu không về mã.
const BASE = 'https://vietotp.com/api/public/v1'
const headers = { Authorization: 'Bearer ' + process.env.VIETOTP_KEY, 'Content-Type': 'application/json' }
async function call(method, path, body) {
const res = await fetch(BASE + path, { method, headers, body: body ? JSON.stringify(body) : undefined })
const json = await res.json()
if (!json.ok) throw new Error(json.error.code + ': ' + json.error.message)
return json.data
}
const { order } = await call('POST', '/orders', { kind: 'OTP', service: 'shopee', maxPrice: 4000 })
console.log('Số:', order.phone)
const deadline = Date.now() + 5 * 60_000
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 4000))
const { order: o } = await call('GET', '/orders/' + order.code)
if (o.status === 'SUCCESS') { console.log('Mã:', o.otp); process.exit(0) }
if (o.status !== 'WAITING') { console.log('Đơn kết thúc:', o.status); process.exit(1) }
}
await call('POST', '/orders/' + order.code + '/cancel')import os, time, requests
BASE = "https://vietotp.com/api/public/v1"
s = requests.Session()
s.headers["Authorization"] = "Bearer " + os.environ["VIETOTP_KEY"]
def call(method, path, body=None):
data = s.request(method, BASE + path, json=body, timeout=130).json()
if not data["ok"]:
raise RuntimeError(data["error"]["code"] + ": " + data["error"]["message"])
return data["data"]
order = call("POST", "/orders", {"kind": "OTP", "service": "shopee", "maxPrice": 4000})["order"]
print("Số:", order["phone"])
deadline = time.time() + 300
while time.time() < deadline:
time.sleep(4)
o = call("GET", "/orders/" + order["code"])["order"]
if o["status"] == "SUCCESS":
print("Mã:", o["otp"])
break
if o["status"] != "WAITING":
print("Đơn kết thúc:", o["status"])
break
else:
call("POST", "/orders/" + order["code"] + "/cancel")Câu hỏi thường gặp
API có mất phí riêng không?
Không. API dùng chung số dư với tài khoản trên web, mỗi đơn trừ đúng giá hiển thị trong danh sách dịch vụ, không phí ẩn.
Không về mã qua API có được hoàn tiền không?
Có, giống hệt trên web. Hết giờ mà chưa có mã, đơn tự chuyển sang EXPIRED và hoàn tiền 100% vào số dư. Bạn cũng có thể huỷ trước khi mã về để nhận lại tiền ngay.
Đơn thuê qua API có hiện trên web không?
Có. Đơn thuê qua API nằm trong mục Đơn hàng như đơn thuê trên web, và ngược lại API cũng đọc được đơn thuê trên web.
Nạp tiền qua API được không?
Chưa. Bạn nạp tiền bằng mã QR trên web, số dư dùng được ngay cho cả web và API.
Tôi lỡ để lộ khoá API thì làm gì?
Bạn vào Tài khoản, chọn Khoá API và bấm Thu hồi ở khoá đó, khoá ngừng hoạt động ngay. Sau đó tạo khoá mới cho phần mềm của bạn. Đổi mật khẩu cũng thu hồi mọi khoá.