eBay Browse API keyword search を GAS で叩く方法(サンプルコード付き)

eBay API

Browse API の item_summary/search は、eBay 上の商品をキーワードで検索し、一覧用のサマリ情報を取得できる APIです。

「特定の商品IDの詳細を取る(getItem)」のではなく、 「キーワードから商品一覧を探す」用途に特化しているのが特徴です。

たとえば次のような場面でよく使われます。

  • 相場リサーチ用に商品一覧を取得したい
  • カテゴリやブランド単位で商品を収集したい
  • GAS やスプレッドシートを使って、自動で検索結果を蓄積したい

この記事では、eBay Browse API の keyword search(item_summary/search)を、Google Apps Script(GAS)から呼び出す方法を紹介します。

このサンプルコードの特徴は次のとおりです。

  • ✅ キーワード(q)と取得件数(limit)を指定するだけで検索可能
  • ✅ セラーID・フィードバックスコア・カテゴリ・状態などを配列形式で返却
  • ✅ トークン取得ロジックは getItem 記事と共通(client_credentials + CacheService)
  • filter / sortオプション引数として受け取れる構造

1. 使用する API

Browse API / item_summary.search

  • キーワード(q)を指定して、商品一覧(itemSummaries)を取得する API です。
  • 価格・送料・セラー・状態など、一覧表示に必要な情報をひとまとめに取得できます。
  • 詳細な説明文や ItemSpecifics は省略されることが多く、あくまで「サマリ」として使う想定です。
  • API Docs:item_summary/search Browse API

2. 事前準備

  1. eBay Developer Program に登録
  2. App を作成して Client ID / Client Secret を取得
  3. GAS のプロジェクト設定 →「スクリプトのプロパティ」に各パラメータを保存

※Google Apps Script → 歯車 → プロジェクトのプロパティ → スクリプトのプロパティ

