ClaudeでAmazon広告の検索用語レポートを取り、除外キーワード候補を出す
Amazon広告のv3レポートで検索用語レポート(spSearchTerm)を非同期に取得し、集計した表をClaudeに渡して無駄クリックの語を選び、除外キーワードの登録リクエストまで組む手順です。
Amazon広告のスポンサープロダクト広告で、クリックされたのに売れていない検索用語を探すには、v3のレポートAPIで検索用語レポートを取り、費用と成果で絞ってから除外キーワードの候補にします。Claudeが役立つのは、絞った後の「この語は外してよいか」という意図の判断と、登録用のリクエスト本文を組む作業です。
取得そのものは、リクエストを出してから完成を待ち、ファイルを受け取る非同期の流れになります。この記事では、その3段階と、集計、Claudeへの渡し方、除外キーワードの登録リクエストまでを順に扱います。Google広告で同じことをするときの読み方はClaudeでGoogle広告の検索語句を読み、除外キーワードと広告文を見直すにあります。こちらはCSVを画面から書き出す入口で、Amazon広告は取得がAPIになる点が違います。
v3レポートは「依頼、待機、ダウンロード」の3段階
v3のレポートは、結果をその場で返しません。まず作成のリクエストを送ると、reportIdとPENDINGの状態が返ります。次に状態を確認するリクエストを繰り返し、COMPLETEDになったらダウンロード用のURLを受け取ります。
Amazonが公開しているPostmanコレクションでは、この3段階が次のエンドポイントで書かれています。
検索用語レポートを受け取るまで
- 1
作成を依頼する
POST /reporting/reportsにレポートの条件を送ります。レスポンスのstatusはPENDINGで、urlはnullです。 - 2
状態を確かめる
GET /reporting/reports/{reportId}を、statusが変わるまで間隔を空けて繰り返します。 - 3
ファイルを受け取る
COMPLETEDのレスポンスに入るurlからファイルを取ります。urlExpiresAtに有効期限が付きます。
どのリクエストにも、3つの認証ヘッダーが付きます。Amazon-Advertising-API-ClientId、Amazon-Advertising-API-Scope(プロファイルID)、Authorization: Bearerのアクセストークンです。作成のリクエストには、Content-Type: application/vnd.createasyncreportrequest.v3+jsonも指定します。
状態確認を自動で繰り返すなら、次のような最小のスクリプトになります。jqが入っている前提で、使うのは上の2つのエンドポイントだけです。
while true; do
res=$(curl -s "$API_URL/reporting/reports/$REPORT_ID" \
-H "Amazon-Advertising-API-ClientId: $CLIENT_ID" \
-H "Amazon-Advertising-API-Scope: $PROFILE_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN")
status=$(echo "$res" | jq -r '.status')
case "$status" in
PENDING) sleep 30 ;;
COMPLETED) echo "$res" | jq -r '.url' > report-url.txt; break ;;
*) echo "status=$status"; echo "$res" | jq -r '.failureReason'; exit 1 ;;
esac
donePENDINGの間は30秒空けて確認し、COMPLETEDならURLをファイルに書いて抜けます。それ以外の状態では、状態名とfailureReasonを表示して止めます。待機時間は目安なので、レート制限に当たる場合は延ばしてください。
検索用語レポートの依頼本文
検索用語を取るのはreportTypeIdがspSearchTermのレポートです。Postmanコレクションの「SP daily search term report」の依頼本文は、次の形です。日付とレポート名は読み替えてください。
curl -s -X POST "$API_URL/reporting/reports" \
-H "Content-Type: application/vnd.createasyncreportrequest.v3+json" \
-H "Amazon-Advertising-API-ClientId: $CLIENT_ID" \
-H "Amazon-Advertising-API-Scope: $PROFILE_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d @search-term-request.json{
"name": "SP search term report",
"startDate": "2026-09-20",
"endDate": "2026-10-19",
"configuration": {
"adProduct": "SPONSORED_PRODUCTS",
"groupBy": ["searchTerm"],
"columns": [
"campaignId", "adGroupId", "keywordId", "targeting",
"searchTerm", "impressions", "clicks", "cost",
"purchases1d", "purchases7d", "purchases14d", "purchases30d",
"date"
],
"reportTypeId": "spSearchTerm",
"timeUnit": "DAILY",
"format": "GZIP_JSON"
}
}サンプルのcolumnsには、除外の判断に直接使う列がそろっています。
| 列 | 除外判断での役目 |
|---|---|
searchTerm | 除外判断での役目実際に検索された語。除外候補そのもの |
targeting | 除外判断での役目広告が出る起点になった指定 |
keywordId | 除外判断での役目起点のキーワードを特定するID |
impressions / clicks | 除外判断での役目表示とクリックの量 |
cost | 除外判断での役目無駄になった費用 |
purchases1d からpurchases30d | 除外判断での役目1日、7日、14日、30日の購入数 |
campaignId / adGroupId | 除外判断での役目除外を登録する場所 |
date | 除外判断での役目DAILYのときに日ごとの行を作る |
売上金額の列は、このサンプルには入っていません。列は依頼側が選ぶ仕組みなので、売上まで見たいときは、Amazon Adsのレポートタイプ一覧でspSearchTermに使える列を調べてからcolumnsに足します。
出力形式のGZIP_JSONは、gzipで圧縮したJSONです。完成したレスポンスのサンプルには、fileSize、generatedAt、failureReasonも並びます。失敗したときの理由はfailureReasonに入る構造なので、状態確認のスクリプトはCOMPLETEDだけでなく、この欄も見る作りにしておくと原因を追いやすくなります。
ダウンロードしたファイルを集計してからClaudeに渡す
timeUnitをDAILYにしたので、同じ検索用語が日付の数だけ行になります。Claudeに行をそのまま渡すと、行数が多くなるうえ、足し算をClaudeの暗算に任せることになります。集計は手元のスクリプトに任せ、Claudeには集計済みの表を渡します。
import csv, gzip, json, sys
from collections import defaultdict
rows = json.load(gzip.open(sys.argv[1], "rt", encoding="utf-8"))
agg = defaultdict(lambda: {"impressions": 0, "clicks": 0,
"cost": 0.0, "purchases7d": 0, "days": set()})
for r in rows:
key = (r.get("campaignId"), r.get("adGroupId"),
r.get("targeting"), r.get("searchTerm"))
a = agg[key]
a["impressions"] += r.get("impressions") or 0
a["clicks"] += r.get("clicks") or 0
a["cost"] += float(r.get("cost") or 0)
a["purchases7d"] += r.get("purchases7d") or 0
a["days"].add(r.get("date"))
with open("search-terms-summary.csv", "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["campaignId", "adGroupId", "targeting", "searchTerm",
"impressions", "clicks", "cost", "purchases7d", "days"])
for (c, g, t, s), a in sorted(agg.items(), key=lambda x: -x[1]["cost"]):
w.writerow([c, g, t, s, a["impressions"], a["clicks"],
round(a["cost"], 2), a["purchases7d"], len(a["days"])])
print("rows:", len(rows), "terms:", len(agg),
"total cost:", round(sum(a["cost"] for a in agg.values()), 2))このスクリプトは、ファイルがJSON配列である前提で書いています。最初に1行だけ中身を見て、配列か1行ずつのJSONかを確かめてから実行してください。
最後のprintが出す合計費用は、後の検算に使います。Claudeに表を渡したら、同じ合計になっているかを答えさせ、一致しなければ先へ進みません。この検算の考え方は、Google広告の記事と同じです。
Claudeにファイルを渡すときの上限は、チャットで1ファイル500MB、1チャット20ファイルまでです。集計後のCSVなら、まず収まります。
無駄クリックの語をClaudeに選ばせる
絞り込みの条件は、数字で先に決めておくほうが後で説明がつきます。たとえば「クリックが10回以上で、購入が0の語」を一次候補にします。10回という値に根拠はなく、1クリックの単価と商品の利益から、自分のアカウントに合わせて決めるものです。
逆算の例を挙げます。数値は架空です。1クリックの平均単価が40円、購入1件あたりの利益が800円、クリックから購入に至る割合が2%の商品なら、1クリックの期待利益は800円×0.02で16円で、単価の40円を下回ります。一方、クリックが10回あって購入が0なら費用は400円で、購入1件分の利益の半分にあたります。購入率が2%なら、10回で購入ゼロは約82%の確率で起きるので、この段階では「無関係と断定できる」とまでは言えません。10回は、判断を始める最低ラインとして置く値です。購入率が高い商品ほど、同じ回数でゼロになる確率は下がり、外す根拠が強まります。
その上で、Claudeには語の意図を判断させます。数字だけでは、同じ「購入0」でも性質の違う語が混ざるからです。
添付の search-terms-summary.csv は Amazon広告(スポンサープロダクト)の検索用語を
期間で集計したものです。まず cost の合計を答えてください。
次に、clicks が10以上で purchases7d が0の行だけを対象に、
searchTerm を次の4つに分類してください。
A: 商品と無関係(除外して問題が少ない)
B: 商品に近いが、自社の仕様と合わない(例:サイズ・素材違い)
C: 購入前の情報収集の語(除外すると機会を失うかもしれない)
D: 判断できない
分類は表で出し、理由を1行ずつ付けてください。
商品のカテゴリは「(ここに商品の説明)」です。商品の説明を渡すのが要点です。検索用語だけでは、その語が自社商品に合うかをClaudeは決められません。「ステンレスのタンブラー」と書いて渡せば、「プラスチック」を含む語はBに入ります。
Cの扱いは、除外するかどうかの判断が分かれます。「タンブラー 選び方」のような情報収集の語は、その場で買われなくても、後日の購入につながることがあります。購入の計測期間を短く取っているほど「購入0」に見えやすいので、Cは初回の除外から外し、期間を延ばして再集計してから決める運用が無難です。
分類の結果は、そのまま登録せず、Aから順に人が読みます。購入が0でも、daysが1か2日しかない語は、データが少なすぎて判断できません。表のdays列を見て、Claudeの理由が「たまたま売れなかった語」を無関係と言い切っていないかを確かめてください。
購入数の読み方にも注意が要ります。サンプルのcolumnsには1日、7日、14日、30日の4つの購入数が並びます。期間の終わりに近い日のクリックは、30日後の購入をまだ含んでいません。直近の数日を含めて判断すると、売れる語を除外候補に入れてしまいます。集計のendDateは、少なくとも見ている購入の期間より前にずらして取るのが安全です。
ClaudeとFunnelを連携する方法のように、広告媒体を横断して見る連携もあります。ただ、除外キーワードのように検索用語の粒度まで下りる作業は、媒体ごとのレポートを取る今回の方法のほうが向いています。
除外キーワードの登録リクエストを組む
候補が決まったら、登録用のリクエストにします。スポンサープロダクトの除外キーワードは、Postmanコレクションでは2つのエンドポイントに分かれています。
| 登録先 | エンドポイント | 本文の配列名 |
|---|---|---|
| 広告グループの除外キーワード | エンドポイントPOST /sp/negativeKeywords | 本文の配列名negativeKeywords |
| キャンペーンの除外キーワード | エンドポイントPOST /sp/campaignNegativeKeywords | 本文の配列名campaignNegativeKeywords |
どちらに登録するかは、語の性質で決めます。ある商品の広告グループにだけ合わない語(サイズ違いなど)は広告グループ向けにして、ほかの商品への影響を避けます。どの商品にも無関係な語(別カテゴリの語など)は、キャンペーン向けにまとめて外すと登録の件数が減ります。迷う語は、影響範囲の狭い広告グループ向けから始めます。
広告グループ向けのサンプルは、次の形です。
{
"negativeKeywords": [
{
"campaignId": "(キャンペーンID)",
"adGroupId": "(広告グループID)",
"matchType": "NEGATIVE_EXACT",
"state": "ENABLED",
"keywordText": "(除外する語)"
}
]
}サンプルで使われているマッチタイプはNEGATIVE_EXACTだけです。ほかのマッチタイプを使う場合は、Amazon Adsの該当APIの仕様で、指定できる値を確かめてから本文に入れます。
Claudeに頼むのは、分類のAと、人が承認したBの行を、この形のJSONに変換する作業です。
承認した行だけを、/sp/negativeKeywords に送る本文のJSONにしてください。
campaignId と adGroupId は表の値をそのまま使い、
matchType は NEGATIVE_EXACT、state は ENABLED にします。
keywordText は searchTerm をそのまま入れ、表にない語は足さないでください。
最後に、件数と、含めた searchTerm の一覧を出してください。「表にない語は足さない」と指定するのは、Claudeが親切心で似た語を追加するのを防ぐためです。出てきたJSONは、件数と語の一覧を元の承認リストと突き合わせてから送ります。
ここまでの作業は、レポートの取得が読み取りで、除外の登録が書き込みという違いがあります。取得は何度やり直しても広告に影響しません。登録は、すぐに配信へ反映される操作です。Postmanコレクションには更新(PUT)と削除のエンドポイントも並ぶので、誤った語を入れた場合は、状態をPAUSEDにして止められます。
つまずきやすい点
落とし穴
状態確認を連打して待たない
作成の直後はPENDINGが続きます。確認の間隔を空けて繰り返すスクリプトにし、COMPLETED以外の状態やfailureReasonが入ったときは止まる作りにしてください。
- 署名付きURLには期限があります。
urlExpiresAtが付くので、COMPLETEDを確認したらすぐにダウンロードします。期限が切れたら、同じreportIdの状態確認をもう一度送って、新しいURLを受け取る形になるかを試します。 - 行の合計がAmazon広告の画面とずれることがあります。
groupByとcolumnsの組み合わせで行の粒度が変わるためです。画面と比べるときは、期間、広告の種類、集計の単位をそろえます。 - v2とv3のエンドポイントを混ぜると動きません。コレクションには
/v2/reports系とv3の/reporting/reports系が両方あり、リクエスト本文の形も違います。ここで扱ったのは後者です。 - 広告の種類が違えば書式も変わります。サンプルの
adProductはSPONSORED_PRODUCTSで、スポンサーブランドの除外キーワードは/sb/negativeKeywordsと別の書式になります。
EC側の広告費と売上をまとめて聞きたい場合は、ClaudeとPolar Analyticsを連携する方法が参考になります。検索用語単位の除外は、このレポート取得の流れで扱います。
まとめ
Amazon広告の除外キーワードは、取得、集計、判断、登録を分けると安全に進められます。取得と集計はスクリプトに任せ、Claudeには集計済みの表と商品の説明を渡して語の意図を分類させます。登録は、人が承認した行だけをJSONにして送ります。
最初に試すなら、書き込みをせず、取得から分類までで止めてください。分類の理由を読んで納得できる語が増えてから、登録のリクエストへ進むほうが、除外しすぎる失敗を避けられます。