eBay Message API:getConversationsとgetConversationでバイヤーとのメッセージを取得する

前回の記事はこちら

【連載#21】eBay Message API:getConversationsとgetConversationでバイヤーとのメッセージを取得する

はじめに

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

今回から新しい API カテゴリ【Message API】に突入します。これは eBay が従来の Trading API 版 CS 管理機能(GetMemberMessages)を全面的に REST 化した、新しい会話管理 API「M2M Public API Service」です。バイヤーからの問い合わせへの対応は、カスタマーサービス業務の中核であり、応答速度が出品者評価(Feedback)やアカウントヘルスに直結します。本記事では、REST 版 Message API の入口となる 2 つのエンドポイント——getConversations と getConversation——を実務視点で徹底解説します。

前回(#20)は Inventory Mapping API シリーズの締めくくりとして、AI 推奨結果を Inventory API に渡して高品質な出品を自動生成する方法を解説しました。今回からは、出品後に発生するバイヤーとのコミュニケーション管理——すなわちメッセージ API の世界に踏み込みます。

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

  • GET /conversation(getConversations)を使い、conversation_type・conversation_status・reference_id などのパラメータを組み合わせてバイヤーからの未読メッセージ一覧を効率よく取得する方法。
  • GET /conversation/{conversation_id}(getConversation)で特定の会話スレッド内の全メッセージをページネーションしながら取得する方法。
  • start_time / end_time による差分同期の設計方針と、生産環境で躓きやすいパラメータ制約・エラーコードの対処法。

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

「第14回で Trading API の GetMemberMessages を実装したのに、なぜまた別の API が必要なのでしょうか?」

これは Message API を初めて目にした開発者の多くが抱く、ごく自然な疑問です。結論から言うと、eBay は Trading API の段階的廃止を正式にアナウンスしており、全 API を REST / GraphQL ベースの新世代アーキテクチャへ移行する計画を進めています。GetMemberMessages もその対象であり、将来的に REST 版 Message API(M2M Public API Service)に完全置き換えられます。今から REST 版に移行しておくことは、長期的な保守コスト削減に直結します。

Trading API 版との最大の違いは、会話タイプの明確な分離です。GetMemberMessages では eBay からの公式通知もバイヤーからの問い合わせも混在したまま返却されていました。REST 版では conversation_type パラメータにより、FROM_EBAY(eBay からの公式通知・アラート)と FROM_MEMBERS(バイヤーとの直接メッセージ)を最初のリクエスト時点で明確に分離します。これにより「返信が必要なバイヤーメッセージだけを抽出する」処理が格段にシンプルになり、不要なデータを取得する無駄がなくなります。

ページネーション仕様も標準化されました。Trading API が独自の EntriesPerPage / PageNumber 方式を採用していたのに対し、REST 版は limit と offset によるシンプルなオフセット方式を採用しており、他の eBay REST API と同じ感覚で実装できます。また、FROM_MEMBERS に限り start_time / end_time による時刻フィルタリングが可能になり、「前回ポーリング以降の新着メッセージだけを差分取得する」設計が容易になっています。

以下の表に Trading API 版と REST 版の主要な違いをまとめます。

項目 Trading API: GetMemberMessages REST: Message API
認証方式 XML + Auth Token OAuth 2.0 Bearer Token
会話種別の分離 なし(全メッセージ混在) FROM_EBAY / FROM_MEMBERS で明示分離
ページネーション EntriesPerPage + PageNumber limit + offset
時刻フィルタ StartCreationTime + EndCreationTime start_time / end_time(FROM_MEMBERS のみ)
廃止予定 廃止予定あり 現行推奨 API

基本的な使い方(ベースライン):getConversationsで未読会話一覧を取得する

まず最小限の実装で動かしてみましょう。GET /conversation を叩いて、バイヤーからの未読会話一覧を取得します。conversation_type=FROM_MEMBERS と conversation_status=UNREAD の組み合わせが、カスタマーサービス自動化の第一歩です。OAuth 2.0 のアクセストークンを取得済みであることを前提とします(認証フローは第1回を参照してください)。

# get_conversations_baseline.py
import requests

BASE_URL = "https://api.ebay.com/commerce/message/v1"

def get_unread_conversations(access_token: str) -> dict:
    """バイヤーからの未読会話一覧を取得する(最小実装)"""
    url = f"{BASE_URL}/conversation"
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json",
        "X-EBAY-C-MARKETPLACE-ID": "EBAY_JP",
    }
    params = {
        "conversation_type": "FROM_MEMBERS",   # 必須: バイヤーとのメッセージ
        "conversation_status": "UNREAD",        # 任意: 未読のみ絞り込み
        "limit": 25,                            # 1回あたりの取得件数 (最大50)
        "offset": 0,                            # 開始位置
    }
    response = requests.get(url, headers=headers, params=params, timeout=30)
    response.raise_for_status()
    return response.json()

