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.

Cần khoá API? Tạo khoá trong tài khoản.

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.
Địa chỉ gốc
https://vietotp.com/api/public/v1

Khoá 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ũ.
Header
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.

Thành công
{
  "ok": true,
  "data": { "username": "shopcuaban", "balance": 150000, "currency": "VND" }
}
Lỗi
{
  "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
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
kindOTP, SMS hoặc ZALO
serviceMã dịch vụ để thuê số, ví dụ shopee
name, serverTên ứng dụng và máy chủ, ví dụ Shopee, Máy chủ 2
priceGiá mỗi đơn, tính bằng đồng
availablefalse là đang tạm ngừng bán
networksNhà mạng chọn được; rỗng là không chọn được nhà mạng
sendSMS và Zalo: nơi nhận, nội dung chọn sẵn và số tin tối đa
Một dòng trả về
{
  "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ườngBắt buộcÝ nghĩa
kindCóOTP, SMS hoặc ZALO
serviceCóTrường service lấy từ danh sách dịch vụ
networkKhôngMột nhà mạng trong networks; bỏ trống là ngẫu nhiên
maxPriceKhôngGiá cao nhất bạn chấp nhận, tính bằng đồng
curl
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ư
EXPIREDHết giờ không về mã, tiền đã hoàn vào số dư
FAILEDKhông tạo được đơn, tiền đã hoàn vào số dư
Đơn đã có mã
{
  "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
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
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ạiGiới hạn
Mỗi khoá API600 yêu cầu mỗi phút
Mỗi địa chỉ IP1.200 yêu cầu mỗi phút
Thuê số, thuê lại300 lần mỗi phút mỗi tài khoản
Huỷ đơn300 lần mỗi phút mỗi tài khoản
Gửi tin120 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ý
UNAUTHORIZED401Thiếu header Authorization hoặc khoá sai định dạng
INVALID_KEY401Khoá không đúng hoặc đã bị thu hồi
INVALID_INPUT400Dữ liệu gửi lên chưa hợp lệ, xem error.message
INSUFFICIENT_BALANCE402Số dư không đủ, nạp thêm tiền trên web
PRICE_CHANGED409Giá đã cao hơn maxPrice; error.newPrice là giá mới
SERVICE_UNAVAILABLE404, 409Dịch vụ không có hoặc đang tạm ngừng
OUT_OF_STOCK409Tạm hết số, thử máy chủ hoặc nhà mạng khác
NETWORK_INVALID400Nhà mạng không có trong networks của dịch vụ
NOT_FOUND404Không có đơn này trong tài khoản của bạn
CANCEL_TOO_EARLY409Đơn chưa đủ 30 giây, đợi rồi huỷ lại
ORDER_ENDED409Đơn đã kết thúc, không thao tác thêm được
INVALID_CONTENT400Nội dung tin không được phép
ORDER_UNCERTAIN503Chưa xác định được kết quả, tiền đã hoàn; thuê lại nếu cần
RATE_LIMITED429Gửi quá nhanh, chờ theo Retry-After
OTP_PAUSED503Tạm dừng thuê sim OTP, thử lại sau ít phút
MAINTENANCE503VietOTP đang bảo trì
API_DISABLED503API đ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ã.

Node.js 18 trở lên
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')
Python 3 (requests)
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á.