---
title: "Peringatan Kegagalan Pembayaran Stripe: Sengketa, Penolakan, dan Webhook Mati"
description: "Stripe memberi tahu lewat email soal sengketa dan pembayaran gagal. Cara mengubah event webhook Stripe jadi push, notifikasi mendesak, atau panggilan."
date: 2026-09-11
author: Nooc
authorAvatarLink: /images/avatars/nooc.webp
authorLink: https://nooc.me
tags:
  - Stripe
  - pembayaran
  - webhook
  - sengketa
  - SaaS
  - peringatan
---

# Peringatan Kegagalan Pembayaran Stripe: Sengketa, Penolakan, dan Webhook Mati

Stripe sudah tahu kapan sengketa dibuka, kapan pembayaran langganan gagal, dan kapan payout terpental. Ia memberi tahu Anda lewat email. Untuk sesuatu yang lebih nyaring, daftarkan satu endpoint webhook tambahan di Stripe yang mengarah ke URL channel Echobell, langgani segelintir tipe event, lalu pilih tipe notifikasi sesuai tenggat yang melekat pada masing-masing. Sengketa dan peringatan dini penipuan punya jam yang sedang berjalan; perpanjangan yang gagal biasanya tidak.

Panduan ini membahas event Stripe mana yang layak menyela Anda, cara mengarahkan Stripe ke Echobell dalam sekitar lima menit, field template yang benar-benar ada di tiap objek, dan konsekuensi yang Anda terima kalau melewatkan verifikasi tanda tangan.

## Event yang benar-benar layak menyela Anda

Peringatan pembayaran selalu rusak dengan cara yang sama: seseorang melanggani `payment_intent.succeeded` karena rasanya menyenangkan, ponsel bergetar empat puluh kali sehari, dan enam minggu kemudian sebuah notifikasi sengketa lewat begitu saja tanpa dibaca. Mulailah dari tenggatnya. Kalau melewatkan event selama delapan jam tidak berbiaya apa-apa, ia tidak perlu sampai ke Anda dalam delapan detik.

