TRADOM PaymentDocs

支払い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.succeeded
  • payment_intent.canceled
  • payment_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はありません。

このページの内容