> ## 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.

# Menangani Kegagalan Pembayaran

> Deteksi pembayaran yang gagal dari webhook dan API, baca alasan kegagalan, tampilkan kepada pelanggan dengan aman, dan tentukan kapan harus mencoba lagi atau mengumpulkan metode pembayaran baru.

<Info>
  Ketika pembayaran gagal, Dodo Payments memberi tahu Anda **mengapa** melalui `error_code` yang terstandarisasi dan `error_message` yang dapat dibaca manusia. Panduan ini menjelaskan cara membaca kolom tersebut, menentukan apakah percobaan ulang layak dilakukan, dan memulihkan pembayaran tanpa mengekspos informasi sensitif kepada pelanggan.
</Info>

## Bagaimana Dodo Payments Melaporkan Kegagalan

Setiap pembayaran yang gagal — baik checkout satu kali maupun perpanjangan langganan — memiliki kolom kegagalan yang sama pada objek pembayaran:

| Kolom           | Tipe           | Deskripsi                                                                                                                                                                                      |
| --------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`        | string         | `failed` untuk pembayaran yang gagal. Status non-sukses lainnya mencakup `cancelled`, `requires_customer_action`, dan `requires_payment_method`.                                               |
| `error_code`    | string \| null | Alasan kegagalan yang terstandarisasi, misalnya `INSUFFICIENT_FUNDS` atau `PROCESSING_ERROR`. Lihat referensi [Kegagalan Transaksi](/api-reference/transaction-failures) untuk daftar lengkap. |
| `error_message` | string \| null | Penjelasan kegagalan yang dapat dibaca manusia.                                                                                                                                                |
| `retry_attempt` | integer        | `0` untuk tagihan awal. `1` atau lebih tinggi mengidentifikasi percobaan ulang perpanjangan langganan yang dijadwalkan.                                                                        |

<Note>
  `error_code` dan `error_message` adalah `null` sampai pembayaran benar-benar gagal. Selalu periksa `status` terlebih dahulu, lalu baca kolom error.
</Note>

## Webhook `payment.failed`

Cara paling andal untuk mendeteksi kegagalan adalah webhook `payment.failed`. Event tersebut membungkus seluruh objek pembayaran dalam `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"
    }
  }
}
```

Handler minimal membaca `error_code` dan melakukan routing berdasarkan nilainya:

<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>
  Selalu verifikasi tanda tangan webhook sebelum memprosesnya. Lihat [panduan Webhook](/developer-resources/webhooks) untuk penyiapan lengkap, termasuk verifikasi tanda tangan dan idempotensi.
</Tip>

## Tentukan Apakah Perlu Mencoba Lagi: Penolakan Lunak vs. Keras

`error_code` memberi tahu Anda apakah mencoba lagi dengan metode pembayaran yang sama layak dilakukan.

| Jenis penolakan     | Artinya                                                                                                                  | Tindakan yang harus dilakukan                                                                       |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| **Penolakan lunak** | Sementara atau dapat diperbaiki (misalnya `INSUFFICIENT_FUNDS`, `PROCESSING_ERROR`, `NETWORK_ERROR`, `TRY_AGAIN_LATER`). | Mencoba lagi — setelah jeda atau setelah pelanggan memperbaiki input mereka — dapat berhasil.       |
| **Penolakan keras** | Bersifat final (misalnya `STOLEN_CARD`, `LOST_CARD`, `DO_NOT_HONOR`, `FRAUDULENT`).                                      | **Jangan** mencoba lagi dengan kartu yang sama. Minta pelanggan menggunakan metode pembayaran lain. |

Referensi [Kegagalan Transaksi](/api-reference/transaction-failures) mencantumkan jenis penolakan dan tindakan yang disarankan untuk setiap `error_code`.

## Menangani Kegagalan saat Checkout vs. Perpanjangan

Cara pemulihan bergantung pada apakah pelanggan sedang hadir.

<Tabs>
  <Tab title="At checkout (customer present)">
    Pelanggan sedang aktif melakukan checkout. Tampilkan pesan yang jelas dan biarkan mereka mencoba lagi segera atau menggunakan kartu lain.

    * `requires_payment_method` — pelanggan tidak pernah memberikan metode pembayaran: mereka tidak memasukkan detail kartu, atau diminta memberikan metode pembayaran tetapi tidak melakukan tindakan apa pun. Ini biasanya merupakan **drop-off** saat checkout, bukan penolakan — hubungi kembali pelanggan untuk menyelesaikan pembayaran (lihat [Pemulihan Keranjang yang Ditinggalkan](/features/recovery/abandoned-cart-recovery)).
    * `requires_customer_action` — autentikasi tambahan (seperti 3DS) diperlukan; minta pelanggan menyelesaikannya. Lihat [Penanganan 3D Secure](/features/payment-methods/cards#3d-secure-authentication).
  </Tab>

  <Tab title="On subscription renewal (customer not present)">
    Pelanggan tidak sedang hadir, jadi Anda tidak dapat meminta tindakan mereka secara langsung. Ketika perpanjangan gagal, langganan berpindah ke `on_hold` dan `subscription.on_hold` dipicu.

    * **Penolakan lunak** dicoba kembali secara otomatis oleh [Percobaan Ulang Pembayaran Langganan](/features/recovery/payment-retries).
    * **Penolakan keras** (dan percobaan ulang yang telah habis) sebaiknya dipulihkan dengan [Dunning Langganan](/features/recovery/subscription-dunning), yang mengirim email kepada pelanggan untuk memperbarui metode pembayaran mereka.

    Lihat [Panduan Integrasi Langganan](/developer-resources/subscription-integration-guide#handling-subscription-on-hold) untuk alur lengkap ditahan → mengaktifkan kembali.
  </Tab>
</Tabs>

## Mencoba Lagi Pembayaran yang Gagal

* **Langganan:** Aktifkan [Percobaan Ulang Pembayaran Langganan](/features/recovery/payment-retries) untuk memulihkan penolakan lunak tanpa pekerjaan integrasi. Anda juga dapat memicu pemulihan dengan meminta pelanggan memperbarui metode pembayaran mereka melalui [API Pembaruan Metode Pembayaran](/api-reference/subscriptions/update-payment-method), yang akan menagih tunggakan apa pun.
* **Pembayaran satu kali:** Kirim ulang checkout atau `payment_link` agar pelanggan dapat mencoba lagi dengan metode lain. Tidak ada percobaan ulang otomatis untuk pembayaran satu kali.

<Warning>
  Jangan mencoba lagi penolakan keras dengan kartu yang sama. Jaringan kartu dapat menandai penolakan berulang sebagai tindakan penyalahgunaan, yang menurunkan tingkat otorisasi Anda.
</Warning>

## Tampilkan Error kepada Pelanggan dengan Aman

Tampilkan pesan yang ramah kepada pelanggan — jangan pernah menampilkan `error_code` mentah.

```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>
  **Jangan pernah mengungkapkan alasan sebenarnya untuk `STOLEN_CARD`, `LOST_CARD`, `PICKUP_CARD`, atau `FRAUDULENT`.** Menampilkan informasi ini dapat memberi petunjuk kepada pelaku penipuan. Tampilkan pesan penolakan umum dan catat `error_code` spesifik hanya secara internal.
</Warning>

## Terkait

<CardGroup cols={2}>
  <Card title="Transaction Failures" icon="circle-exclamation" href="/api-reference/transaction-failures">
    Setiap kode penolakan, jenisnya, dan tindakan yang disarankan.
  </Card>

  <Card title="Error Codes" icon="triangle-exclamation" href="/api-reference/error-codes">
    Error API dan logika bisnis yang bukan merupakan penolakan kartu.
  </Card>

  <Card title="Subscription Payment Retries" icon="arrow-rotate-right" href="/features/recovery/payment-retries">
    Pemulihan otomatis penolakan lunak pada perpanjangan langganan.
  </Card>

  <Card title="Subscription Dunning" icon="repeat" href="/features/recovery/subscription-dunning">
    Rangkaian email untuk memulihkan penolakan keras.
  </Card>

  <Card title="Payment Webhooks" icon="webhook" href="/developer-resources/webhooks/intents/payment">
    Skema payload lengkap untuk event pembayaran.
  </Card>

  <Card title="Testing Failures" icon="flask" href="/miscellaneous/testing-process">
    Kartu pengujian yang mensimulasikan penolakan dan kegagalan perpanjangan.
  </Card>
</CardGroup>
