TECH JOURNAL

Shopee OpenPlatform で商品を一括同期する — 多言語・多通貨対応の設計パターン

ShareXB!
Shopee OpenPlatform で商品を一括同期する — 多言語・多通貨対応の設計パターン
目次

はじめに

Shopee は東南アジア・台湾の EC プラットフォームとして、シンガポール・マレーシア・インドネシア・タイ・台湾・フィリピン・ベトナムなど複数国に展開している。1 つの商品を複数国のショップに同時出品する際、各国の言語・通貨・税制・カテゴリの違いを自動処理する設計が必要になる。

Shopee OpenPlatform の Product API を使った実装を解説する。

認証: HMAC-SHA256 署名

Shopee API の認証は OAuth のアクセストークンに加えて、リクエストごとに HMAC-SHA256 署名 を生成する必要がある。

import crypto from "crypto"
 
function generateSignature(
  partnerKey: string,
  partnerId: number,
  path: string,
  timestamp: number,
  accessToken: string,
  shopId: number
): string {
  const baseString = `${partnerId}${path}${timestamp}${accessToken}${shopId}`
  return crypto
    .createHmac("sha256", partnerKey)
    .update(baseString)
    .digest("hex")
}
 
// API リクエストの構築
async function callShopeeApi(
  path: string,
  params: Record<string, unknown>,
  shopId: number,
  accessToken: string
) {
  const timestamp = Math.floor(Date.now() / 1000)
  const sign = generateSignature(PARTNER_KEY, PARTNER_ID, path, timestamp, accessToken, shopId)
 
  const url = new URL(`https://partner.shopeemobile.com${path}`)
  url.searchParams.set("partner_id", String(PARTNER_ID))
  url.searchParams.set("shop_id", String(shopId))
  url.searchParams.set("timestamp", String(timestamp))
  url.searchParams.set("access_token", accessToken)
  url.searchParams.set("sign", sign)
 
  const response = await fetch(url.toString(), {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(params),
  })
  return response.json()
}

商品の出品: addItem

Shopee で商品を出品するには addItem を呼ぶ。必要なフィールドが多く、カテゴリ別に必須の attribute が変わる点が難しい。

async function addProduct(shopId: number, accessToken: string, product: ShopeeProduct) {
  return callShopeeApi(
    "/api/v2/product/add_item",
    {
      original_price: product.price,
      description: product.description,
      weight: product.weight, // 単位: kg
      item_name: product.name,
      item_sku: product.sku,
      logistics: product.logistics, // 配送方法の配列
      category_id: product.categoryId,
      attribute_list: product.attributes,
      image: {
        image_url_list: product.imageUrls,
      },
      // 多言語対応: 言語コードとテキストのリスト
      description_info: {
        extended_description: {
          field_list: [
            { field_type: "text", text: product.description },
          ],
        },
      },
    },
    shopId,
    accessToken
  )
}

多国展開: ショップごとに異なるカテゴリ ID

Shopee はマーケットプレイス(国)ごとにカテゴリ体系が異なる。「衣類 > メンズ > Tシャツ」の category_id が台湾(TW)とタイ(TH)で違う。

// 国別カテゴリマッピングのキャッシュ
const categoryCache = new Map<string, Map<string, number>>()
 
async function getCategoryId(
  country: string,
  shopId: number,
  accessToken: string,
  categoryPath: string
): Promise<number> {
  const cacheKey = country
  if (!categoryCache.has(cacheKey)) {
    const tree = await fetchCategoryTree(shopId, accessToken)
    categoryCache.set(cacheKey, buildCategoryMap(tree))
  }
  const map = categoryCache.get(cacheKey)!
  const id = map.get(categoryPath)
  if (!id) throw new Error(`Category not found: ${categoryPath} in ${country}`)
  return id
}

多言語対応: 翻訳テキストの管理

複数国に同時出品する場合、商品名・説明文の翻訳が必要になる。翻訳テキストをコンテンツ管理システムで管理し、出品時に各国の言語版を取り出す設計にするとよい。

interface ProductTranslation {
  locale: string  // "ja", "zh-TW", "th", "en"
  name: string
  description: string
}
 
interface ProductData {
  sku: string
  price: Record<string, number>  // { "TW": 299, "TH": 350 } など通貨ごとの価格
  translations: ProductTranslation[]
  imageUrls: string[]
  weightKg: number
}
 
async function publishToAllMarkets(product: ProductData) {
  const markets = [
    { country: "TW", shopId: TW_SHOP_ID, accessToken: TW_TOKEN, locale: "zh-TW" },
    { country: "TH", shopId: TH_SHOP_ID, accessToken: TH_TOKEN, locale: "th" },
    // ...
  ]
 
  const results = await Promise.allSettled(
    markets.map(async (market) => {
      const translation = product.translations.find((t) => t.locale === market.locale)
        ?? product.translations.find((t) => t.locale === "en") // フォールバック: 英語
      if (!translation) throw new Error(`No translation for ${market.locale}`)
 
      const categoryId = await getCategoryId(
        market.country,
        market.shopId,
        market.accessToken,
        "clothing/mens/tshirt" // 内部パス
      )
 
      return addProduct(market.shopId, market.accessToken, {
        name: translation.name,
        description: translation.description,
        price: product.price[market.country],
        sku: product.sku,
        categoryId,
        imageUrls: product.imageUrls,
        weight: product.weightKg,
        attributes: [],
        logistics: await getDefaultLogistics(market.shopId, market.accessToken),
      })
    })
  )
 
  return results.map((r, i) => ({
    market: markets[i].country,
    success: r.status === "fulfilled",
    error: r.status === "rejected" ? String(r.reason) : undefined,
  }))
}

在庫の一括更新: updateStockQty

複数国の在庫を一元管理している場合、注文発生時に全市場の在庫を更新する。

async function syncInventory(
  shopId: number,
  accessToken: string,
  itemId: number,
  modelId: number,
  newStock: number
) {
  return callShopeeApi(
    "/api/v2/product/update_stock",
    {
      item_id: itemId,
      stock_list: [
        {
          model_id: modelId,
          seller_stock: [{ stock: newStock }],
        },
      ],
    },
    shopId,
    accessToken
  )
}

まとめ

Shopee OpenPlatform は HMAC 署名が必須で最初の壁が高いが、一度ラッパーを作ると後は通常の REST API と同じように使える。多国展開では国別カテゴリキャッシュと翻訳管理の設計が肝になる。

ShareXB!

この記事を書いた人

渡部 誠也

執行役員 / CTO

独立系 SIer で Web・組み込み・基幹システムの開発を経験し、2017 年に illustrious へ。CTO としてシステム開発事業を立ち上げ、要件定義からコーディングまで一貫して担う。EC に特化した Web アプリケーションを数多く手がける。

ECの業務やシステムについて、
ご相談ください。

いまの運用で困っていること、実現したいことから、一緒に整理します。