CSVの手動ダウンロードをAPIに置き換える前に決めること

API化を考え始めたとき、最初に決めたのは「何を作るか」ではなく「何を再現するか」でした。目指したのは集計機能を持った便利なツールではなく、いま手動でダウンロードしている2つのタブの生データを、そのままAPIで取れるようにすることです。

「集計するAPI」ではなく「同じ行を返すAPI」

この線引きを最初に決めないと、取得側に集計の処理まで持たせてしまい、あとで作り直すことになります。私たちも、集計や税抜き変換、親SKUへのまとめまで担う機能を先に作り、後で撤去しました(理由は後半で説明します)。

取得側の役割は「手動ダウンロードのタブと同じ列・同じ粒度の行を返すこと」だけに絞る。これが最初の設計判断です。なお、楽天市場とYahoo!ショッピングは流入データを取るAPIが無く、認証の更新にも人の手が要るため、手動CSVのまま据え置いています(判断の基準は「複数モールの売上管理を1枚に集約する設計」で説明しています)。

2026年以降のShopifyアクセストークン取得の経路

最初に時間を取られたのは、2026年1月以降、ストアの管理画面で「レガシーカスタムアプリ」を新しく作れなくなっていたことです。管理画面の中だけでアプリを作ってトークンを発行する従来の経路は、私たちが作業した2026年7月時点では使えませんでした。

アプリ作成からトークン取得まで

代わりに通したのは、次の経路です。

  1. Dev Dashboard でアプリを作成する
  2. Partners 側でカスタム配布(特定のストアだけに入れる配布方法)を設定する
  3. 発行されたインストールリンクから、対象のストアへアプリを入れる
  4. OAuthの認可コード方式で、Client ID・Client Secret・認可コード(code)を /admin/oauth/access_token に渡し、アクセストークンと交換する
  5. 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の公開アプリを運用し、撤去したことがあります。撤去は「アプリを削除すれば終わり」ではなく、順番がありました。

撤去の順番

  1. 証拠を残す(掲載ページや管理画面の状態を画像とHTMLで保存する)
  2. App Store の掲載を限定公開にする
  3. すべてのストアからアプリをアンインストールする
  4. アプリ本体を削除する
  5. サーバーやデータベースなどのインフラを止める

この順を守らないと、Shopifyから、GDPR関連のWebhookに応答が無いという警告が届きます。インフラは最後に止める、と覚えておけば迷いません。

アプリ本体の削除は取り消せない

アプリ本体を削除すると、そのアプリの Client ID は復元できません。再開したくなったら、アプリを作るところからやり直しです。削除は「一時停止」ではなく「作り直し前提の終わり方」だと理解したうえで実行します。

自動化してよい範囲と、人が持つ範囲

ここまでの作業のうち、機械に任せているのは次の範囲です。

  • ShopifyQLで売上・セッションの生データを毎回同じ形で取ること
  • 一時エラーの再試行と、失敗したときの理由の記録
  • ログに出るトークンを伏せ字にすること

人が持っているのは次の範囲です。

  • Client Secret の入力と保管
  • 手動ダウンロードとAPIの行が一致したかの確認と、切り替えの判断
  • アプリを撤去するかどうかの判断と、取り消せない削除の実行

取得を自動にしても、「この数字は正しいか」と「取り消せない操作をするか」は人の側に残ります。そこを最初に分けておくと、仕組みを長く保守しやすくなります。