ブログ · · 約 8 分で読めます

Shopify の Webhook の仕様:HMAC 検証・5 秒のタイムアウト・再送と購読の削除

Shopify の Webhook の HMAC 検証、1 秒と 5 秒のタイムアウト、4 時間で 8 回の再送と購読の削除、重複と順番を公式で確かめて表にし、HMAC が合わない 3 つの原因と Node の検証のコードを示します。

要点

  • Shopify は、応答が 5 秒以内に返らないか 2xx 以外のとき、4 時間で最大 8 回再送します。8 回続けて失敗すると、Admin API で作った購読は自動で削除されます。
  • 署名は X-Shopify-Hmac-Sha256 ヘッダーの base64 の HMAC-SHA256 で、鍵はアプリの client secret、対象は生の本文です。
  • HMAC が合わない原因の多くは、JSON に変換した後の本文で計算している、hex で比べている、鍵を取り違えている、の 3 つです。

Shopify の Webhook は、応答が 5 秒以内に返らないか 2xx 以外だった場合、4 時間で最大 8 回再送されます。8 回続けて失敗すると、Admin API で作った購読は自動で削除されます。署名は X-Shopify-Hmac-Sha256 ヘッダーに base64 で入った HMAC-SHA256 で、鍵はアプリの client secret、計算の対象は届いたままの本文です。この記事では、2026-09-27 に Shopify の公式のドキュメントで確かめた仕様を表にまとめ、HMAC が合わないときの原因と、受け口の組み方を示します。

仕様の一覧

項目 仕様 出典
署名のヘッダー X-Shopify-Hmac-Sha256(HTTPS の配信だけ) 配信の形
署名の方式 HMAC-SHA256、base64 HTTPS の配信
署名の鍵 アプリの client secret 同上
署名の対象 生の本文(express.json() などで変換する前) 同上
接続のタイムアウト 1 秒 同上
要求全体のタイムアウト 5 秒 同上
成功の判定 200 番台 トラブルシューティング
再送 失敗すると、その後 4 時間で 8 回 HTTPS の配信
購読の削除 8 回続けて失敗すると、Admin API で作った購読を自動で削除 同上
警告のメール アプリの緊急連絡用の開発者のメールアドレスに送る 同上
重複の見分け X-Shopify-Webhook-Id(配信ごとに一意) 重複の無視
順番 同じトピックの中でも、同じリソースの別のトピックの間でも保証しない Webhook の概要
届くことの保証 常に届くとは限らない。定期的な照合を勧める 同上

届くヘッダー

ヘッダー 中身
X-Shopify-Topic トピックの名前(例: products/update)
X-Shopify-Hmac-Sha256 base64 の HMAC の署名
X-Shopify-Shop-Domain イベントを起こしたストアの myshopify.com のドメイン
X-Shopify-API-Version 本文の形に使った API の版
X-Shopify-Webhook-Id 配信ごとに一意のキー。重複の除去に使う
X-Shopify-Triggered-At Shopify が配信を起こした時刻
X-Shopify-Event-Id 同じ操作から出たすべての配信に共通の ID
X-Shopify-Name(任意) 購読に付けた名前

Shopify は、HTTP/2 ではヘッダーの名前が小文字になることが多いので、コードでは大文字と小文字を区別せずに扱うよう書いています。

HMAC が合わない 3 つの原因

Shopify の開発者フォーラムには、HMAC の検証の失敗の相談が繰り返し出ています(NestJS で合わない例、技術的な支援を求める例)。原因はほとんどが次の 3 つです。

  1. JSON に変換した後の本文で計算している。Shopify の説明も「express.json() のような本文の変換のミドルウェアを使っていると、検証のコードより先に本文が変換される」と注意しています。
  2. hex で比べている。ヘッダーの値は base64 です。digest('hex') の値と比べても一致しません。
  3. 鍵を取り違えている。鍵は Webhook ごとの値ではなく、アプリの client secret です。開発用と本番用のアプリで client secret が違う点にも注意します。

Node での検証

Shopify のドキュメントの例と同じく、生の本文から base64 の HMAC を計算し、定数時間で比べます。

import crypto from 'node:crypto';
import express from 'express';

const app = express();
const CLIENT_SECRET = process.env.SHOPIFY_CLIENT_SECRET!;

// Webhook の経路だけ本文を Buffer のまま受け取る。express.json() より前に置く
app.post('/webhooks/shopify', express.raw({ type: 'application/json' }), async (req, res) => {
  const received = req.get('x-shopify-hmac-sha256') ?? '';
  const digest = crypto.createHmac('sha256', CLIENT_SECRET).update(req.body).digest('base64');

  const a = Buffer.from(digest, 'base64');
  const b = Buffer.from(received, 'base64');
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);

  // 重複の除去: 配信ごとに一意の X-Shopify-Webhook-Id を一意制約つきで記録
  const webhookId = req.get('x-shopify-webhook-id')!;
  const r = await db.query('INSERT INTO shopify_deliveries (id) VALUES ($1) ON CONFLICT DO NOTHING', [webhookId]);
  if (r.rowCount === 0) return res.sendStatus(200);

  await queue.add('shopify', {
    topic: req.get('x-shopify-topic'),
    shop: req.get('x-shopify-shop-domain'),
    triggeredAt: req.get('x-shopify-triggered-at'),
    body: req.body.toString('utf8'),
  });
  res.sendStatus(200); // 5 秒以内に返す
});