プロパティ名値 / 説明
CLIENT_ID1eBay App の Client ID
CLIENT_SECRET1eBay App の Client Secret
MARKETPLACE_ID任意(既定: EBAY_US

MARKETPLACE_IDEBAY_US / EBAY_GB / EBAY_DE / EBAY_AU

getItem 記事(「eBay Browse API getItem を GAS で叩く方法」)で設定済みであれば、そのまま流用できます。


3. サンプルコード

下記コードを GAS にそのまま貼り付ければ実行できます。
1 商品 1 行、列は [0..15] 固定の 2次元配列として返します。

filter / sort は **第3引数 **“ で指定でき、不要な場合は省略可能です。

サンプルコード(クリックして開く)
/***********************
 * eBay Browse API: keyword search(item_summary/search)
 * - キーワード(q)で検索し、itemSummaries を取得
 * - アプリ認証トークンは getItem と共通
 * - 出力形式は getItem と同じ [0..15] を 1商品=1配列で返す
 *   → 返り値は「2次元配列(行:商品)」になります
 ***********************/

const CLIENT_ID  = PropertiesService.getScriptProperties().getProperty('CLIENT_ID1');
const CLIENT_SEC = PropertiesService.getScriptProperties().getProperty('CLIENT_SECRET1');
const MARKETPLACE_ID =
  PropertiesService.getScriptProperties().getProperty('MARKETPLACE_ID') || 'EBAY_US';

const EBAY_OAUTH_TOKEN_URL = 'https://api.ebay.com/identity/v1/oauth2/token';
const EBAY_BROWSE_SEARCH_BASE = 'https://api.ebay.com/buy/browse/v1/item_summary/search';

/** 動作テスト(開発時用:シンプル版) */
function demo_searchByKeyword() {
  const kw    = "Canon EF 24mm F2.8 Lens"; // ← テストしたいキーワード
  const limit = 5;                           // 取得件数(最大 200 まで指定可能)

  const rows = ebayBrowseSearchByKeyword(kw, limit); // options なし(filter/sort なし)
  //Logger.log(JSON.stringify(rows, null, 2)); // スプレッドシート貼り付け前に確認用
}

/** 動作テスト(filter / sort を指定する例) */
function demo_searchByKeywordWithFilter() {
  const kw    = "Canon EF";
  const limit = 20;

  // 例:日本出品の新品・中古のみ、価格の安い順
  const options = {
    filter: 'conditionIds:{1000|3000},itemLocationCountry:{JP}',
    sort:   'price' // 昇順。降順は '-price'
  };

  const rows = ebayBrowseSearchByKeyword(kw, limit, options);
  //Logger.log(JSON.stringify(rows, null, 2));
}

/**
 * キーワード検索の入口関数
 * @param {string} keyword   - 検索キーワード(q)
 * @param {number} [limit]   - 最大件数(1〜200, 省略時 10)
 * @param {Object} [options] - 追加パラメータ(例:{ filter, sort })
 * @return {Array<Array>}    - 各行が [0..15] 形式の2次元配列
 */
function ebayBrowseSearchByKeyword(keyword, limit, options) {
  if (!CLIENT_ID || !CLIENT_SEC) {
    throw new Error("CLIENT_ID1 / CLIENT_SECRET1 が未設定です(スクリプトのプロパティに保存してください)");
  }
  if (!keyword) {
    throw new Error("検索キーワード(keyword)が空です");
  }

  const max = (typeof limit === "number" && limit > 0) ? limit : 10;

  // 追加パラメータ(filter / sort など)は options 経由で受け取る
  const extraParams = options || {}; // 例:{ filter: '...', sort: '...' }

  // ① トークン(キャッシュ優先)
  let token = getAppTokenCached();

  // ② API 実行
  let res = callSearchItems(keyword, max, token, extraParams);

  // 401(期限切れ等)の場合は 1 回だけトークン再取得 → 再試行
  if (res._httpCode === 401) {
    token = refreshAppToken();
    res = callSearchItems(keyword, max, token, extraParams);
  }

  // 200 以外は失敗としてログを残して終了
  if (res._httpCode !== 200 || !res._json) {
    Logger.log(`❌ search Error: ${res._httpCode}`);
    Logger.log(res._rawText);
    return [];
  }

  const json = res._json;

  // eBay 側エラー
  if ("errors" in json) {
    Logger.log("❌ APIエラー: " + JSON.stringify(json.errors));
    return [];
  }

  const items = Array.isArray(json.itemSummaries) ? json.itemSummaries : [];
  if (!items.length) {
    Logger.log("ℹ️ 該当する itemSummaries がありません");
    return [];
  }

  // itemSummary 1件ごとに [0..15] 配列へ整形
  const rows = items.map(displayItemSummary);

  Logger.log(`✅ keyword="${keyword}" / 件数=${rows.length}`);
  return rows;
}

/**
 * itemSummary から、getItem と同じ [0..15] 配列を生成
 * (description / itemSpecifics が無いことが多いので、ある範囲だけ埋める)
 * @param {Object} data - itemSummary
 * @return {Array}      - [0..15]
 */
function displayItemSummary(data) {
  // --- 基本情報の抽出 ---
  const title = data.title || "";
  const image = data.image?.imageUrl || "";
  const price = data.price?.value || "";
  const currency = data.price?.currency || "";

  // 送料(最初のオプション)
  const shippingOpt = data.shippingOptions?.[0] || {};
  const shipping = shippingOpt.shippingCost?.value || "";
  const shippingCurrency = shippingOpt.shippingCost?.currency || "";

  // 在庫・販売ステータス(itemEndDate ベース簡易判定)
  const status = "instock";

  // セラー情報
  const seller = data.seller?.username || "";
  const feedbackPercent = data.seller?.feedbackPercentage || "";
  const feedbackScore   = data.seller?.feedbackScore || "";

  // カテゴリ・コンディション
  const categoryId   = data.categoryId || "";
  const categoryName = data.categoryPath || "";
  const condition    = data.condition || "";
  const conditionId  = data.conditionId || "";

  // URL・所在地
  const itemUrl = data.itemWebUrl || "";
  const loc = data.itemLocation;
  const location = loc ? `${loc.postalCode || ""} ${loc.country || ""}`.trim() : "";

  // item_summary には通常 description は含まれない → shortDescription を代用 or 空
  let description = data.shortDescription || "";
  description = description
    .replace(/<br\s*\/?>(?i)/g, "\n")
    .replace(/<\/p>(?i)/g, "\n")
    .replace(/<[^>]*>/g, "")
    .trim();

  // item_summary では localizedAspects がない/少ないことが多い
  let itemSpecifics = [];
  if (Array.isArray(data.localizedAspects)) {
    itemSpecifics = data.localizedAspects.map(sp => {
      const name = sp.name || "";
      const value = Array.isArray(sp.value) ? sp.value.join(", ") : (sp.value || "");
      return [name, value];
    });
  }

  // 日付系(JST 文字列)
  const ListDate = toJST(data.itemCreationDate) || "";
  const EndDate  = toJST(data.itemEndDate) || "";

  // ログ(必要なら)
  Logger.log(`Title: ${title}`);
  Logger.log(`Price: ${price} ${currency} + Shipping: ${shipping} ${shippingCurrency}`);
  Logger.log(`Seller: ${seller} / Feedback: ${feedbackPercent}% (${feedbackScore})`);
  Logger.log(`Item URL: ${itemUrl}`);

  // --- getItem と同じ配列形式で返却([0..15]) ---
  return [
    title,        // 0
    price,        // 1
    shipping,     // 2
    status,       // 3
    seller,       // 4
    feedbackScore,// 5
    categoryId,   // 6
    categoryName, // 7
    condition,    // 8
    conditionId,  // 9
    itemUrl,      //10
    image,        //11
    description,  //12
    ListDate,     //13
    EndDate,      //14
    itemSpecifics //15
  ];
}

/**
 * Browse API: item_summary/search 呼び出し(HTTP)
 * @param {string} keyword      - q=
 * @param {number} limit        - limit=
 * @param {string} token        - Bearer トークン
 * @param {Object} [extraParams]- 追加パラメータ(例:{ filter, sort })
 * @return {{_httpCode:number,_rawText:string,_json:Object|null}}
 */
function callSearchItems(keyword, limit, token, extraParams) {
  // クエリパラメータを組み立て
  const params = {
    q: keyword,
    limit: String(limit),
    ...(extraParams || {}) // filter, sort などをマージ
  };

  const qs = Object.keys(params)
    .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
    .join("&");

  const url = `${EBAY_BROWSE_SEARCH_BASE}?${qs}`;

  const headers = {
    Authorization: `Bearer ${token}`,
    "Content-Type": "application/json",
    "X-EBAY-C-MARKETPLACE-ID": MARKETPLACE_ID,
    "Accept-Language": "en-US",
  };

  const opt = { method: "get", headers, muteHttpExceptions: true };
  const resp = UrlFetchApp.fetch(url, opt);
  const code = resp.getResponseCode();

  return {
    _httpCode: code,
    _rawText: resp.getContentText(),
    _json: safeJson(resp.getContentText())
  };
}

/** アプリトークン(client_credentials)をキャッシュ優先で取得 */
function getAppTokenCached() {
  const cache = CacheService.getScriptCache();
  const c = cache.get("EBAY_APP_TOKEN");
  if (c) return c;

  const t = refreshAppToken();
  if (t) cache.put("EBAY_APP_TOKEN", t, 60 * 25); // 25分キャッシュ(有効30分想定)
  return t;
}

/** 強制的に新しいトークンを発行(401時など) */
function refreshAppToken() {
  const basic = Utilities.base64Encode(`${CLIENT_ID}:${CLIENT_SEC}`);

  const payload =
    "grant_type=client_credentials&scope=" +
    encodeURIComponent("https://api.ebay.com/oauth/api_scope"); // Browseの読み取りはこれで十分

  const resp = UrlFetchApp.fetch(EBAY_OAUTH_TOKEN_URL, {
    method: "post",
    headers: {
      Authorization: `Basic ${basic}`,
      "Content-Type": "application/x-www-form-urlencoded",
    },
    payload: payload,
    muteHttpExceptions: true,
  });

  if (resp.getResponseCode() !== 200) {
    Logger.log("Token error");
    Logger.log(resp.getContentText());
    return null;
  }
  return JSON.parse(resp.getContentText()).access_token;
}

/** ISO8601 → JST "yyyy/mm/dd" 文字列に整形(表示用の簡易版) */
function toJST(iso) {
  if (!iso) return "";
  const d = new Date(iso);                         // UTC ベース
  const j = new Date(d.getTime() + 9 * 3600 * 1000); // JST = UTC+9
  const y = j.getFullYear();
  const m = String(j.getMonth() + 1).padStart(2, "0");
  const dd = String(j.getDate()).padStart(2, "0");
  return `${y}/${m}/${dd}`;
}

/** JSON を安全にパース(失敗時 null) */
function safeJson(txt) {
  try { return JSON.parse(txt); } catch (e) { return null; }
}

4. コード概要

◆ demo_searchByKeyword()

  • 動作確認用のテスト関数です。
  • キーワードと取得件数を決めて実行し、返ってきた配列(rows)をログやスプレッドシートに出力して確認できます。
  • filter / sort を使わない、もっともシンプルな呼び出し例です。
function demo_searchByKeyword() {
  const kw    = "Canon EF 24mm F2.8 Lens";
  const limit = 5;
  const rows  = ebayBrowseSearchByKeyword(kw, limit); // options なし
}

◆ demo_searchByKeywordWithFilter()

  • filter / sort を **第3引数 **“ で指定する例です。
  • 実際の運用で「新品・中古のみ」「日本出品のみ」「価格順」などにしたいときは、こちらの形をベースに編集します。
function demo_searchByKeywordWithFilter() {
  const kw    = "Canon EF";
  const limit = 20;

  const options = {
    // 新品(1000) + 中古(3000) かつ 日本出品
    filter: 'conditionIds:{1000|3000},itemLocationCountry:{JP}',
    // 価格の安い順
    sort:   'price'
  };

  const rows = ebayBrowseSearchByKeyword(kw, limit, options);
}

◆ ebayBrowseSearchByKeyword(keyword, limit, options)

  • keyword search の「入口関数」です。
  • 処理の流れは次の通りです。
  • スクリプトプロパティに CLIENT_ID1 / CLIENT_SECRET1 が入っているかチェック
  • limit 未指定時は 10 件に自動的に初期値に戻す
  • options から filter / sort などの追加パラメータを受け取る
  • getAppTokenCached() でトークン取得(キャッシュ優先)
  • callSearchItems()item_summary/search を実行
  • HTTP ステータス 401 の場合はトークン再取得 → 1回だけ再試行
  • 200 以外、または errors が含まれる場合はログ出力して空配列 [] を返却
  • 正常時は itemSummariesdisplayItemSummary() で [0..15] に整形

filter / sort をコードに反映するポイント はここです:

const extraParams = options || {}; // 例:{ filter: '...', sort: '...' }
...
let res = callSearchItems(keyword, max, token, extraParams);
...
res = callSearchItems(keyword, max, token, extraParams); // 401 再試行時も同じ extraParams を渡す

extraParams の中身は、そのまま callSearchItems() に渡され、クエリパラメータとして URL に反映されます。


◆ displayItemSummary(data)

  • itemSummary 1件分の JSON を、 [0..15] 配列に変換します。
  • 抜き出している主な項目は次の通りです。
index内容
0title
1price
2shipping
3status(ここでは "instock" 固定)
4seller(セラー ID)
5feedbackScore
6categoryId
7categoryPath
8condition
9conditionId
10itemUrl
11image(画像 URL)
12description(shortDescription からタグ除去)
13ListDate(itemCreationDate → JST)
14EndDate(itemEndDate → JST)
15itemSpecifics([name, value] の配列)

※ item_summary では descriptionlocalizedAspects が返ってこない場合もあるため、その場合は空文字/空配列として扱っています。


◆ callSearchItems(keyword, limit, token, extraParams)

  • Browse API の item_summary/search を HTTP 経由で呼び出す関数です。
  • extraParams に渡された filter / sort などが、そのままクエリパラメータとしてマージされます。
const params = {
  q: keyword,
  limit: String(limit),
  ...(extraParams || {}) // ← ここで filter / sort を統合
};

filter / sort の具体例

  • 新品のみ
  • options = { filter: 'conditionIds:{1000}' }
  • 新品 + 中古
  • options = { filter: 'conditionIds:{1000|3000}' }
  • 日本出品のみ
  • options = { filter: 'itemLocationCountry:{JP}' }
  • 価格 100〜200USD のみ
  • options = { filter: 'price:[100..200]' }
  • 中古 + 日本出品 + 価格 100〜200USD
  • options = { filter: 'conditionIds:{3000},itemLocationCountry:{JP},price:[100..200]' }

sort の例:

  • 価格昇順
  • options = { sort: 'price' }
  • 価格降順
  • options = { sort: '-price' }

実際の呼び出しは、例えば次のようになります。

const options = {
  filter: 'conditionIds:{3000},itemLocationCountry:{JP},price:[100..200]',
  sort:   '-price' // 高い順
};

const rows = ebayBrowseSearchByKeyword('Canon EF', 50, options);

◆ トークン管理・ユーティリティ

  • getAppTokenCached() / refreshAppToken() / toJST() / safeJson() は、getItem 記事と同じ構成です。
  • 実行ごとにトークン取得をやり直さず、CacheService で 25分だけ再利用することで、API 呼び出し回数とレイテンシを抑えています。
  • 401 エラーのときだけ refreshAppToken() を呼び出し、1回だけ再試行しています。

5. キーワード検索で取得できる情報

今回のサンプルでは、キーワード検索の結果から、主に次のような情報を 1 行にまとめています。

  • 商品タイトル(title)
  • 価格・通貨(price / currency)
  • 送料(shippingCost)
  • セラー ID・フィードバック(username / feedbackScore / feedbackPercentage)
  • カテゴリ ID・カテゴリ名(categoryId / categoryPath)
  • コンディション(condition / conditionId)
  • 商品ページの URL(itemWebUrl)
  • 画像 URL(image.imageUrl)
  • 出品日・終了日(itemCreationDate / itemEndDate → JST)

この配列構造は、getItem 記事(「eBay Browse API getItem を GAS で叩く方法」)と揃えているため、

  • 「最初は keyword search で一覧を取得」
  • 「気になる ItemID を getItem で詳細取得」

といった流れにもスムーズに対応できます。


6. まとめ

eBay Browse API の item_summary/search を使うと、キーワードベースで商品一覧を取得し、リサーチ用のデータを一度に集めることができます。

今回のサンプルでは、

  • キーワードと limit(+必要なら options)を渡すだけで検索
  • 結果を getItem 記事(「eBay Browse API getItem を GAS で叩く方法」)と同じ配列に整形
  • トークン取得は client_credentials + CacheService で自動管理
  • filter / sort は第3引数 options として渡し、コード側では共通処理で URL に統合

という形にしているため、既存のツールやシートにも組み込みやすい構造になっています。

あとは、filter / sort をもう少し作り込んで、カテゴリ ID やセラー ID で絞り込んだりすることで、

  • 自分用のリサーチシート
  • 特定カテゴリの価格ウォッチツール
  • 日本人セラー限定の一覧

などに応用していくことができます。

ぜひ、getItem 記事(「eBay Browse API getItem を GAS で叩く方法」)と合わせて、GAS × eBay Browse API による自動リサーチの第一歩として活用してみてください。