if __name__ == "__main__":
    TOKEN = "v^1.1#i^1#..."   # ← ここに実際のOAuthトークンをセット
    result = get_unread_conversations(TOKEN)

    total = result.get("total", 0)
    print(f"未読会話総数: {total}")

    for conv in result.get("conversations", []):
        conv_id  = conv.get("conversationId", "")
        subject  = conv.get("subject", "(件名なし)")
        buyer    = conv.get("buyer", {}).get("username", "")
        print(f"  [{conv_id}] {subject}  (バイヤー: {buyer})")

レスポンスの conversations 配列には、各会話のメタデータ(conversationId・subject・buyer・creationDate・messageStatus など)が含まれます。ここで取得した conversationId を使って、次の getConversation でスレッド内の全メッセージを取得します。

補足: conversation_type の 2 種類について

FROM_MEMBERS はバイヤーとセラー間の直接メッセージ(購入前の質問・交渉・クレームなど)に使用します。FROM_EBAY は eBay が送信する公式通知(ポリシー違反通知・セキュリティアラート・システムメッセージ等)に使用します。カスタマーサービス自動化においては FROM_MEMBERS が主要ターゲットです。FROM_EBAY のメッセージは自動返信の対象ではなく、モニタリング・アーカイブ目的での取得が中心になります。

特定の商品(Listing)に紐付いたメッセージだけを絞り込みたい場合は、reference_id と reference_type を組み合わせます。例えば、item_id が「123456789012」の商品に関する未読メッセージだけを取得したい場合は、params に reference_id="123456789012" と reference_type="LISTING" を追加します。これにより、複数商品を管理するセラーが「この SKU に関する問い合わせだけを優先処理する」ワークフローを実装できます。

# 特定Listing(item_id)のメッセージに絞り込む例
params_with_listing = {
    "conversation_type": "FROM_MEMBERS",
    "conversation_status": "UNREAD",
    "reference_id": "123456789012",   # eBay の item_id
    "reference_type": "LISTING",
    "limit": 50,
    "offset": 0,
}

reference_type には現在 LISTING が主要な値ですが、eBay の将来の拡張に備えて文字列として定数管理することを推奨します。

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

ベースライン実装は単純に見えますが、実務で使うとすぐにいくつかの「罠」に気づきます。以下では、実際に頻繁に発生するトラブルとその回避策を具体的に解説します。

1. conversation_type は必須パラメータ——指定し忘れると即 400 エラー

getConversations のドキュメントを斜め読みして「とりあえず全件取ってみよう」と conversation_type を省略してリクエストを送ると、eBay はすぐさま HTTP 400 Bad Request を返します。このパラメータは仕様上 required(必須)と明記されており、省略した場合はリクエスト自体が受け付けられません。

エラーレスポンスの例は次のようになります。

{
  "errors": [
    {
      "errorId": 850001,
      "domain": "API_MESSAGE",
      "category": "REQUEST",
      "message": "Invalid request. The 'conversation_type' field is required.",
      "parameters": [
        { "name": "fieldName", "value": "conversation_type" }
      ]
    }
  ]
}
注意: conversation_type は getConversation(個別取得)でも必須です。

