eBay Sell REST API:Trading APIからREST APIへ:OAuth移行と最初のInventory API呼び出し
前回の記事はこちら
【連載#24】eBay Sell REST API:Trading APIからREST APIへ:OAuth移行と最初のInventory API呼び出し
はじめに
本記事は、全42回にわたる「eBay API 実践ガイド」の第24回です。
第3回から第23回まで、私たちは Trading API(SOAP形式)と Message API を中心に、出品・在庫管理・注文処理・バルクメッセージ対応まで幅広い実装を積み重ねてきました。今回からは新しいフェーズ【Sell REST - 出品】の幕開けとして、モダンな REST API の世界へと本格的に軸足を移します。
第1回で構築した OAuth 2.0 認証基盤がいよいよ本領を発揮します。Trading API 時代の devID / appID / certID + UserToken という複雑な認証の組み合わせに代わり、すっきりとした Bearer Token でのAPI呼び出しが標準となります。第1回で実装した eBayTokenManager クラスはそのまま活用できます。
この記事で得られること:
- Inventory API の設計思想(SKU 中心設計)と Trading API(ItemID 中心設計)との根本的な構造の違いを理解し、なぜ3ステップの出品フローになるのかを把握する。
- OAuth Bearer Token を使った PUT /inventory_item/{sku} の実際の呼び出し方を、最小構成から生産レベルの実装まで段階的に習得する。
- 「完全上書き」仕様による意図しないデータ消失、1日250回の修正上限、Inventory API と Trading API 間の非互換性という移行時の3大トラップと、その具体的な回避策を身につける。
背景・なぜこれが重要か (Motivation)
「Trading API がまだ使えるなら、わざわざ REST API に移行する必要ある?」
これは非常に正直な疑問です。確かに現時点では Trading API も動作しており、第3〜17回で構築したシステムは今日でも稼働しています。しかしこの判断を先延ばしにし続けることには、無視できない技術的リスクが潜んでいます。
eBay の API ロードマップは明確に REST と GraphQL API への集中投資を宣言しています。新機能(Promoted Listings の高度な入札制御、次世代の Offer 管理、Dynamic Shipping 等)は REST / GraphQL API でのみ提供されるケースが増え続けており、Trading API 側にはバックポートされません。言い換えれば、Trading API に留まり続けることは「今のシステムを維持できても、eBay マーケットプレイスの進化についていけなくなる」リスクを恒常的に抱えることを意味します。
もう一つの現実的な懸念は「SOAP の学習コスト」です。Trading API が SOAP ベースであるため、新しいチームメンバーのオンボーディングや外部ライブラリとの連携に余分なコストがかかります。REST API への移行は、将来の開発速度を高めるための先行投資でもあります。
本連載では、Trading API の知識を持つ読者が混乱なく Inventory API へ移行できるよう、両者の対応関係を丁寧に解説していきます。第3〜23回で積み上げた知識は無駄になりません。UUIDによる冪等性・バリデーションファースト・エラーハンドリングといったエンジニアリング思想はそのまま REST の世界でも通用します。
基本的な使い方(ベースライン):createOrReplaceInventoryItemで最初のInventory Itemを作る
Inventory API の最初のエンドポイントは PUT /inventory_item/{sku}(operationId: createOrReplaceInventoryItem)です。このメソッドは SKU をキーとして、商品の在庫数・コンディション・商品詳細をeBayのシステムに登録します。Trading API の AddFixedPriceItem(第4回)と大きく異なるのは、この時点では「まだeBayサイトには出品されていない」という点です。まずは最小構成で動作を確認しましょう。
必須ヘッダーは Authorization(Bearer Token)、Content-Type(application/json)、そして Content-Language の3つです。Content-Language は日本向けの出品であっても en-US の指定が基本です。この設定を忘れると 400 エラーで弾かれます。
# inventory_item_basic.py import requests EBAY_INVENTORY_BASE = "https://api.ebay.com/sell/inventory/v1" def create_inventory_item(sku: str, token: str) -> dict: """最小構成でInventory Itemを作成する。 Args: sku: 出品者が定義するSKU(最大50文字、全在庫でユニーク) token: 第1回で取得したOAuth 2.0 Bearer Token Returns: {"status_code": int, "sku": str} 201 = 新規作成成功 / 204 = 既存SKUへの更新成功 """ url = f"{EBAY_INVENTORY_BASE}/inventory_item/{sku}" headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", "Content-Language": "en-US", # 日本向けでも en-US が基本 } payload = { "availability": { "shipToLocationAvailability": { "quantity": 10 } }, "condition": "NEW", "product": { "title": "Classic Denim Jacket - Size M", "description": "<p>A classic vintage denim jacket.</p>", "aspects": { "Brand": ["Levi's"], "Size": ["M"], "Color": ["Blue"] }, "imageUrls": [ "https://i.ebayimg.com/images/g/example/jacket_main.jpg" ] } } resp = requests.put(url, headers=headers, json=payload) resp.raise_for_status() return {"status_code": resp.status_code, "sku": sku} if __name__ == "__main__": TOKEN = "v^1.1#i^1#..." # 第1回のOAuthフローで取得 SKU = "JACKET-DENIM-M-001" result = create_inventory_item(SKU, TOKEN) print(f"HTTP {result['status_code']} - SKU: {result['sku']}") # 201: 新規作成成功(レスポンスボディなし) # 204: 既存SKUへの更新成功(レスポンスボディなし)
Trading API では、出品後に eBay が発行する ItemID がすべての操作の起点でした(ReviseFixedPriceItem・GetItem・EndFixedPriceItem、いずれも ItemID 指定)。一方 Inventory API は、出品者が事前に定義した SKU がすべての操作の起点になります。「在庫は自分のシステムで管理するもの」というパラダイムシフトが根底にあります。SKU は最大50文字で、自分の全在庫を通じてユニークである必要があります。例えばeBayとAmazonで同じ商品を扱う場合、"EBAY-JP-JACKET-001" のようにプラットフォームプレフィックスを付ける設計も実務では有効です。
実務で躓く場面・深いポイント (Core)
Inventory API は Trading API と比べてシンプルに見えますが、その設計の根本にある「分離の思想」と仕様上のいくつかの落とし穴を理解しないまま実装を進めると、後で深刻な問題を引き起こします。経験者が必ずぶつかる3大トラップを解説します。
1. Inventory Itemを作っただけでは、商品はまだeBayに出品されていない
これが Inventory API を初めて使う Trading API 経験者が最初に混乱するポイントです。Trading API の AddFixedPriceItem(第4回)は「1回のAPIコールで出品完了」するモノリシックな設計でした。しかし Inventory API は責務を分離した設計になっており、eBayサイト上に商品を公開するまでに最低3つのステップが必要です。
ステップ1 — createOrReplaceInventoryItem(本記事の内容): 「この商品はどんなものか・在庫は何個か」というマスターデータをeBayのシステムに登録する。
ステップ2 — createOffer(次回 #25 の内容): 「この商品をどのマーケットプレイスに・いくらで・どのポリシーで売るか」というオファー条件を設定する。
ステップ3 — publishOffer(次回 #25 の内容): オファーを実際に公開し、eBayのサイト上に商品が表示される状態にする。
本記事では Inventory Item の作成(ステップ1)のみを扱います。ステップ1が完了した後、eBay のセラーハブ(Seller Hub)の「Inventory」タブでは商品が登録済みとして確認できますが、「Active Listings」タブには表示されません。これは正常な動作です。あわてて同じ SKU で AddFixedPriceItem を呼び出すと、後述する「非互換性の罠」に陥ります。
2. 「完全上書き」の仕様を知らずに既存データを消してしまう事故
Inventory API の PUT /inventory_item/{sku} はべき等(Idempotent)なメソッドですが、「部分更新」ではなく「完全上書き(Full Replace)」です。送信したペイロードの内容がそのままレコードに保存され、送らなかったフィールドは削除されます。
例えば、最初に product.upc(JANコード)を含めて Inventory Item を登録しておき、後から在庫数(availability.shipToLocationAvailability.quantity)だけを変更しようとして UPC を含まないペイロードを送ると、UPC の情報がまるごと消えてしまいます。eBay の Product Catalog とのマッチング精度が落ち、検索順位への影響も懸念されます。
この事故を防ぐベストプラクティスは、更新の前に必ず GET /inventory_item/{sku} で現在の完全なデータを取得し、変更したい箇所だけをマージした上で PUT を送ることです。後述の「堅牢な実装」セクションの safe_upsert() がこのパターンを実装しています。なお eBay 公式ドキュメントもこの「取得してからマージして更新」のアプローチをベストプラクティスとして明記しています。
Inventory Item の作成・更新には、1日あたり最大250回というレート制限が設けられています。この上限はセラーアカウント単位で適用され、上限に達するとその日はそれ以上の更新がブロックされます。数千件の SKU を持つセラーが一斉に全商品の在庫を更新しようとすると、この上限に引っかかることがあります。
大量更新が必要な場合は、bulkCreateOrReplaceInventoryItem(POST /inventory_item/bulk_create_or_replace)を活用してください。1回のAPIコールで最大25件のInventory Itemを同時に作成・更新できるため、リクエスト消費を大幅に削減できます。また優先度ロジック(在庫残数が少ないものを先に更新するなど)を実装し、重要なアイテムから処理する設計を推奨します。
3. Inventory APIとTrading APIの出品は「別の世界」:混在運用が招くカオス
これが移行期に最も危険なトラップです。Inventory API で createOrReplaceInventoryItem → createOffer → publishOffer の手順で作成した出品は、Trading API の ReviseFixedPriceItem・RelistFixedPriceItem・EndFixedPriceItem では操作できません。逆に Trading API の AddFixedPriceItem で作成した出品は、Inventory API の updateOffer・endOffer では操作できません。
この非互換性を知らずに、「既存の Trading API 出品は Trading API で管理しながら、新商品だけ Inventory API で登録する」という並行運用を安易に始めると、数ヶ月後に「どの SKU がどちらのAPIで管理されているか把握できない」という管理の破綻を招きます。ReviseFixedPriceItem を呼んだら 404 が返ってきて初めて気づく、という状況は実務では致命的です。
移行の原則として、1つの SKU は必ずどちらか一方のAPI系統で管理するという鉄則を徹底してください。既存商品を Inventory API へ移行するときは、まず Trading API 側で EndFixedPriceItem を呼び出して出品を終了させてから、Inventory API で新規登録するのが安全な手順です。
頻出エラーコード早見表
| HTTPステータス / エラー | 主な原因 | 対処法 |
|---|---|---|
| 400 / 25002 | SKU が50文字超、または空文字・禁止文字を含む | SKU を英数字・ハイフン・アンダースコアのみ・50文字以内に整形する |
| 400 / 25003 | condition の値が ConditionEnum に存在しない(例: "Used" をそのまま送った) | CONDITION_MAP で Trading API 値を "USED_EXCELLENT" 等のInventory API値に変換する |
| 400 / 25004 | product.title が80文字を超えている | title[:80] でトリミングしてから送る(Python スライス) |
| 401 / Unauthorized | Bearer Token の有効期限切れ(有効期限は2時間) | 第1回実装の eBayTokenManager.get_valid_token() で自動リフレッシュする |
| 404 / 25013 | GET 時に指定した SKU の Inventory Item が存在しない | get_inventory_item() で None チェックし、新規作成フローへ分岐する |
堅牢な実装:Trading APIデータ構造からInventory Item形式への安全な移行アダプター
ここでは、Trading API(AddFixedPriceItem)時代のデータ構造を Inventory API のペイロードに変換するアダプター関数と、「完全上書き」問題を回避する safe_upsert() メソッドを実装します。型アノテーション・docstring・入力バリデーション・ログ出力を含む生産レベルの実装です。
TradingItemData は第3〜17回の Trading API 実装で使ってきたデータモデルの典型例を dataclass で表現したものです。adapt_trading_to_inventory_item() がアダプター関数で、TradingItemData を Inventory API のペイロード dict に変換します。InventoryItemManager クラスの safe_upsert() では、更新前に既存データを GET で取得し、ディープマージしてから PUT を送ることで「完全上書き」による意図しないフィールド消失を防ぎます。
# inventory_manager.py from __future__ import annotations import logging from dataclasses import dataclass from typing import Any import requests logger = logging.getLogger(__name__) EBAY_INVENTORY_BASE = "https://api.ebay.com/sell/inventory/v1" # Trading API コンディション文字列 → Inventory API ConditionEnum マッピング CONDITION_MAP: dict[str, str] = { "New": "NEW", "Used": "USED_EXCELLENT", "Very Good": "USED_VERY_GOOD", "Good": "USED_GOOD", "Acceptable": "USED_ACCEPTABLE", "For parts or not working": "FOR_PARTS_OR_NOT_WORKING", } @dataclass class TradingItemData: """Trading API(AddFixedPriceItem)時代のデータ構造を表すデータクラス。 第3〜17回のTrading API実装で使ってきたデータモデルを想定。 """ sku: str title: str description: str condition: str # Trading API 形式 (例: "New") quantity: int brand: str image_urls: list[str] aspects: dict[str, list[str]] | None = None upc: str | None = None ean: str | None = None def adapt_trading_to_inventory_item(data: TradingItemData) -> dict[str, Any]: """Trading API データ構造を Inventory API ペイロードに変換するアダプター。 Args: data: Trading API 形式のアイテムデータ Returns: createOrReplaceInventoryItem に渡すペイロード dict Raises: ValueError: condition が CONDITION_MAP に存在しない場合 """ condition = CONDITION_MAP.get(data.condition) if condition is None: raise ValueError( f"未対応のコンディション: '{data.condition}'. " f"対応値: {list(CONDITION_MAP.keys())}" ) aspects: dict[str, list[str]] = data.aspects or {"Brand": [data.brand]} if "Brand" not in aspects: aspects["Brand"] = [data.brand] product: dict[str, Any] = { "title": data.title[:80], # eBay タイトル最大80文字 "description": data.description, "aspects": aspects, "imageUrls": data.image_urls[:12], # 最大12枚 } if data.upc: product["upc"] = data.upc if data.ean: product["ean"] = data.ean return { "availability": { "shipToLocationAvailability": {"quantity": data.quantity} }, "condition": condition, "product": product, } def _deep_merge(base: dict, override: dict) -> dict: """辞書を再帰的にマージする(override 側の値が優先される)。""" result = dict(base) for key, val in override.items(): if (key in result and isinstance(result[key], dict) and isinstance(val, dict)): result[key] = _deep_merge(result[key], val) else: result[key] = val return result class InventoryItemManager: """Inventory API の /inventory_item を安全に操作するクライアント。""" def __init__(self, token: str) -> None: self._session = requests.Session() self._session.headers.update({ "Authorization": f"Bearer {token}", "Content-Type": "application/json", "Content-Language": "en-US", }) def get_inventory_item(self, sku: str) -> dict[str, Any] | None: """既存のInventory Itemを取得する。存在しない場合はNoneを返す。 Args: sku: 取得対象のSKU Returns: Inventory Item の dict、存在しない場合は None """ url = f"{EBAY_INVENTORY_BASE}/inventory_item/{sku}" resp = self._session.get(url) if resp.status_code == 404: return None resp.raise_for_status() return resp.json() def safe_upsert( self, sku: str, new_payload: dict[str, Any], ) -> dict[str, Any]: """「完全上書き」仕様に対応した安全な Inventory Item 作成・更新。 既存レコードを取得してディープマージしてから PUT を行うことで、 意図せずフィールドを消してしまう事故を防ぐ。 Args: sku: 作成・更新対象のSKU(最大50文字、全在庫でユニーク) new_payload: adapt_trading_to_inventory_item() で生成したペイロード Returns: {"sku": str, "created": bool, "status_code": int} Raises: ValueError: SKU が50文字を超える場合 requests.HTTPError: API がエラーを返した場合 """ if len(sku) > 50: raise ValueError(f"SKU は50文字以内にしてください: '{sku}'") # 完全上書き対策: 先に既存データを取得してマージする existing = self.get_inventory_item(sku) if existing is not None: logger.info("SKU '%s' 既存データ取得 → マージして更新します", sku) payload = _deep_merge(existing, new_payload) else: logger.info("SKU '%s' → 新規作成します", sku) payload = new_payload url = f"{EBAY_INVENTORY_BASE}/inventory_item/{sku}" resp = self._session.put(url, json=payload) resp.raise_for_status() is_created = (resp.status_code == 201) logger.info( "SKU '%s': %s 完了 (HTTP %d)", sku, "作成" if is_created else "更新", resp.status_code, ) return {"sku": sku, "created": is_created, "status_code": resp.status_code} # ── 使用例 ──────────────────────────────────────────────────────────────── if __name__ == "__main__": from ebay_token_manager import eBayTokenManager # 第1回実装済みクラス tm = eBayTokenManager() token = tm.get_valid_token() manager = InventoryItemManager(token) # Trading API 時代のデータ(移行元) old_data = TradingItemData( sku="JACKET-DENIM-M-001", title="Classic Denim Jacket Size M", description="<p>Vintage denim jacket in mint condition.</p>", condition="New", quantity=5, brand="Levi's", image_urls=["https://i.ebayimg.com/images/g/example/jacket.jpg"], aspects={"Brand": ["Levi's"], "Size": ["M"], "Color": ["Blue"]}, upc="012345678901", ) # アダプターで変換 → 安全にupsert payload = adapt_trading_to_inventory_item(old_data) result = manager.safe_upsert(old_data.sku, payload) print(f"完了: SKU={result['sku']}, 新規作成={result['created']}")
safe_upsert() 内の GET → PUT の2ステップの間にトークンが失効することは稀ですが、長時間バッチ処理を行う場合はセッション開始時に get_valid_token() でリフレッシュしておくことを推奨します。requests.Session() を使い回すことで TCP 接続のオーバーヘッドを削減し、大量の SKU を処理するバッチのスループットを向上させています。1,000件を超えるバッチ処理では、セッションの再利用だけで処理時間が20〜30%改善するケースもあります。
パフォーマンス・スケーリング視点 (深度)
Trading APIとInventory APIの並行運用戦略と段階的移行ロードマップ
数千〜数万件の商品を扱う実務では、一夜にして全商品を Inventory API へ移行するのは現実的ではありません。稼働中のシステムを止めずに段階的に移行するための3フェーズ戦略を解説します。
【フェーズ1: 棚卸しと分類(〜2週間)】まず、現在 Trading API で管理している全 SKU を棚卸しします。「アクティブな出品(現在売れているもの)」「長期間売れていない在庫」「新規追加予定の商品」の3カテゴリに分類します。このフェーズではAPIの変更はまだ行いません。最重要の成果物は、各 SKU に対して api_type("trading" or "inventory")と ebay_item_id を記録する管理テーブルです。このテーブルが後のフェーズで生命線になります。
【フェーズ2: 新規商品からInventory APIを適用(〜1ヶ月)】新しく出品する商品はすべて Inventory API(createOrReplaceInventoryItem → createOffer → publishOffer)で登録します。既存の Trading API 出品はそのまま維持します。このフェーズで管理テーブルへの記録ロジックを実装し、「どの SKU がどちらのAPI管轄か」を常に把握できる状態にします。
【フェーズ3: 既存商品の段階的移行(1〜3ヶ月)】優先度の低い商品(長期在庫・売上の少ないもの)から順に移行します。各商品の移行手順は: (1) Trading API 側で EndFixedPriceItem を呼び出して出品を終了 → (2) eBay のシステムが終了を処理するまで数分待機 → (3) adapt_trading_to_inventory_item() でデータ変換 → (4) safe_upsert() で Inventory Item 登録 → (5) createOffer → publishOffer(次回 #25 で実装)で再出品、の順です。1件ずつ完結させ、管理テーブルを更新してから次の商品に進みます。
1日250回の修正上限との兼ね合いも重要です。フェーズ3で大量移行を行う場合、1日に何件移行できるかをあらかじめ計算し、上限の8割程度(200件/日)を目安に処理件数を制限するロジックを実装してください。上限に達した場合は処理を翌日に持ち越すキューイング機構が必要です。
並行運用期間中の最大のリスクは「APIの取り違え」です。管理テーブルを参照せずにコードをハードコードすると、Trading API 管理の商品に対して Inventory API の更新を呼んで 404 が返り、そのエラーを握りつぶした結果として在庫が同期されないという「サイレント障害」が発生します。管理テーブルのルックアップは必ずAPIコールの前に行い、型チェック(api_type のバリデーション)も実装してください。
まとめ
本記事では、連載の新フェーズ【Sell REST - 出品】の幕開けとして、Trading API から Inventory API への移行の第一歩を実装しました。
ベースライン: OAuth Bearer Token を使い PUT /inventory_item/{sku}(createOrReplaceInventoryItem)を呼び出す最小構成を確認し、SKU 中心設計という Inventory API の根本的なパラダイムを理解しました。
深いポイント: Inventory Item を作成しただけではまだ出品されない(Offer が必要な3ステップ設計)、「完全上書き」仕様による意図しないデータ消失事故、1日250回の修正上限と bulk エンドポイントによる効率化、そして Inventory API と Trading API 間の出品非互換性という4つの重要なトラップとその回避策を解説しました。
スケーリング: 数万件規模の既存 Trading API 商品を、サービスを止めずに Inventory API へ移行するための3フェーズ戦略と、並行運用期間中の管理テーブルによる SKU 追跡の重要性を解説しました。
第1〜23回で積み上げたエンジニアリング思想(冪等性・バリデーションファースト・エラーハンドリング)は、Inventory API の実装においても変わらず重要です。むしろそれらの思想がベースとなることで、モダンな REST API への移行がスムーズに進みます。
次のステップ
次回(#25)では、本記事で登録した Inventory Item に対して createOffer と publishOffer を呼び出し、ついに eBay のサイト上に商品を公開する手順を解説します。価格・配送ポリシー・返品ポリシーの設定方法と、Offer のステータス管理(PUBLISHED / UNPUBLISHED)の詳細についても深く掘り下げます。お楽しみに!