Cảnh báo thanh toán thất bại trên Stripe: tranh chấp, từ chối và webhook chết

Stripe báo bằng email khi có tranh chấp hay thanh toán thất bại. Cách biến sự kiện webhook Stripe thành push, cảnh báo khẩn hoặc cuộc gọi.

Cập nhật

Mục lục

Stripe đã biết khi nào một tranh chấp được mở, khi nào việc thu tiền gói đăng ký thất bại và khi nào một khoản chi trả bị trả về. Nó báo cho bạn bằng email. Muốn thứ gì đó ồn ào hơn, hãy đăng ký thêm một endpoint webhook trong Stripe trỏ tới URL kênh Echobell, đăng ký vài loại sự kiện, rồi chọn kiểu thông báo tương ứng với thời hạn gắn với từng loại. Tranh chấp và cảnh báo gian lận sớm có đồng hồ đang chạy; một lần gia hạn thất bại thì thường không.

Hướng dẫn này nói về những sự kiện Stripe nào xứng đáng cắt ngang bạn, cách trỏ Stripe sang Echobell trong khoảng năm phút, những trường mẫu thực sự tồn tại trên từng đối tượng, và cái giá bạn chấp nhận khi bỏ qua việc xác minh chữ ký.

Những sự kiện thực sự xứng đáng cắt ngang bạn

Cảnh báo thanh toán luôn hỏng theo cùng một kiểu: ai đó đăng ký payment_intent.succeeded vì nó cho cảm giác dễ chịu, điện thoại rung bốn mươi lần mỗi ngày, và sáu tuần sau một thông báo tranh chấp trôi qua mà không ai đọc. Hãy bắt đầu từ thời hạn. Nếu bỏ lỡ sự kiện trong tám giờ chẳng tốn gì, thì nó không cần đến tay bạn trong tám giây.

Sự kiệnVì sao quan trọngKiểu đề xuất
charge.dispute.createdBạn có một khoảng thời gian hạn chế để phản hồi — thường 7 đến 21 ngày tùy mạng lưới thẻ. Bỏ lỡ là thua tự động.Cuộc gọi
radar.early_fraud_warning.createdNgân hàng phát hành đã báo với Stripe rằng một giao dịch có thể là gian lận. Hoàn tiền trước khi nó thành tranh chấp chính thức là hành động vẫn còn khả dụng, và cửa sổ ấy rất ngắn.Cuộc gọi
payout.failedTiền Stripe đã thu không tới được ngân hàng của bạn. Mọi thứ phía sau — trả lương, tính đường băng tài chính — giờ đều sai.Cuộc gọi
invoice.payment_failedRời bỏ ngoài ý muốn. Nó tự giải quyết đủ thường xuyên để một cuộc gọi là quá mức, nhưng các tài khoản lớn nhất đáng được nhìn ngay trong ngày.Khẩn
customer.subscription.deletedRời bỏ tự nguyện. Đáng biết hôm nay, không đáng để đánh thức bạn.Thường
payment_intent.succeededChẳng có gì hỏng cả. Chính sự kiện này huấn luyện bạn phớt lờ năm cái còn lại.Không cần

Các mức này tương ứng với ba kiểu thông báo của Echobell: Thường là push bình thường, Khẩn xuyên qua hầu hết chế độ tập trung, còn Cuộc gọi hiện lên như một cuộc gọi đến nên vẫn đổ chuông qua Không làm phiền. Mỗi người đăng ký chọn mức riêng cho từng kênh, nên người đồng sáng lập có thể nhận tranh chấp bằng cuộc gọi trong khi đồng nghiệp hỗ trợ nhận bằng push.

Bạn cần gì

  • Một tài khoản Stripe có quyền vào tab Webhooks trong Workbench
  • Đã cài Echobell (App Store / Google Play)
  • Năm phút. Không máy chủ, không triển khai, không code — trừ khi bạn muốn xác minh chữ ký, phần đó nằm ở mục cuối.

Bước 1 — Mỗi loại sự kiện một kênh

Rất dễ nảy ra ý định làm một kênh duy nhất tên "Stripe" rồi ném hết vào đó. Đừng. Mẫu nội dung và liên kết dashboard khác nhau giữa tranh chấp, hóa đơn và khoản chi trả, vì mỗi thứ mang một đối tượng khác nhau — và toàn bộ ý nghĩa của cách bố trí này là thông báo tự nói cho bạn biết chuyện gì đã xảy ra mà không phải mở gì cả.

Tạo kênh đặt tên theo sự kiện: Stripe Disputes. Viết mẫu tiêu đề và nội dung sao cho đọc gọn ngay trên màn hình khóa:

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

