Kết nối sàn TMĐT

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 402 error_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ênh

GET /v1/ecomplatformurls

TênKiểuBắt buộcMô tả
typeintCó7 (Shopee), 11 (Lazada), 9 (TikTok Shop)

Response (HTTP 200)

TrườngKiểuMô tả
company_idintID công ty
idstringMã yêu cầu (UUID)
typeintLoại sàn
urlstringLink trang uỷ quyền của sàn (đã chứa state)
callback_originstringOrigin của trang callback sẽ gửi postMessage về. Chỉ nhận message từ origin này
date_createdintThờ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 = url sau khi có link; mở popup sau await dễ 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ànEndpoint
ShopeePOST /v1/ecomshopee/oauthcomplete
LazadaPOST /v1/ecomlazada/oauthcomplete
TikTok ShopPOST /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ườngKiểuBắt buộcMô tả
codestringCódata.code từ message
statestringCódata.state từ message
shop_idstringCó (Shopee)data.shop_id từ message. Lazada / TikTok Shop không cần

Response (HTTP 200)

TrườngKiểuMô tả
idintID kênh (EcomPlatform)
typeintLoại sàn
titlestringTên gian hàng lấy từ sàn
is_newint1 = 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

Mã lỗiMô tả
error_role_not_availableKhông có quyền setting.sale (HTTP 403)
error_feature_requires_planGói chưa có tính năng kết nối sàn (HTTP 402)
error_marketplace_two_factor_requiredChủ shop (chính người gọi) chưa bật 2FA (HTTP 403)
error_marketplace_owner_two_factor_requiredNgườ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_configuredKết nối sàn này chưa khả dụng trên máy chủ (HTTP 503)
error_url_can_not_be_generatedtype không hỗ trợ (HTTP 422)

Bước 2 (message lỗi từ trang callback)

Mã lỗiMô tả
error_oauth_state_invalidstate sai hoặc đã hết hạn (quá 10 phút)
error_shopee_main_account_not_supportedUỷ quyền bằng tài khoản chính (main account) Shopee, chưa hỗ trợ
error_shopee_code_or_shop_id_missingShopee không trả code / shop_id
error_lazada_code_missingLazada không trả code
error_tiktokshop_auth_deniedChủ shop từ chối uỷ quyền
error_tiktokshop_auth_failed, error_tiktokshop_code_missingTikTok Shop uỷ quyền lỗi / không trả code

Bước 3 (hoàn tất)

Mã lỗiMô tả
error_marketplace_two_factor_required, error_marketplace_owner_two_factor_requiredChủ shop chưa bật 2FA (HTTP 403, kiểm trước state)
error_oauth_state_invalidstate sai hoặc hết hạn (HTTP 401)
error_oauth_state_usedstate đã được dùng (HTTP 401)
error_oauth_state_owner_mismatchJWT không phải của người dùng / công ty đã xin link (HTTP 403)
error_oauth_state_store_unavailableTạ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_missingThiếu hoặc sai code / shop_id (HTTP 422)
error_quota_limit_exceedVượ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_companyGian 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_progressGian 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_failedSàn từ chối mã uỷ quyền (hết hạn / đã dùng) (HTTP 502)
error_shopee_shop_not_supported_only_vn_localChỉ hỗ trợ shop Shopee Việt Nam, không xuyên biên giới (HTTP 422)
error_lazada_seller_not_supported_only_vn_localChỉ hỗ trợ seller Lazada Việt Nam nội địa (HTTP 422)
error_lazada_seller_not_activeSeller Lazada không ở trạng thái hoạt động (HTTP 422)
error_lazada_seller_info_failedKhông lấy được thông tin seller (HTTP 502)
error_tiktokshop_not_seller_accountTà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_localChỉ hỗ trợ shop TikTok Shop Việt Nam, người bán nội địa (HTTP 422)
error_tiktokshop_get_shops_failedKhông lấy được danh sách shop đã uỷ quyền (HTTP 502)
error_feature_requires_planGói chưa có tính năng kết nối sàn (HTTP 402)

On this page