パスパラメータ conversation_id を指定しているからといって省略できません。ページネーション関連のラッパー関数を作る際は、必ず conversation_type を引数として受け取り、常にリクエストに含める設計にしてください。

2. limit の最大値は 50——大量取得時は offset ページネーションが必須

limit パラメータのデフォルト値は 25 で、上限は 50 です。「一括取得したい」と limit=100 や limit=200 を指定しても、API は 400 エラーを返すか、silently に上限値の 50 に丸め込んで返します。未読メッセージが大量に溜まっているアカウント(例えば数日間放置した場合)では、1 回のリクエストでは全件取得できないケースがほとんどです。

正しいアプローチは offset によるページネーションです。レスポンスに含まれる total フィールドが全件数を示しており、「offset + 取得件数 >= total」になるまでループを回します。以下にシンプルなページネーションループのスニペットを示します。

def get_all_conversations(access_token: str, conv_type: str) -> list[dict]:
    """全会話をページネーションで取得する"""
    url = f"{BASE_URL}/conversation"
    headers = {"Authorization": f"Bearer {access_token}",
               "X-EBAY-C-MARKETPLACE-ID": "EBAY_JP"}
    all_convs = []
    offset = 0
    limit = 50   # 常に最大値を使う

    while True:
        params = {"conversation_type": conv_type,
                  "limit": limit, "offset": offset}
        resp = requests.get(url, headers=headers, params=params, timeout=30)
        resp.raise_for_status()
        data = resp.json()

        convs = data.get("conversations", [])
        all_convs.extend(convs)

        total = data.get("total", 0)
        offset += len(convs)

        if offset >= total or not convs:
            break   # 全件取得完了

    return all_convs
補足: offset ページネーションの既知の問題

offset ベースのページネーションは「取得中に新着が追加された場合に重複・欠落が発生しうる」という既知の問題を抱えています。リアルタイム性が重要な本番環境では、取得後に重複排除(conversationId をキーとした dedup)をかけることを推奨します。

3. start_time / end_time は FROM_MEMBERS 専用——FROM_EBAY で使うとエラー

差分同期で「前回チェック以降の新着メッセージだけを取得したい」という要件は非常に一般的です。start_time と end_time パラメータはまさにそのためにありますが、これらは conversation_type=FROM_MEMBERS の場合にしか利用できません。FROM_EBAY に対して start_time を指定すると、エラーが返るか、パラメータが無視されて全件が返却される不安定な挙動を示します。

FROM_EBAY の時刻フィルタリングが必要な場合は、クライアント側でレスポンスを受け取った後に creationDate フィールドで絞り込む後処理フィルタリングを実装してください。ただし FROM_EBAY のメッセージ量は一般に少ないため、全件取得してクライアント側でフィルタする方式でも実用上の問題は少ないでしょう。

start_time / end_time のフォーマットは ISO 8601 形式(UTC)です。Python での生成例を示します。

from datetime import datetime, timezone, timedelta

# 過去24時間のメッセージを取得する場合
now = datetime.now(timezone.utc)
start = now - timedelta(hours=24)

params = {
    "conversation_type": "FROM_MEMBERS",   # 必須: FROM_MEMBERSのみ有効
    "start_time": start.strftime("%Y-%m-%dT%H:%M:%S.000Z"),
    "end_time":   now.strftime("%Y-%m-%dT%H:%M:%S.000Z"),
    "limit": 50,
    "offset": 0,
}

頻出エラーコード早見表

Message API で実際に遭遇しやすいエラーコードと対処法を以下にまとめます。

