eBay Trading API で商品を出品する方法(スプレッドシート+GAS)

eBay API

AddItem は、eBay 上に新しい商品を出品するための Trading API です。タイトル、価格、数量、カテゴリ、画像、説明文など、出品に必要な情報をまとめて送信し、1 商品を登録できます。

この記事では、Google Apps Script(GAS)から AddItem を呼び出し、スプレッドシートの入力内容と Google ドライブの HTML 説明文をもとに 1 商品を出品する基本フロー を紹介します。


今回のゴール

このページで目指すゴールは、次の 3 つです。

  1. 「設定」シートに API キーとトークンを保存する
  2. 「出品」シートにタイトル・価格・画像URL・ItemSpecifics などを入力する
  3. GAS から AddItem を呼び出し、シートの内容どおりに 1 品を出品する

商品説明は、Google ドライブに置いた HTML ファイルを読み込み、

<Description><![CDATA[ ... HTML ... ]]></Description>

の形でそのまま渡します。

※CDATA は「HTML をそのまま安全に XML に埋め込むための保護枠」です。特殊文字(< や &)で XML が壊れるのを防ぐために使用します。


1. 使用する API と認証方式

今回使用するのは、eBay の Trading API / AddItem です。

  • SOAP 形式(XML)でリクエストを送る
  • 出品・End・Relist など「売り手側の操作」を担当する API 群
  • 認証は Auth’n’Auth 方式の User Token を使用(※eBay API は OAuth 2.0 も利用できますが、Trading API での単品出品では Auth’n’Auth が最も簡単・確実なため、本記事ではこちらを採用しています)

テストの段階では、AddItem の代わりに VerifyAddItem を呼ぶことで、 「実際には出品せずに、XML が妥当かどうか」を確認できます。

記事の本体では AddItem で説明しますが、運用前の検証では VerifyAddItem への差し替えをおすすめします。


2. スプレッドシートの準備

2-1. 設定シート(API キーとトークン)

まずは、Trading API を呼び出すためのキー類を 「設定」シート にまとめておきます。

例として、次のようなセル配置を想定しています。

項目セル説明
APPID (CERT-NAME)E6X-EBAY-API-CERT-NAME に渡す
DEVID (DEV-NAME)E7X-EBAY-API-DEV-NAME に渡す
eBayAuthTokenE8Auth’n’Auth の User Token

GAS 側では、次のように 定数としてセル位置だけ定義し、あとはコード中で getRange().getValue() で取得しています。

※この記事の本文部分ではコード全文を掲載せず、要点のみを抜粋して説明します。コード全文は、記事末尾の「付録:GAS コード全文」に掲載しています。

const SHEET_NAME_SETTING = '設定';
const ADDR_SET_APPID     = 'E6';
const ADDR_SET_DEVID     = 'E7';
const ADDR_SET_AUTHTOKEN = 'E8';

const setsheet = SpreadsheetApp
  .getActiveSpreadsheet()
  .getSheetByName(SHEET_NAME_SETTING);

2-2. 出品シート(商品ごとの情報)

次に、実際に出品したい商品の情報を入力する 「出品」シート を用意します。

この記事で使っている主な項目は次のとおりです。

用途セル説明
出品タイトルD17Title
販売価格D21StartPrice
BestOffer 設定D22(記事中では固定で ON としています)
BestOffer 最低出品価格D23MinimumBestOfferPrice
説明 HTML ファイル名D27Google ドライブ上のファイル名など
出品数(数量)D72Quantity
カスタムラベル(SKU)E73SKU
eBay カテゴリ IDD70PrimaryCategory.CategoryID
ConditionIDD65ConditionID
コンディション説明D66ConditionDescription
発送元ロケーションD71Location
支払ポリシー名D78SellerPaymentProfile
返品ポリシー名D77SellerReturnProfile
送料ポリシー名D79SellerShippingProfile
画像 URL 一覧D84~AA84<PictureURL> を横並びで入力
ItemSpecifics 項目名+値D90~AA91上段:項目名 / 下段:値

GAS 側では、これらも 「セル位置だけを定数化」 し、