| Event | Kenapa penting | Tipe yang disarankan |
| --- | --- | --- |
| `charge.dispute.created` | Waktu Anda untuk merespons terbatas — [biasanya 7 sampai 21 hari, tergantung jaringan kartu](https://docs.stripe.com/disputes/responding). Terlewat berarti kalah otomatis. | Panggilan |
| `radar.early_fraud_warning.created` | Penerbit kartu sudah memberi tahu Stripe bahwa sebuah tagihan mungkin penipuan. Mengembalikan dana sebelum menjadi sengketa formal adalah langkah yang masih tersedia, dan jendelanya sempit. | Panggilan |
| `payout.failed` | Uang yang dikumpulkan Stripe tidak sampai ke bank Anda. Semua yang mengikutinya — gaji, hitungan runway — sekarang salah. | Panggilan |
| `invoice.payment_failed` | Churn tak disengaja. Cukup sering selesai sendiri sehingga panggilan terasa berlebihan, tetapi akun terbesar layak dilihat di hari yang sama. | Mendesak |
| `customer.subscription.deleted` | Churn sukarela. Layak diketahui hari ini, tidak layak membangunkan Anda. | Normal |
| `payment_intent.succeeded` | Tidak ada yang rusak. Justru event inilah yang melatih Anda mengabaikan lima yang lain. | Tidak usah |

Tingkatan ini memetakan ke tiga [tipe notifikasi](/id/docs/notification) Echobell: **Normal** adalah push biasa, **Mendesak** menembus sebagian besar mode fokus, dan **Panggilan** muncul sebagai telepon masuk sehingga berdering menembus Do Not Disturb. Tiap pelanggan memilih levelnya sendiri per channel, jadi co-founder bisa menerima sengketa sebagai panggilan sementara rekan support menerimanya sebagai push.

## Yang Anda butuhkan

- Akun Stripe dengan akses ke [tab Webhooks di Workbench](https://dashboard.stripe.com/webhooks)
- Echobell terpasang ([App Store](https://apps.apple.com/app/apple-store/id6743597198?pt=128151925&ct=blog-stripe-payment-failure-alerts-id&mt=8) / [Google Play](https://play.google.com/store/apps/details?id=one.echobell.echobellandroid))
- Lima menit. Tanpa server, tanpa deploy, tanpa kode — kecuali Anda ingin verifikasi tanda tangan, yang ada di bagian terakhir.

## Langkah 1 — Satu channel per tipe event

Godaannya adalah membuat satu channel bernama "Stripe" dan mengirim semuanya ke sana. Jangan. Template body dan tautan dashboard berbeda antara sengketa, invoice, dan payout, karena masing-masing membawa objek yang berbeda — dan seluruh tujuan susunan ini adalah agar notifikasinya sendiri memberi tahu apa yang terjadi tanpa Anda membuka apa pun.

Buat channel dengan nama event-nya: `Stripe Disputes`. Tulis template judul dan body supaya terbaca tuntas di layar kunci:

```
Title: 🔴 Dispute opened — {{data.object.reason}}
Body: Amount: {{data.object.amount}} {{data.object.currency}}
Charge: {{data.object.charge}}
Status: {{data.object.status}}
```

Atur template tautan di pengaturan lanjutan agar catatan notifikasi membuka halaman yang tepat:

```
https://dashboard.stripe.com/disputes/{{data.object.id}}
```

Lalu langgani diri Anda dengan tipe **Panggilan**, dan salin URL webhook dari tampilan detail channel. Bentuknya seperti `https://hook.echobell.one/t/<channel-token>`.

<Callout type="info">Nyalakan **POST Only** di pengaturan lanjutan channel. Stripe selalu mengirim POST, dan sakelar itu membuat URL yang ditempel di jendela obrolan atau tab browser tidak lagi bisa memicu peringatan sengketa palsu.</Callout>

## Langkah 2 — Arahkan Stripe ke channel

Di dashboard Stripe, buka [tab Webhooks](https://dashboard.stripe.com/webhooks) dan buat event destination:

<Steps>

<Step>

Klik **Create an event destination**, pilih **Your account**, dan biarkan versi API pada default akun Anda.

</Step>

<Step>

Pilih tepat satu tipe event — `charge.dispute.created` untuk channel ini. Saran Stripe sendiri adalah [hanya melanggani event yang dibutuhkan integrasi Anda](https://docs.stripe.com/webhooks); di sini hal itu juga menjaga template tetap jujur, karena setiap payload yang datang berbentuk sama.

</Step>

<Step>

Pilih **Webhook endpoint** sebagai tipe tujuan dan tempel URL channel Echobell.

</Step>

<Step>

Simpan, lalu gunakan **Send test event** — atau `stripe trigger charge.dispute.created` dari CLI — dan pastikan ponsel Anda berdering.

</Step>

</Steps>

Ulangi untuk tiap channel yang Anda buat. Stripe mengizinkan hingga 16 endpoint webhook per akun, lebih dari cukup untuk satu per tingkat peringatan.

## Langkah 3 — Apa yang sebenarnya tiba

Stripe mengirim [objek Event](https://docs.stripe.com/api/events/object) sebagai JSON. Echobell membaca body apa adanya, jadi setiap field bisa dialamati di template dan kondisi dengan notasi titik:

```json
{
  "id": "evt_1P...",
  "type": "charge.dispute.created",
  "livemode": true,
  "created": 1757548800,
  "data": {
    "object": {
      "id": "dp_1P...",
      "amount": 4900,
      "currency": "usd",
      "reason": "fraudulent",
      "status": "needs_response",
      "charge": "ch_3P...",
      "evidence_details": { "due_by": 1759449600 }
    }
  }
}
```

Tiga hal dari payload ini sering mengejutkan:

**Nominal berupa bilangan bulat dalam satuan mata uang terkecil.** `amount` sebesar `4900` berarti $49.00. Template Echobell menyisipkan dan membandingkan nilai tetapi tidak melakukan aritmetika, jadi `{{data.object.amount}}` tampil sebagai `4900`. Beri label jujur (`Amount: 4900 (cents)`), atau gunakan forwarder di bagian terakhir untuk membaginya dengan 100 sebelum dikirim.

**Timestamp dalam detik Unix.** `{{data.object.evidence_details.due_by}}` tampil sebagai `1759449600`, bukan tanggal. Kalau yang penting adalah adanya tenggat, bukan jam persisnya, buang saja dari template — halaman sengketa menampilkannya — dan biarkan template tautan yang bekerja.

**Nama field berbeda per objek.** Sengketa punya `amount`; invoice punya `amount_due`, `customer_email`, `attempt_count`, dan `hosted_invoice_url`; payout punya `failure_message` dan `arrival_date`; peringatan dini penipuan punya `fraud_type`, `actionable`, serta `charge` berupa ID teks biasa. Variabel yang tidak ada dirender sebagai string kosong alih-alih error, jadi template yang disalin dari channel yang salah gagal tanpa suara. Inilah alasan praktis satu channel per tipe event.

## Langkah 4 — Saring dengan kondisi, bukan dengan tekad

[Kondisi](/id/docs/conditions) channel memakai sintaks ekspresi yang sama dengan template, tanpa kurung kurawal, dan dijalankan sebelum apa pun dikirim.

Yang wajib ada di setiap channel Stripe:

```
livemode == true
```

Lalu lintas mode uji — `stripe trigger` Anda sendiri, rekan yang mengutak-atik sandbox — tidak lagi sampai ke ponsel Anda. Tambahkan setelah Anda memastikan rangkaiannya bekerja, bukan sebelumnya.

Untuk channel invoice gagal, ambang batas menjaga akun kecil tetap di luar malam Anda:

```
livemode == true && data.object.amount_due > 20000
```

Dibaca "di atas $200", dalam sen. Dan kalau Anda lebih ingin melihat percobaan ulang yang benar-benar macet ketimbang setiap percobaan pertama:

```
livemode == true && data.object.attempt_count > 1
```

Kalau Anda terlanjur mengarahkan satu endpoint dengan beberapa tipe event ke satu channel, kondisi bisa memisahkannya kembali:

```
type == "charge.dispute.created" || type == "payout.failed"
```

## Langkah 5 — Jauhkan jalur peringatan dari hal yang rusak

Bagian ini lebih berharga daripada templatenya.

Endpoint webhook produksi Anda adalah tempat pemenuhan terjadi: ia memberi akses, menulis ke basis data, mengirim tanda terima. Karena itu, ia juga endpoint yang ikut mati saat aplikasi Anda mati. Ketika itu terjadi, Stripe [mencoba ulang hingga tiga hari dengan backoff eksponensial](https://docs.stripe.com/webhooks) dan mengirimi Anda email — dan email tentang webhook yang tak terkirim terlihat sama persis dengan email Stripe lainnya, itulah kenapa ia baru ditemukan hari Senin.

Penyebab matinya webhook secara diam-diam sangat membosankan. Stripe memperlakukan redirect `3xx` sebagai kegagalan, jadi endpoint yang mulai mengalihkan `http` ke `https` atau menambahkan garis miring di akhir berhenti menerima event. Ia butuh TLS 1.2 atau lebih tinggi, jadi sertifikat kedaluwarsa atau salah konfigurasi sudah cukup. `403` dari aturan WAF yang ditambahkan seseorang minggu lalu juga.

Endpoint kedua yang mengarah langsung ke Echobell tidak berbagi satu pun dari itu. Ia URL lain di host lain dengan sertifikat lain, dan terus berdering saat aplikasi Anda tumbang. Aturannya bisa digeneralisasi: *jalur yang memberi tahu Anda ada yang rusak tidak boleh melewati hal yang rusak itu.*

Kegagalan endpoint Anda sendiri tetap perlu terlihat. Periksa tab **Event deliveries** di Workbench saat ada yang terasa janggal — ia menampilkan `Delivered`, `Pending`, dan `Failed` per event, beserta status HTTP tiap percobaan. Stripe mengizinkan pengiriman ulang event hingga 15 hari dari dashboard, atau 30 hari dengan `stripe events resend` di CLI, jadi celah yang ketahuan dalam dua minggu masih bisa ditambal.

<Callout type="warn">URL channel Echobell adalah kredensial bearer: siapa pun yang memegangnya bisa memicu channel itu. Mengarahkan Stripe langsung ke sana berarti tidak ada yang memverifikasi header `Stripe-Signature`, jadi URL yang bocor adalah mesin peringatan palsu, bukan kebocoran data. Jauhkan dari repositori dan tangkapan layar, gunakan **Reset Token** kalau ia lolos, dan baca bagian berikutnya kalau kompromi ini mengganggu Anda.</Callout>

## Opsional — Verifikasi tanda tangan lebih dulu

Kalau Anda ingin tanda tangan Stripe benar-benar diperiksa dan nominalnya diformat seperti uang, taruh forwarder kecil di depan. Cloudflare Worker berikut memverifikasi event, langsung mengembalikan `200` sesuai permintaan Stripe, lalu mengirim payload datar ke Echobell:

```js
import Stripe from "stripe";

export default {
  async fetch(request, env, ctx) {
    const stripe = new Stripe(env.STRIPE_SECRET_KEY);
    const body = await request.text();

    let event;
    try {
      event = await stripe.webhooks.constructEventAsync(
        body,
        request.headers.get("stripe-signature"),
        env.STRIPE_WEBHOOK_SECRET,
      );
    } catch {
      return new Response("invalid signature", { status: 400 });
    }

    const invoice = event.data.object;
    ctx.waitUntil(
      fetch(env.ECHOBELL_HOOK_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          customer: invoice.customer_email || invoice.customer,
          amount: (invoice.amount_due / 100).toFixed(2),
          currency: invoice.currency.toUpperCase(),
          attempt: invoice.attempt_count,
          externalLink: invoice.hosted_invoice_url,
        }),
      }),
    );

    return new Response("ok", { status: 200 });
  },
};
```

Template di sisi lain jadi jauh lebih rapi, karena pembentukannya sudah terjadi di kode:

```
Title: 💳 Payment failed — {{currency}} {{amount}}
Body: Customer: {{customer}}
Attempt #{{attempt}}
```

`externalLink` adalah variabel khusus: jika template tautan tidak diatur, Echobell memakainya sebagai tautan catatan notifikasi, sehingga halaman invoice yang di-host hanya sejauh satu ketukan.

Perhatikan bentuk kompromisnya: ini kini adalah potongan infrastruktur yang bisa gagal dengan sendirinya, persis yang diperingatkan Langkah 5. Jalan tengah yang masuk akal: verifikasi tanda tangan di channel bervolume tinggi, tempat peringatan palsu akan mengganggu, dan biarkan channel sengketa terhubung langsung — di sana biaya dering palsu adalah satu tatapan bingung, sedangkan biaya dering yang terlewat adalah nominal yang disengketakan.

## Yang tidak diberikan susunan ini

- **Tidak ada rotasi on-call atau eskalasi.** Semua pelanggan channel panggilan berdering sekaligus. Itu keunggulan pada empat orang dan masalah pada empat puluh; pada empat puluh Anda butuh platform manajemen insiden.
- **Tidak ada deduplikasi.** Stripe [tidak menjamin urutan event](https://docs.stripe.com/webhooks) dan bisa mengirim event yang sama lebih dari sekali. Dua dering untuk satu sengketa itu mungkin.
- **Tidak ada tanda terima.** Tidak ada catatan bahwa seorang manusia melihatnya, dan tidak ada eskalasi ke orang kedua kalau tidak ada yang merespons.
- **Tidak ada peringatan "pembayaran berhenti".** Stripe memancarkan event saat sesuatu terjadi, tidak pernah saat sesuatu berhenti. Kalau checkout Anda rusak, tak ada event yang terpicu sama sekali. Untuk itu perlu tugas terjadwal di sisi Anda yang mem-ping channel ketika jumlah tagihan satu jam terakhir nol — [dead man's switch berbasis cron](/id/blog/cron-job-failure-alerts).

## Pemecahan masalah

**Event uji menampilkan `200` di Stripe tetapi tidak ada notifikasi.** Echobell menjawab `200` dengan body JSON bahkan saat tidak mengirim apa pun — lihat body respons di tab Event deliveries. `success: false` dengan token berpanjang valid berarti token channel-nya salah. Kalau `success` bernilai `true`, penyebab paling mungkin adalah kondisi: `livemode == true` memang dirancang memblokir semua event uji.

**Stripe melaporkan `405 Method Not Allowed`.** Channel menyalakan POST Only dan sesuatu mengirim GET. Stripe sendiri selalu POST, jadi itu pratinjau tautan atau tab browser, bukan Stripe.

**Notifikasi datang dengan field kosong.** Template menunjuk objek yang salah — `{{data.object.amount}}` di channel invoice, padahal field-nya `amount_due`. Kirim satu event asli, buka di dashboard, dan baca JSON-nya.

**Pengiriman mulai gagal setelah berminggu-minggu berjalan.** Periksa sertifikat dan redirect apa pun di depan URL. Untuk URL channel yang dipakai langsung ini jarang; untuk forwarder yang Anda deploy sendiri, itulah tersangka biasanya.

## Pertanyaan yang sering diajukan

### Bisakah Stripe menelepon saya saat sengketa dibuka?

Tidak dengan sendirinya. Stripe memberi tahu lewat email, di dashboard, melalui event `charge.dispute.created`, dan lewat push kalau Anda memakai aplikasi Stripe Dashboard. Untuk dering sungguhan, arahkan event itu ke channel dengan tipe langganan **Panggilan**.

### Apakah saya perlu menulis kode untuk menghubungkan Stripe ke Echobell?

Tidak. Stripe mengirim JSON ke URL HTTPS publik mana pun, dan URL channel Echobell adalah salah satunya. Kode hanya perlu kalau Anda ingin header `Stripe-Signature` diverifikasi atau nominalnya diformat ulang.

### Amankah memberi Stripe URL webhook pihak ketiga?

Ini kompromi yang disengaja. Payload yang dikirim Stripe berisi metadata pelanggan dan pembayaran, dan Echobell tidak menyimpan payload webhook mentah secara permanen — notifikasi yang dirender ada di perangkat Anda. Yang Anda lepas adalah verifikasi tanda tangan: siapa pun yang tahu URL-nya bisa mengirim pemalsuan yang meyakinkan. Perlakukan seperti kunci API, dan pakai pola forwarder untuk apa pun yang ingin Anda verifikasi.

### Kenapa peringatan saya menampilkan 4900 dan bukan $49.00?

Stripe mengirim nominal sebagai bilangan bulat dalam satuan mata uang terkecil, dan template Echobell tidak melakukan aritmetika. Cantumkan satuannya di template, atau bagi dengan 100 di forwarder sebelum mengirim.

### Bagaimana mencegah event mode uji membangunkan saya?

Tambahkan kondisi `livemode == true` ke channel. Stripe menandai setiap event sandbox dan `stripe trigger` sebagai `livemode: false`.

### Bisakah co-founder saya menerima peringatan yang sama tanpa membayar kursi?

Bisa. Bagikan tautan channel; tiap pelanggan memilih tipe notifikasinya sendiri. Satu orang menerima sengketa sebagai panggilan sementara yang lain sebagai push biasa, dan tidak ada biaya per kursi untuk pelanggan.

### Haruskah saya memasang peringatan untuk pembayaran berhasil?

Hanya sebentar, dan hanya selagi bisnisnya cukup kecil sehingga tiap pembayaran masih terasa sebagai peristiwa. Begitu notifikasi pembayaran berhasil menjadi rutinitas, ia mulai mengikis respons Anda terhadap yang benar-benar penting — mekanisme inti di balik [kelelahan peringatan](/id/blog/fix-alert-fatigue-developer-guide).

## Penutup

Seluruh susunannya adalah satu event destination Stripe per channel Echobell, satu kondisi `livemode == true`, dan disiplin menyisakan tingkat panggilan untuk event yang punya jam berjalan. Sengketa dan peringatan dini penipuan punya. Perpanjangan gagal di paket $9 tidak punya, dan berpura-pura sebaliknya adalah persis cara Anda akhirnya tertidur saat yang punya jam itu datang.

[Unduh Echobell untuk iPhone](https://apps.apple.com/app/apple-store/id6743597198?pt=128151925&ct=blog-stripe-payment-failure-alerts-id&mt=8) atau [dapatkan di Google Play](https://play.google.com/store/apps/details?id=one.echobell.echobellandroid), buat channel sengketa lebih dulu, dan jalankan sekali `stripe trigger charge.dispute.created` sebelum mempercayakan apa pun yang sungguhan ke jalur ini.

---

## Terkait

- [Dokumentasi integrasi webhook](/id/docs/webhook)
- [Referensi kondisi channel](/id/docs/conditions)
- [Panduan developer mengatasi kelelahan peringatan](/id/blog/fix-alert-fatigue-developer-guide)
- [Peringatan kegagalan cron job](/id/blog/cron-job-failure-alerts)
- [Notifikasi webhook Zapier ke ponsel](/id/blog/zapier-webhook-notifications-to-phone)