HTTPステータス / errorId エラーメッセージ(抜粋) 原因と対処法
400 / 850001 field 'conversation_type' is required conversation_type を省略した。必ず FROM_MEMBERS か FROM_EBAY を指定すること。
400 / 850002 Invalid value for 'limit'. Maximum allowed value is 50. limit に 50 超の値を指定した。limit=50 に修正し offset でページネーションする。
400 / 850010 start_time is not supported for conversation_type FROM_EBAY FROM_EBAY に start_time を指定した。FROM_MEMBERS のみ対応。クライアント側でフィルタする。
401 / 1001 Invalid access token. Token has expired. OAuth トークンが失効。トークンを再取得して Bearer ヘッダを更新する。
404 / 850100 Conversation not found. conversation_id が存在しないか、自分のアカウントに紐付いていない。getConversations で取得した ID のみ getConversation に渡すこと。
429 / — Too many requests. レート制限に到達。Retry-After ヘッダの秒数だけ待機してからリトライする。

堅牢な実装:全会話の一括取得と個別メッセージ収集クラス

ここまでのポイントをすべて盛り込んだ、プロダクションレベルの実装を示します。EbayMessageClient クラスとして実装し、型アノテーション・docstring・入力バリデーション・例外処理・レート制限対応を備えています。getConversations で全会話を offset ページネーションで取得し、各会話に対して getConversation で全メッセージを取得後、JSON ファイルに保存するところまでをワンクラスで完結させます。

# message_client.py
import json
import time
import logging
from dataclasses import dataclass, field
from typing import Optional, Iterator
import requests

logger = logging.getLogger(__name__)
BASE_URL = "https://api.ebay.com/commerce/message/v1"


@dataclass
class ConversationFilter:
    """会話取得フィルタ設定。

    Attributes:
        conversation_type: 必須。"FROM_MEMBERS" または "FROM_EBAY"。
        status: 任意。"UNREAD" / "READ" / "ACTIVE" / "ARCHIVE" / "DELETE"。
        other_party_username: 任意。特定バイヤーのユーザー名でフィルタ。
        reference_id: 任意。特定 Listing の item_id。
        reference_type: 任意。reference_id と合わせて使用(例: "LISTING")。
        start_time: 任意。ISO 8601形式(FROM_MEMBERS のみ有効)。
        end_time: 任意。ISO 8601形式(FROM_MEMBERS のみ有効)。
    """
    conversation_type: str
    status: Optional[str] = None
    other_party_username: Optional[str] = None
    reference_id: Optional[str] = None
    reference_type: Optional[str] = None
    start_time: Optional[str] = None
    end_time: Optional[str] = None