const SHEET_NAME_LIST   = '出品';
const ADDR_TITLE        = 'D17';     // 出品タイトル
const ADDR_LIST_PRICE   = 'D21';     // 販売価格
// ... 省略 ...
const listsheet = SpreadsheetApp
  .getActiveSpreadsheet()
  .getSheetByName(SHEET_NAME_LIST);

という形で使っています。


3. 商品説明を HTML ファイルで管理する

3-1. HTML ファイルを Google ドライブに保存

商品説明は、eBay の商品ページで表示される HTML を想定しています。

今回は、

  • ローカルで HTML を作成
  • Google ドライブ(GAS プロジェクトと同じフォルダ)にアップロード
  • ファイル名を「出品」シートのセルに書いておく

という運用にしています。

例:

  • ドライブ内に C0011.html を保存
  • 「出品」シートの D27 に、このファイル名(あるいはキーとなる名称)を入力

3-2. makedesc() で HTML を読み込んで Description に渡す

GAS 側では、説明文生成専用の関数 makedesc() を用意し、

  1. 「出品」シートの ADDR_HTML_FILE からファイル名を取得
  2. GAS プロジェクトと同じフォルダ内から、そのファイルの ID を探す
  3. ファイル本体を getBlob().getDataAsString() で文字列として読み込む
  4. HTML エンティティ(※HTML や XML が誤解しやすい < や & を安全に表現するための特殊な記法)(&amp; など)を必要に応じてデコード
  5. 改行コードを削除して 1 行の文字列に整形

という流れで、XML/API にそのまま埋め込める安全な文字列として返します。

処理のイメージは次のような感じです。

function makedesc() {
  // 1. シートから HTML ファイル名を取得
  const filename = listsheet.getRange(ADDR_HTML_FILE).getValue();

  // 2. 同じフォルダ内からファイル ID を取得(実装は環境に合わせて)
  const htmlId = getFileIdFromSameFolder(filename);

  // 3. ファイル内容を文字列で取得
  let html = DriveApp.getFileById(htmlId)
                    .getBlob()
                    .getDataAsString();

  // 4. よく使う HTML エンティティをデコード
  html = decodeHtmlEntities(html);

  // 5. 改行を削除(XML に埋め込むため 1 行に整形)
  html = html.replace(/[\n\r]/g, '');

  return html;
}

getFileIdFromSameFolder() は、「このスプレッドシートが置かれているフォルダ」を基準にファイル名で一致する HTML を探すための関数です。


4. HTML エンティティのデコード処理

ブラウザから保存した HTML には、

  • &amp;(アンパサンド)
  • &quot;(ダブルクォーテーション)
  • &#39;(シングルクォーテーション)
  • &nbsp;(ノーブレークスペース)

といった HTML エンティティ(※HTML や XML が誤解しやすい < や & を安全に表現するための特殊な記法) が含まれていることがあります。そのまま だとeBay側の表示に問題がある場合があったため、decodeHtmlEntities() という関数を用意し、通常の文字に戻しています。

