> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dodopayments.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Xử Lý Thanh Toán Thất Bại

> Phát hiện thanh toán thất bại từ webhooks và API, đọc lý do thất bại, hiển thị an toàn cho khách hàng và quyết định khi nào thử lại hoặc thu thập phương thức thanh toán mới.

<Info>
  Khi một khoản thanh toán thất bại, Dodo Payments sẽ cho bạn biết **tại sao** thông qua một `error_code` đã chuẩn hóa và một `error_message` có thể đọc được. Hướng dẫn này cho thấy cách đọc các trường đó, quyết định xem có đáng thử lại không, và khôi phục thanh toán mà không tiết lộ thông tin nhạy cảm cho khách hàng.
</Info>

## Cách Dodo Payments Báo Cáo Một Thất Bại

Mỗi thanh toán thất bại — dù là thanh toán một lần hay gia hạn đăng ký — đều mang cùng các trường thất bại trên đối tượng thanh toán:

| Field           | Type           | Description                                                                                                                                                                                               |
| --------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`        | string         | `failed` cho một payment thất bại. Các trạng thái không thành công khác bao gồm `cancelled`, `requires_customer_action` và `requires_payment_method`.                                                     |
| `error_code`    | string \| null | Lý do thất bại được chuẩn hóa, chẳng hạn như `INSUFFICIENT_FUNDS` hoặc `PROCESSING_ERROR`. Xem tài liệu tham khảo [Transaction Failures](/api-reference/transaction-failures) để biết danh sách đầy đủ.   |
| `error_message` | string \| null | Nội dung giải thích dễ hiểu về lỗi, được viết cho bạn thay vì cho customer. Đối với `error_code` được chuẩn hóa, nội dung này là tiêu đề kèm hành động được đề xuất, không phải văn bản thô từ processor. |
| `retry_attempt` | integer        | `0` cho charge ban đầu. `1` hoặc cao hơn xác định một lần retry theo lịch cho renewal của subscription.                                                                                                   |

<Note>
  `error_code` và `error_message` là `null` cho đến khi khoản thanh toán thực sự thất bại. Luôn kiểm tra `status` trước, sau đó đọc các trường lỗi.
</Note>

<Warning>
  `error_message` từ merchant API là **nội dung dành cho merchant**. Nội dung này có thể nêu lý do thực sự khiến payment bị từ chối, bao gồm cả các lý do liên quan đến fraud, vì vậy không bao giờ được hiển thị trực tiếp cho customer. Thay vào đó, hãy ánh xạ `error_code` sang nội dung an toàn cho customer của riêng bạn, như minh họa trong [Hiển thị lỗi an toàn cho customer](#surface-errors-to-customers-safely).
</Warning>

## Webhook `payment.failed`

Cách đáng tin cậy nhất để phát hiện lỗi là webhook `payment.failed`. Event bao bọc toàn bộ payment object trong `data`:

```json payment.failed payload expandable theme={null}
{
  "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"
    }
  }
}
```

Một handler tối thiểu sẽ đọc `error_code` và định tuyến dựa trên giá trị này:

<CodeGroup>
  ```javascript Node.js expandable theme={null}
  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 });
  });
  ```

  ```python Python expandable theme={null}
  import os
  from fastapi import FastAPI, Request
  from standardwebhooks import Webhook

  app = 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}
  ```
</CodeGroup>

<Tip>
  Luôn xác minh chữ ký của webhook trước khi xử lý. Xem [hướng dẫn Webhooks](/developer-resources/webhooks) để biết toàn bộ quy trình thiết lập, bao gồm xác minh chữ ký và idempotency.
</Tip>

## Quyết định có nên Retry hay không: Soft Decline và Hard Decline

`error_code` cho biết việc retry cùng payment method có đáng thực hiện hay không.

| Decline type     | What it means                                                                                                                | What to do                                                                                            |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| **Soft decline** | Tạm thời hoặc có thể khắc phục (chẳng hạn như `INSUFFICIENT_FUNDS`, `PROCESSING_ERROR`, `NETWORK_ERROR`, `TRY_AGAIN_LATER`). | Retry — sau một khoảng thời gian chờ hoặc sau khi customer sửa thông tin đã nhập — có thể thành công. |
| **Hard decline** | Mang tính kết thúc (chẳng hạn như `STOLEN_CARD`, `LOST_CARD`, `DO_NOT_HONOR`, `FRAUDULENT`).                                 | **Không** retry cùng card. Yêu cầu customer sử dụng payment method khác.                              |

Tài liệu tham khảo [Transaction Failures](/api-reference/transaction-failures) liệt kê loại decline và hành động được đề xuất cho mọi `error_code`.

## Xử lý lỗi tại Checkout và khi Renewal

Cách khôi phục phụ thuộc vào việc customer có đang hiện diện hay không.

<Tabs>
  <Tab title="At checkout (customer present)">
    Customer đang chủ động thực hiện checkout. Hiển thị thông báo rõ ràng và cho phép họ retry ngay lập tức hoặc sử dụng card khác.

    * `requires_payment_method` — customer chưa từng cung cấp payment method: họ chưa nhập thông tin card hoặc được nhắc nhập nhưng không thực hiện hành động nào. Đây thường là **drop-off** trong checkout, không phải decline — hãy tiếp cận lại customer để hoàn tất payment (xem [Khôi phục giỏ hàng bị bỏ quên](/features/recovery/abandoned-cart-recovery)).
    * `requires_customer_action` — cần xác thực bổ sung (chẳng hạn như 3DS); yêu cầu customer hoàn tất bước này. Xem [Xử lý 3D Secure](/features/payment-methods/cards#3d-secure-authentication).
  </Tab>

  <Tab title="On subscription renewal (customer not present)">
    Customer không hiện diện, vì vậy bạn không thể yêu cầu họ thực hiện thao tác theo thời gian thực. Khi renewal thất bại, subscription chuyển sang `on_hold` và `subscription.on_hold` được kích hoạt.

    * **Soft decline** được tự động thử lại bằng [Subscription Payment Retries](/features/recovery/payment-retries).
    * **Hard decline** (và các lần retry đã hết) nên được khôi phục bằng [Subscription Dunning](/features/recovery/subscription-dunning), tính năng này gửi email cho customer để cập nhật payment method.

    Xem [Hướng dẫn tích hợp Subscription](/developer-resources/subscription-integration-guide#handling-subscription-on-hold) để biết toàn bộ quy trình on-hold → reactivate.
  </Tab>
</Tabs>

## Retry Payment Thất bại

* **Subscriptions:** Bật [Subscription Payment Retries](/features/recovery/payment-retries) để khôi phục soft decline mà không cần thực hiện thêm công việc tích hợp. Bạn cũng có thể kích hoạt khôi phục bằng cách yêu cầu customer cập nhật payment method thông qua [Update Payment Method API](/api-reference/subscriptions/update-payment-method), API này sẽ charge mọi khoản còn nợ.
* **One-time payments:** Gửi lại checkout hoặc `payment_link` để customer có thể thử lại bằng method khác. One-time payments không có cơ chế retry tự động.

<Warning>
  Không retry hard decline với cùng card. Các card network có thể đánh dấu những lần decline lặp lại là hành vi lạm dụng, làm giảm authorization rate của bạn.
</Warning>

## Hiển thị Lỗi an toàn cho Customer

Hiển thị cho customer một thông báo thân thiện — không bao giờ hiển thị `error_code` thô và cũng không bao giờ hiển thị `error_message` dành cho merchant.

<Info>
  Trên các giao diện do Dodo Payments kiểm soát — checkout, [Customer Portal](/features/customer-portal) và email dunning — việc ánh xạ này đã được thực hiện sẵn cho bạn, bao gồm cả phương án dự phòng hiển thị thông báo chung cho các decline liên quan đến fraud. Bạn chỉ cần áp dụng ánh xạ dưới đây khi hiển thị lỗi trong sản phẩm của riêng mình.
</Info>

```javascript Customer-facing messaging expandable theme={null}
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.";
}
```

<Warning>
  **Không bao giờ tiết lộ lý do thực sự của `STOLEN_CARD`, `LOST_CARD`, `PICKUP_CARD` hoặc `FRAUDULENT`.** Việc hiển thị các lý do này có thể cung cấp thông tin cho đối tượng gian lận. Hãy hiển thị thông báo decline chung và chỉ ghi log `error_code` cụ thể trong nội bộ.
</Warning>

## Liên quan

<CardGroup cols={2}>
  <Card title="Transaction Failures" icon="circle-exclamation" href="/api-reference/transaction-failures">
    Mọi mã decline, loại decline và hành động được đề xuất.
  </Card>

  <Card title="Error Codes" icon="triangle-exclamation" href="/api-reference/error-codes">
    Các lỗi API và logic nghiệp vụ không phải là card decline.
  </Card>

  <Card title="Subscription Payment Retries" icon="arrow-rotate-right" href="/features/recovery/payment-retries">
    Tự động khôi phục soft decline trong các lần renewal của subscription.
  </Card>

  <Card title="Subscription Dunning" icon="repeat" href="/features/recovery/subscription-dunning">
    Các chuỗi email giúp khôi phục hard decline.
  </Card>

  <Card title="Payment Webhooks" icon="webhook" href="/developer-resources/webhooks/intents/payment">
    Schema payload đầy đủ cho các event payment.
  </Card>

  <Card title="Testing Failures" icon="flask" href="/miscellaneous/testing-process">
    Các card kiểm thử mô phỏng decline và lỗi renewal.
  </Card>
</CardGroup>
