AddItem は、eBay 上に新しい商品を出品するための Trading API です。タイトル、価格、数量、カテゴリ、画像、説明文など、出品に必要な情報をまとめて送信し、1 商品を登録できます。
この記事では、Google Apps Script(GAS)から AddItem を呼び出し、スプレッドシートの入力内容と Google ドライブの HTML 説明文をもとに 1 商品を出品する基本フロー を紹介します。
今回のゴール
このページで目指すゴールは、次の 3 つです。
- 「設定」シートに API キーとトークンを保存する
- 「出品」シートにタイトル・価格・画像URL・ItemSpecifics などを入力する
- 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) | E6 | X-EBAY-API-CERT-NAME に渡す |
| DEVID (DEV-NAME) | E7 | X-EBAY-API-DEV-NAME に渡す |
| eBayAuthToken | E8 | Auth’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. 出品シート(商品ごとの情報)

次に、実際に出品したい商品の情報を入力する 「出品」シート を用意します。
この記事で使っている主な項目は次のとおりです。
| 用途 | セル | 説明 |
|---|---|---|
| 出品タイトル | D17 | Title |
| 販売価格 | D21 | StartPrice |
| BestOffer 設定 | D22 | (記事中では固定で ON としています) |
| BestOffer 最低出品価格 | D23 | MinimumBestOfferPrice |
| 説明 HTML ファイル名 | D27 | Google ドライブ上のファイル名など |
| 出品数(数量) | D72 | Quantity |
| カスタムラベル(SKU) | E73 | SKU |
| eBay カテゴリ ID | D70 | PrimaryCategory.CategoryID |
| ConditionID | D65 | ConditionID |
| コンディション説明 | D66 | ConditionDescription |
| 発送元ロケーション | D71 | Location |
| 支払ポリシー名 | D78 | SellerPaymentProfile |
| 返品ポリシー名 | D77 | SellerReturnProfile |
| 送料ポリシー名 | D79 | SellerShippingProfile |
| 画像 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() を用意し、
- 「出品」シートの
ADDR_HTML_FILEからファイル名を取得 - GAS プロジェクトと同じフォルダ内から、そのファイルの ID を探す
- ファイル本体を
getBlob().getDataAsString()で文字列として読み込む - HTML エンティティ(※HTML や XML が誤解しやすい < や & を安全に表現するための特殊な記法)(
&など)を必要に応じてデコード - 改行コードを削除して 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 には、
&(アンパサンド)"(ダブルクォーテーション)'(シングルクォーテーション) (ノーブレークスペース)
といった HTML エンティティ(※HTML や XML が誤解しやすい < や & を安全に表現するための特殊な記法) が含まれていることがあります。そのまま だとeBay側の表示に問題がある場合があったため、decodeHtmlEntities() という関数を用意し、通常の文字に戻しています。
function decodeHtmlEntities(str) {
return str
.replace(/&nbsp;/g, ' ')
.replace(/&amp;(?!nbsp;)/g, '&')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'");
}
5. AddItem の XML を組み立てる流れ
ここまでの準備ができたら、あとは 出品シートの値を読み取り、AddItem の XML を組み立てるだけです。
実際のコードでは、次のような流れになっています。
- 「設定」シートから APPID / DEVID / AuthToken を取得
- 「出品」シートからタイトル・価格・数量・SKU・各種ポリシー名を取得
makedesc()で HTML 説明文を取得- 画像 URL の行(D84~AA84)を配列として取得し
<PictureURL>に展開 - ItemSpecifics 領域(D90~AA91)を
getItemSpecificsArray()で[Name, Value, ...]に変換 - それらをもとに
<Item> ... </Item>の XML を文字列で組み立て - 最後に
<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の中の&を&に一括置換- ヘッダ情報(DEV-NAME / CERT-NAME など)を付けて
UrlFetchApp.fetch()で送信
という流れになっています。
6. 実行とレスポンス確認

GAS から listdata() を実行すると、Trading API による出品処理が行われ、その結果が「出品」シートの結果欄に反映されます。
結果の書き込み先は、次のように分けています。
| セル | 内容 |
|---|---|
| D102 | Ack(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(/&nbsp;/g, ' ')
.replace(/&amp;(?!nbsp;)/g, '&')
.replace(/&quot;/g, '"')
.replace(/&#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, '&');
// =======================================
// 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;
}
