Skip to main content
When a payment fails, Dodo Payments tells you why through a standardized error_code and a human-readable error_message. This guide shows how to read those fields, decide whether a retry is worthwhile, and recover the payment without exposing sensitive information to customers.

How Dodo Payments Reports a Failure

Every failed payment — whether a one-time checkout or a subscription renewal — carries the same failure fields on the payment object:
FieldTypeDescription
statusstringfailed for a failed payment. Other non-success states include cancelled, requires_customer_action, and requires_payment_method.
error_codestring | nullThe standardized failure reason, for example INSUFFICIENT_FUNDS or PROCESSING_ERROR. See the Transaction Failures reference for the full list.
error_messagestring | nullA human-readable explanation of the failure.
retry_attemptinteger0 for the original charge. 1 or higher identifies a scheduled subscription renewal retry.
error_code and error_message are null until a payment actually fails. Always check status first, then read the error fields.

The payment.failed Webhook

The most reliable way to detect a failure is the payment.failed webhook. The event wraps the full payment object in data:
payment.failed payload
{
  "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"
    }
  }
}
A minimal handler reads error_code and routes on it:
import { Webhook } from "standardwebhooks";
import express from "express";

const app = express();
// Mount the raw body parser so the exact payload is available for verification
app.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 });
});
Always verify the webhook signature before processing. See the Webhooks guide for the full setup, including signature verification and idempotency.

Decide Whether to Retry: Soft vs. Hard Declines

The error_code tells you whether retrying the same payment method is worthwhile.
Decline typeWhat it meansWhat to do
Soft declineTemporary or correctable (for example INSUFFICIENT_FUNDS, PROCESSING_ERROR, NETWORK_ERROR, TRY_AGAIN_LATER).Retrying — after a delay, or once the customer fixes their input — can succeed.
Hard declineTerminal (for example STOLEN_CARD, LOST_CARD, DO_NOT_HONOR, FRAUDULENT).Do not retry the same card. Ask the customer for a different payment method.
The Transaction Failures reference lists the decline type and recommended action for every error_code.

Handling Failures at Checkout vs. on Renewal

How you recover depends on whether the customer is present.
The customer is actively checking out. Surface a clear message and let them retry immediately or use another card.
  • requires_payment_method — the customer never provided a payment method: they didn’t enter card details, or were prompted for one and took no action. This is usually a checkout drop-off, not a decline — re-engage the customer to complete payment (see Abandoned Cart Recovery).
  • requires_customer_action — additional authentication (such as 3DS) is needed; have the customer complete it. See 3D Secure handling.

Retrying a Failed Payment

  • Subscriptions: Enable Subscription Payment Retries to recover soft declines with no integration work. You can also trigger recovery by having the customer update their payment method via the Update Payment Method API, which charges any outstanding dues.
  • One-time payments: Resend the checkout or payment_link so the customer can try again with a different method. There is no automatic retry for one-time payments.
Do not retry hard declines against the same card. Card networks can flag repeated declines as abusive, which hurts your authorization rate.

Surface Errors to Customers Safely

Show customers a friendly message — never the raw error_code.
Customer-facing messaging
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.";
}
Never reveal the real reason for STOLEN_CARD, LOST_CARD, PICKUP_CARD, or FRAUDULENT. Surfacing these can tip off a fraudulent actor. Show a generic decline message and only log the specific error_code internally.

Transaction Failures

Every decline code, its type, and the recommended action.

Error Codes

API and business-logic errors that are not card declines.

Subscription Payment Retries

Automatic recovery of soft declines on subscription renewals.

Subscription Dunning

Email sequences that recover hard declines.

Payment Webhooks

Full payload schema for payment events.

Testing Failures

Test cards that simulate declines and renewal failures.
Last modified on June 17, 2026