ReviseFixedPriceItemで出品済みの価格・在庫・タイトルを安全に更新する
前回の記事はこちら
eBay Trading API:ReviseFixedPriceItemで出品済みの価格・在庫・タイトルを安全に更新する
はじめに
本記事は、全42回にわたる「eBay API 実践ガイド」の第7回です。
これまでの連載で、単一商品(#5)およびバリエーション商品(#6)の新規出品プロセスが完成しました。しかし、EC サイトの運用において「出品して終わり」ということはあり得ません。為替変動による価格調整、SEO 改善のためのタイトル変更など、出品後データの更新(Revision) は日常業務の大半を占めます。
この記事で得られること:
ReviseFixedPriceItemを用いた部分更新(Partial Update)のアーキテクチャ理解と空タグの危険性。- 変更不可フィールドと
DeletedFieldの正しい使い方。 - CSV を用いた一括更新バッチと、HTTP 429 (Too Many Requests) を防ぐ Exponential Backoff(指数バックオフ) の実装。
背景・なぜこれが重要か (Motivation)
eBay の Trading API において、既存の出品を更新するメソッドは主に ReviseFixedPriceItem です。この API は非常に強力で、タイトルから画像、Item Specifics に至るまで、出品時に設定したほぼすべての項目を後から書き換えることができます。
しかし、その強力さゆえに、リクエストの組み方を一つ間違えると「更新したかったのは価格だけなのに、商品説明が全部消えてしまった」という大事故を引き起こします。「何を送り、何を送らないべきか」。この 部分更新(Delta Update) の思想を理解することが、安全な運用システムの絶対条件となります。
基本的な使い方(ベースライン):部分更新の原則
ReviseFixedPriceItem の最大の特徴は、「XML に含めたタグだけが更新され、省略したタグは現状維持される」 という点です。
例えば、ItemID: 112233445566 の商品の「タイトル」と「価格」だけを変更したい場合、ベースラインの XML は以下のようになります。
<?xml version="1.0" encoding="utf-8"?> <ReviseFixedPriceItemRequest xmlns="urn:ebay:apis:eBLBaseComponents"> <ErrorLanguage>en_US</ErrorLanguage> <WarningLevel>High</WarningLevel> <Item> <ItemID>112233445566</ItemID> <Title>New SEO Optimized Title for Vintage Camera</Title> <StartPrice currencyID="USD">289.99</StartPrice> </Item> </ReviseFixedPriceItemRequest>
送信するXML eBay側の処理
───────────────── ─────────────────────────
<Title>新しい値</Title> → Titleを「新しい値」に上書き (反映)
<StartPrice>289</StartPrice> → Priceを289に上書き (反映)
(Descriptionタグなし) → Descriptionは現状維持 (安全)
<Description></Description> → Descriptionを空に上書き (危険)
実務で躓く場面・深いポイント (Core)
ここからは、エンジニアがよくハマる「更新 API の罠」を解説します。
1. 空タグによる意図せぬデータ消失 (Accidental Wipe)
最も多いバグがこれです。プログラム側で「今回は価格だけ更新するから、タイトルは空文字でいいや」と考えて <Title></Title> を送信すると、eBay は「タイトルを空にしろ」と解釈します。
後述する CSV 一括更新スクリプトでは、「CSV の空欄」をプログラム側でフィルタリングし、XML タグ自体を生成しない(現状維持にする) という厳格な制御を行っています。
2. 変更不可フィールドの存在
Revise API でも変更できない(または条件付きでしか変更できない)フィールドがあります。これを変更しようとすると Error 21916635 などで弾かれます。
| フィールド | 制約内容 |
|---|---|
| PrimaryCategory | 出品から14日経過後、または販売実績がある場合は変更不可。 |
| ListingType | 固定(Fixed Price から Auction への変更などは不可)。 |
| VariationSpecificsSet | バリエーションの「軸の名前(例: Color)」の変更は不可(値の追加は可)。 |
3. 項目の明示的な削除 (DeletedField)
空タグが使えない項目において、既存データを完全に削除するためには eBay 特有の <DeletedField> タグを使用します。
<ReviseFixedPriceItemRequest xmlns="urn:ebay:apis:eBLBaseComponents"> <DeletedField>Item.SubTitle</DeletedField> <DeletedField>Item.SecondaryCategory</DeletedField> <DeletedField>Item.ShippingDetails.ShippingServiceOptions</DeletedField> # 配送オプション全体が削除されます。FreeShipping設定も消えるため注意! <Item><ItemID>112233445566</ItemID></Item> </ReviseFixedPriceItemRequest>
4. バリエーション商品(Multi-SKU)に関する罠
本記事の実装は 単一商品(Single-SKU) が対象です。前回(#6)で解説したバリエーション商品に対して、本記事のようにトップレベルの <Quantity> を送信しても無視されます。バリエーションの在庫を更新する場合は、特定の <Variation> ブロック内で <SKU> と <Quantity> を指定する必要があります。
5. UUID は Revise では冪等性を保証しない
第5回で紹介した <UUID> による冪等性(Idempotency)保証は、実は Add 系の呼び出し(新規出品)に限定された機能 です。ReviseFixedPriceItem に UUID を送っても無害ですが、二重更新を防ぐ効果はありません。そのため、バッチ処理側で適切なリトライ制御を行う必要があります。
堅牢な実装:動的 XML ビルダーによる安全な更新関数
「意図せぬデータ消失の防止」「NoneType クラッシュ対策」「処理スキップの明示」をすべて組み込んだ堅牢な更新関数を実装します。今回はバッチ処理等でも使い回せるよう、Rate Limit 対策のバックオフ関数も同じファイルに定義します。
# revise_item.py import requests import xml.etree.ElementTree as ET import html import time import random from dataclasses import dataclass, field from enum import Enum from typing import List from config import eBayConfig from ebay_token_manager import eBayTokenManager class ReviseStatus(Enum): SUCCESS = "success" SKIPPED = "skipped" @dataclass class ReviseResult: item_id: str status: ReviseStatus warnings: List[str] = field(default_factory=list) def _get_text(node: ET.Element, tag: str, ns: dict) -> str: """要素が存在しない場合の NoneType クラッシュを防ぐヘルパー関数""" if node is None: return "" el = node.find(tag, ns) return el.text if el is not None and el.text is not None else "" def api_call_with_backoff(fn, max_retries=3): """HTTP 429エラー時に待機時間を倍増させながらリトライするヘルパー""" for attempt in range(max_retries): try: return fn() except requests.exceptions.HTTPError as e: if e.response is not None and e.response.status_code == 429: # Too Many Requests wait = (2 ** attempt) + random.uniform(0, 1) print(f"[警告] Rate limited. Retrying in {wait:.2f}s...") time.sleep(wait) else: raise raise Exception("Max retries exceeded after HTTP errors") def revise_single_item(config: eBayConfig, token: str, item_id: str, updates: dict) -> ReviseResult: """ 指定された項目のみを安全に部分更新する関数。 """ if not item_id: raise ValueError("ItemID is required for revision.") xml_elements = [] if updates.get('title'): xml_elements.append(f"<Title>{html.escape(updates['title'])}</Title>") if updates.get('price') is not None: try: price_val = float(updates['price']) xml_elements.append(f'<StartPrice currencyID="USD">{price_val}</StartPrice>') except ValueError: raise ValueError("Price must be numeric.") if updates.get('quantity') is not None: try: qty_val = int(updates['quantity']) xml_elements.append(f"<Quantity>{qty_val}</Quantity>") except ValueError: raise ValueError("Quantity must be an integer.") # 【ポイント】 更新項目がない場合はAPIコールをスキップし、状態を明確に返す if not xml_elements: return ReviseResult(item_id=item_id, status=ReviseStatus.SKIPPED) inner_xml = "\n".join(xml_elements) headers = { "X-EBAY-API-CALL-NAME": "ReviseFixedPriceItem", "X-EBAY-API-SITEID": "0", "X-EBAY-API-COMPATIBILITY-LEVEL": "1323", "X-EBAY-API-IAF-TOKEN": token, "Content-Type": "text/xml" } xml_payload = f"""<?xml version="1.0" encoding="utf-8"?> <ReviseFixedPriceItemRequest xmlns="urn:ebay:apis:eBLBaseComponents"> <ErrorLanguage>en_US</ErrorLanguage> <WarningLevel>High</WarningLevel> <Item> <ItemID>{html.escape(str(item_id))}</ItemID> {inner_xml} </Item> </ReviseFixedPriceItemRequest> """ def execute_request(): res = requests.post( config.trading_api_url, headers=headers, data=xml_payload.encode('utf-8'), timeout=30 ) res.raise_for_status() return res # バックオフ関数を使ってリクエストを安全に実行 response = api_call_with_backoff(execute_request) # レスポンス解析 namespace = {'ns': 'urn:ebay:apis:eBLBaseComponents'} root = ET.fromstring(response.text) ack = _get_text(root, 'ns:Ack', namespace) warnings_list = [] if ack == 'Warning': warnings_list = [ _get_text(e, 'ns:LongMessage', namespace) for e in root.findall('ns:Errors', namespace) if _get_text(e, 'ns:SeverityCode', namespace) == 'Warning' ] if ack not in ['Success', 'Warning']: errors = [ { "code": _get_text(e, 'ns:ErrorCode', namespace), "message": _get_text(e, 'ns:LongMessage', namespace) } for e in root.findall('ns:Errors', namespace) if _get_text(e, 'ns:SeverityCode', namespace) == 'Error' ] raise Exception(f"Revision Failed for {item_id}: {errors}") return ReviseResult(item_id=item_id, status=ReviseStatus.SUCCESS, warnings=warnings_list)
実践:CSVを用いた一括更新バッチ処理とバックオフ制御
実務では「年末商戦に向けて、CSVで数百件の商品のタイトルと価格を一気に変更したい」といったバッチ処理が求められます。
用意するCSV (bulk_updates.csv) の例:
item_id,title,price,quantity 112233445566,Holiday Special Vintage Camera,250.00, 998877665544,Limited Edition Lens 50mm,,5 776655443322,,,
※ 3行目の 776655443322,,, は更新項目がすべて空欄のため、プログラム内で SKIPPED 扱いとなります(実際の運用では事前に除外して構いません)。
Exponential Backoff(指数バックオフ)による Rate Limit 対策
Trading API の Rate Limit は「1日あたりの呼び出し回数上限(Daily Call Limit)」と「瞬間的な過負荷による 429 Too Many Requests」の両方が存在します。429 エラーが出た際に即座にリトライするとブロックされるため、前述の指数バックオフ機構が必須です。
# bulk_revise_from_csv.py import csv from config import eBayConfig from ebay_token_manager import eBayTokenManager from revise_item import revise_single_item, ReviseStatus def run_bulk_update(csv_file_path: str): config = eBayConfig() manager = eBayTokenManager(config.client_id, config.client_secret, config.refresh_token, config.env) stats = {"success": 0, "skipped": 0, "error": 0} with open(csv_file_path, mode='r', encoding='utf-8-sig') as f: reader = csv.DictReader(f) for row in reader: item_id = row.get('item_id', '').strip() if not item_id: continue # CSVの空欄をフィルタリングし、意図せぬデータ消失(空タグ送信)を防ぐ updates = {k: v for k, v in row.items() if k != 'item_id' and v.strip() != ''} try: token = manager.get_token() result = revise_single_item(config, token, item_id, updates) if result.status == ReviseStatus.SKIPPED: print(f"[SKIPPED]: {item_id} (No updates)") stats["skipped"] += 1 else: warn_text = f" (Warnings: {len(result.warnings)})" if result.warnings else "" print(f"[SUCCESS]: {item_id}{warn_text}") stats["success"] += 1 except Exception as e: print(f"[ERROR] updating {item_id}: {e}") stats["error"] += 1 print("\n--- Batch Update Complete ---") print(f"Success: {stats['success']} | Skipped: {stats['skipped']} | Errors: {stats['error']}") if __name__ == "__main__": run_bulk_update("bulk_updates.csv")
パフォーマンス・スケーリング視点 (深度)
上記で作成した CSV 一括更新バッチは、「タイトル、説明、Item Specifics などのカタログ情報を一括改修する(例: 季節ごとのSEO対策)」 という目的においては最適です。
しかし、「自社倉庫の在庫が1個売れたから、eBay の在庫数もすぐ減らしたい」「15分おきに価格と在庫だけを他モールと同期させたい」といった高頻度のトランザクション同期に、この
ReviseFixedPriceItem をループで回すのはシステム崩壊の原因になります。Revise は eBay 内部で商品カタログ全体を再インデックスするため負荷が大きいです。
- ReviseFixedPriceItem: カタログ情報(タイトル等)の変更。日次・週次ベースのバッチ処理や夜間の分散実行。
- ReviseInventoryStatus: 「価格」と「在庫数」だけを更新する場合。次回解説するこちらの軽量 API を使用します。
まとめ
本記事では、出品済み商品のデータを安全にメンテナンスするための ReviseFixedPriceItem の使い方を解説しました。
- ベースライン: 変更差分だけを XML に含める「部分更新(Delta Update)」の原則。
- 深いポイント: 空欄によるデータ消失の防止、DeletedField の使い方、そして安全な XML 解析とステータス管理。
- スケーリング: HTTP 429 を防ぐ指数バックオフ制御と、目的(カタログ改修 vs 高頻度在庫同期)に応じた API の使い分け。
次のステップ
今回、パフォーマンスの章で触れた「軽量な在庫同期 API」の正体について掘り下げます。
次回(#8)は、「ReviseInventoryStatusで複数商品の在庫・価格を高速同期する」 です。前回のバリエーション出品で仕込んだ InventoryTrackingMethod=SKU がここで真価を発揮します。多店舗展開の在庫連動を組むエンジニア必見の内容です!お楽しみに。
技術的なサポートやご質問について
APIの実装や仕様に関してご不明な点がございましたら、以下のeBay Japan 技術サポート窓口までお気軽にお問い合わせください:
ebayjapan-techsupport@ebay.com