再試行と重複に対応した、サーバー側の決済フローを作れます。
1. ID とアクセス権を準備
有効なストアとテスト済みの決済方法を用意します。次の 設定 → API アクセスで、必要なプロジェクトだけに限定した読み書き可能な認証情報を作成します。トークンはバックエンドに保管し、ブラウザーコードや公開リポジトリに置かないでください。
次の ID をコピーします: プロジェクト API ID と ストア API ID 。ストアの 基本 → API ID 欄にあります。これらは UUID で、読みやすいプロジェクト識別子や注文番号とは違います。自分の API ホスト名を使ってください。
確認先: ストア → IPNで、次を指定する前に署名シークレットを作成します: ipn_url。HTTPS の受信エンドポイントは加盟店 VPS から到達できる必要があります。
2. 請求書を作成
プレースホルダーを置き換え、バックエンドからこのリクエストを送ります。金額は小数文字列を使い、浮動小数点の計算は避けてください。
curl --fail-with-body --request POST \
'https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/invoices' \
--header 'Authorization: Bearer YOUR_MERCHANT_API_TOKEN' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: order-1042-attempt-1' \
--data '{
"amount": "10.00",
"currency": "EUR",
"order_id": "order-1042",
"description": "Example order",
"ipn_url": "https://shop.example.com/payments/wholly-ipn",
"metadata": {
"cart_id": "cart-1042"
}
}'次を保存します: data.invoice_id 。注文と関連付け、顧客を次の URL にリダイレクトします: links.checkout。新しい決済試行には一意の冪等性キーを使います。タイムアウト時の再試行は、 同じ認証情報・キー・完全に同じ本文バイト列.
を使ってください。次を省略すると、 payment_methods ストアの受付方法を使います。チェーンのスラッグとティッカーで請求書ごとに絞れますが、未承認の資産を有効にすることはありません。 全リクエスト項目とレスポンス例 →
3. IPN・Webhook・両方から選ぶ
IPN は請求書のライフサイクルに従います。ストアのデフォルト IPN URL を設定するか、次の値で上書きします: ipn_url 。請求書ごとに指定できます。 Webhook は選択したイベントをエンドポイントに通知します。たとえば invoice.settled.
署名形式は同じですが、 シークレットは別です。IPN はストアの IPN シークレット、Webhook は各エンドポイント固有のシークレットを使います。どちらも API Bearer トークンでは署名しません。
両方を同じアプリに送る場合、内容が重なる通知を想定してください。注文に二重で入金処理しないようにします。
4. 通知を検証して保存
- JSON 解析前に正確な生のリクエスト本文を読み、次を検証します:
Wholly-Signature。対応するシークレットと時刻・リプレイ確認を使います。公式 SDK には検証機能があります。 - 署名済み本文のプロジェクト、ストア、請求書、イベント識別情報を検証します。未署名の配信ヘッダーは認証の根拠ではありません。
- 一意の次のキーでイベントを永続保存します:
event_id。その後すばやく HTTP 2xx を返し、注文はバックグラウンドワーカーで処理します。 - リクエスト内の任意のホストではなく、設定済み API ホストから現在の請求書を取得します。保存したプロジェクト、ストア、金額、通貨、注文参照と比較してください。
5. 決済完了時に一度だけ処理
次の方式なら、 イベントベース の受信処理で、 event_type = invoice.settledを処理し、現在の status = settled と例外ポリシーを確認します。データベーストランザクションと注文の一意制約で、一度だけ履行してください。
確定の速いチェーンでは、両方のイベントが送られる場合があります: payment.received と invoice.settled と status = settled。他のチェーンでは payment.received の時点で processing の場合があります。どちらもエラーではありません。
イベントの重複判定には次を使います: event_id。シーケンスだけで判定しないでください。別のイベント種別が同じシーケンスを持つ場合があります。状態ベースの SDK ハンドラーでは、代わりに請求書リビジョンをまとめ、イベント種別に関係なく状態を確認します。 そのリビジョン集約とイベント種別フィルターを混ぜないでください。 どちらの方式でも、注文レベルの重複防止が必要です。
次の項目を確認します: requires_review 。手動解決の内容も、履行前に確認してください。 amount_status = paid だけでは承認を証明しません。最上位の支払資産フィールドは決済完了の概要を示し、 payment_info に詳細な入金と見積もりデータが入ります。 全ステータス・イベント・例外ルール →
6. 再送と復旧をテスト
少額決済、重複配信、期限切れ請求書、受信先の一時停止をテストします。再配信で注文に2回クレジットを付けてはいけません。順不同のイベントで新しい状態を上書きしないようにしてください。
次の項目を確認します: ストア → IPN / Webhook → 履歴 → 詳細または請求書詳細の配信欄で確認できます。再送は記録済みイベントの再利用で、新しい決済完了ではありません。
処理クレジット不足では入金は続きますが、IPN/Webhook は停止します。未処理注文を API で照合し、回復後に保持された配信を処理してください。ブラウザーのリダイレクトや顧客のスクリーンショットだけで履行してはいけません。