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

Stripe memberi tahu lewat email soal sengketa dan pembayaran gagal. Cara mengubah event webhook Stripe jadi push, notifikasi mendesak, atau panggilan.

Diperbarui

Daftar Isi

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.

EventKenapa pentingTipe yang disarankan
charge.dispute.createdWaktu Anda untuk merespons terbatas — biasanya 7 sampai 21 hari, tergantung jaringan kartu. Terlewat berarti kalah otomatis.Panggilan
radar.early_fraud_warning.createdPenerbit 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.failedUang yang dikumpulkan Stripe tidak sampai ke bank Anda. Semua yang mengikutinya — gaji, hitungan runway — sekarang salah.Panggilan
invoice.payment_failedChurn tak disengaja. Cukup sering selesai sendiri sehingga panggilan terasa berlebihan, tetapi akun terbesar layak dilihat di hari yang sama.Mendesak
customer.subscription.deletedChurn sukarela. Layak diketahui hari ini, tidak layak membangunkan Anda.Normal
payment_intent.succeededTidak ada yang rusak. Justru event inilah yang melatih Anda mengabaikan lima yang lain.Tidak usah

Tingkatan ini memetakan ke tiga tipe notifikasi 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
  • Echobell terpasang (App Store / Google Play)
  • 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>.

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.

Langkah 2 — Arahkan Stripe ke channel

Di dashboard Stripe, buka tab Webhooks dan buat event destination:

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

Pilih tepat satu tipe event — charge.dispute.created untuk channel ini. Saran Stripe sendiri adalah hanya melanggani event yang dibutuhkan integrasi Anda; di sini hal itu juga menjaga template tetap jujur, karena setiap payload yang datang berbentuk sama.

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

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

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 sebagai JSON. Echobell membaca body apa adanya, jadi setiap field bisa dialamati di template dan kondisi dengan notasi titik:

{
  "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 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 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.

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.

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:

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

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.

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 atau dapatkan di Google Play, buat channel sengketa lebih dulu, dan jalankan sekali stripe trigger charge.dispute.created sebelum mempercayakan apa pun yang sungguhan ke jalur ini.


Terkait