CSVの手動ダウンロードをAPIに置き換える前に決めること
API化を考え始めたとき、最初に決めたのは「何を作るか」ではなく「何を再現するか」でした。目指したのは集計機能を持った便利なツールではなく、いま手動でダウンロードしている2つのタブの生データを、そのままAPIで取れるようにすることです。
「集計するAPI」ではなく「同じ行を返すAPI」
この線引きを最初に決めないと、取得側に集計の処理まで持たせてしまい、あとで作り直すことになります。私たちも、集計や税抜き変換、親SKUへのまとめまで担う機能を先に作り、後で撤去しました(理由は後半で説明します)。
取得側の役割は「手動ダウンロードのタブと同じ列・同じ粒度の行を返すこと」だけに絞る。これが最初の設計判断です。なお、楽天市場とYahoo!ショッピングは流入データを取るAPIが無く、認証の更新にも人の手が要るため、手動CSVのまま据え置いています(判断の基準は「複数モールの売上管理を1枚に集約する設計」で説明しています)。
2026年以降のShopifyアクセストークン取得の経路
最初に時間を取られたのは、2026年1月以降、ストアの管理画面で「レガシーカスタムアプリ」を新しく作れなくなっていたことです。管理画面の中だけでアプリを作ってトークンを発行する従来の経路は、私たちが作業した2026年7月時点では使えませんでした。
アプリ作成からトークン取得まで
代わりに通したのは、次の経路です。
- Dev Dashboard でアプリを作成する
- Partners 側でカスタム配布(特定のストアだけに入れる配布方法)を設定する
- 発行されたインストールリンクから、対象のストアへアプリを入れる
- OAuthの認可コード方式で、Client ID・Client Secret・認可コード(code)を
/admin/oauth/access_tokenに渡し、アクセストークンと交換する shpca_で始まるアクセストークンを受け取る
認証情報を渡すだけで自動発行される方式(client_credentials)は、私たちが確かめた範囲ではShopifyでは使えませんでした。認可コードを一度受け取ってトークンと交換する、OAuthの一往復が必要です。
Secretはコードに書かず、ログにも残さない
Client Secret は、作業をしていたAIエージェントに値を見せず、人が直接 .env ファイルに入力しました。あわせて、実行ログにトークン(shpca_ などの接頭辞で始まる文字列)がそのまま出ないよう、ログに書く時点で伏せ字にする処理を入れています。トークンの交換はコードで済ませても、Secretの受け渡しだけは人の手に残す設計です。
ShopifyQLで手動ダウンロードと同じ行を返す
トークンが取れたら、ShopifyQLでデータを引きます。実務で引っかかった点は次の4つです。
- 集計は店舗のタイムゾーン基準で行われる(日本の店舗なら日本時間)
- クエリの中の日付はクォートで囲まない
- 返ってくる rows は、列名をキーにした辞書の並びになる
- parseErrors は、エラーメッセージの文字列が並んだ配列で返る
手動ダウンロードしていた2タブを、そのままクエリにする
売上のタブを再現するクエリは次のとおりです。
FROM sales
SHOW total_sales, orders
GROUP BY order_utm_source, product_variant_sku
TIMESERIES day
流入元(UTM)が空の行や、SKUが空の行も含めてそのまま返します。手動ダウンロードのCSVにあった行を、取得側で絞り込まないためです。
セッションのタブは次のクエリで再現しています。
FROM sessions
SHOW sessions, sessions_with_cart_additions,
sessions_that_reached_checkout, sessions_that_completed_checkout
GROUP BY landing_page_path
TIMESERIES day
こちらも商品ページだけに絞らず、ブログ記事などを含めた全ページのセッションを返します。手動ダウンロードのタブがページの種類を絞っていなかったので、それに揃えました。
生データを行ごとに突き合わせてから置き換える
取得の処理はClaude Codeに書かせましたが、置き換えてよいかの判断は実データで行いました。手動ダウンロードした行と、ShopifyQLで取った行を、サンプル4件で1件ずつ突き合わせ、4件とも一致したことを確かめてから手動ダウンロードをやめています。「動くはず」の段階で切り替えないことが、後で数字を疑わずに済む前提になります。
一時エラーと恒久エラーを分けて再試行する
ShopifyQLの呼び出しでは、実際に THROTTLED(呼び出し回数の制限)や INTERNAL_SERVER_ERROR が返ってくることがありました。INTERNAL_SERVER_ERROR が返り続ける障害が起き、しばらくして回復したこともあります。
一時エラーは再試行し、それでも駄目なら理由を返す
そのため、THROTTLED と INTERNAL_SERVER_ERROR は「一時エラー」として扱い、間隔を空けながら再試行する対象にしています。再試行し尽くしても失敗した場合は、空の結果を黙って返さず、理由を添えて返します。
空で返してしまうと、週次の数字が急に0になったとき、それが「本当に売上が0だった」のか「取得に失敗した」のかを見分けられません。集計表の数字が静かに間違っている状態が、いちばん気づきにくい事故です。
恒久エラーは再試行しない
一方、リクエストの書き方の誤りのように、何度繰り返しても直らない種類のエラーは、その場でエラーとして止めます。両者を同じに扱うと、直らないエラーに再試行を重ねて時間だけが過ぎます。
集計はシート側に任せ、取得側は生データだけを返す
最初に作った版では、取得したデータをその場で集計する機能まで持たせていました。一度作ったうえで、すべて撤去しています。
撤去した理由
集計の処理を取得側に持たせると、集計の定義を変えたいときに取得側のコードまで触ることになります。反対に、取得側は手動ダウンロードと同じ形の生データを返すだけにしておけば、集計側を変えても取得側には手を入れずに済みます。
いまは、送料を除く、子SKUを親SKUにまとめる、税抜きに直す、分析用のカテゴリを割り当てる、といった加工は、もともと手動ダウンロードの行を受けていたスプレッドシートの数式がそのまま担っています。取得の方法が手動CSVからAPIに変わっても、シートの数式は書き直していません。
アプリをやめるときの撤去順
APIで取るためにアプリを作るなら、やめるときの手順も先に知っておく価値があります。relmeaでは別の用途でShopifyの公開アプリを運用し、撤去したことがあります。撤去は「アプリを削除すれば終わり」ではなく、順番がありました。
撤去の順番
- 証拠を残す(掲載ページや管理画面の状態を画像とHTMLで保存する)
- App Store の掲載を限定公開にする
- すべてのストアからアプリをアンインストールする
- アプリ本体を削除する
- サーバーやデータベースなどのインフラを止める
この順を守らないと、Shopifyから、GDPR関連のWebhookに応答が無いという警告が届きます。インフラは最後に止める、と覚えておけば迷いません。
アプリ本体の削除は取り消せない
アプリ本体を削除すると、そのアプリの Client ID は復元できません。再開したくなったら、アプリを作るところからやり直しです。削除は「一時停止」ではなく「作り直し前提の終わり方」だと理解したうえで実行します。
自動化してよい範囲と、人が持つ範囲
ここまでの作業のうち、機械に任せているのは次の範囲です。
- ShopifyQLで売上・セッションの生データを毎回同じ形で取ること
- 一時エラーの再試行と、失敗したときの理由の記録
- ログに出るトークンを伏せ字にすること
人が持っているのは次の範囲です。
- Client Secret の入力と保管
- 手動ダウンロードとAPIの行が一致したかの確認と、切り替えの判断
- アプリを撤去するかどうかの判断と、取り消せない削除の実行
取得を自動にしても、「この数字は正しいか」と「取り消せない操作をするか」は人の側に残ります。そこを最初に分けておくと、仕組みを長く保守しやすくなります。
