Claude Code×Shopify・Stripe API【2026】EC運営と売上分析をAIエージェントで効率化
商品ページの説明文が抜けている、セールが終わったのに「通常価格」が残っている、今月の解約はどれくらいか、決済に失敗したお客様に連絡できているか——EC運営には「毎月ちゃんと見たいのに手が回らない」確認作業がたくさんあります。この記事では、Claude Code(ターミナルで動くAnthropicのコーディングエージェント)をShopify Admin APIとStripe APIにつなぎ、読み取り専用の権限だけで点検・集計・下書きまでを任せる構成を、動くPythonコードと設定ファイル付きで解説します。ストアや決済の「変更」は、最後まで人が行う前提です。
- ShopifyはAdmin GraphQL APIを
requestsでPOSTし、pageInfo.endCursorでページング。スコープはread_products・read_ordersなど読み取りだけ - Stripeは制限付きキー(
rk_)に読み取り権限だけを付け、auto_paging_iter()で全件取得。金額は最小通貨単位(JPYは小数なし、USDはセント) - 集計・突合はスクリプトで決定的に。Claude Codeには解釈・修正案・文面の下書きを任せ、反映は人が行う
- 「変更しない」はキーの権限・permissions・hooksの三重で強制する。CLAUDE.mdは「お願い」、設定は「鍵」
何ができるか:EC運営の5つのユースケース
どれもデータを読んで、判断材料と下書きを作る作業です。ストアの商品や決済を書き換える作業は含めません。
| ユースケース | 使うAPI | Claude Codeの役割 | 人の役割 |
|---|---|---|---|
| ① 商品データの一括チェック(説明文の抜け・在庫0・価格の異常) | Shopify(products) | 結果の解釈、修正案CSVの記入 | 承認し、管理画面で反映 |
| ② 商品説明文の下書き | Shopify(商品属性) | 属性から下書きを作成 | 事実・表現の確認、公開 |
| ③ 売上・返金・解約の月次集計とMRR推移 | Stripe(PaymentIntent・Refund・Subscription) | 集計の実行、所見のまとめ | 施策の判断 |
| ④ 失敗した決済の一覧化とフォロー文面 | Stripe(PaymentIntent・Invoice) | 失敗理由別の整理、文面の下書き | 送付の判断・送信 |
| ⑤ Shopify注文とStripe入金の突合 | Shopify(orders)+Stripe | 突合の実行、不一致の確認観点の整理 | 調査・経理処理 |