function decodeHtmlEntities(str) {
  return str
    .replace(/&amp;nbsp;/g, ' ')
    .replace(/&amp;amp;(?!nbsp;)/g, '&amp;')
    .replace(/&amp;quot;/g, '"')
    .replace(/&amp;#39;/g, "'");
}

5. AddItem の XML を組み立てる流れ

ここまでの準備ができたら、あとは 出品シートの値を読み取り、AddItem の XML を組み立てるだけです。

実際のコードでは、次のような流れになっています。

  1. 「設定」シートから APPID / DEVID / AuthToken を取得
  2. 「出品」シートからタイトル・価格・数量・SKU・各種ポリシー名を取得
  3. makedesc() で HTML 説明文を取得
  4. 画像 URL の行(D84~AA84)を配列として取得し <PictureURL> に展開
  5. ItemSpecifics 領域(D90~AA91)を getItemSpecificsArray()[Name, Value, ...] に変換
  6. それらをもとに <Item> ... </Item> の XML を文字列で組み立て
  7. 最後に <AddItemRequest> ... </AddItemRequest> で囲んで eBay に送信

例えば、説明文と画像まわりの部分だけ抜き出すと、次のようなイメージです。

let xmllist = '<Item>';

// ... SKU, CategoryID, Title, Condition などを組み立て ...

// 説明文(HTML)は CDATA でそのまま渡す
xmllist += `<Description><![CDATA[${description}]]></Description>`;

// 画像 URL 一覧
xmllist += '<PictureDetails>';
xmllist += `<GalleryType>${galleryType}</GalleryType>`;

for (let k = 0; k < pictureURL.length; k++) {
  if (!pictureURL[k]) continue;
  xmllist += `<PictureURL><![CDATA[${pictureURL[k]}]]></PictureURL>`;
}

xmllist += '</PictureDetails>';

// ... 価格、数量、SellerProfiles、ItemSpecifics など ...

xmllist += '<Country>JP</Country><Currency>USD</Currency><Site>US</Site></Item>';

ここまでを組み立てたあと、

  • xmllist の中の &&amp; に一括置換
  • ヘッダ情報(DEV-NAME / CERT-NAME など)を付けて UrlFetchApp.fetch() で送信

という流れになっています。


6. 実行とレスポンス確認

GAS から listdata() を実行すると、Trading API による出品処理が行われ、その結果が「出品」シートの結果欄に反映されます。

結果の書き込み先は、次のように分けています。

セル内容
D102Ack(Success / Failure / Warning)
F102成功時:ItemID / 失敗時:エラーメッセージ
D103生レスポンス(XML 文字列)

7. まとめと今後の発展

Trading API を使って 1 品を出品するための基本構成を紹介しました。まずは、このミニマム構成で「入力した情報がそのまま出品される」状態を安定させることが重要です。仕組みが固まったら、画像処理や ItemSpecifics の入力補助など、作業を減らす工夫が次のステップになります。さらに、VerifyAddItem を併用した安全な確認フローや、複数商品の出品処理への拡張も検討できます。

付録:GAS コード全文

以下では、記事中で説明した処理を 3 つのブロックに分けて掲載します。

  • 設定値の取得まわり
  • HTML 説明文の読み込み
  • AddItem XML の組み立て&出品
1.設定値の取得まわり
// 出品シート
const SHEET_NAME_LIST        = '出品';
const ADDR_TITLE             = 'D17';          // 出品タイトル
const ADDR_LIST_PRICE        = 'D21';          // 販売価格
const ADDR_BEST_OFFER        = 'D22';          // BestOffer設定
const ADDR_MIN_PRICE         = 'D23';          // BestOffer最低出品価格
const ADDR_HTML_FILE         = 'D27';          // 商品説明
const ADDR_SALES_QTY         = 'D72';          // 出品数(数量)
const ADDR_CUSTOM_LABEL      = 'E73';          // カスタムラベル(SKU)
const ADDR_EBAY_CAT_ID       = 'D70';          // eBayカテゴリID
const ADDR_CONDITION_ID      = 'D65';          // ConditionID
const ADDR_COND_DESCRIPTION  = 'D66';          // コンディション説明
const ADDR_SHIPPING_POLICY   = 'D79';          // 送料ポリシー名
const ADDR_RETURN_POLICY     = 'D77';          // 返品ポリシー名
const ADDR_PAYMENT_POLICY    = 'D78';          // 支払ポリシー名
const ADDR_LIST_LOCATION     = 'D71';          // 発送元ロケーション
const ADDR_IMG_LIST          = 'D84:AA84';     // 画像URL一覧(横並び)
const ADDR_ITEM_SPECIFICS    = 'D90:AA91';     // ItemSpecifics用

// 結果出力(出品結果)
const ADDR_RESULT_STAT       = 'D102';         // Ack(Success / Failure)
const ADDR_RESULT_ID         = 'F102';         // ItemID もしくはエラーメッセージ
const ADDR_RESULT_RES        = 'D103';         // 生レスポンス(XML文字列)

// 設定シート(APIキー類)
const SHEET_NAME_SETTING     = '設定';
const ADDR_SET_APPID         = 'E6';           // 設定シート内 APPID(CERT-NAME 用)
const ADDR_SET_DEVID         = 'E7';           // 設定シート内 DEVID(DEV-NAME 用)
const ADDR_SET_AUTHTOKEN     = 'E8';           // 設定シート内 eBayAuthToken

// シートオブジェクト(コンテナバインド前提)
const setsheet  = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME_SETTING);
const listsheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME_LIST);
2.HTML 説明文の読み込み
function makedesc() {
  // 1. シートから HTML ファイル名を取得
  const filename = listsheet.getRange(ADDR_HTML_FILE).getValue();
 
  // 2. 同じフォルダ内からファイル ID を取得(実装は環境に合わせて)
  const htmlId = getFileIdFromSameFolder(filename);
 
  // 3. ファイル内容を文字列で取得
  let html = DriveApp.getFileById(htmlId)
                    .getBlob()
                    .getDataAsString();
 
  // 4. よく使う HTML エンティティをデコード
  html = decodeHtmlEntities(html);
 
  // 5. 改行を削除(XML に埋め込むため 1 行に整形)
  html = html.replace(/[\n\r]/g, '');
 
  return html;
}

