ローカル開発での Shopify Webhook バグのほとんどは 1 行に帰着します:HMAC は base64 エンコードであって hex ではありません。.digest('hex') を呼んで X-Shopify-Hmac-Sha256 と比較すると、正しい共有シークレットでもチェックは決して通りません。この 1 文字でシニアエンジニアが午後を失うのを見てきました。
base64 の罠
Stripe は hex。GitHub は hex(sha256= プレフィックス付き)。Shopify は base64。3 つのプロバイダー、3 つのエンコーディング。検証器の最初のバージョンは、ほぼ確実に別プロジェクトのスニペットをコピーして静かに失敗します。
検証はこうあるべきです:
const hmac = crypto
.createHmac('sha256', SHOPIFY_API_SECRET)
.update(rawBody) // raw body, not JSON-parsed
.digest('base64'); // base64, not hex
const valid = crypto.timingSafeEqual(
Buffer.from(hmac),
Buffer.from(req.headers['x-shopify-hmac-sha256']),
);
エンコーディング以外に 2 つ重要です。ボディは生の、パースされていないバイトでなければならない。そして比較はタイミングセーフでなければならない——文字列の等価は 1 バイトずつ情報を漏らします。
なぜトンネルが楽にするか
Shopify の shopify app dev コマンドも使えます。内部トンネルを立ち上げます。動きます。でも dev サーバーを独自のプロセスで包み、ログを特定の方法で飲み込み、リプレイボタンを与えません。「hello world」を超えるアプリ開発では、安定した公開 URL とリクエストキャプチャ付きの localhost トンネルが、CLI が初期セットアップで節約する以上の時間を節約します。
npx portpreview 3000
HTTPS URL を Partners ダッシュボードのアプリの Webhook 設定に貼り、テストストアからイベントをトリガーすると、リクエストはすべてのヘッダーがそのままローカルハンドラーに着地します。
テストストア:Shopify ドキュメントがざっと流す部分
何かを配線する前に知っておくこと 2 つ:
- 開発ストアはすべての Webhook イベントを発火します。注文作成、フルフィルメント、在庫——全部。何も偽装する必要はない。dev ストアにアプリをインストールしてクリックするだけ。
- Webhook シークレットはアプリごと、配信チャネルごとに異なります。EventBridge や Pub/Sub も購読している場合、HMAC の挙動は違います。ここでは素の HTTPS Webhook 配信の話です。
ステップごとのセットアップ
- 使うポート(3000 が一般的)でアプリをローカル起動。
npx portpreview 3000を実行し、出力された HTTPS URL をコピー。- Shopify Partners ダッシュボードでアプリを開き、Configuration → Webhooks へ。
- Webhook エンドポイントを
https://your-tunnel.portpreview.dev/api/webhooks/shopify(またはアプリが使うパス)に設定。 - dev ストアにアプリをインストールし、イベントをトリガー——下書き注文の作成、商品のフルフィル、在庫変更。
- リクエストの着地を見る。ヘッダーとボディを検査。ハンドラー修正のたびにキャプチャしたリクエストをリプレイ。
繰り返し見るミス
検証前にボディがパースされている
app.use(express.json()) を Webhook ルートの前に置くと、検証しようとする頃には生バイトが消えています。raw-body パーサーを Webhook パスだけにマウントするか、JSON パースの前にリクエストストリームから手動でボディを取り出します。
シークレットの取り違え
Partners アプリシークレットは Storefront API トークンと同じではありません。Webhook HMAC はアプリシークレットを使います。env ファイルの shp_xxx を見ているなら、間違ったものを取っています。
同一に見えるテストイベント
Shopify の「Send test notification」ボタンは、プレースホルダーボディの合成イベントを配信します。そのテストイベントの署名は本物ですが、payload は固定です。現実的な payload 形状テストには、dev ストア自体からイベントをトリガーします。
アプリ開発でリプレイは譲れない
Shopify は失敗した Webhook を指数バックオフで最大 48 時間リトライします。寛大ですが、反復中は次のリトライを待ちたくありません。最初の配信を トンネルのリクエスト履歴でキャプチャし、ローカルハンドラーに対してオンデマンドでリプレイします。
ローカルで足りなくなるとき
ショップ登録フロー、GDPR Webhook、サブスクリプション課金の変更は、Shopify 自身のアカウント状態と連携するため、デプロイ済み環境に対して検証する方が楽です。それ以外——payload パース、署名検証、ビジネスロジック——はローカルが速い。
全プロバイダーの基礎となる署名の数学は、Webhook 署名検証ガイドを読んでください。リプレイ内蔵のトンネルが欲しければ PortPreview のウェイトリストへ。