app.use(express.json());

NestJS では、NestFactory.create(AppModule, { rawBody: true }) で起動すると、RawBodyRequest の req.rawBody で変換前の本文(Buffer)を受け取れます(NestJS のドキュメント)。ほかのフレームワークでの取り方は生の本文の取り方の記事で扱います。

5 秒以内に返す受け口

5 秒のタイムアウトは、主要なサービスの中でも短い部類です。各社のタイムアウトの秒数の比較はタイムアウトの記事で扱います。Shopify のトラブルシューティングも「タイムアウトの失敗を直すには、応答を返した後まで処理を遅らせる」よう書いています。受け口では次のことだけを行います。

  1. HMAC を検証する。
  2. X-Shopify-Webhook-Id で重複を弾く。
  3. キューに入れる。
  4. 200 を返す。

商品の同期や外部の API の呼び出しは、キューの先のワーカーで行います。Shopify は接続を使い回すために Keep-Alive を有効にすることも勧めています。

再送と購読の削除への備え

8 回の再送は失敗から 4 時間で終わり、8 回続けて失敗した購読は消えます。受け口が 4 時間以上止まると、その間のイベントは届かず、購読も作り直しになります。

  • 購読が消えたら: 原因を直してから購読を作り直します。消えていた間のイベントは再送されないので、Admin API でデータを取り直して状態を合わせます。
  • 定期的な照合: Shopify は、Webhook が常に届くとは限らないとして、定期的に Shopify からデータを取り直す照合の仕事を勧めています。障害がなくても、日に 1 回などの照合を用意しておくと取りこぼしに気づけます。
  • 警告のメールの宛先: 失敗の警告はアプリの緊急連絡用の開発者のメールアドレスに届きます。個人のアドレスではなく、チームで読めるアドレスにしておきます。
  • 配信の状況: Dev Dashboard のアプリの Overview と Logs で、配信の状況を確かめられます。

順番の扱い

Shopify は順番を保証しないとしたうえで、ヘッダーの X-Shopify-Triggered-At か、本文の updated_at で並べるよう勧めています。同じリソースの更新が前後して届いたときは、保存している updated_at より古い更新を捨てる形にします。

UPDATE products
SET title = :title, updated_at = :updated_at
WHERE id = :id AND (updated_at IS NULL OR updated_at < :updated_at);

重複の扱いは重複と冪等性の記事で、署名の失敗の切り分けは署名検証が失敗する原因の記事で詳しく扱いました。

自社が送る側になるとき

Shopify のように自社のサービスから顧客やアプリへ Webhook を送る側になるなら、再送・署名・配信の記録・失敗の通知を用意する必要があります。再送の期間の決め方は再送の設計の記事で扱いました。

Webhook Admin は、これらを API から使えます。署名は Standard Webhooks の形式で、再送は既定では約 28 時間で 8 回(送信先ごとに回数と間隔を選べます)、タイムアウトは 15 秒で、送信ごとの応答のコードと応答の先頭 1,024 バイトを記録します。失敗が続くと Slack・Teams・Chatwork・メール・Webhook で知らせ、5 日続いた送信先は自動で止めます(止めた送信先は API で再開できます)。

自社の送信の仕組みを作る前に試したい場合は、Webhook Admin を無料で登録できます(月 5 万通まで)。https://app.webhookadmin.com/signup

よくある質問

HMAC を hex で比べると失敗するのはなぜですか。

Shopify は X-Shopify-Hmac-Sha256 に HMAC-SHA256 の値を base64 で入れて送るからです。計算した値を digest('hex') で文字列にすると、同じバイト列でも表記が違うので一致しません。digest('base64') で計算するか、ヘッダーの値を base64 で戻したバイト列と比べます。

購読が消えてしまったら、どうすれば戻せますか。

受け口の問題を直してから、購読を作り直します。Shopify のトラブルシューティングも、原因を直してから Webhook の購読を作り直すよう書いています。消えていた間のイベントは届かないので、Admin API でデータを取り直して状態を合わせます。

重複はどう見分けますか。

X-Shopify-Webhook-Id ヘッダーを使います。Shopify は、この値を永続的な保存先で確かめ、既にあれば処理を飛ばして成功を返し、無ければ処理して保存するよう書いています。X-Shopify-Event-Id は同じ操作から出た複数の配信に共通の値なので、重複の判定には使いません。

関連記事