APIやJSONの基本から確認したい方は、初心者向けのIT基礎シリーズ第4章「API・JSON入門」を先に読むと、この先のコードが追いやすくなります。
準備と認証:開発ストア・テストモード・最小権限のキー
Shopify:開発ストアとAdmin APIのトークン
本番ストアにいきなりつながず、まず開発ストア(開発者向けの検証用ストア)にテスト用の商品・注文を入れて試します。ストア用のアプリ(カスタムアプリ)を作成し、Admin APIのスコープを読み取り系だけ選んでインストール、アクセストークンを取得します。
| スコープ | 用途 | メモ |
|---|---|---|
read_products | 商品・バリアント・価格 | 商品チェック、説明文の下書き |
read_inventory | 在庫数 | 在庫0の検出 |
read_orders | 注文 | 既定では直近60日分。古い注文にはread_all_ordersが別途必要 |
Admin APIはGraphQLが推奨です。エンドポイントはPOST https://{shop}.myshopify.com/admin/api/{version}/graphql.json、ヘッダーX-Shopify-Access-Tokenにトークンを入れます。APIバージョン(例:2026-07)は四半期ごとにリリースされ、古いものは順次サポート終了になるので、環境変数で差し替えられるようにします。
Stripe:テストモードと「制限付きキー」
Stripeは本番と切り離されたテストモード(サンドボックス)で開発します。テスト用カードで決済・失敗・返金・サブスクを作れるので、集計ロジックの検証に向いています。Stripe CLIのstripe triggerでイベントを発生させたり、stripe listenでWebhookをローカルに転送したりもできます(CLIは人が手元で使う道具とし、Claude Codeには後述の設定で使わせません)。
APIキーは、すべての操作ができるシークレットキー(sk_)ではなく、制限付きキー(Restricted key、rk_)を発行し、リソースごとに必要なものだけ「読み取り」にします。
| リソース(例) | 権限 | 使う場面 |
|---|---|---|
| PaymentIntents | 読み取り | 売上集計・決済失敗・突合 |
| Refunds(Charges) | 読み取り | 返金の集計 |
| Subscriptions・Invoices | 読み取り | MRR・解約・サブスクの支払い失敗 |
| Customers・その他すべて | なし | 顧客IDだけで足りるなら付けない |
区分や名称はダッシュボードで確認しながら設定してください(執筆時点)。足りなければAPIが権限エラーを返すので、最小から始めて「足りなければ足す」のが安全です。
pip install requests stripe python-dotenv
# .env の中身
SHOPIFY_SHOP=your-dev-store
SHOPIFY_ADMIN_TOKEN=shpat_xxxxxxxx
SHOPIFY_API_VERSION=2026-07
STRIPE_API_KEY=rk_test_xxxxxxxx.envはGitにコミットせず、Claude CodeのReadからも除外します(後述のdeny)。本番用のrk_live_は別キーとして発行し、用途・発行日を記録しておけば、不要になったとき確実に失効できます。シークレットキー(sk_live_)は、AIエージェントの作業環境に置かないでください。
プロジェクト構成
EC運営データ用のリポジトリを1つ作り、そこでClaude Codeを起動します。テーマやアプリのコードとは分けると、権限設定をシンプルに保てます。
ec-ops/
├── CLAUDE.md # ルール・数字の定義
├── .env # トークン(Git・Claudeの対象外)
├── .claude/settings.json # permissions / hooks
├── .claude/hooks/block_write_api.py
├── .claude/skills/monthly-ec-report/SKILL.md
├── scripts/shopify/ # client.py, fetch_products.py, check_products.py, fetch_orders.py
├── scripts/stripe/ # common.py, monthly_summary.py, mrr.py, failed_payments.py, reconcile.py
├── reports/ # 取得・集計結果=事実(Git管理外)
├── proposals/ # 修正案CSV=提案(人が承認)
└── drafts/ # 説明文・フォロー文面の下書き出力先をreports(事実)/proposals・drafts(提案)に分けているのがポイントです。permissionsで「提案の置き場だけ書き込み可」にでき、レビューする人も事実と提案を取り違えません。
Shopify Admin GraphQL:商品の取得と一括チェック
GraphQLクライアントとcursorページング
Admin GraphQLは1つのエンドポイントにクエリをPOSTするだけなので、requestsで十分です。依存が少ないほど、Claude Codeに読ませて直させるときの見通しもよくなります。
"""Shopify Admin GraphQL API の薄いクライアント(読み取り専用)"""
import os
import time
import requests
from dotenv import load_dotenv
load_dotenv()
VERSION = os.environ.get("SHOPIFY_API_VERSION", "2026-07")
ENDPOINT = f"https://{os.environ['SHOPIFY_SHOP']}.myshopify.com/admin/api/{VERSION}/graphql.json"
HEADERS = {"X-Shopify-Access-Token": os.environ["SHOPIFY_ADMIN_TOKEN"]}
def graphql(query: str, variables: dict | None = None) -> dict:
if query.lstrip().lower().startswith("mutation"):
raise ValueError("mutation is not allowed in this project") # 書き込みはコードでも止める
for attempt in range(5):
resp = requests.post(ENDPOINT, json={"query": query, "variables": variables or {}},
headers=HEADERS, timeout=30)
if resp.status_code == 429 or resp.status_code >= 500:
time.sleep(2 ** attempt)
continue
resp.raise_for_status()
body = resp.json()
errors = body.get("errors") or []
if any(e.get("extensions", {}).get("code") == "THROTTLED" for e in errors):
time.sleep(2 ** attempt) # クエリコストの上限に達したら待って再試行
continue
if errors:
raise RuntimeError(errors)
return body["data"]
raise RuntimeError("retries exhausted")
def paginate(query: str, root: str, variables: dict | None = None):
"""pageInfo.endCursor を $cursor に渡して全ページをたどる"""
variables = {**(variables or {}), "cursor": None}
while True:
conn = graphql(query, variables)[root]
yield from conn["nodes"]
if not conn["pageInfo"]["hasNextPage"]:
return
variables["cursor"] = conn["pageInfo"]["endCursor"]ShopifyのGraphQLはリクエスト数ではなく「クエリのコスト」で制限されます(leaky bucket方式)。firstの件数とネストでコストが決まり、1クエリあたりの上限もあります。応答のextensions.costに消費量と残量が入るので、件数調整の目安にします。
"""商品とバリアントを reports/products.jsonl に保存する"""
import json
from client import paginate
QUERY = """
query Products($cursor: String) {
products(first: 25, after: $cursor, query: "status:active OR status:draft") {
pageInfo { hasNextPage endCursor }
nodes {
handle title status productType descriptionHtml tracksInventory totalInventory
variants(first: 30) { nodes { sku price compareAtPrice } }
}
}
}
"""
with open("reports/products.jsonl", "w", encoding="utf-8") as f:
for product in paginate(QUERY, "products"):
f.write(json.dumps(product, ensure_ascii=False) + "\n")products(first: 25)×variants(first: 30)はコスト上限を意識した値です。first: 250のように大きくするとエラーになります。商品数が非常に多いストアでは、非同期で全件をファイル出力するBulk Operationsを検討します。
ルールで点検し、修正案CSVの雛形を作る
「説明文が60文字未満」「販売中で在庫0」「価格が0以下」「比較価格(通常価格)が販売価格以下」「同じ商品タイプの中央値から5倍以上離れた価格」の5ルールです。しきい値はストアに合わせて調整します。
"""ルールで商品を点検し、修正案CSVの雛形を作る(ストアには反映しない)"""
import csv
import json
import re
from collections import defaultdict
from decimal import Decimal
from statistics import median
MIN_DESC = 60 # 説明文が短すぎる目安(文字)
RATIO = 5 # 同じ商品タイプの中央値から5倍以上離れた価格は要確認
products = [json.loads(line) for line in open("reports/products.jsonl", encoding="utf-8")]
by_type = defaultdict(list)
for p in products:
by_type[p["productType"]] += [Decimal(v["price"]) for v in p["variants"]["nodes"]]
issues = [] # (handle, sku, issue, detail)
for p in products:
text = re.sub(r"<[^>]+>|\s", "", p["descriptionHtml"] or "")
if len(text) < MIN_DESC:
issues.append((p["handle"], "", "description_short", f"{len(text)}文字"))
if p["status"] == "ACTIVE" and p["tracksInventory"] and (p["totalInventory"] or 0) <= 0:
issues.append((p["handle"], "", "active_out_of_stock", "販売中で在庫0"))
prices = by_type[p["productType"]]
med = median(prices) if len(prices) >= 5 else None
for v in p["variants"]["nodes"]:
price = Decimal(v["price"])
if price <= 0:
issues.append((p["handle"], v["sku"], "price_zero", str(price)))
if v["compareAtPrice"] and Decimal(v["compareAtPrice"]) <= price:
issues.append((p["handle"], v["sku"], "compare_at_not_higher", f"{price} / {v['compareAtPrice']}"))
if med and (price > med * RATIO or price * RATIO < med):
issues.append((p["handle"], v["sku"], "price_outlier", f"price={price} median={med}"))
# proposed_value・reason は Claude Code が、approved_by は人が埋める
with open("proposals/fix_proposals.csv", "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["handle", "sku", "issue", "current", "proposed_value", "reason", "approved_by"])
w.writerows([*i, "", "", ""] for i in issues)
print(f"{len(products)} products, {len(issues)} issues -> proposals/fix_proposals.csv")線引きはスクリプトが検出、Claude Codeが修正案の記入、反映は人です。approved_byに名前が入った行だけを、管理画面や公式の一括編集機能で反映します。tracksInventoryを見ているのは、受注生産やデジタル商品など在庫管理しない商品は在庫0でも正常だからです。誤検知の理由をルール側に入れておくと、指摘の質が上がります。
説明文が短い商品は、Claude Codeに商品属性と、同カテゴリで説明文が充実している商品を参考にdrafts/descriptions/へ下書きさせます。商品データにない効能や数値を書かないルールが重要で、健康・美容系では薬機法などで使えない表現もあります。事実確認と公開は担当者が行います。
Stripe:売上・返金・解約・MRRの月次集計と決済失敗の一覧
共通処理:制限付きキーの確認と最小通貨単位
Stripeのamountは最小通貨単位の整数です。USDの1000は10.00ドル(セント)ですが、JPYは小数のない通貨なので1000はそのまま1,000円。ここを誤ると円の売上が100分の1で表示されます。変換は1か所の関数にまとめ、キーがrk_でなければ起動しないようにもしておきます。
"""Stripe共通:制限付きキーの確認、最小通貨単位の変換、JSTの月範囲"""
import os
from datetime import datetime, timedelta, timezone
from decimal import Decimal
import stripe
from dotenv import load_dotenv
load_dotenv()
stripe.api_key = os.environ["STRIPE_API_KEY"]
if not stripe.api_key.startswith("rk_"):
raise SystemExit("読み取り専用の制限付きキー(rk_...)を設定してください")
JST = timezone(timedelta(hours=9))
# 小数のない通貨。JPY の 1000 は「1,000円」、USD の 1000 は「10.00ドル」
ZERO_DECIMAL = {"bif", "clp", "djf", "gnf", "jpy", "kmf", "krw", "mga",
"pyg", "rwf", "ugx", "vnd", "vuv", "xaf", "xof", "xpf"}
def to_major(amount: int, currency: str) -> Decimal:
return Decimal(amount) if currency.lower() in ZERO_DECIMAL else Decimal(amount) / 100
def month_range(ym: str) -> tuple[int, int]:
y, m = map(int, ym.split("-"))
start = datetime(y, m, 1, tzinfo=JST)
end = datetime(y + m // 12, m % 12 + 1, 1, tzinfo=JST)
return int(start.timestamp()), int(end.timestamp())通貨一覧は執筆時点の公式ドキュメントに基づきます。3桁小数の通貨などにも特別なルールがあるため、多通貨で販売する場合は公式の通貨一覧を確認してください。
月次集計:売上・返金・新規・解約
一覧APIは1回最大100件なので、auto_paging_iter()で次ページを自動的にたどります。createdで期間を絞ってから回すのが基本です。
"""月次の売上・返金・新規/解約サブスクを通貨別に集計する"""
import csv
import sys
from collections import Counter
import stripe
from common import month_range, to_major
ym = sys.argv[1] # 例: 2026-08
start, end = month_range(ym)
period = {"gte": start, "lt": end}
sales, refunds, new_subs, churned = Counter(), Counter(), Counter(), Counter()
for pi in stripe.PaymentIntent.list(created=period, limit=100).auto_paging_iter():
if pi.status == "succeeded":
sales[pi.currency] += pi.amount_received
for rf in stripe.Refund.list(created=period, limit=100).auto_paging_iter():
if rf.status == "succeeded":
refunds[rf.currency] += rf.amount
for sub in stripe.Subscription.list(status="all", created=period, limit=100).auto_paging_iter():
if sub.status not in ("incomplete", "incomplete_expired"):
new_subs[sub.currency] += 1
for sub in stripe.Subscription.list(status="canceled", limit=100).auto_paging_iter():
if sub.ended_at and start <= sub.ended_at < end: # canceled_at ではなく実際の終了日時
churned[sub.currency] += 1
with open(f"reports/stripe_summary_{ym}.csv", "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["currency", "gross", "refunds", "net", "new_subs", "churned_subs"])
for cur in sorted(set(sales) | set(refunds) | set(new_subs) | set(churned)):
gross, ref = to_major(sales[cur], cur), to_major(refunds[cur], cur)
w.writerow([cur.upper(), gross, ref, gross - ref, new_subs[cur], churned[cur]])- 旧来の
ChargeではなくPaymentIntentで数えるのは、現在の決済フローがPaymentIntent中心だからです。手数料控除後の入金ベースが必要な経理用途ならBalanceTransactionから集計する方法もあります。どちらを「売上」と呼ぶかはCLAUDE.mdで定義します - 解約は
canceled_atではなくended_atで判定します。「期間終了時に解約」の場合、canceled_atは申し込み時点を指すことがあり、解約月がずれるためです
MRR推移
MRR(月次経常収益)は、有効なサブスクの契約額を月額に換算した合計です。年額は1/12、週額は52/12倍にします。
"""現在のMRRを通貨別に計算し、reports/mrr_history.csv に1行ずつ追記する(毎月末に実行)"""
import csv
from collections import Counter
from datetime import datetime
from decimal import Decimal
import stripe
from common import JST, to_major
TO_MONTHLY = {"day": Decimal(365) / 12, "week": Decimal(52) / 12, "month": Decimal(1), "year": Decimal(1) / 12}
mrr = Counter()
for sub in stripe.Subscription.list(status="active", limit=100).auto_paging_iter():
for item in sub["items"]["data"]: # sub.items は dict の items() と衝突するので添字で取る
price = item["price"]
rec = price.get("recurring")
if not rec or price.get("unit_amount") is None:
continue # 従量・段階制などは対象外(定義はCLAUDE.md)
amount = Decimal(price["unit_amount"]) * (item.get("quantity") or 1)
mrr[sub.currency] += amount * TO_MONTHLY[rec["interval"]] / rec["interval_count"]
today = datetime.now(JST).date().isoformat()
with open("reports/mrr_history.csv", "a", newline="", encoding="utf-8") as f:
for cur, value in sorted(mrr.items()):
csv.writer(f).writerow([today, cur.upper(), round(to_major(int(value), cur), 2)])このスクリプトは毎月末に実行して1行ずつ追記するスナップショット方式です。過去分をstart_date・ended_atから再計算することもできますが、現在の価格で計算することになり、プラン変更や割引は反映されません。「過去をすぐ見たい」なら再計算、「正確さ優先」ならスナップショット、というトレードオフです。なおsub.itemsは辞書のitems()メソッドと衝突するためsub["items"]と書きます。AIが書いたコードでも見落としやすい落とし穴です。
決済失敗の一覧
"""直近30日の決済失敗を一覧化する。メール・氏名は出力せず顧客IDだけ残す"""
import csv
import time
import stripe
from common import to_major
since = {"gte": int(time.time()) - 30 * 86400}
rows = []
# 単発決済:失敗すると requires_payment_method に戻り、last_payment_error が残る
for pi in stripe.PaymentIntent.list(created=since, limit=100).auto_paging_iter():
err = pi.last_payment_error
if pi.status == "requires_payment_method" and err:
rows.append([pi.id, pi.customer, to_major(pi.amount, pi.currency), pi.currency, err.get("decline_code")])
# サブスク:支払いに失敗した請求書は open のまま attempt_count が増える
for inv in stripe.Invoice.list(status="open", created=since, limit=100).auto_paging_iter():
if inv.attempt_count:
rows.append([inv.id, inv.customer, to_major(inv.amount_due, inv.currency), inv.currency, "invoice_failed"])
with open("reports/failed_payments.csv", "w", newline="", encoding="utf-8") as f:
csv.writer(f).writerows([["id", "customer_id", "amount", "currency", "reason"], *rows])出力は顧客ID(cus_...)だけで、メールや氏名は取りません。フォロー文面は{{customer_name}}のようなプレースホルダーで下書きし、差し込みと送信は人がメール配信ツールなどで行います。Claude Codeが個人情報に触れないまま、decline_code別(expired_cardならカード更新の案内、insufficient_fundsなら再請求日の案内など)の文面を用意できます。StripeのBillingには自動再試行や顧客への通知メールの設定もあるので、重複しないか先に確認しましょう。
Shopify注文×Stripe入金の突合
前提として、Shopifyの標準決済であるShopify Paymentsの取引は自社のStripeアカウントには入りません。突合が意味を持つのは、外部の決済連携アプリや自社開発の仕組みでShopifyの注文代金をStripeで受けている場合や、請求書払い・定期購入の一部をStripe側で請求している場合です。
突合のキーは、連携側がPaymentIntentのmetadataにShopifyの注文IDを入れているかで決まります。入っていれば確実に突合でき、なければ「金額と時刻が近い」ものを候補として人に回すしかありません。注文は次のクエリで取得し、order_id, name, created_at, totalの列でCSVに保存しておきます(ページングは商品と同じpaginateを使います)。
query Orders($cursor: String, $q: String) {
orders(first: 100, after: $cursor, query: $q, sortKey: CREATED_AT) {
pageInfo { hasNextPage endCursor }
nodes {
id name createdAt displayFinancialStatus
totalPriceSet { shopMoney { amount currencyCode } }
}
}
}
# 変数の例: {"q": "created_at:>=2026-08-01 created_at:<2026-09-01"}"""Shopify注文とStripe入金を突合する。確定できないものは candidate / unmatched として人に回す"""
import csv
import sys
from datetime import datetime
from decimal import Decimal
import stripe
from common import month_range, to_major
KEY = "shopify_order_id" # 連携側が metadata に入れているキー名(環境ごとに確認)
ym = sys.argv[1]
start, end = month_range(ym)
payments = [p for p in stripe.PaymentIntent.list(created={"gte": start, "lt": end}, limit=100).auto_paging_iter()
if p.status == "succeeded"]
by_meta = {p.metadata.get(KEY): p for p in payments if p.metadata.get(KEY)}
used, out = set(), []
for o in csv.DictReader(open(f"reports/orders_{ym}.csv", encoding="utf-8")):
amount = Decimal(o["total"])
p = by_meta.get(o["order_id"].rsplit("/", 1)[-1]) # gid://shopify/Order/123 -> 123
if p:
ok = to_major(p.amount_received, p.currency) == amount
out.append([o["name"], p.id, "matched" if ok else "amount_mismatch"])
used.add(p.id)
continue
t = datetime.fromisoformat(o["created_at"].replace("Z", "+00:00")).timestamp()
near = [p for p in payments if p.id not in used and abs(p.created - t) < 900
and to_major(p.amount_received, p.currency) == amount]
status = "candidate" if len(near) == 1 else ("ambiguous" if near else "unmatched_shopify")
out.append([o["name"], near[0].id if len(near) == 1 else "", status])
if len(near) == 1:
used.add(near[0].id)
out += [["", p.id, "unmatched_stripe"] for p in payments if p.id not in used]
with open(f"reports/reconcile_{ym}.csv", "w", newline="", encoding="utf-8") as f:
csv.writer(f).writerows([["shopify_order", "payment_intent", "status"], *out])結果はmatched・amount_mismatch・candidate(推定)・ambiguous(候補が複数)・unmatched_shopify/unmatched_stripeに分かれます。AIに「それらしく」マッチさせないのが肝心です。Claude Codeには、不一致の行について「部分返金」「送料の扱い」「タイムゾーンで月をまたいだ」など確認すべき観点を挙げてもらうのが向いています。
metadataにShopifyの注文IDを必ず入れる設計にしておくと、突合がほぼ機械的に終わります。
CLAUDE.mdに「数字の定義」と「やらないこと」を書く
Claude Codeは、プロジェクト直下のCLAUDE.mdを起動のたびに読み込みます(/initで雛形を作れます)。EC・決済では特に数字の定義が効きます。「売上」「MRR」「解約」の定義が毎回ぶれると、月ごとのレポートを比較できなくなるからです。
# CLAUDE.md(EC運営・売上分析)
## 目的
- Shopify・Stripeのデータを読み取り、点検・集計・下書きを作る。ストアや決済は変更しない
## データの取り方(APIを直接叩かず、必ずスクリプト経由)
- 商品点検: scripts/shopify/fetch_products.py → check_products.py
- 月次: scripts/stripe/monthly_summary.py YYYY-MM / MRR: mrr.py / 決済失敗: failed_payments.py
- 突合: scripts/shopify/fetch_orders.py YYYY-MM → scripts/stripe/reconcile.py YYYY-MM
- 金額の合計・比較は reports/ のCSVの値を使う。暗算で再集計しない
## 数字の定義
- 売上 = 成功した PaymentIntent の amount_received。返金はその月に作成された成功分
- MRR = 有効なサブスクの月額換算(年額は1/12)。割引・従量課金は含まない
- 解約 = その月に ended_at を迎えたサブスク。月の区切りは JST
- 金額は最小通貨単位(JPYは小数なし、USDはセント)→ common.to_major で変換済み
## 下書きのルール
- 商品説明: drafts/descriptions/<handle>.md。商品データにない効能・数値・比較を書かない
- 決済失敗のフォロー: drafts/followups/。顧客名は {{customer_name}} のまま。カード番号をメールで求めない
- 修正案: proposals/fix_proposals.csv の proposed_value と reason を埋める。approved_by は空のまま
## 禁止
- 返金・キャンセル・価格/在庫/顧客情報の変更を行うコードを書く・実行する
- .env を読む、reports/ の内容を外部に送る
permissionsとhooksで「変更しない」を強制する
CLAUDE.mdの「返金しない」はお願いにすぎません。守らせたいことは.claude/settings.jsonで強制します。allowは確認なしで実行、askは毎回確認、denyは禁止です。
{
"permissions": {
"allow": [
"Bash(python scripts/shopify/fetch_products.py:*)",
"Bash(python scripts/shopify/check_products.py:*)",
"Bash(python scripts/shopify/fetch_orders.py:*)",
"Bash(python scripts/stripe/monthly_summary.py:*)",
"Bash(python scripts/stripe/mrr.py:*)",
"Bash(python scripts/stripe/failed_payments.py:*)",
"Bash(python scripts/stripe/reconcile.py:*)",
"Read(./reports/**)",
"Edit(./drafts/**)",
"Edit(./proposals/**)"
],
"ask": ["Edit(./scripts/**)"],
"deny": [
"Read(./.env)",
"Bash(curl:*)",
"Bash(wget:*)",
"Bash(stripe:*)",
"Bash(shopify:*)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "python .claude/hooks/block_write_api.py" }]
}
]
}
}- allowは取得・集計スクリプトと、提案の置き場への書き込みだけ。日常作業が確認なしで回る範囲に絞ります
- scriptsの編集はask。集計ロジックの変更は数字を変えるので、差分を人が見てから通します
- curl・wget・Stripe CLI・Shopify CLIはdeny。スクリプトを迂回してAPIを叩いたり、CLIから返金などを実行したりする経路をふさぎます
- .envはdeny。スクリプトが環境変数として読むので、Claude自身が中身を見る必要はありません
さらにhooksで、書き込み系APIを呼ぶコードがファイルに入ること自体を止めます。PreToolUseフックはツール実行前に呼ばれ、exit code 2で終了すると操作をブロックし、stderrの内容を理由としてClaudeに返します。
"""PreToolUse フック:書き込み系APIを呼ぶコードがファイルに入るのを止める"""
import json
import re
import sys
PATTERNS = [r"stripe\.\w+\.(create|modify|delete|cancel|capture)\b", r"^\s*mutation\b"]
tool_input = json.load(sys.stdin).get("tool_input", {})
text = tool_input.get("content") or tool_input.get("new_string") or ""
for pattern in PATTERNS:
if re.search(pattern, text, flags=re.MULTILINE):
print("書き込み系APIはこのリポジトリでは実装しません(CLAUDE.md参照)", file=sys.stderr)
sys.exit(2) # exit 2 = ブロック。stderr が理由として Claude に返る正規表現の検出には抜けがありえます。だからこそ最後の防衛線はAPIキーの権限です。読み取り専用の制限付きキーとread_スコープだけのトークンなら、仮に制限をすり抜けても書き込みはAPI側で拒否されます。「キーの権限 → permissions → hooks → CLAUDE.md」と層を重ねて守るのが、決済を扱うときの基本です。考え方はAIエージェントを本番運用する安全設計ガイドでも解説しています。
Claude Codeとの対話例
リポジトリでclaudeを起動して頼みます。以下は流れのイメージです(実際の出力は毎回異なり、件数は説明用の例です)。
例1:商品データの点検と修正案
> 商品データを点検して、修正案CSVを作って。
説明文が短い商品は上位5件だけ下書きもお願い。
● Bash(python scripts/shopify/fetch_products.py)
● Bash(python scripts/shopify/check_products.py)
⎿ 412 products, 37 issues -> proposals/fix_proposals.csv
● Read(proposals/fix_proposals.csv)
37件の内訳(CSVの集計どおり):
description_short 19件 … 同じ仕入れ先の新商品に集中しています
compare_at_not_higher 6件 … セール終了後に「通常価格」が残っている形です
price_outlier 3件 … うち1件は 12000 が 120 になった桁ミスの疑い
● Edit(proposals/fix_proposals.csv) ← proposed_value と reason を記入
● Write(drafts/descriptions/linen-shirt-navy.md) ほか4件
ストアへの反映は行っていません。price_outlier は価格設定の意図を
知っている方の確認が必要です。人はproposals/fix_proposals.csvで承認する行に名前を入れ、管理画面で反映します。特に価格の異常値は「本当に誤りか」を価格設定の意図を知る人が判断します。わざと安くしている目玉商品を直してしまっては本末転倒です。
例2:月次レポートと決済失敗のフォロー
> 8月の月次レポートと、決済失敗のフォロー文面の下書きを作って。
● Bash(python scripts/stripe/monthly_summary.py 2026-08)
● Bash(python scripts/stripe/failed_payments.py)
● Read(reports/stripe_summary_2026-08.csv) / Read(reports/mrr_history.csv) ...
● Write(reports/monthly_2026-08.md)
● Write(drafts/followups/expired_card.md)
● Write(drafts/followups/insufficient_funds.md)
決済失敗は expired_card と insufficient_funds が中心です。
Stripe側の自動再請求・通知メールと重複しないか確認してください。公式MCPサーバーの使いどころ
MCP(Model Context Protocol)はAIアプリと外部ツールをつなぐ共通規格です。ShopifyとStripeはいずれもAIエージェント向けのMCPサーバーを公開しています(執筆時点)。Shopifyの開発者向けMCP(Dev MCP)はストアのデータではなくドキュメント検索やGraphQLスキーマの参照を助けるもので、クエリを書くときに便利です。Stripeの公式MCPサーバー・エージェント向けツールキットは、顧客・請求・返金などのAPI操作をツールとして呼べます。
# Shopify の開発者向けMCP(ドキュメント・GraphQLスキーマの参照。執筆時点)
claude mcp add shopify-dev -- npx -y @shopify/dev-mcp@latest
# Stripe のリモートMCP(URL・認証方式は公式ドキュメントで確認)
claude mcp add --transport http stripe https://mcp.stripe.com| 方式 | 向いている場面 | 注意点 |
|---|---|---|
| 自前スクリプト | 月次集計・突合など、同じ数字を再現したい定型業務 | ロジックをレビュー・テストできる |
| MCPサーバー | 「この顧客の直近の請求は?」のようなその場の調べもの、ドキュメント参照 | 書き込み系ツールも含まれうる。テストモード+読み取り専用の権限で使う |
パッケージ名・URL・認証方式は変わることがあるため、導入時は公式ドキュメントで確認してください。MCPはツールを簡単に増やせる分、「何ができてしまうか」を把握しにくくなります。/mcpで接続中のサーバーを確認し、不要なものは外しておきます。
Skills化とヘッドレスでの月次実行
毎月同じ手順のレポートはSkillsにまとめます。.claude/skills/<name>/SKILL.mdに手順を書くと、Claude Codeは関連する依頼のときに読み込んで使います。CLAUDE.mdは「いつでも守るルール」、Skillは「特定の作業の手順」です。
---
name: monthly-ec-report
description: StripeとShopifyのデータから月次のEC売上レポート(売上・返金・解約・MRR・決済失敗・突合)を作る。月次レポートや売上集計を頼まれたときに使う。
---
1. 対象月 YYYY-MM を確認(指定がなければ先月)
2. monthly_summary.py → failed_payments.py → fetch_orders.py → reconcile.py の順に実行
3. reports/monthly_YYYY-MM.md に「サマリー3行/売上表/MRR推移/決済失敗の内訳/突合の不一致一覧」を書く
4. 数値はCSVから転記する。「事実」と「所見」を分け、原因は断定せず確認点として書く定期実行はclaude -pのヘッドレス(非対話)実行で行います。途中で人が確認できないので、--allowedToolsは対話時より狭くし、書き込みはreports/だけに限定します。
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
YM=$(date -d "last month" +%Y-%m) # GNU date の書き方
claude -p "monthly-ec-report の手順で ${YM} のレポートを作成して" \
--allowedTools "Bash(python scripts/stripe/monthly_summary.py:*)" "Bash(python scripts/stripe/mrr.py:*)" \
"Bash(python scripts/stripe/failed_payments.py:*)" "Bash(python scripts/stripe/reconcile.py:*)" \
"Bash(python scripts/shopify/fetch_orders.py:*)" "Read" "Write(./reports/**)" \
--output-format json > "logs/monthly_${YM}.json"レポートをSlackやNotionに届けたいなら、Claude Code×Slack・Notion API連携と組み合わせます。その場合も通知に載せるのは集計値までにし、顧客単位のデータはチャンネルに流しません。
注意点:決済・個人情報・PCI DSS・金額の単位
- カード番号を扱わない:Stripeの構成ではカード番号はStripe側で扱われ、APIからも全桁は取得できません(下4桁やブランド程度)。PCI DSS(カード業界のセキュリティ基準)の観点でも、AIの作業環境にカード情報を持ち込まない構成を守ります
- 個人情報は「取らない・渡さない」:分析に氏名やメールが本当に必要かをまず疑います。必要な場合も、社内規程と、利用するClaudeのプラン・契約のデータ取り扱い条件を確認してからにします。
reports/はGitにコミットしません - 金額の型:Stripeは最小通貨単位の整数、Shopifyは
"1200.0"のような文字列。どちらもDecimalで扱い、floatの丸め誤差を避けます。通貨の違う金額は足しません - タイムゾーン:Stripeの
createdはUnix時刻、Shopifyの検索はストアのタイムゾーン基準。月の区切りをそろえないと月末の注文がずれて突合が合いません - APIバージョンとレート制限:どちらもAPIバージョンがあり、制限の方式も異なります(上限値は公式で確認)。バージョン更新時は開発ストア・テストモードで集計結果が変わらないか確かめます
当社が支援の現場で採っている設計の考え方
当社が生成AIエージェントの導入を支援する際、売上や決済に関わるデータでは①読み取りと集計だけ → ②提案と下書き(反映は人) → ③限定的な書き込み(対象・上限・承認者・ログを先に決める)の順に範囲を広げることを基本にしています。
このサイト自体も、Claude Codeが作業ブランチで変更し、人が確認してマージする流れで運用しています。「AIが作り、人が通す」関所を1つ置くだけで、任せられる範囲は大きく広がります。仕組みは【自社事例】AIエージェントでメディアサイトを運用する仕組みで公開しています。
EC業界でもAI活用は前提になりつつあり、たとえばShopifyは、社内でAIを使うことを前提とする方針をCEOが社内メモで示したと報じられています(2025年)。一方で決済や顧客対応では、どこまでAIに任せ、どこで人が判断するかの線引きが各社の課題です。
よくある質問
Shopify Paymentsで決済している場合もStripeと突合できますか?
一般的なShopify Paymentsの決済は、利用者自身のStripeアカウントには入らないため、Stripe APIとの突合の対象にはなりません。その場合はShopifyの注文データと、Shopify Paymentsの入金・取引データ同士で照合します。Stripeとの突合が意味を持つのは、外部の決済連携や請求書払い、サブスクなど、Stripe側で決済している経路がある場合です。
Stripeのシークレットキーではなく制限付きキーを使う理由は?
シークレットキーは返金や顧客情報の更新を含むすべての操作ができるため、漏えいやAIの誤操作が起きたときの被害が大きくなります。制限付きキーならリソースごとに読み取りだけを許可でき、集計や一覧化に必要な権限に絞れます。キーの権限は、AIへの指示やpermissions設定よりも確実な最後の防衛線になります。
Claude Codeに商品の価格や在庫を直接更新させてもいいですか?
おすすめしません。価格や在庫の誤りは売上やクレームに直結し、元に戻しにくいからです。まずは点検結果と修正案のCSVまでをClaude Codeに任せ、反映は人が管理画面や公式の一括編集機能で行う運用から始めてください。書き込みを自動化する場合も、開発ストアで十分に検証し、人の承認ステップと変更ログを必ず残します。
MRRの数字がStripeダッシュボードと一致しません。
MRRは定義によって値が変わります。割引や従量課金、試用期間、支払い遅延中の契約を含めるかどうかで差が出るため、ダッシュボードの数字と完全には一致しないことがあります。社内で使う定義をCLAUDE.mdに明記し、スクリプトはその定義どおりに計算させ、差が出たときにどの定義の違いかを説明できる状態にしておくことが大切です。
まとめ|「読むのはAI、変えるのは人」でEC運営を軽くする
Shopify Admin APIとStripe APIをClaude Codeにつなぐと、商品データの点検、月次の売上・MRR集計、決済失敗のフォロー、注文と入金の突合といった「大事だけど手が回らない」作業を、毎月の仕組みとして回せるようになります。成功のポイントは、読み取り専用のキーとスコープから始め、数字はスクリプトで決定的に作り、Claude Codeには解釈と下書きを任せ、反映は人が行うことです。まずは開発ストアとテストモードでcheck_products.pyとmonthly_summary.pyを動かし、対話でレポートを1本作ってみてください。営業・顧客管理のデータとつなぐ方法はClaude Code×HubSpot・Salesforce API、シリーズ全体の地図はAIエージェントで開発してみよう|API連携編をご覧ください。
業務自動化のスキルを、キャリアと収入につなげる
APIとAIエージェントで業務を自動化できる人材は、社内でも社外でも求められています。次の一歩に役立つ記事です。
- AI・IT業界への転職ガイド【2026】:作ったものをポートフォリオに。未経験からの進め方と転職エージェントの選び方
- 仕事用の外付けモニターおすすめ3選【2026】:エディタ・ターミナル・ブラウザを並べると作業効率が段違い