eBay Inventory Mapping API:PythonとGraphQLでAI推奨の出品プレビューを作成する

前回の記事はこちら

【連載#18】eBay Inventory Mapping API:PythonとGraphQLでAI推奨の出品プレビューを作成する

はじめに

本記事は、全42回にわたる「eBay API 実践ガイド」の第18回です。

第2回から第17回まで、実に16回にわたって Trading API(SOAP)の世界を歩んできました。GeteBayDetails によるメタデータ取得、EPS 画像アップロード、AddFixedPriceItem による出品、在庫管理、注文処理、フィードバック自動化——これらすべてが SOAP プロトコルと XML の上に成り立つ、長年の実績ある技術でした。そして前回(#17)の GetFeedback / LeaveFeedback を最後に、Trading API 連載はひとつの完結を迎えます。

本回は、連載にとっての重要な技術的転換点となります。ここからは、eBay が次世代の API 基盤として推進する GraphQL API の世界に入ります。その第一弾が、本記事で扱う「Inventory Mapping API(インベントリマッピング API)」です。これは 2025 年に正式リリースされた eBay 初の GraphQL API であり、SOAP や REST とは根本的に異なるプロトコルを採用しています。既存の商品データ(GTIN・UPC・EAN・ISBN などの標準商品コード)を AI が解析し、最適な eBay カテゴリや Item Specifics(商品の詳細スペック)を自動推薦する、次世代の出品支援機能です。

大量の商品を eBay に出品する際、最大のボトルネックのひとつが「カテゴリの選定」と「Item Specifics(ブランド・カラー・サイズ等)の手入力」です。これらをすべて人手で行うと 1 商品あたり数分のコストがかかり、数千件規模になると現実的ではありません。Inventory Mapping API の AI 推薦機能を活用することで、この作業を大幅に削減できます。

この記事で得られること:

  • eBay 初の GraphQL API(Inventory Mapping API)の全体構造と、従来の SOAP・REST との根本的な違い——「なぜ POST 1 本だけで動くのか」を正確に理解する。
  • startListingPreviewsCreation mutation を Python + requests ライブラリで呼び出し、AI 推薦プレビュータスクを起動してタスク ID を確実に取得する実装手順。
  • Sandbox 環境で「本当にテストできること」と「できないこと」の正確な線引き——モックデータの落とし穴を理解し、テスト戦略を正しく設計する方法。

背景・なぜこれが重要か (Motivation)

「REST API と何が違うの? GraphQL って難しそう……」

GraphQL という言葉を初めて聞いたとき、多くの開発者がこう感じます。確かに、SOAP でも REST でもない第三の選択肢であり、構文も独特です。しかし、実際に触れてみると、eBay の Inventory Mapping API における GraphQL の使い方は非常にシンプルです。エンドポイントが 1 つ(https://api.ebay.com/graphql)に統一され、HTTP メソッドも POST のみ。リクエストボディに「何をしたいか(mutation)」と「何を返してほしいか(フィールド指定)」を同時に書くだけです。REST のように「エンドポイントをどのパスにするか」を設計する必要がなく、最初のハードルを越えれば、むしろすっきりした構造に感じるはずです。

では、なぜ eBay はこの機能を GraphQL で提供するのでしょうか。答えは「柔軟なフィールド選択」にあります。出品プレビューの結果には、カテゴリ情報・推薦 Item Specifics・タイトル・画像など多くのフィールドが含まれます。REST では全フィールドが固定のレスポンスとして返り、不要なデータも転送されます。GraphQL ならクライアントが「今必要なフィールドだけ」を指定できるため、通信効率が高く、将来 API がフィールドを追加しても既存クライアントコードへの影響が最小化されます。

そして、この機能が解決する実務課題は明確です。たとえば、電子機器を 500 件出品するシナリオを考えてみてください。

【課題1: カテゴリ選定の手間】 eBay のカテゴリ構造は数万件に及び、「Sony ヘッドフォン」を出品するためだけでも Consumer Electronics > Portable Audio & Headphones > Headphones という深い階層を探索する必要があります。これを 500 件分、人手でやることは現実的ではありません。

【課題2: Item Specifics 入力の品質】 eBay では各カテゴリに「必須の Item Specifics」があり、これが欠落すると出品クオリティスコアが下がり、検索順位に悪影響が出ます。ブランド・タイプ・接続方式・インピーダンスなど、カテゴリごとに要求されるスペックは異なり、網羅的に入力するには専門知識が必要です。

Inventory Mapping API はこの両方を、商品の UPC や EAN をキーにした AI 解析で自動的に推薦します。人間はその推薦結果をレビューして承認するだけという、「人間は最終判断のみ」のワークフローを実現できます。これが、Trading API 時代の手動カテゴリ選定から脱却するための、新世代のアプローチです。

基本的な使い方(ベースライン):GraphQLでstartListingPreviewsCreationを呼び出す

まず、最小限の動作するコードから始めます。ここでは、UPC コードを持つ商品 1 件を Inventory Mapping API に送り、AI 推薦タスクを起動してタスク ID を取得するまでの流れを示します。

SOAP や REST と異なり、GraphQL では以下の 3 点が重要です。まず、エンドポイントは常に 1 つ(https://api.ebay.com/graphql)で固定です。次に、HTTP メソッドは常に POST を使用します。そして、リクエストボディには query(または mutation)文字列とvariables(変数)を JSON として渡します。

# inventory_mapping_basic.py
import os
import requests

GRAPHQL_ENDPOINT = "https://api.ebay.com/graphql"

MUTATION = """
mutation StartListingPreviews($input: StartListingPreviewsCreationInput!) {
    startListingPreviewsCreation(input: $input) {
        errors {
            errorDescription
        }
        listingPreviewsCreationTask {
            id
        }
    }
}
"""

def start_listing_previews(access_token: str) -> str | None:
    """
    startListingPreviewsCreation mutation を呼び出してタスクIDを返す(最小実装)。

    必須ヘッダー:
      Authorization: Bearer <token>
      Content-Type: application/json
      X-EBAY-C-MARKETPLACE-ID: EBAY_US  # 現時点で US のみ対応

    必須 OAuth scope:
      https://api.ebay.com/oauth/api_scope/sell.inventory.mapping
    """
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json",
        "X-EBAY-C-MARKETPLACE-ID": "EBAY_US",
    }

    variables = {
        "input": {
            "externalProducts": [
                {
                    "sku": "HEADPHONE-SONY-XM5",
                    "title": "Sony WH-1000XM5 Wireless Noise Canceling Headphones Black",
                    "images": [
                        "https://example.com/images/sony-wh1000xm5-black.jpg"
                    ],
                    "externalProductIdentifierInput": {
                        "productType": "UPC",        # EAN / ISBN / MPN も指定可能
                        "productId": "027242920972"
                    }
                }
            ]
        }
    }

    payload = {"query": MUTATION, "variables": variables}

    resp = requests.post(GRAPHQL_ENDPOINT, json=payload, headers=headers, timeout=30)
    resp.raise_for_status()
    body = resp.json()

    # GraphQL は HTTP 200 でも errors フィールドにエラーが入ることがある
    if "errors" in body:
        print(f"GraphQL エラー: {body['errors']}")
        return None

    result = body["data"]["startListingPreviewsCreation"]

    # mutation レベルのビジネスエラー(errors 配列)
    if result.get("errors"):
        print(f"API ビジネスエラー: {result['errors']}")
        return None

    task_id = result["listingPreviewsCreationTask"]["id"]
    print(f"タスクID取得成功: {task_id}")
    return task_id


if __name__ == "__main__":
    token = os.environ["EBAY_ACCESS_TOKEN"]
    start_listing_previews(token)
補足: GraphQL と REST の違い——なぜ POST 1 本なのか

REST API では、エンドポイントの URL パス(例: /sell/inventory/v1/inventory_item/{sku})とHTTP メソッド(GET / POST / PUT / DELETE)の組み合わせで「何をしたいか」を表現します。一方、GraphQL では URL は常に同じ(/graphql)で、「何をしたいか」はリクエストボディの mutation 文字列の中に書きます。そのため、ネットワーク監視ツールから見ると「すべてのリクエストが POST /graphql に見える」という点が、REST に慣れた開発者にとっては最初は戸惑いますが、慣れると非常にシンプルに感じられます。

また、GraphQL では「返してほしいフィールドだけをリクエストに書く」という設計思想があります。上記のコードで listingPreviewsCreationTask { id } とだけ書いているのはそのためです。将来、タスクの result フィールドも一緒に取得したくなったら { id result { completionStatus } } と追記するだけで対応できます——サーバー側の変更は不要です。

実務で躓く場面・深いポイント (Core)

ここでは、実際に Inventory Mapping API を使い始めた際にエンジニアが必ずといっていいほど踏む落とし穴と、その回避策を解説します。Trading API との設計の違いに起因するものが多く、特に GraphQL 初体験のエンジニアには重要なポイントです。

1. US Marketplace 限定であることを見落として失敗する

Inventory Mapping API は、現時点(2025 年)では EBAY_US(米国サイト)のみに対応しています。他のマーケットプレイスのデータを処理しようとしても、API はエラーを返します。

具体的には、X-EBAY-C-MARKETPLACE-ID ヘッダーに EBAY_JP(日本)や EBAY_DE(ドイツ)を指定すると、HTTP 403 が返るか、mutation の errors フィールドにエラーが格納されます。日本の eBay セラーが出品支援に使う場合でも、このヘッダーは必ず EBAY_US に固定する必要があります。また、eBay Motors(米国の自動車サイト)およびそのサブカテゴリも非対応です。車・バイク関連の商品カテゴリには現時点では使用できません。

補足: なぜ US 限定なのか?

Inventory Mapping API の AI エンジンは eBay US の商品カタログをベースに学習されています。AI の推薦精度が保証されるのは EBAY_US のカテゴリ・スペック体系に対してのみであるため、現時点では US に限定されています。将来的には他のマーケットプレイスへの展開が予定されていますが、2025 年のリリース時点では US のみです。

2. Sandbox のモックデータと本番結果の違いを理解していないと検証を誤る

Inventory Mapping API には Sandbox 環境(https://api.sandbox.ebay.com/graphql)が存在します。「Sandbox で試してから Production 投入」という通常の開発フローが使えるという点では、他の eBay API と同じです。しかし、Sandbox における重要な制限を理解していないと、間違った前提でテストしてしまいます。

Sandbox でできること:

  • API の呼び出しフロー全体(認証 → mutation 実行 → タスク ID 取得)をコードで確認できる。
  • エラーハンドリング(HTTP エラー、GraphQL エラー、ビジネスエラー)の実装を検証できる。
  • 後続の結果取得コード(第19回で解説するポーリング処理)との連携フローをテストできる。

Sandbox でできないこと:

  • 返却されるプレビューデータは本物の AI 推薦結果ではなく、ランダムなモックデータ(ダミーカテゴリ・ダミースペック)です。
  • 処理時間も実際の AI 推論コストを反映しておらず、Sandbox では最長 10 分程度のランダムな遅延がシミュレートされます(本番環境では通常数十秒〜数分程度)。
  • AI 推薦精度(正しいカテゴリが推薦されるか等)は Sandbox では一切検証できません。これは必ず本番環境(Production)で少量のサンプルを用いて確認してください。

つまり、Sandbox は「コードの動作確認」には使えますが、「API が正しい結果を返すかの確認」には使えません。この線引きを明確に理解した上でテスト戦略を立てることが、後の手戻りを防ぐ鍵です。

3. externalProducts の入力データ品質が AI 推薦精度を左右する

startListingPreviewsCreation に渡す externalProducts の各フィールドの品質は、AI が返す推薦結果の精度に直結します。「とりあえず title だけ渡せばいいだろう」という安易な実装は、推薦精度の著しい低下を招きます。

images フィールドの重要性: AI は画像からも商品カテゴリや属性を認識します。HTTPS で直接アクセスできる高解像度の商品画像 URL を少なくとも 1 枚は渡すことを強く推奨します。リダイレクトを含む URL や、認証が必要な URL は AI が解析できません。

externalProductIdentifierInput(GTIN 等)の効果: UPC / EAN / ISBN / MPN などの標準商品コードがある場合、必ず指定してください。AI は GTIN をキーにして eBay の商品カタログデータベースを参照でき、カテゴリ推薦と Item Specifics 補完の精度が大幅に向上します。GTIN がない場合は sku と title のみでも動作しますが、推薦精度は低下します。

title の最適化: title には商品の本質的な属性(ブランド・モデル名・主要スペック)を英語で簡潔に記述することが理想です。80 文字以内に重要な情報を凝縮してください。日本語タイトルも受け付けますが、US マーケット向け AI の学習データは英語商品が中心のため、英語タイトルの方が精度は高くなります。

注意: OAuth Scope の設定漏れ

Inventory Mapping API の呼び出しには、OAuth アクセストークンに https://api.ebay.com/oauth/api_scope/sell.inventory.mapping スコープが必要です。このスコープを付与せずにリクエストすると、HTTP 401 Unauthorized が返ります。

第1回〜第17回で使用してきた Trading API 用のトークンには、このスコープは含まれていません。新たに eBay Developer Portal でアプリケーションの設定を確認し、このスコープを追加した上で、ユーザーに再認証(OAuth フロー)を実行させる必要があります。既存のシステムにこの API を組み込む際に最も多い初歩的ミスのひとつです。

頻出エラーコード早見表

GraphQL API では REST とはエラーの表現方法が異なります。HTTP ステータスコードだけを見ていると、エラーを見逃すケースがあります。

エラー種別 発生状況 対策
HTTP 401 Unauthorized OAuth トークンが無効、または sell.inventory.mapping スコープが付与されていない Developer Portal で scope を確認し、ユーザーに再認証を促す
HTTP 403 Forbidden X-EBAY-C-MARKETPLACE-ID が EBAY_US 以外(または未指定)、あるいは Motors カテゴリ ヘッダーを "EBAY_US" に固定する。Motors サブカテゴリは非対応
GraphQL errors(HTTP 200) mutation 文字列の構文エラー、または変数の型不一致(例: productType に無効な値を指定) errors[].message を確認。productType は "UPC" "EAN" "ISBN" "MPN" のいずれか
mutation.errors(ビジネスエラー、HTTP 200) HTTP は 200 だが startListingPreviewsCreation.errors 配列にエラーが含まれる。空の externalProducts を渡した場合など errors[].errorDescription を確認して原因を特定する。externalProducts は 1 件以上必須

堅牢な実装:型安全・入力検証・エラー処理を備えたInventoryMappingClientクラス

ここまでの落とし穴をすべて踏まえた上で、プロダクションレベルの Python クラスを実装します。型アノテーション・docstring・入力バリデーション・エラーハンドリングを完備した、実運用に耐えうる設計です。

# inventory_mapping_client.py
"""
eBay Inventory Mapping API クライアント。
startListingPreviewsCreation mutation を呼び出し、タスクIDを確実に取得する。

依存: requests>=2.28.0
必須 OAuth scope: https://api.ebay.com/oauth/api_scope/sell.inventory.mapping
"""
from __future__ import annotations

import logging
from dataclasses import dataclass
from typing import Optional

import requests

logger = logging.getLogger(__name__)

GRAPHQL_ENDPOINT = "https://api.ebay.com/graphql"
SANDBOX_ENDPOINT  = "https://api.sandbox.ebay.com/graphql"

# ExternalProductIdentifierEnum で許可される値
VALID_PRODUCT_TYPES = frozenset({"UPC", "EAN", "ISBN", "MPN"})

START_MUTATION = """
mutation StartListingPreviews($input: StartListingPreviewsCreationInput!) {
    startListingPreviewsCreation(input: $input) {
        errors {
            errorDescription
        }
        listingPreviewsCreationTask {
            id
        }
    }
}
"""


@dataclass
class ExternalProductIdentifier:
    """標準商品コード(GTIN 等)を表すデータクラス。"""

    product_type: str  # "UPC" | "EAN" | "ISBN" | "MPN"
    product_id: str

    def __post_init__(self) -> None:
        if self.product_type not in VALID_PRODUCT_TYPES:
            raise ValueError(
                f"product_type は {VALID_PRODUCT_TYPES} のいずれかを指定してください: "
                f"'{self.product_type}'"
            )
        if not self.product_id.strip():
            raise ValueError("product_id は空にできません")


@dataclass
class ExternalProduct:
    """
    Inventory Mapping API に送る商品データを表すデータクラス。

    AI 推薦精度を高めるには:
      1. images を必ず 1 枚以上含める(HTTPS の公開 URL)
      2. identifier(UPC/EAN/ISBN/MPN)を指定する
      3. title を英語・80 文字以内で記述する
    """

    sku: str
    title: str
    images: list[str]
    identifier: Optional[ExternalProductIdentifier] = None

    def __post_init__(self) -> None:
        if not self.sku.strip():
            raise ValueError("sku は空にできません")
        if not self.title.strip():
            raise ValueError("title は空にできません")
        if len(self.title) > 80:
            logger.warning(
                "SKU '%s': title が 80 文字を超えています(%d 文字)。"
                "AI 推薦精度に影響する可能性があります。",
                self.sku, len(self.title),
            )
        if not self.images:
            logger.warning(
                "SKU '%s': images が未指定です。AI 推薦精度が低下する可能性があります。",
                self.sku,
            )
        for url in self.images:
            if not url.startswith("https://"):
                raise ValueError(
                    f"SKU '{self.sku}': 画像 URL は HTTPS である必要があります: {url}"
                )

    def to_graphql_input(self) -> dict:
        """GraphQL mutation の variables 用 dict に変換する。"""
        payload: dict = {
            "sku": self.sku,
            "title": self.title,
            "images": self.images,
        }
        if self.identifier:
            payload["externalProductIdentifierInput"] = {
                "productType": self.identifier.product_type,
                "productId": self.identifier.product_id,
            }
        return payload


class InventoryMappingClient:
    """
    eBay Inventory Mapping API クライアント。

    GraphQL エンドポイントに startListingPreviewsCreation mutation を送信し、
    AI 推薦プレビュータスクの ID を返す。
    結果の取得は非同期(別途ポーリングまたは Notification API が必要)。

    Sandbox 環境:
        use_sandbox=True を指定すると api.sandbox.ebay.com に接続する。
        Sandbox ではモックデータが返るため、AI 推薦精度の検証には使用できない。
        コードフローの動作確認(認証・エラーハンドリング等)に限り使用すること。

    Raises:
        ValueError: 引数が不正な場合(空トークン等)
        requests.HTTPError: HTTP エラー(401/403 等)が発生した場合
        RuntimeError: GraphQL エラーまたは API ビジネスエラーが発生した場合
    """

    def __init__(
        self,
        access_token: str,
        use_sandbox: bool = False,
        timeout: int = 30,
    ) -> None:
        if not access_token.strip():
            raise ValueError("access_token は空にできません")
        self._endpoint = SANDBOX_ENDPOINT if use_sandbox else GRAPHQL_ENDPOINT
        self._timeout  = timeout
        self._session  = requests.Session()
        self._session.headers.update(
            {
                "Authorization": f"Bearer {access_token}",
                "Content-Type": "application/json",
                # 現時点では EBAY_US 以外は非対応(Motors も非対応)
                "X-EBAY-C-MARKETPLACE-ID": "EBAY_US",
            }
        )
        logger.info("InventoryMappingClient 初期化完了 [endpoint=%s]", self._endpoint)

    def start_listing_previews_creation(
        self,
        products: list[ExternalProduct],
    ) -> str:
        """
        startListingPreviewsCreation mutation を実行してタスク ID を返す。

        Args:
            products: AI 推薦プレビューを作成する商品リスト(1 件以上)

        Returns:
            タスク ID(str)。後続のポーリング処理や Notification API で使用する。

        Raises:
            ValueError: products が空の場合
            requests.HTTPError: HTTP 401/403 等のエラー
            RuntimeError: GraphQL 実行エラーまたは API ビジネスエラー
        """
        if not products:
            raise ValueError("products は 1 件以上指定してください")

        variables = {
            "input": {
                "externalProducts": [p.to_graphql_input() for p in products],
            }
        }

        logger.info(
            "startListingPreviewsCreation 呼び出し: %d 件の商品", len(products)
        )

        try:
            response = self._session.post(
                self._endpoint,
                json={"query": START_MUTATION, "variables": variables},
                timeout=self._timeout,
            )
            response.raise_for_status()
        except requests.Timeout:
            logger.error("API タイムアウト(%d 秒)", self._timeout)
            raise
        except requests.HTTPError as exc:
            # 401: scope 不足、403: marketplace 非対応 など
            logger.error(
                "HTTP エラー %s: %s",
                exc.response.status_code,
                exc.response.text[:500],
            )
            raise

        body = response.json()

        # GraphQL プロトコルレベルのエラー(構文エラー・型不一致 等)
        # HTTP は 200 だが body["errors"] にエラーが含まれる
        if "errors" in body:
            logger.error("GraphQL 実行エラー: %s", body["errors"])
            raise RuntimeError(f"GraphQL エラー: {body['errors']}")

        mutation_data = (body.get("data") or {}).get(
            "startListingPreviewsCreation", {}
        )

        # API ビジネスレベルのエラー(HTTP 200、mutation.errors に格納)
        biz_errors = mutation_data.get("errors") or []
        if biz_errors:
            descriptions = [e["errorDescription"] for e in biz_errors]
            logger.error("API ビジネスエラー: %s", descriptions)
            raise RuntimeError(f"Inventory Mapping API エラー: {descriptions}")

        task    = (mutation_data.get("listingPreviewsCreationTask")) or {}
        task_id: Optional[str] = task.get("id")

        if not task_id:
            raise RuntimeError(
                "レスポンスにタスク ID が含まれていません。"
                f"予期しないレスポンス形式: {body}"
            )

        logger.info("タスク ID 取得成功: %s", task_id)
        return task_id

上記のクライアントを実際に使う呼び出し例を示します。

# main.py
import os
import logging
from inventory_mapping_client import (
    InventoryMappingClient,
    ExternalProduct,
    ExternalProductIdentifier,
)

logging.basicConfig(level=logging.INFO)

def main() -> None:
    access_token = os.environ["EBAY_ACCESS_TOKEN"]

    # Sandbox でテストする場合は use_sandbox=True を指定する
    # ただし Sandbox の結果はモックデータ——AI 推薦精度の検証には使えない
    client = InventoryMappingClient(access_token=access_token, use_sandbox=False)

    products = [
        ExternalProduct(
            sku="SONY-WH1000XM5-BLK",
            title="Sony WH-1000XM5 Wireless Noise Canceling Headphones Black",
            images=["https://example.com/images/wh1000xm5-black.jpg"],
            identifier=ExternalProductIdentifier(
                product_type="UPC",
                product_id="027242920972",
            ),
        ),
        ExternalProduct(
            sku="APPLE-AIRPODS-PRO2",
            title="Apple AirPods Pro 2nd Generation with MagSafe Case USB-C",
            images=["https://example.com/images/airpods-pro2.jpg"],
            identifier=ExternalProductIdentifier(
                product_type="UPC",
                product_id="195949206399",
            ),
        ),
    ]

    task_id = client.start_listing_previews_creation(products)
    print(f"タスク投入完了。タスクID: {task_id}")
    print("次のステップ: このIDを使って結果をポーリングしてください(第19回参照)")

if __name__ == "__main__":
    main()

パフォーマンス・スケーリング視点 (深度)

大量商品を一括投入する際のバッチ設計と非同期化の展望

実務で数千件の商品を Inventory Mapping API に投入する場合、単純にループで 1 件ずつ呼び出すのは非効率であり、レート制限(Rate Limit)に引っかかる可能性もあります。ここでは、大量商品を安全にバッチ処理するための設計ポイントを解説します。

【バッチサイズの設計】

startListingPreviewsCreation の 1 リクエストに含めることができる externalProducts の件数は eBay の公式ドキュメントで確認してください。実務上は 1 リクエストあたり 50 件程度を目安にバッチ分割するのが安定した運用につながります。バッチが大きすぎると AI 処理に時間がかかりタスクが FAILED になるリスクが上がります。バッチ間には 1 秒程度のウェイトを入れてレート制限を回避しましょう。

# batch_submission.py
"""
大量商品を一定サイズのバッチに分割して Inventory Mapping API に投入する。
"""
import time
import logging
from itertools import islice
from inventory_mapping_client import InventoryMappingClient, ExternalProduct

logger = logging.getLogger(__name__)

BATCH_SIZE    = 50   # 1 リクエストあたりの推奨件数
DELAY_SECONDS = 1.0  # バッチ間のウェイト(レート制限対策)


def chunked(iterable, size: int):
    """iterable を size 件ごとのチャンクに分割するジェネレータ。"""
    it = iter(iterable)
    while chunk := list(islice(it, size)):
        yield chunk


def submit_in_batches(
    client: InventoryMappingClient,
    products: list[ExternalProduct],
) -> list[str]:
    """
    商品リストをバッチに分割してタスクを投入し、タスク ID リストを返す。

    Args:
        client  : InventoryMappingClient インスタンス
        products: 全商品リスト

    Returns:
        タスク ID のリスト(バッチ数 == len(返り値))

    Raises:
        RuntimeError: いずれかのバッチで投入に失敗した場合
    """
    batches  = list(chunked(products, BATCH_SIZE))
    task_ids: list[str] = []

    logger.info(
        "合計 %d 件を %d バッチ(各最大 %d 件)で投入します",
        len(products), len(batches), BATCH_SIZE,
    )

    for idx, batch in enumerate(batches, start=1):
        logger.info(
            "バッチ %d/%d を投入中 (%d 件)...", idx, len(batches), len(batch)
        )
        try:
            task_id = client.start_listing_previews_creation(batch)
            task_ids.append(task_id)
            logger.info("バッチ %d 完了: タスクID=%s", idx, task_id)
        except (RuntimeError, Exception) as exc:
            logger.error("バッチ %d 投入失敗: %s", idx, exc)
            raise

        if idx < len(batches):
            time.sleep(DELAY_SECONDS)

    logger.info("全バッチ投入完了。タスクID一覧: %s", task_ids)
    return task_ids
補足: Notification API との組み合わせによる非同期化

上記のバッチ投入で取得したタスク ID を使って「処理が完了したか」を確認する方法は大きく 2 つあります。第19回で解説する「ポーリング(定期的な問い合わせ)」と、eBay の Notification API を使った「プッシュ通知(イベント駆動)」です。

Notification API では LISTING_PREVIEW_CREATION_TASK_STATUS というイベントトピックを購読することで、タスクが完了した瞬間に Webhook で通知を受け取ることができます。数十バッチを同時に投入する大規模システムでは、ポーリングよりも Notification API との組み合わせの方がサーバーリソースの無駄遣いが少なく、リアルタイム性も高くなります。本連載では第19回でポーリング実装を詳しく解説した後、将来の回で Notification API との統合についても触れる予定です。

まとめ

本記事では、連載第18回として Trading API(SOAP)から eBay 初の GraphQL API への転換点を迎え、Inventory Mapping API の入門から実践的な実装までを解説しました。

ベースライン: GraphQL は POST 1 本・エンドポイント固定のシンプルな構造。startListingPreviewsCreation mutation に externalProducts(sku / title / images / 標準商品コード)を渡すと、非同期で AI 推薦タスクが起動してタスク ID が返る。

深いポイント: US Marketplace 限定(X-EBAY-C-MARKETPLACE-ID: EBAY_US 必須)、sell.inventory.mapping スコープの取得、Sandbox はコード動作確認に使えるが AI 推薦精度の検証にはモックデータのため使えない、入力データ品質(HTTPS 画像・GTIN・英語タイトル)が推薦精度を左右する——という 4 つのポイントを押さえた。

スケーリング: バッチサイズ 50 件程度を目安に商品をチャンク分割して投入し、Notification API(LISTING_PREVIEW_CREATION_TASK_STATUS)との組み合わせでイベント駆動型の非同期アーキテクチャへ発展させる展望を描いた。

Trading API の SOAP の世界から GraphQL の世界への第一歩は、思ったよりシンプルです。最初のリクエストが成功してタスク ID を受け取った瞬間——「GraphQL、案外やれる」と感じるはずです。

次のステップ

タスクを投入しても、肝心の AI 推薦結果(推薦カテゴリ・Item Specifics・タイトル)はまだ取得できていません。Inventory Mapping API はタスクが非同期で処理されるため、完了するまで待って結果を取得する仕組みが別途必要です。

次回(#19)「Inventory Mapping API②:タスク完了をポーリングしてプレビュー結果を取得する」では、listingPreviewsCreationTaskById query を使って定期的にタスクの completionStatus を確認し、COMPLETED になったタイミングで推薦カテゴリ・Item Specifics・タイトルを取得・活用する実装を詳しく解説します。お楽しみに!

トップに戻る