Đặt mẫu liên kết trong cài đặt nâng cao để bản ghi thông báo mở đúng trang:

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

Rồi tự đăng ký với kiểu Cuộc gọi, và sao chép URL webhook từ màn hình chi tiết kênh. Nó trông như https://hook.echobell.one/t/<channel-token>.

Bật POST Only trong cài đặt nâng cao của kênh. Stripe luôn gửi POST, và công tắc này khiến việc dán URL vào cửa sổ chat hay thanh địa chỉ trình duyệt không còn kích hoạt được một cảnh báo tranh chấp giả.

Bước 2 — Trỏ Stripe tới kênh

Trong dashboard Stripe, mở tab Webhooks và tạo một event destination:

Bấm Create an event destination, chọn Your account, và để nguyên phiên bản API mặc định của tài khoản.

Chọn đúng một loại sự kiện — charge.dispute.created cho kênh này. Chính Stripe cũng khuyên chỉ đăng ký những sự kiện mà tích hợp của bạn cần; ở đây việc đó còn giữ cho mẫu trung thực, vì mọi payload đến đều cùng một hình dạng.

Chọn Webhook endpoint làm kiểu đích rồi dán URL kênh Echobell.

Lưu lại, rồi dùng Send test event — hoặc stripe trigger charge.dispute.created từ CLI — và xác nhận điện thoại đổ chuông.

Lặp lại cho từng kênh bạn đã tạo. Stripe cho phép tối đa 16 endpoint webhook mỗi tài khoản, thừa đủ để mỗi mức cảnh báo một cái.

Bước 3 — Thứ thực sự đến nơi

Stripe gửi đối tượng Event dưới dạng JSON. Echobell đọc phần thân nguyên vẹn, nên mọi trường đều truy cập được trong mẫu và điều kiện bằng ký pháp chấm:

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

Ba điều ở payload này thường gây bất ngờ:

Số tiền là số nguyên theo đơn vị tiền tệ nhỏ nhất. amount bằng 4900 nghĩa là $49.00. Mẫu của Echobell nội suy và so sánh giá trị nhưng không làm số học, nên {{data.object.amount}} hiển thị 4900. Hoặc ghi nhãn cho trung thực (Amount: 4900 (cents)), hoặc dùng bộ chuyển tiếp ở mục cuối để chia cho 100 trước khi gửi.

Dấu thời gian là giây Unix. {{data.object.evidence_details.due_by}} hiện ra là 1759449600, không phải một ngày. Nếu điều quan trọng là có thời hạn chứ không phải giờ chính xác, hãy bỏ nó khỏi mẫu — trang tranh chấp có hiển thị — và để mẫu liên kết làm việc.

Tên trường khác nhau theo đối tượng. Tranh chấp có amount; hóa đơn có amount_due, customer_email, attempt_counthosted_invoice_url; khoản chi trả có failure_messagearrival_date; cảnh báo gian lận sớm có fraud_type, actionable, và charge chỉ là một chuỗi ID. Biến không tồn tại sẽ hiển thị thành chuỗi rỗng chứ không báo lỗi, nên một mẫu sao chép nhầm kênh sẽ hỏng trong im lặng. Đây chính là lý do thực tế của nguyên tắc mỗi loại sự kiện một kênh.

Bước 4 — Lọc bằng điều kiện, không bằng ý chí

Điều kiện của kênh dùng cùng cú pháp biểu thức với mẫu, chỉ bỏ ngoặc nhọn, và chạy trước khi bất cứ thứ gì được gửi đi.

Điều kiện nên có ở mọi kênh Stripe:

livemode == true

Lưu lượng chế độ thử — những lần stripe trigger của chính bạn, một đồng nghiệp nghịch trong sandbox — không còn tới điện thoại bạn nữa. Hãy thêm nó sau khi đã xác nhận đường dây chạy được, không phải trước.

Với kênh hóa đơn thất bại, một ngưỡng giữ những tài khoản nhỏ ở ngoài buổi tối của bạn:

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

Đọc là "trên $200", tính bằng xu. Còn nếu bạn muốn thấy những lần thử lại thực sự tắc thay vì mọi lần thất bại đầu tiên:

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

Nếu bạn lỡ trỏ một endpoint với nhiều loại sự kiện vào một kênh, điều kiện sẽ tách chúng ra lại:

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

Bước 5 — Giữ đường cảnh báo nằm ngoài thứ sẽ hỏng

Đây là phần đáng giá hơn các mẫu.

Endpoint webhook chạy thật của bạn là nơi việc giao hàng diễn ra: nó cấp quyền truy cập, ghi vào cơ sở dữ liệu, gửi biên nhận. Vì thế nó cũng chính là endpoint sập khi ứng dụng của bạn sập. Khi đó, Stripe thử lại tối đa ba ngày với khoảng lùi theo cấp số nhân và gửi cho bạn một email — mà email về webhook chưa giao được trông y hệt mọi email Stripe khác, nên nó thường được tìm thấy vào thứ Hai.

