Tài liệu API

Tạo link tiếp thị và lấy đơn hàng phát sinh từ chính những link đó. Lấy khoá API

Bắt đầu

Địa chỉ gốc của mọi đường dẫn:

https://server.hoantien24h.vn/api/pub/v1

Mọi lượt gọi phải mang khoá ở header X-Hoantien-Api-Key. Cũng nhận Authorization: Bearer ht_live_... nếu thư viện của bạn tiện hơn với cách đó.

curl https://server.hoantien24h.vn/api/pub/v1/me \
  -H "X-Hoantien-Api-Key: ht_live_..."

Đừng đặt khoá trong mã chạy ở trình duyệt. Khoá thay mặt tài khoản của bạn tạo link và đọc đơn. API cố ý không trả header chia sẻ tài nguyên giữa các gốc, nên gọi thẳng từ trình duyệt sẽ bị chặn. Hãy gọi từ máy chủ của bạn.

Khoá thử nghiệm

Khoá có tiền tố ht_test_ gọi được mọi đường dẫn nhưng khi tạo link sẽ trả về link giả, không đụng tới sàn và không tiêu hạn mức tạo link thật. Dùng nó khi viết và chạy thử mã, rồi mới đổi sang khoá ht_live_.

GET /orders — lấy đơn hàng

Mặc định chỉ trả đơn phát sinh từ link bạn tạo qua API.

Tham sốMô tả
sub_idLọc theo mã theo dõi của bạn.
link_idLọc theo đúng một link.
statusMột trong pending, confirmed, paid, cancelled.
fromTừ ngày. 2026-09-01 được hiểu là cả ngày đó theo giờ Việt Nam. Gửi chuỗi ISO-8601 đầy đủ nếu cần chính xác hơn.
toĐến ngày, cùng quy ước.
limitMỗi trang, tối đa 100, mặc định 20.
cursorCon trỏ trang sau, lấy từ paging.next_cursor.
scopeĐể all nếu muốn thấy cả đơn cá nhân của bạn, không chỉ đơn từ API.
curl "https://server.hoantien24h.vn/api/pub/v1/orders?sub_id=nh%C3%B3m%20zalo%20%237&status=paid" \
  -H "X-Hoantien-Api-Key: ht_live_..."
{
  "success": true,
  "data": [
    {
      "id": "2605090JDW730P",
      "status": "paid",
      "platform": "shopee",
      "sub_id": "nhóm zalo #7",
      "link_id": "A7bK2x9",
      "commission": 5166,
      "items": [
        { "name": "...", "quantity": 2, "price": 100000, "amount": 200000, "shop": "Shop A" }
      ],
      "purchased_at": "2026-08-20T02:11:00.000Z",
      "confirmed_at": "2026-08-24T09:00:00.000Z",
      "paid_at": "2026-09-05T01:00:00.000Z"
    }
  ],
  "paging": { "has_more": true, "next_cursor": "MTc1NjA..." }
}

price là giá một đơn vị, amount là thành tiền cả dòng đã nhân số lượng và trừ khuyến mãi. Đừng nhân price với quantity để ra thành tiền.

Vòng đời một đơn, và vì sao tiền về chậm

Đây là phần hay gây hiểu nhầm nhất, nên nói thẳng: đơn không về theo thời gian thực.

Trạng tháiNghĩa là gì
pendingĐơn đã ghi nhận, sàn chưa xác nhận hoàn thành.
confirmedSàn đã báo hoàn thành. Bắt đầu chờ kỳ đối soát.
paidTiền đã vào ví của bạn. Chỉ tới lúc này mới rút được.
cancelledĐơn bị huỷ hoặc hoàn. Có thể xảy ra sau khi đã từng hiện ra.

Đơn Shopee được đồng bộ vài lần mỗi ngày nên đơn mới có độ trễ tính bằng giờ, không phải bằng giây. Từ lúc mua tới lúc tiền thật vào ví thường mất khoảng hai tuần rưỡi, vì còn phải chờ sàn đối soát và trả tiền.

Vì vậy đừng dựng hệ thống trả thưởng cho người dùng cuối dựa trên trạng thái pending. Một phần đơn sẽ chuyển thành cancelled.

GET /stats, /balance, /me

/stats?group_by=day hoặc group_by=sub_id trả về số đơn, hoa hồng, số đơn đã trả tiền và số đơn huỷ theo từng nhóm. Ngày được gom theo giờ Việt Nam.

/balance trả số dư khả dụng, số đang chờ và tổng đã rút, đơn vị đồng. /me trả trạng thái tài khoản, hạn mức đang áp và tỷ lệ hoa hồng của bạn.

Hạn mức

Hạn mức tính theo tài khoản, không theo từng khoá, nên tạo thêm khoá không làm tăng hạn mức. Mỗi phản hồi mang các header RateLimit-Limit, RateLimit-RemainingRateLimit-Reset; hãy đọc chúng để tự điều tiết thay vì cứ gọi tới khi bị chặn.

Vượt hạn mức trả về mã 429 kèm errorCode: "RATE_LIMIT". Xem hạn mức hiện tại của bạn ở bảng điều khiển.

Lỗi

Mọi lỗi đều có cùng hình dạng, và errorCode là thứ nên dựa vào chứ không phải câu chữ:

{ "success": false, "errorCode": "RATE_LIMIT", "message": "..." }
Mã lỗiNghĩa
MISSING_API_KEYKhông gửi khoá.
INVALID_API_KEYKhoá sai định dạng hoặc không tồn tại.
KEY_REVOKEDKhoá đã bị thu hồi.
HUB_NOT_ACTIVETài khoản chưa được duyệt dùng API.
HUB_BLOCKEDQuyền dùng API đã bị chặn, xem message để biết lý do.
USER_BANNEDTài khoản bị tạm khoá.
RATE_LIMITVượt hạn mức, xem header RateLimit-Reset.
MISSING_URLThiếu trường url khi tạo link.
UNKNOWN_PLATFORMKhông nhận ra link thuộc sàn nào.
INVALID_SHOPEE_URLLink Shopee không phải link sản phẩm, ví dụ link video hoặc trang shop.
PRODUCT_NOT_SUPPORTEDSản phẩm này không tạo được link tiếp thị.
ORDER_NOT_FOUNDKhông có đơn đó, hoặc đơn không thuộc về bạn.
UPSTREAM_ERRORSàn hoặc nhà cung cấp trung gian đang lỗi. Thử lại sau.

Các sàn được hỗ trợ

API hiện phục vụ ba sàn. Link của sàn khác sẽ bị từ chối với mã UNKNOWN_PLATFORM.

SànTên miền nhận đượcĐếm lượt bấmSố tiền hoa hồng
Shopeeshopee.vn, shp.ee, shope.eeCó, amount là số đồng
Shopee Foodspf.shopee.vn, shopeefood.vnKhông, và cũng không có thông tin món ăn
TikTok Shoptiktok.comtext là số đồng, amountnull

Không cần khai sàn khi tạo link, hệ thống tự nhận từ đường dẫn. Nhận cả link rút gọn lẫn cả đoạn văn bản chia sẻ có link nằm lẫn bên trong, kiểu nút Chia sẻ của Shopee chép ra nguyên một đoạn quảng cáo.

Đúng sàn vẫn chưa đủ, phải đúng link sản phẩm. Link trang chủ, trang tìm kiếm, trang cửa hàng, link video và link phát trực tiếp đều bị từ chối với mã INVALID_SHOPEE_URL kèm trường kind cho biết dán nhầm loại nào. Riêng link video và phát trực tiếp bị chặn có chủ ý: chúng không sinh ra hoa hồng.

TikTok Shop chặt hơn hẳn: chỉ tạo được link cho sản phẩm của shop đã mở hợp tác, nên tỷ lệ bị từ chối với mã PRODUCT_NOT_SUPPORTED khá cao. Đây là giới hạn của bên cung cấp, không phải của chúng tôi, và không có cách nào biết trước một sản phẩm có tạo được link hay không ngoài việc thử.

Webhook: nhận đơn ngay, khỏi hỏi định kỳ

Khai một địa chỉ https ở bảng điều khiển. Mỗi khi có đơn mới hoặc đơn đổi trạng thái, chúng tôi gửi ngay tới đó, không phải chờ bạn hỏi.

Sự kiệnKhi nào bắn
order.createdĐơn lần đầu được ghi nhận.
order.updatedĐơn đổi trạng thái, kể cả lúc chuyển sang paid tức tiền đã vào ví.

Thân gửi tới có dạng:

{
  "event": "order.created",
  "event_id": "9f2c...",
  "created_at": "2026-09-03T04:30:57.705Z",
  "data": { ... giống hệt một phần tử của GET /orders ... }
}

Kiểm chữ ký trước khi tin. Băm HMAC-SHA256 chuỗi <X-Hoantien-Timestamp>.<thân thô> bằng khoá ký của bạn, rồi so với header X-Hoantien-Signature.

// Express: PHẢI lấy thân THÔ, không dùng express.json() cho đường này
app.post('/webhook/hoantien24h', express.raw({ type: '*/*' }), (req, res) => {
  const moc = req.get('X-Hoantien-Timestamp')
  const kyMongDoi = 'sha256=' + crypto
    .createHmac('sha256', process.env.HOANTIEN_WEBHOOK_SECRET)
    .update(moc + '.' + req.body.toString())
    .digest('hex')

  if (kyMongDoi !== req.get('X-Hoantien-Signature')) return res.sendStatus(403)

  res.sendStatus(200)          // trả 2xx TRƯỚC, xử lý sau
  const su = JSON.parse(req.body.toString())
  xuLy(su)                     // nhớ chống trùng theo su.event_id
})

Hai chỗ dễ sai: băm trên thân đã qua bộ phân tích JSON (thứ tự khoá và khoảng trắng đổi, chữ ký đúng cũng thành sai), và quên rằng mốc thời gian nằm TRƯỚC dấu chấm.

Trả 2xx trong vòng 8 giây. Quá hạn hoặc trả mã lỗi thì chúng tôi thử lại 5 lần, giãn dần 1 phút, 5 phút, 30 phút, 2 giờ, 6 giờ. Nên trả 2xx ngay rồi xử lý bất đồng bộ, đừng để chúng tôi chờ bạn ghi cơ sở dữ liệu xong.

Hỏng liên tiếp 20 lượt thì địa chỉ tự bị tắt, và bạn đọc được lý do ở bảng điều khiển. Bật lại bất cứ lúc nào.

Cùng một sự kiện có thể tới hai lần. Hãy chống trùng theo event_id, đừng dựa vào việc chúng tôi gửi đúng một lần. Sự kiện thử gửi từ nút “Gửi thử” mang thêm "test": true, đừng cộng nó vào sổ sách.

Địa chỉ nhận phải là https và không được trỏ vào mạng nội bộ hay địa chỉ vòng lặp. Đây là chỗ máy chủ của chúng tôi tự gọi vào, nên mở cho địa chỉ nội bộ là mở đường dò quét mạng riêng.

Vẫn nên hỏi định kỳ một nhịp thưa

Webhook không thay thế hoàn toàn GET /orders. Nếu máy chủ của bạn chết quá 9 tiếng thì chúng tôi đã bỏ cuộc và sự kiện đó mất luôn. Một lượt quét đối soát mỗi vài giờ là đủ để bắt lại phần thiếu.