class EbayMessageClient:
    """eBay Message API クライアント(プロダクションレベル)。"""

    MAX_LIMIT = 50
    DEFAULT_TIMEOUT = 30

    VALID_TYPES = {"FROM_MEMBERS", "FROM_EBAY"}
    VALID_STATUSES = {"ACTIVE", "ARCHIVE", "DELETE", "READ", "UNREAD"}

    def __init__(self, access_token: str,
                 marketplace_id: str = "EBAY_JP") -> None:
        """
        Args:
            access_token: OAuth 2.0 アクセストークン(必須)。
            marketplace_id: マーケットプレイス ID(デフォルト: EBAY_JP)。
        Raises:
            ValueError: access_token が空の場合。
        """
        if not access_token or not access_token.strip():
            raise ValueError("access_token は空にできません。")
        self._token = access_token.strip()
        self._marketplace_id = marketplace_id
        self._session = requests.Session()
        self._session.headers.update({
            "Authorization": f"Bearer {self._token}",
            "Content-Type": "application/json",
            "X-EBAY-C-MARKETPLACE-ID": self._marketplace_id,
        })

    def _validate_filter(self, f: ConversationFilter) -> None:
        """ConversationFilter の入力値を事前検証する。"""
        if f.conversation_type not in self.VALID_TYPES:
            raise ValueError(
                f"conversation_type は {self.VALID_TYPES} のいずれかを指定してください。"
                f"受け取った値: {f.conversation_type!r}"
            )
        if f.status and f.status not in self.VALID_STATUSES:
            raise ValueError(
                f"status は {self.VALID_STATUSES} のいずれかを指定してください。"
                f"受け取った値: {f.status!r}"
            )
        if (f.start_time or f.end_time) and f.conversation_type != "FROM_MEMBERS":
            raise ValueError(
                "start_time / end_time は conversation_type='FROM_MEMBERS' の場合のみ"
                "使用できます。FROM_EBAY では利用不可です。"
            )

    def _get(self, path: str, params: dict) -> dict:
        """内部 GET リクエスト(レート制限・タイムアウト対応)。"""
        url = f"{BASE_URL}{path}"
        try:
            resp = self._session.get(
                url, params=params, timeout=self.DEFAULT_TIMEOUT
            )
            if resp.status_code == 429:
                retry_after = int(resp.headers.get("Retry-After", 60))
                logger.warning(
                    f"レート制限に到達しました。{retry_after} 秒待機します..."
                )
                time.sleep(retry_after)
                resp = self._session.get(
                    url, params=params, timeout=self.DEFAULT_TIMEOUT
                )
            resp.raise_for_status()
            return resp.json()
        except requests.exceptions.HTTPError as exc:
            logger.error(
                f"HTTP エラー: {exc.response.status_code} | "
                f"Body: {exc.response.text[:300]}"
            )
            raise
        except requests.exceptions.Timeout:
            logger.error(
                f"リクエストタイムアウト ({self.DEFAULT_TIMEOUT}s): {url}"
            )
            raise

    def iter_conversations(
        self, f: ConversationFilter
    ) -> Iterator[dict]:
        """全会話をページネーションで逐次返すジェネレータ。

        Args:
            f: 取得条件を指定した ConversationFilter。
        Yields:
            各会話のメタデータ辞書。
        """
        self._validate_filter(f)

        params: dict = {"conversation_type": f.conversation_type}
        if f.status:
            params["conversation_status"] = f.status
        if f.other_party_username:
            params["other_party_username"] = f.other_party_username
        if f.reference_id:
            params["reference_id"] = f.reference_id
            params["reference_type"] = f.reference_type or "LISTING"
        if f.start_time:
            params["start_time"] = f.start_time
        if f.end_time:
            params["end_time"] = f.end_time

        offset = 0
        while True:
            params["limit"] = self.MAX_LIMIT
            params["offset"] = offset
            data = self._get("/conversation", params)
            conversations = data.get("conversations", [])
            if not conversations:
                break
            yield from conversations
            total = data.get("total", 0)
            offset += len(conversations)
            if offset >= total:
                break

    def get_conversation_messages(
        self, conversation_id: str, conversation_type: str
    ) -> list[dict]:
        """特定会話の全メッセージをページネーションで取得する。

        Args:
            conversation_id: getConversations で取得した会話 ID。
            conversation_type: "FROM_MEMBERS" または "FROM_EBAY"。
        Returns:
            全メッセージのリスト(時系列順)。
        Raises:
            ValueError: conversation_id が空の場合。
        """
        if not conversation_id or not conversation_id.strip():
            raise ValueError("conversation_id は空にできません。")
        if conversation_type not in self.VALID_TYPES:
            raise ValueError(
                f"conversation_type は {self.VALID_TYPES} のいずれかを指定してください。"
            )

        messages: list[dict] = []
        offset = 0
        while True:
            params = {
                "conversation_type": conversation_type,
                "limit": self.MAX_LIMIT,
                "offset": offset,
            }
            data = self._get(f"/conversation/{conversation_id}", params)
            msgs = data.get("messages", [])
            messages.extend(msgs)
            total = data.get("total", 0)
            offset += len(msgs)
            if not msgs or offset >= total:
                break
        return messages

    def fetch_and_save(
        self,
        f: ConversationFilter,
        output_path: str,
    ) -> int:
        """全会話とメッセージを取得し JSON ファイルに保存する。

        Args:
            f: 取得条件フィルタ。
            output_path: 保存先ファイルパス(.json)。
        Returns:
            保存した会話件数。
        """
        result: list[dict] = []
        for conv in self.iter_conversations(f):
            conv_id = conv.get("conversationId", "")
            logger.info(f"会話取得中: {conv_id}")
            messages = self.get_conversation_messages(
                conv_id, f.conversation_type
            )
            result.append({
                "conversation": conv,
                "messages": messages,
            })

        with open(output_path, "w", encoding="utf-8") as fp:
            json.dump(result, fp, ensure_ascii=False, indent=2)

        logger.info(
            f"{len(result)} 件の会話を {output_path} に保存しました。"
        )
        return len(result)