Nguyên nhân webhook chết lặng lẽ đều rất nhàm. Stripe coi chuyển hướng 3xx là thất bại, nên một endpoint bắt đầu chuyển http sang https hoặc thêm dấu gạch chéo cuối sẽ ngừng nhận sự kiện. Nó đòi TLS 1.2 trở lên, nên một chứng chỉ hết hạn hoặc cấu hình sai là đủ. Một 403 từ luật WAF ai đó thêm tuần trước cũng vậy.

Một endpoint thứ hai trỏ thẳng vào Echobell không chia sẻ bất cứ điều nào trong số đó. Nó là URL khác trên máy chủ khác với chứng chỉ khác, và nó vẫn đổ chuông trong lúc ứng dụng của bạn nằm im. Quy tắc này khái quát được: đường báo cho bạn biết có thứ hỏng thì không nên chạy qua chính thứ đang hỏng.

Bạn vẫn muốn thấy được sự cố của endpoint của mình. Hãy xem tab Event deliveries trong Workbench khi thấy có gì đó lấn cấn — nó hiển thị Delivered, PendingFailed theo từng sự kiện, kèm mã HTTP của mỗi lần thử. Stripe cho gửi lại một sự kiện trong vòng 15 ngày từ dashboard, hoặc 30 ngày với stripe events resend từ CLI, nên một lỗ hổng phát hiện trong hai tuần vẫn vá được.

URL kênh Echobell là một thông tin xác thực dạng bearer: ai giữ nó đều có thể kích hoạt kênh. Trỏ Stripe thẳng vào đó nghĩa là không ai xác minh header Stripe-Signature, nên một URL bị lộ là cỗ máy cảnh báo giả, không phải rò rỉ dữ liệu. Đừng để nó trong kho mã và ảnh chụp màn hình, dùng Reset Token nếu nó lọt ra ngoài, và đọc mục sau nếu đánh đổi này làm bạn khó chịu.

Tùy chọn — Xác minh chữ ký trước

Nếu bạn muốn chữ ký Stripe thực sự được kiểm tra và số tiền được định dạng như tiền, hãy đặt một bộ chuyển tiếp nhỏ ở phía trước. Cloudflare Worker sau xác minh sự kiện, trả 200 ngay như Stripe yêu cầu, rồi gửi cho Echobell một payload phẳng:

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 });
  },
};

Mẫu ở đầu bên kia đẹp hơn hẳn, vì việc định hình đã làm xong trong code:

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

externalLink là biến đặc biệt: khi không đặt mẫu liên kết, Echobell dùng nó làm liên kết cho bản ghi thông báo, nên trang hóa đơn được host chỉ cách một cú chạm.

Hãy để ý hình dạng của sự đánh đổi: đây giờ là một mảnh hạ tầng có thể tự hỏng, đúng thứ mà Bước 5 cảnh báo. Một dung hòa hợp lý là xác minh chữ ký ở kênh khối lượng lớn, nơi cảnh báo giả sẽ gây phiền, và để kênh tranh chấp nối thẳng — ở đó cái giá của một hồi chuông vô cớ là một cái nhìn ngơ ngác, còn cái giá của một hồi chuông bị bỏ lỡ là số tiền bị tranh chấp.

Những gì cách này không cho bạn

  • Không có lịch trực hay leo thang. Tất cả người đăng ký kênh cuộc gọi đều đổ chuông cùng lúc. Với bốn người đó là ưu điểm, với bốn mươi người là vấn đề; ở mức bốn mươi bạn cần một nền tảng quản lý sự cố.
  • Không khử trùng lặp. Stripe không bảo đảm thứ tự sự kiện và có thể gửi cùng một sự kiện nhiều lần. Hai hồi chuông cho một tranh chấp là chuyện có thể xảy ra.
  • Không có xác nhận đã đọc. Không gì ghi lại rằng có người đã thấy, và không gì chuyển sang người thứ hai nếu chẳng ai phản hồi.
  • Không có cảnh báo "thanh toán đã dừng". Stripe phát sự kiện khi có chuyện xảy ra, chứ không bao giờ khi mọi thứ ngừng lại. Nếu trang thanh toán của bạn hỏng, sẽ không có sự kiện nào được kích hoạt. Cái đó cần một tác vụ định kỳ ở phía bạn, ping một kênh khi số giao dịch trong giờ vừa rồi bằng không — một công tắc người chết dựa trên cron.

Khắc phục sự cố

