ブログ · · 約 8 分で
Shopify の Webhook の仕様:HMAC 検証・5 秒のタイムアウト・再送と購読の削除
Shopify の
要点
- Shopify は、
応答が 5 秒以内に 返らないか 2xx 以外の とき、 4 時間で 最大 8 回再送します。 8 回続けて 失敗すると、 Admin API で 作った 購読は 自動で 削除されます。 - 署名は
X-Shopify-Hmac-Sha256 ヘッダーの base64 の HMAC-SHA256 で、 鍵は アプリの client secret、 対象は 生の 本文です。 - HMAC が
合わない 原因の 多くは、 JSON に 変換した 後の 本文で 計算している、 hex で 比べている、 鍵を 取り違えている、 の 3 つです。
Shopify のX-Shopify-Hmac-Sha256 ヘッダーに
仕様の一覧
| 項目 | 仕様 | 出典 |
|---|---|---|
| 署名の |
X-Shopify-Hmac-Sha256 |
配信の |
| 署名の |
HMAC-SHA256、 |
HTTPS の |
| 署名の |
アプリの |
同上 |
| 署名の |
生のexpress.json() などで |
同上 |
| 接続の |
1 秒 | 同上 |
| 要求全体の |
5 秒 | 同上 |
| 成功の |
200 番台 | トラブルシューティング |
| 再送 | 失敗すると、 |
HTTPS の |
| 購読の |
8 回続けて |
同上 |
| 警告の |
アプリの |
同上 |
| 重複の |
X-Shopify-Webhook-Id |
重複の |
| 順番 | 同じ |
Webhook の |
| 届く |
常に |
同上 |
届くヘッダー
| ヘッダー | 中身 |
|---|---|
X-Shopify-Topic |
トピックのproducts/update) |
X-Shopify-Hmac-Sha256 |
base64 の |
X-Shopify-Shop-Domain |
イベントをmyshopify.com の |
X-Shopify-API-Version |
本文の |
X-Shopify-Webhook-Id |
配信ごとに |
X-Shopify-Triggered-At |
Shopify が |
X-Shopify-Event-Id |
同じ |
X-Shopify-Name |
購読に |
Shopify は、
HMAC が合わない 3 つの原因
Shopify の
- JSON に
変換した 。後の 本文で 計算している Shopify の 説明も 「 express.json()のような本文の 変換の ミドルウェアを 使っていると、 検証の コードより 先に 本文が 変換される」 と 注意しています。 - hex で
比べている 。ヘッダーの 値は base64 です。 digest('hex')の値と 比べても 一致しません。 - 鍵を
取り違えている 。鍵は Webhook ごとの 値ではなく、 アプリの client secret です。 開発用と 本番用の アプリで client secret が 違う 点にも 注意します。
Node での検証
Shopify の
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 で
5 秒以内に返す受け口
5 秒の
- HMAC を
検証する。 X-Shopify-Webhook-Idで重複を 弾く。 - キューに
入れる。 - 200 を
返す。
商品の
再送と購読の削除への備え
8 回の
- 購読が
消えたら : 原因を直してから 購読を 作り直します。 消えていた間の イベントは 再送されないので、 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 Admin は、
自社の
よくある質問
HMAC を hex で比べると失敗するのはなぜですか。
Shopify は
購読が消えてしまったら、どうすれば戻せますか。
受け口の
重複はどう見分けますか。
X-Shopify-Webhook-Id ヘッダーを