# ─── 実行例 ─────────────────────────────────────────────────────────────
if __name__ == "__main__":
    import os
    from datetime import datetime, timezone, timedelta

    logging.basicConfig(level=logging.INFO)

    TOKEN = os.environ["EBAY_ACCESS_TOKEN"]
    client = EbayMessageClient(access_token=TOKEN)

    # 過去72時間の未読バイヤーメッセージを全件取得
    now = datetime.now(timezone.utc)
    start = now - timedelta(hours=72)
    flt = ConversationFilter(
        conversation_type="FROM_MEMBERS",
        status="UNREAD",
        start_time=start.strftime("%Y-%m-%dT%H:%M:%S.000Z"),
        end_time=now.strftime("%Y-%m-%dT%H:%M:%S.000Z"),
    )
    saved = client.fetch_and_save(flt, "unread_conversations.json")
    print(f"保存完了: {saved} 件")

fetch_and_save メソッドはジェネレータ(iter_conversations)を使って会話を 1 件ずつ処理するため、大量の会話がある場合でもメモリ使用量を一定に保てます。各会話のメッセージ取得(get_conversation_messages)も内部でページネーションを行うため、長大なスレッドでも安全に全件取得できます。

注意: OAuth トークンの管理について

OAuth トークンは環境変数(EBAY_ACCESS_TOKEN)から読み込む設計にしてください。ソースコードにトークンをハードコーディングすると、Git リポジトリへの誤コミットによる情報漏洩リスクが生じます。トークンの有効期限は通常 2 時間ですので、長時間バッチ処理を行う場合はリフレッシュトークンを使ったトークン自動更新機能(第1回参照)と組み合わせてください。

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

大量会話を効率的に処理する差分同期(デルタ同期)戦略

規模が大きいセラーアカウントでは、未読メッセージが数千件に達することもあります。毎回全件取得(フルスキャン)するのは API のレート制限を消費するだけでなく、処理時間も長くなります。REST 版 Message API が提供する start_time パラメータを活用した差分同期(デルタ同期)が、スケーラブルな設計の鍵です。

差分同期の基本戦略は次の通りです。まず、前回のポーリング成功時刻を永続ストレージ(データベースやファイル)に記録します。次回ポーリング時は、その時刻を start_time として指定することで、新着分のみを取得できます。この方式により、API コール数とデータ転送量を大幅に削減できます。

# delta_sync.py
import json
import os
from datetime import datetime, timezone
from message_client import EbayMessageClient, ConversationFilter

CHECKPOINT_FILE = "last_sync_time.json"

def load_checkpoint() -> str:
    """前回同期時刻を ISO 8601 形式で返す(初回は7日前)。"""
    if os.path.exists(CHECKPOINT_FILE):
        with open(CHECKPOINT_FILE) as fp:
            data = json.load(fp)
            return data.get("last_sync_time", "")
    # 初回実行: 過去7日分を取得
    from datetime import timedelta
    start = datetime.now(timezone.utc) - timedelta(days=7)
    return start.strftime("%Y-%m-%dT%H:%M:%S.000Z")

def save_checkpoint(sync_time: str) -> None:
    """同期完了時刻を保存する。"""
    with open(CHECKPOINT_FILE, "w") as fp:
        json.dump({"last_sync_time": sync_time}, fp)

