支払いeventを受け取る
Webhookの登録、署名検証、冪等処理、event参照
TRADOM Paymentは、PaymentIntentの状態変化を登録済みURLへ配信します。Webhookはat-least-onceで届くため、イベントIDで冪等に処理してください。
受信先を登録
Webhook受信先の登録にはsecret keyを使います。
const endpoint = await paymentIntentClient.webhookEndpoints.create({
url: "https://shop.example/webhooks/tradom-payment",
});
if (!endpoint.secret) {
throw new Error("Webhook secret was not returned");
}
await saveWebhookSecret(endpoint.secret);whsec_… は作成時に一度だけ返ります。安全なsecret storeへ即時保存してください。list() のレスポンスにはsecretが含まれません。
受信URLは外部から到達可能なHTTP(S) URLである必要があります。private IP、loopbackなどはSSRF対策で拒否されます。
署名を検証
署名対象は受信したraw bodyです。JSONとしてparseし直した文字列では検証できません。
import {
constructEvent,
PaymentError,
} from "@tradom/payment";
const TRADOM_PAYMENT_WEBHOOK_SECRET =
process.env.TRADOM_PAYMENT_WEBHOOK_SECRET;
if (!TRADOM_PAYMENT_WEBHOOK_SECRET) {
throw new Error("TRADOM_PAYMENT_WEBHOOK_SECRET is required");
}
export async function POST(request: Request) {
try {
const event = constructEvent({
payload: await request.text(),
signature: request.headers.get("Tradom-Signature") ?? "",
secret: TRADOM_PAYMENT_WEBHOOK_SECRET,
});
if (await isProcessed(event.id)) {
return new Response("ok");
}
if (event.type === "payment_intent.succeeded") {
await fulfilOrder(event.data.object.metadata);
}
await markProcessed(event.id);
return new Response("ok");
} catch (error) {
if (error instanceof PaymentError) {
return new Response("invalid webhook", { status: 400 });
}
throw error;
}
}署名形式:
Tradom-Signature: t=<unix>,v1=<hex hmac-sha256(secret, "<t>.<raw-body>")>
Tradom-Event-Id: evt_…SDKの既定timestamp許容差は300秒です。比較はconstant-timeで行われます。
イベント種別
現行本番が配信するイベントは次の3種類です。
payment_intent.succeededpayment_intent.canceledpayment_intent.refunded
payment_intent.created、refund.created、refund.updated は配信されません。
再送
配信はイベント発生時に1回同期試行され、失敗すると10分ごとのcronで最長72時間再送されます。試行回数は初回を含めて最大5回(初回+再送4回)で、backoffはありません。複数endpointの一部が失敗した場合、成功済みendpointに同じイベントが再配信される可能性もあります。
各試行のtimeoutは5秒で、2xxのみ成功、3xx redirectは追従されず失敗扱いです。必ず event.id または Tradom-Event-Id で重複排除し、受信したらすぐに2xxを返して重い処理は非同期化してください(5秒を超えると再送され、重複配信になります)。
署名secretのrotate・endpointの無効化・削除を行うAPIは現在ありません。secretが漏えいした場合は新しいendpointを登録し、旧URLを自社側で無効化してください。
イベントAPIで追いつく
Webhookを取り逃した場合はEvents APIを利用できます。
const events = await paymentIntentClient.events.list();
const event = await paymentIntentClient.events.retrieve("evt_...");list() は新しい順で最大100件です。現行APIにページネーションparameterはありません。