Sự kiện thử hiện 200 trong Stripe nhưng không có thông báo nào. Echobell trả 200 kèm thân JSON ngay cả khi không gửi gì — hãy xem phần thân phản hồi trong tab Event deliveries. success: false với token đúng độ dài nghĩa là token kênh sai. Nếu successtrue, nguyên nhân có thể là điều kiện: livemode == true chặn mọi sự kiện thử, đúng như thiết kế.

Stripe báo 405 Method Not Allowed. Kênh bật POST Only mà thứ gì đó gửi GET. Bản thân Stripe luôn POST, nên đó là bản xem trước liên kết hoặc một tab trình duyệt, không phải Stripe.

Thông báo đến với các trường trống. Mẫu đang trỏ nhầm đối tượng — {{data.object.amount}} ở kênh hóa đơn, nơi trường đó tên là amount_due. Hãy gửi một sự kiện thật, mở nó trong dashboard và đọc JSON.

Việc gửi bắt đầu thất bại sau nhiều tuần chạy tốt. Kiểm tra chứng chỉ và mọi chuyển hướng đặt trước URL. Với URL kênh dùng trực tiếp thì chuyện này hiếm; với bộ chuyển tiếp bạn tự triển khai, đó là nghi phạm quen thuộc.

Câu hỏi thường gặp

Stripe có gọi điện cho tôi khi mở tranh chấp không?

Tự nó thì không. Stripe báo qua email, trên dashboard, qua sự kiện charge.dispute.created, và qua push nếu bạn dùng ứng dụng Stripe Dashboard. Muốn chuông reo thật, hãy định tuyến sự kiện đó tới một kênh có kiểu đăng ký là Cuộc gọi.

Tôi có phải viết code để nối Stripe với Echobell không?

Không. Stripe gửi JSON tới bất kỳ URL HTTPS công khai nào, và URL kênh Echobell chính là như vậy. Chỉ cần code khi bạn muốn xác minh header Stripe-Signature hoặc định dạng lại số tiền.

Đưa cho Stripe một URL webhook của bên thứ ba có an toàn không?

Đó là một đánh đổi có chủ ý. Payload Stripe gửi chứa siêu dữ liệu khách hàng và thanh toán, còn Echobell không lưu vĩnh viễn payload webhook thô — thông báo đã dựng nằm trên thiết bị của bạn. Thứ bạn từ bỏ là việc xác minh chữ ký: ai biết URL đều có thể gửi cho bạn một bản giả rất thuyết phục. Hãy coi nó như khóa API, và dùng mô hình chuyển tiếp cho những gì bạn muốn xác minh.

Vì sao cảnh báo hiện 4900 thay vì $49.00?

Stripe gửi số tiền dưới dạng số nguyên theo đơn vị tiền tệ nhỏ nhất, và mẫu Echobell không làm số học. Hãy ghi đơn vị trong mẫu, hoặc chia cho 100 trong bộ chuyển tiếp trước khi gửi.

Làm sao để sự kiện chế độ thử không đánh thức tôi?

Thêm điều kiện livemode == true vào kênh. Stripe đánh dấu mọi sự kiện sandbox và stripe triggerlivemode: false.

Người đồng sáng lập có nhận được cùng cảnh báo mà không phải trả tiền chỗ ngồi không?

Có. Chia sẻ liên kết kênh; mỗi người đăng ký chọn kiểu thông báo riêng. Một người nhận tranh chấp bằng cuộc gọi trong khi người kia nhận bằng push thường, và không có phí theo chỗ ngồi cho người đăng ký.

Có nên đặt cảnh báo cho thanh toán thành công?

Chỉ trong thời gian ngắn, và chỉ khi việc kinh doanh còn đủ nhỏ để mỗi giao dịch vẫn là một sự kiện. Ngay khi thông báo thanh toán thành công trở thành thường lệ, nó bắt đầu bào mòn phản ứng của bạn với những cái thực sự quan trọng — cơ chế cốt lõi phía sau mệt mỏi vì cảnh báo.

Kết lại

Toàn bộ cách bố trí là một event destination của Stripe cho mỗi kênh Echobell, một điều kiện livemode == true, và kỷ luật dành riêng mức cuộc gọi cho những sự kiện có đồng hồ đang chạy. Tranh chấp và cảnh báo gian lận sớm thì có. Một lần gia hạn thất bại của gói $9 thì không, và giả vờ ngược lại chính là cách bạn ngủ quên đúng lúc cái có đồng hồ xuất hiện.

Tải Echobell cho iPhone hoặc lấy trên Google Play, tạo kênh tranh chấp trước, và chạy một lần stripe trigger charge.dispute.created trước khi giao cho đường này bất cứ thứ gì thật sự quan trọng.


Liên quan