Resend Webhook を Next.js でローカルにテストするには、生の本文を読み取る App Router POST ルートを作成し、その Svix ヘッダーを Resend 署名シークレットで検証し、Resend ダッシュボードに HTTPS トンネル URL を登録します。 Resend を通じて電子メールを送信し、実際の処理を行います email.sent、 email.delivered、 email.bounced、または email.complained ローカルホスト上のイベント。
Resend Webhook がアプリケーションに伝えること
電子メールが受け入れられたという API 応答は、電子メールが受信者に届いたことを証明するものではありません。配信は非同期で行われます。 Resend Webhook を使用すると、アプリケーションは元の送信リクエストの終了後にメッセージの状態を更新し、不正なアドレスを抑制し、バウンスを報告し、苦情に対応できます。の 公式 Resend Webhook ドキュメント に、イベント タイプとダッシュボードの設定を示します。
ローカル Webhook テストは、POST がルートに到達するかどうかだけでなく、ステート マシン全体をカバーする必要があります。各イベントの電子メール ID を、送信時に作成されたレコードと関連付けます。状態を遷移として扱います: 受け入れ、送信、配信、遅延、バウンス、苦情、オープン、またはクリック (該当する場合)。後の複製によって、より有用な状態が上書きされたり、同じアラートが 2 回トリガーされたりしてはなりません。
Next.js App Router ルートを作成する
署名形式用に保守されているベリファイアをインストールします。
npm install svix
次に、ノードランタイムルートを作成します。 Resend は元の本文に署名するため、次を使用します request.text() 解析する前に 1 回だけ。
// app/api/webhooks/resend/route.ts
import { Webhook } from 'svix';
export const runtime = 'nodejs';
export async function POST(request: Request) {
const payload = await request.text();
const headers = {
'svix-id': request.headers.get('svix-id') ?? '',
'svix-timestamp': request.headers.get('svix-timestamp') ?? '',
'svix-signature': request.headers.get('svix-signature') ?? '',
};
let event: ResendEvent;
try {
const webhook = new Webhook(process.env.RESEND_WEBHOOK_SECRET!);
event = webhook.verify(payload, headers) as ResendEvent;
} catch {
return Response.json({ error: 'Invalid webhook signature' }, { status: 400 });
}
await enqueueResendEvent({
deliveryId: headers['svix-id'],
event,
});
return Response.json({ received: true });
}
署名シークレットはこの Webhook エンドポイントに属し、通常はプロバイダー固有のプレフィックスで始まります。 Resend Webhook 設定から、次のような無視されるローカル環境ファイルにコピーします。 .env.local。これは、電子メールの送信に使用される Resend API キーではありません。
3 つの Svix ヘッダーが重要な理由
svix-id配信を一意に識別し、最良の冪等性キーです。svix-timestamp署名を時刻にバインドし、検証者が許容範囲外の古いリクエストを拒否できるようにします。svix-signature本文の認証に使用される 1 つ以上のバージョン付き署名を含めることができます。
やむを得ない理由がない限り、ヘッダー文字列を分割してこのプロトコルを実装しないでください。 SDK は、エンコード、複数の署名、タイムスタンプ チェックを処理します。 Resend は、署名シークレットとこれらのヘッダーを検証に使用することを明示的に推奨します。より深い 署名検証ガイド raw バイトとタイミングセーフ チェックが重要な理由を説明します。
Next.js を公開し、エンドポイントを登録する
- 走る
npm run devアプリケーションがポート 3000 でリッスンしていることを確認します。 - スタート
npx portpreview 3000別の端末で。 - Resend で、エンドポイントが次のような Webhook を作成します。
https://YOUR-TUNNEL.portpreview.dev/api/webhooks/resend。 - アプリケーションが処理する電子メール イベントのみを選択します。
- エンドポイントの署名シークレットをコピーします
RESEND_WEBHOOK_SECRETそして、Next.js を再起動して、変数をロードします。 - 検証済みのドメインを使用してメッセージを送信し、ローカル ルートに到達するイベントを検査します。
パブリック URL をセッションに対して安定した状態に保ちます。トンネル起点が変更された場合は、再度テストする前に Resend エンドポイントを編集します。古い URL で構成されたエンドポイントは、たとえローカルホスト自体が正常であっても、新しいプロセスに到達できません。
型指定されたイベント ディスパッチャを使用する
Webhook ペイロードは 1 つの狭いディスパッチャに入力する必要があります。必須フィールドを検証し、認識されないイベント タイプをサーバー障害として扱うことなく監視可能にします。
async function processEvent(event: ResendEvent) {
switch (event.type) {
case 'email.delivered':
await markDelivered(event.data.email_id, event.created_at);
break;
case 'email.bounced':
await markBounced(event.data.email_id, event.data.bounce?.message);
await suppressIfPermanent(event.data);
break;
case 'email.complained':
await suppressRecipients(event.data.to);
await alertCompliance(event.data.email_id);
break;
default:
await recordUnhandledEvent(event);
}
}
すべてのイベントに同一のデータがあると想定するのではなく、ペイロード タイプを現在の Resend スキーマに合わせて維持します。たとえば、バウンスの詳細と受信者リストは、特定のイベントにのみ関連する場合があります。イベント タイプ、プロバイダーの電子メール ID、イベント タイムスタンプ、およびサポート調査のために編集された最小限のペイロードを保存します。
再試行をテストする前に処理を冪等にする
Webhook システムは、実際には少なくとも 1 回の配信動作を提供します。タイムアウトは、データベースのコミット後、プロバイダーが 200 応答を受信する前に発生する可能性があります。その後、プロバイダーは、すでに適用されているリクエストを再試行します。使用する svix-id を一意の配信キーとして使用し、状態変更と同じトランザクションに挿入します。
await db.transaction(async (tx) => {
const inserted = await tx.webhookDelivery.insertOnce({
provider: 'resend',
deliveryId,
});
if (!inserted) return;
await applyEmailEvent(tx, event);
});
1 つの電子メールが複数のイベント タイプを正当に受信するため、電子メール ID のみによって重複を排除しないでください。データ モデルに応じて、配信レベルの一意のキーと状態遷移ルールの両方を保持します。読む Webhook の再試行とべき等性パターン イベントを請求、抑制、または顧客通知に結び付ける前に。
イベントを失わずにすぐに戻る
署名検証はリクエスト パス内で適切です。遅いビジネスの仕事はそうではありません。検証されたイベントを永続化またはエンキューし、2xx を返します。永続書き込みの前に戻った場合、プロセスのクラッシュによりイベントが失われる可能性があります。いくつかのリモート API を待機すると、エンドポイントがタイムアウトになり、再試行が求められる場合があります。データベース受信トレイ テーブルは、多くの場合、最も単純なローカルおよび運用設計です。
有用なテスト イベントを生成する
送信および配達されました
検証済みのドメインから自分が管理するアドレスに送信します。送信 API によって返された電子メール ID を記録し、受信イベントが同じ行を更新することを確認します。配信のタイミングは受信側サーバーによって異なるため、イベントがすぐに到着したり、単純な順序で到着したりすると想定しないでください。
バウンス
無関係なドメインへのトラフィックを作り出すのではなく、Resend の文書化されたテスト アドレスまたはテスト機能を使用してください。一時的な条件が再試行ポリシーに従っている一方で、永続的なエラーによって今後のメールが抑制されることを確認します。遅延イベントごとに自動的に抑制しないでください。
苦情
苦情処理は、配信可能性とコンプライアンスの両方のロジックです。繰り返しの Webhook によって繰り返しアラートが作成されないことを確認し、ポリシーに従って影響を受ける受信者が後のキャンペーンから除外されていることを確認してください。
Resend Webhook エラーのトラブルシューティング
署名検証は常に失敗します
API キーではなく、エンドポイントの署名シークレットが読み込まれていることを確認します。使用する await request.text()、JSON を解析して再文字列化せず、3 つの Svix ヘッダーをすべて正確な値で渡します。変更後に開発サーバーを再起動します .env.local。
ルートは 404 または 405 を返します
App Router ルート ファイルには名前を付ける必要があります route.ts 目的の URL セグメントの下に移動してエクスポートします POST。ミドルウェアがトンネル要求をロケールまたはログイン ページに書き換えているかどうかを確認します。パブリック URL をcurlでテストし、実際の応答を検査します。
Resend は処理が成功したにもかかわらず再試行を示す
成功したすべてのブランチがすぐに 2xx を返すことを確認します。データベースの更新後にエラーがスローされると、500 と重複した再試行が発生する可能性があります。処理をトランザクション化して冪等にし、応答レイテンシーを検査します。
イベントは到着しますが、メールにリンクできません
元の Resend 送信応答からのプロバイダーの電子メール ID を保持します。件名行や受信者のアドレスを識別子として依存しないでください。これらのフィールドは一意ではなく、相関するには十分安定していません。
再生されたキャプチャがタイムスタンプ検証に失敗する
これは、通常のベリファイアを介して古い署名付きリクエストを再実行するときに予想されることです。そのタイムスタンプは、許可された許容範囲を超えている可能性があります。可能な場合はプロバイダーの再配信を優先します。分離されたビジネス ロジック テストの場合は、一度検証し、サニタイズされたイベント フィクスチャを保存し、ディスパッチャを個別にテストします。の リプレイガイド この境界を説明します。
電子メール イベント テストのセキュリティとプライバシー
- 決して暴露しないでください
RESEND_API_KEYまたは、ソース、ブラウザー バンドル、スクリーンショット、またはリクエスト ログ内のエンドポイント署名シークレット。 - イベントを解析または永続化する前に確認してください。
- 共有トンネル キャプチャからの受信者、件名、ヘッダー、メッセージ メタデータを秘匿化します。
- 未加工の Webhook ペイロードに保持制限を適用します。サポートとコンプライアンスが必要なもののみを保存します。
- 運用環境とは別のローカル エンドポイント シークレットを使用し、テスト エンドポイントが削除されたときにそれをローテーションします。
最終的な設計は、パブリック HTTPS エンドポイント、生の本体の検証、永続的な冪等性、高速な確認応答、および非同期状態の処理など、展開後も同様に機能する必要があります。 App Router 固有の raw ボディの詳細については、 Next.js Webhook ローカルホスト ガイド。
