{ "business_id": "bus_P3SXLcppjXgagmHS", "type": "payment.failed", "timestamp": "2025-08-04T05:36:41.609359Z", "data": { "payload_type": "Payment", "payment_id": "pay_2IjeQm4hqU6RA4Z4kwDee", "status": "failed", "error_code": "PROCESSING_ERROR", "error_message": "An error occurred while processing your card. Try again in a little bit.", "retry_attempt": 0, "subscription_id": null, "currency": "USD", "total_amount": 400, "payment_method": "card", "card_last_four": "0119", "card_network": "VISA", "payment_link": "https://test.checkout.dodopayments.com/cbq", "customer": { "customer_id": "cus_8VbC6JDZzPEqfB", "email": "test@acme.com", "name": "Test user" } }}
ミニマルハンドラーはerror_codeを読み取り、それにルートします。
import { Webhook } from "standardwebhooks";import express from "express";const app = express();// Mount the raw body parser so the exact payload is available for verificationapp.use(express.raw({ type: "application/json" }));const webhook = new Webhook(process.env.DODO_PAYMENTS_WEBHOOK_KEY);app.post("/webhooks/dodo", async (req, res) => { // Verify the signature against the raw body before trusting the payload const payload = req.body.toString(); await webhook.verify(payload, req.headers); const event = JSON.parse(payload); if (event.type === "payment.failed") { const payment = event.data; console.log( `Payment ${payment.payment_id} failed: ${payment.error_code} (${payment.error_message})` ); if (payment.subscription_id) { // Subscription renewal — Dodo retries soft declines for you await flagSubscriptionPaymentIssue(payment.subscription_id, payment.error_code); } else { // One-time payment — prompt the customer to try again await notifyCustomerOfFailedPayment(payment.customer.customer_id, payment.error_code); } } res.json({ received: true });});
import osfrom fastapi import FastAPI, Requestfrom standardwebhooks import Webhookapp = FastAPI()webhook = Webhook(os.environ["DODO_PAYMENTS_WEBHOOK_KEY"])@app.post("/webhooks/dodo")async def handle_webhook(request: Request): # Verify the signature before trusting the payload payload = await request.body() webhook.verify(payload, dict(request.headers)) event = await request.json() if event["type"] == "payment.failed": payment = event["data"] print( f"Payment {payment['payment_id']} failed: " f"{payment['error_code']} ({payment['error_message']})" ) if payment["subscription_id"]: # Subscription renewal — Dodo retries soft declines for you flag_subscription_payment_issue(payment["subscription_id"], payment["error_code"]) else: # One-time payment — prompt the customer to try again notify_customer_of_failed_payment(payment["customer"]["customer_id"], payment["error_code"]) return {"received": True}
const CUSTOMER_MESSAGES = { INSUFFICIENT_FUNDS: "Your card has insufficient funds. Please use another card.", EXPIRED_CARD: "Your card has expired. Please use a card with a valid expiry date.", INCORRECT_CVC: "The security code (CVC) is incorrect. Please re-enter it.",};function customerMessage(errorCode) { // Sensitive declines must never reveal the real reason const SENSITIVE = ["STOLEN_CARD", "LOST_CARD", "PICKUP_CARD", "FRAUDULENT"]; if (SENSITIVE.includes(errorCode)) { return "Your card was declined. Please contact your bank or use another card."; } return CUSTOMER_MESSAGES[errorCode] ?? "Your payment could not be processed. Please try another card.";}