function decodeHtmlEntities(str) {
  return str
    .replace(/&amp;nbsp;/g, ' ')
    .replace(/&amp;amp;(?!nbsp;)/g, '&amp;')
    .replace(/&amp;quot;/g, '"')
    .replace(/&amp;#39;/g, "'");
}

function getFileIdFromSameFolder(filename) {
  const ssId   = SpreadsheetApp.getActive().getId();
  const ssFile = DriveApp.getFileById(ssId);

  // 親フォルダ(通常は1つ)
  const parents = ssFile.getParents();
  const folder  = parents.hasNext() ? parents.next() : DriveApp.getRootFolder();

  // ---- ファイル名一致で検索(最速) ----
  let it = folder.getFilesByName(filename);
  if (it.hasNext()) return it.next().getId();

  // ---- 念のため Search API で補強 ----
  it = DriveApp.searchFiles(`'${folder.getId()}' in parents and title = '${filename}'`);
  if (it.hasNext()) return it.next().getId();

  throw new Error(`同フォルダ内に "${filename}" が見つかりません(フォルダ: ${folder.getName()})`);
}
3.AddItem XML の組み立て&出品
// =======================================
// メイン処理:出品データを読み取り、Trading API(AddItem)で出品
// 戻り値: [stat, tim, iid]
//   stat : Ack(Success / Failure)
//   tim  : Timestamp
//   iid  : ItemID またはエラーメッセージ
// =======================================
function listdata() {

  // --- 設定シートから認証情報を取得 ---
  const appid     = setsheet.getRange(ADDR_SET_APPID).getValue();
  const devid     = setsheet.getRange(ADDR_SET_DEVID).getValue();
  const authtoken = setsheet.getRange(ADDR_SET_AUTHTOKEN).getValue();

  // --- 出品シートから入力項目を取得 ---
  const sku                 = listsheet.getRange(ADDR_CUSTOM_LABEL).getValue();      // カスタムラベル(SKU)
  const categoryID          = listsheet.getRange(ADDR_EBAY_CAT_ID).getValue();      // eBayカテゴリID
  const title               = listsheet.getRange(ADDR_TITLE).getValue();            // タイトル
  const condition           = listsheet.getRange(ADDR_CONDITION_ID).getValue();     // ConditionID
  const conditionDescription= listsheet.getRange(ADDR_COND_DESCRIPTION).getValue(); // コンディション説明
  const description         = makedesc();                                           // HTML説明文(別関数で生成)
  const pictureURL          = listsheet.getRange(ADDR_IMG_LIST).getValues()[0];     // 画像URL配列(1行分)
  const quantity            = listsheet.getRange(ADDR_SALES_QTY).getValue();        // 数量
  const startPrice          = listsheet.getRange(ADDR_LIST_PRICE).getValue();       // 販売価格
  const bestOfferEnabled    = 1;                                                    // 現状は常に BestOffer ON
  const minimumBestOfferPrice = listsheet.getRange(ADDR_MIN_PRICE).getValue();      // BestOffer 最低価格(※セル定義は別途)
  const listingDuration     = 'GTC';                                                // 期間固定(Good ’Til Cancelled)
  const listingType         = 'FixedPriceItem';                                     // 固定価格出品
  const location            = listsheet.getRange(ADDR_LIST_LOCATION).getValue();    // 発送元ロケーション
  const galleryType         = 'Gallery';                                            // ギャラリー表示
  const shippingProfileName = listsheet.getRange(ADDR_SHIPPING_POLICY).getValue();  // 送料ポリシー名
  const returnProfileName   = listsheet.getRange(ADDR_RETURN_POLICY).getValue();    // 返品ポリシー名
  const paymentProfileName  = listsheet.getRange(ADDR_PAYMENT_POLICY).getValue();   // 支払ポリシー名
  const spec                = getItemSpecificsArray();                              // ItemSpecifics配列 [Name, Value, Name, Value, ...]

  // --- デバッグログ(必要に応じて削除可) ---
  Logger.log([sku, categoryID, title, condition, conditionDescription]);
  Logger.log(pictureURL);
  Logger.log(condition);

  // =======================================
  // AddItem 用 <Item> ブロック生成
  // =======================================
  let xmllist = '<Item>';

  xmllist += `<SKU>${sku}</SKU>`;
  xmllist += `<PrimaryCategory><CategoryID>${categoryID}</CategoryID></PrimaryCategory>`;
  xmllist += `<Title>${title}</Title>`;
  xmllist += `<ConditionID>${condition}</ConditionID>`;

  // コンディション説明(任意)
  if (conditionDescription !== '') {
    xmllist += `<ConditionDescription>${conditionDescription}</ConditionDescription>`;
  }

  // 説明文は CDATA でラップ(HTMLタグをそのまま渡すため)
  xmllist += `<Description><![CDATA[${description}]]></Description>`;

  // 画像
  xmllist += '<PictureDetails>';
  xmllist += `<GalleryType>${galleryType}</GalleryType>`;

  for (let k = 0; k < pictureURL.length; k++) {
    if (!pictureURL[k]) continue;
    Logger.log(pictureURL[k]);
    xmllist += `<PictureURL><![CDATA[${pictureURL[k]}]]></PictureURL>`;
  }

  xmllist += '</PictureDetails>';

  // 数量・価格
  xmllist += `<Quantity>${quantity}</Quantity>`;
  xmllist += `<StartPrice>${startPrice}</StartPrice>`;

  // Best Offer
  if (bestOfferEnabled > 0) {
    xmllist += '<BestOfferDetails><BestOfferEnabled>true</BestOfferEnabled></BestOfferDetails>';
    xmllist += `<ListingDetails><MinimumBestOfferPrice currencyID="USD">${minimumBestOfferPrice}</MinimumBestOfferPrice></ListingDetails>`;
  }

  // 出品期間・セラープロファイル
  xmllist += `<ListingDuration>${listingDuration}</ListingDuration>`;
  xmllist += '<SellerProfiles>';
  xmllist += `<SellerPaymentProfile><PaymentProfileName>${paymentProfileName}</PaymentProfileName></SellerPaymentProfile>`;
  xmllist += `<SellerReturnProfile><ReturnProfileName>${returnProfileName}</ReturnProfileName></SellerReturnProfile>`;
  xmllist += `<SellerShippingProfile><ShippingProfileName>${shippingProfileName}</ShippingProfileName></SellerShippingProfile>`;
  xmllist += '</SellerProfiles>';

  xmllist += `<ListingType>${listingType}</ListingType>`;
  xmllist += `<Location>${location}</Location>`;

  // ItemSpecifics
  xmllist += '<ItemSpecifics>';
  for (let k = 0; k < spec.length; k += 2) {
    if (!spec[k]) continue;
    xmllist += `<NameValueList><Name>${spec[k]}</Name><Value>${spec[k + 1]}</Value></NameValueList>`;
  }
  xmllist += '</ItemSpecifics>';

  // 共通情報(国・通貨・サイト)
  xmllist += '<Country>JP</Country><Currency>USD</Currency><Site>US</Site></Item>';

  // 特殊文字処理
  // ※CDAT A部分は影響を受けませんが、他要素内の & を一括でエスケープ
  xmllist = xmllist.replace(/&/g, '&amp;');

  // =======================================
  // Trading API(AddItem)呼び出し
  // =======================================
  const url = 'https://api.ebay.com/ws/api.dll';

  const headers = {
    'X-EBAY-API-DEV-NAME': devid,       // Developer ID
    'X-EBAY-API-CERT-NAME': appid,      // Cert ID(ここではappidに格納)
    'X-EBAY-API-CALL-NAME': 'AddItem',  // 'AddItem' or 'VerifyAddItem'
    'X-EBAY-API-SITEID': '0',           // US = 0
    'X-EBAY-API-REQUEST-ENCODING': 'XML',
    'X-EBAY-API-COMPATIBILITY-LEVEL': '1119',
  };

  // AddItemRequest 本体
  const xml =
    '<?xml version="1.0" encoding="utf-8"?>' +
    '<AddItemRequest xmlns="urn:ebay:apis:eBLBaseComponents">' +
      '<RequesterCredentials>' +
        `<eBayAuthToken>${authtoken}</eBayAuthToken>` +
      '</RequesterCredentials>' +
      '<ErrorLanguage>en_US</ErrorLanguage>' +
      '<WarningLevel>High</WarningLevel>' +
      xmllist +
    '</AddItemRequest>';

  Logger.log(xml);

  const options = {
    method: 'post',
    contentType: 'application/xml',
    headers: headers,
    payload: xml,
  };

  // API呼び出し
  let response = UrlFetchApp.fetch(url, options).getContentText();
  Logger.log(response);

  // =======================================
  // レスポンス解析
  // =======================================
  let stat = '';
  let tim  = '';
  let iid  = '';
  let errmsg = '';

  try {
    stat = String(response).split('<Ack>')[1].split('</Ack>')[0];
  } catch (e) {
    stat = '';
  }

  try {
    tim = String(response).split('<Timestamp>')[1].split('</Timestamp>')[0];
  } catch (e) {
    tim = '';
  }

  try {
    iid = String(response).split('<ItemID>')[1].split('</ItemID>')[0];
  } catch (e) {
    iid = '';
  }

  try {
    const tmpcnt = String(response).split('<LongMessage>');
    for (let kk = 1; kk < tmpcnt.length; kk++) {
      const msg = String(response).split('<LongMessage>')[kk].split('</LongMessage>')[0];
      errmsg += '\n' + msg;
    }
  } catch (e) {
    errmsg = '';
  }

  // Failure時は ItemID の代わりにエラーメッセージを返す
  if (stat === 'Failure') {
    iid = errmsg;
  }

  // 結果をシートに書き込み
  listsheet.getRange(ADDR_RESULT_STAT).setValue(stat);
  listsheet.getRange(ADDR_RESULT_ID).setValue(iid);
  listsheet.getRange(ADDR_RESULT_RES).setValue(String(response));

  // 呼び出し元にも返却
  return [stat, tim, iid];
}

// =======================================
// ItemSpecifics(例: D90:AA91)を
// [Name1, Value1, Name2, Value2, ...] に変換する関数
// =======================================
function getItemSpecificsArray() {

  // ItemSpecifics 領域を取得
  const values = listsheet.getRange(ADDR_ITEM_SPECIFICS).getValues();

  const names = values[0];   // 上段:項目名
  const specs = values[1];   // 下段:値
  const result = [];

  for (let i = 0; i < names.length; i++) {
    let name  = names[i];
    let value = specs[i];

    // Name が空ならスキップ
    if (!name) continue;

    // UPC が空なら "Does Not Apply"
    if (name === 'UPC' && (value === '' || value == null)) {
      value = 'Does Not Apply';
    }

    result.push(name);
    result.push(value);
  }

  Logger.log(result);  // 必要に応じて削除可
  return result;
}