def run_delta_sync(access_token: str) -> None:
    now = datetime.now(timezone.utc)
    now_str = now.strftime("%Y-%m-%dT%H:%M:%S.000Z")
    start_str = load_checkpoint()

    client = EbayMessageClient(access_token=access_token)
    flt = ConversationFilter(
        conversation_type="FROM_MEMBERS",
        start_time=start_str,
        end_time=now_str,
    )

    saved = client.fetch_and_save(
        flt, f"delta_{now.strftime('%Y%m%d_%H%M%S')}.json"
    )
    print(f"差分取得完了: {saved} 件の新着会話")

    # 成功した場合のみチェックポイントを更新
    save_checkpoint(now_str)

チェックポイントファイルの更新は、fetch_and_save が例外なく完了した後に行うことが重要です。途中でエラーが発生した場合は古いチェックポイントを維持することで、次回実行時に該当期間を再取得(冪等な再試行)できます。

ポーリング間隔と API レート制限の設計

Message API のレート制限は eBay の利用規約・API キーのプランによって異なりますが、一般的には 1 日あたりのコール数と 1 秒あたりのコール数の両方に上限があります。カスタマーサービスのポーリング間隔として、以下の指針を推奨します。

営業時間内(JST 9:00〜21:00): 5〜10 分間隔でポーリング。バイヤーからの問い合わせに迅速に応答するため、短い間隔が望ましいですが、レート制限を考慮して最短でも 5 分以上とすることを推奨します。

営業時間外(JST 21:00〜翌 9:00): 30 分〜1 時間間隔でポーリング。自動返信ボット(次回 #22 で解説)と組み合わせることで、営業時間外でもバイヤーへの初期応答を自動化できます。

HTTP 429(レート制限超過)を受け取った場合は、Retry-After ヘッダに指定された秒数だけ待機してからリトライします。前掲の EbayMessageClient._get メソッドにはこの処理が組み込まれています。また、複数アカウントを管理する場合は API キーをアカウントごとに分離することで、レート制限の消費を独立させることができます。

まとめ

本記事では、eBay Message API(M2M Public API Service)の入口となる 2 つのエンドポイント——getConversations と getConversation——を解説しました。

  • ベースライン: conversation_type=FROM_MEMBERS + conversation_status=UNREAD の組み合わせでバイヤーからの未読メッセージ一覧を取得する基本フロー。reference_id + reference_type で特定 Listing への絞り込みができる仕組みも確認しました。
  • 深いポイント: conversation_type は getConversations・getConversation の両方で必須パラメータであること。limit の上限は 50 であり大量取得には offset ページネーションが必須であること。start_time / end_time は FROM_MEMBERS 専用であり FROM_EBAY では利用できない制約があること。これらを知らずに実装すると即座に 400 エラーに直面します。
  • スケーリング: EbayMessageClient クラスによる型安全な実装と、チェックポイントファイルを使った差分同期(デルタ同期)戦略。ポーリング間隔の設計とレート制限への対応により、本番環境での安定した運用が可能になります。

Trading API の GetMemberMessages と比較すると、REST 版はパラメータが整理されており、他の eBay REST API と同じ作法で実装できる点が大きな利点です。また OAuth 2.0 の標準的な認証フローを使用するため、既存のトークン管理基盤(第1回で構築済み)をそのまま流用できます。

次のステップ

バイヤーのメッセージを取得できるようになったら、次の目標は「自動で返信する」ことです。メッセージを読むだけでは CS 自動化の半分しか完成していません。

次回(#22)では、Message API の sendMessage エンドポイントを使い、よくある問い合わせパターン(在庫確認・発送状況・返品手順など)をテンプレートベースで自動返信するボットを構築します。今回実装した EbayMessageClient クラスを拡張して、読み取り→分類→返信の完全な自動化パイプラインを完成させましょう。お楽しみに!

次の記事はこちら

トップに戻る