Kết nối gian hàng (OAuth)
Luồng uỷ quyền OAuth để kết nối gian hàng Shopee, Lazada, TikTok Shop vào Hubport
Gian hàng sàn được kết nối bằng luồng uỷ quyền OAuth trong cửa sổ popup: chủ shop đăng nhập sàn và đồng ý uỷ quyền cho ứng dụng Hubport; Hubport nhận mã uỷ quyền, đổi lấy token (lưu mã hoá phía máy chủ) và tạo kênh. Shop không cần khoá ứng dụng nào. Kết nối lại một gian hàng đã có (ví dụ khi uỷ quyền hết hạn) dùng đúng luồng này; kênh cũ được cập nhật thay vì tạo mới.
Quyền:
setting.sale. Ở Hubport SaaS cần gói có tính năng kết nối sàn (thiếu: HTTP 402error_feature_requires_plan).
Chủ shop phải bật xác thực 2 lớp (Hubport SaaS)
Kết nối sàn cho phép Hubport nhận dữ liệu người mua (tên, số điện thoại, địa chỉ). Vì vậy chủ shop phải bật xác thực 2 lớp
(2FA) trước khi kết nối, kể cả khi người bấm kết nối là nhân viên có quyền setting.sale. Chưa bật thì bước 1 và bước 3 trả HTTP
403 với error_marketplace_two_factor_required (người gọi là chủ shop) hoặc error_marketplace_owner_two_factor_required
(người gọi là nhân viên), kèm detail.enroll_path (trang bật 2FA trong app: Hồ sơ cá nhân > Xác thực 2 lớp).
Khi shop đã có kết nối sàn đang hoạt động, chủ shop luôn phải qua bước 2FA lúc đăng nhập và không tắt được 2FA
(xem Đăng nhập, bước 3).
{
"error": ["error_marketplace_two_factor_required"],
"detail": { "is_owner": true, "enroll_path": "/profile/twofactor" }
}Tổng quan luồng
Trang quản trị (cửa sổ chính) Popup Sàn / Hubport
----------------------------- ----- -------------
1. Người dùng bấm "Kết nối" ──mở popup──> about:blank
2. GET /v1/ecomplatformurls?type=.. ─────────────────────────────────────> trả { url, callback_origin }
3. popup.location = url ──────────────> trang uỷ quyền của sàn
4. chủ shop đăng nhập, đồng ý
5. sàn chuyển về trang callback Hubport
6. trang callback postMessage ──> window.opener
7. Nhận message {code, shop_id?, state}
8. POST /v1/ecom<sàn>/oauthcomplete (JWT) ─────────────────────────────────> tạo / cập nhật kênh1. Lấy link uỷ quyền
GET /v1/ecomplatformurls
| Tên | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
type | int | Có | 7 (Shopee), 11 (Lazada), 9 (TikTok Shop) |
Response (HTTP 200)
| Trường | Kiểu | Mô tả |
|---|---|---|
company_id | int | ID công ty |
id | string | Mã yêu cầu (UUID) |
type | int | Loại sàn |
url | string | Link trang uỷ quyền của sàn (đã chứa state) |
callback_origin | string | Origin của trang callback sẽ gửi postMessage về. Chỉ nhận message từ origin này |
date_created | int | Thời điểm tạo (Unix timestamp) |
{
"company_id": 1001,
"id": "7b0f3c8e-5d1a-4b7e-9f0a-2c6d1e4f8a90",
"type": 7,
"url": "<LINK_UỶ_QUYỀN_CỦA_SÀN>",
"callback_origin": "<CALLBACK_ORIGIN>",
"date_created": 1760000000
}Lazada và TikTok Shop còn có endpoint riêng trả cùng dữ liệu (không có callback_origin): GET /v1/ecomlazada/authurl, GET /v1/ecomtiktokshop/authurl.
state
state nằm sẵn trong url, là chuỗi không cần đọc nội dung. state gắn với công ty và người dùng đã xin link, có hiệu lực 10 phút và chỉ dùng được một lần. Hết hạn thì xin link mới.
2. Mở popup và chờ kết quả
- Mở popup ngay trong sự kiện click (trước khi gọi API ở bước 1) rồi gán
popup.location.href = urlsau khi có link; mở popup sauawaitdễ bị trình duyệt chặn. - Chủ shop đăng nhập sàn và đồng ý uỷ quyền. Sàn chuyển trình duyệt về trang callback của Hubport.
- Trang callback không tạo kênh; nó chỉ gửi kết quả về cửa sổ đã mở popup bằng
window.opener.postMessage(chỉ tới đúng origin của trang đã xin link uỷ quyền, và origin đó phải là trang quản trị Hubport), rồi tự đóng.
Message thành công:
{
"target": "ecomplatform_oauth_code",
"data": { "type": 7, "code": "<CODE>", "shop_id": "123456789", "state": "<STATE>" }
}shop_id chỉ có với Shopee. Lazada và TikTok Shop: { "type", "code", "state" }.
Message lỗi:
{ "target": "ecomplatform_error", "error": "error_oauth_state_invalid" }Khi nhận message, kiểm tra event.source đúng là popup đã mở, event.origin đúng callback_origin, và data.type đúng loại sàn đã chọn.
window.addEventListener("message", async (event) => {
if (event.source !== popup || event.origin !== callbackOrigin) return;
const msg = event.data;
if (msg?.target === "ecomplatform_error") { showError(msg.error); return; }
if (msg?.target !== "ecomplatform_oauth_code" || Number(msg.data.type) !== selectedType) return;
const path = { 7: "/v1/ecomshopee/oauthcomplete", 11: "/v1/ecomlazada/oauthcomplete", 9: "/v1/ecomtiktokshop/oauthcomplete" }[selectedType];
await fetch("https://api.hubport.vn" + path, {
method: "POST",
headers: { "Authorization": jwt, "Content-Type": "application/json" },
body: JSON.stringify({ code: msg.data.code, shop_id: msg.data.shop_id, state: msg.data.state }),
});
});3. Hoàn tất kết nối
| Sàn | Endpoint |
|---|---|
| Shopee | POST /v1/ecomshopee/oauthcomplete |
| Lazada | POST /v1/ecomlazada/oauthcomplete |
| TikTok Shop | POST /v1/ecomtiktokshop/oauthcomplete |
Gọi bằng JWT của đúng người dùng, đúng công ty đã xin link ở bước 1. Mã uỷ quyền chỉ dùng được một lần và sống ngắn (Shopee khoảng 10 phút, Lazada khoảng 30 phút), nên gọi ngay khi nhận message.
Request Body
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
code | string | Có | data.code từ message |
state | string | Có | data.state từ message |
shop_id | string | Có (Shopee) | data.shop_id từ message. Lazada / TikTok Shop không cần |
Response (HTTP 200)
| Trường | Kiểu | Mô tả |
|---|---|---|
id | int | ID kênh (EcomPlatform) |
type | int | Loại sàn |
title | string | Tên gian hàng lấy từ sàn |
is_new | int | 1 = kênh mới tạo, 0 = kết nối lại kênh đã có |
{ "id": 31, "type": 7, "title": "Shop Demo Official", "is_new": 1 }Sau khi kết nối, nên gọi Cập nhật kênh để chọn cửa hàng (store) và kho (receipt_warehouse) nhận đơn, rồi đồng bộ sản phẩm.
Error Codes
Bước 1 (lấy link)
| Mã lỗi | Mô tả |
|---|---|
error_role_not_available | Không có quyền setting.sale (HTTP 403) |
error_feature_requires_plan | Gói chưa có tính năng kết nối sàn (HTTP 402) |
error_marketplace_two_factor_required | Chủ shop (chính người gọi) chưa bật 2FA (HTTP 403) |
error_marketplace_owner_two_factor_required | Người gọi là nhân viên, chủ shop chưa bật 2FA (HTTP 403) |
error_shopee_not_configured, error_lazada_not_configured, error_tiktokshop_not_configured | Kết nối sàn này chưa khả dụng trên máy chủ (HTTP 503) |
error_url_can_not_be_generated | type không hỗ trợ (HTTP 422) |
Bước 2 (message lỗi từ trang callback)
| Mã lỗi | Mô tả |
|---|---|
error_oauth_state_invalid | state sai hoặc đã hết hạn (quá 10 phút) |
error_shopee_main_account_not_supported | Uỷ quyền bằng tài khoản chính (main account) Shopee, chưa hỗ trợ |
error_shopee_code_or_shop_id_missing | Shopee không trả code / shop_id |
error_lazada_code_missing | Lazada không trả code |
error_tiktokshop_auth_denied | Chủ shop từ chối uỷ quyền |
error_tiktokshop_auth_failed, error_tiktokshop_code_missing | TikTok Shop uỷ quyền lỗi / không trả code |
Bước 3 (hoàn tất)
| Mã lỗi | Mô tả |
|---|---|
error_marketplace_two_factor_required, error_marketplace_owner_two_factor_required | Chủ shop chưa bật 2FA (HTTP 403, kiểm trước state) |
error_oauth_state_invalid | state sai hoặc hết hạn (HTTP 401) |
error_oauth_state_used | state đã được dùng (HTTP 401) |
error_oauth_state_owner_mismatch | JWT không phải của người dùng / công ty đã xin link (HTTP 403) |
error_oauth_state_store_unavailable | Tạm thời không xử lý được, thử lại (HTTP 503) |
error_shopee_code_or_shop_id_missing, error_lazada_code_missing, error_tiktokshop_code_missing | Thiếu hoặc sai code / shop_id (HTTP 422) |
error_quota_limit_exceed | Vượt số gian hàng sàn của gói (HTTP 422) |
error_shopee_shop_linked_to_other_company, error_lazada_seller_linked_to_other_company, error_tiktokshop_shop_linked_to_other_company | Gian hàng đã gắn với công ty khác (HTTP 409) |
error_shopee_oauth_in_progress, error_lazada_oauth_in_progress, error_tiktokshop_oauth_in_progress | Gian hàng đang được kết nối ở yêu cầu khác (HTTP 409) |
error_shopee_token_exchange_failed, error_lazada_token_exchange_failed, error_tiktokshop_token_exchange_failed | Sàn từ chối mã uỷ quyền (hết hạn / đã dùng) (HTTP 502) |
error_shopee_shop_not_supported_only_vn_local | Chỉ hỗ trợ shop Shopee Việt Nam, không xuyên biên giới (HTTP 422) |
error_lazada_seller_not_supported_only_vn_local | Chỉ hỗ trợ seller Lazada Việt Nam nội địa (HTTP 422) |
error_lazada_seller_not_active | Seller Lazada không ở trạng thái hoạt động (HTTP 422) |
error_lazada_seller_info_failed | Không lấy được thông tin seller (HTTP 502) |
error_tiktokshop_not_seller_account | Tài khoản TikTok uỷ quyền không phải tài khoản người bán (HTTP 422) |
error_tiktokshop_shop_not_supported_only_vn_local | Chỉ hỗ trợ shop TikTok Shop Việt Nam, người bán nội địa (HTTP 422) |
error_tiktokshop_get_shops_failed | Không lấy được danh sách shop đã uỷ quyền (HTTP 502) |
error_feature_requires_plan | Gói chưa có tính năng kết nối sàn (HTTP 402) |