目次
- 本当に割り込みに値するイベント
- 必要なもの
- ステップ1 — イベントタイプごとに1チャンネル
- ステップ2 — Stripeをそのチャンネルに向ける
- ステップ3 — 実際に届くもの
- ステップ4 — 意志ではなく条件で絞る
- ステップ5 — 通知経路を「壊れるもの」の外に置く
- 任意 — 先に署名を検証する
- この構成で得られないもの
- トラブルシューティング
- よくある質問
- チャージバックが起きたときStripeは電話をかけてくれますか?
- StripeとEchobellをつなぐのにコードは必要ですか?
- サードパーティのwebhook URLをStripeに渡して安全ですか?
- アラートに $49.00 ではなく 4900 と出るのはなぜ?
- テストモードのイベントで起こされないようにするには?
- 共同創業者はシート課金なしで同じアラートを受け取れますか?
- 決済成功にもアラートを付けるべき?
- まとめ
- 関連
チャージバックが起きたこと、サブスクリプションの決済が失敗したこと、入金が戻されたこと——Stripeはすべて把握していて、メールで知らせてくれます。もっと大きな音が必要なら、Stripeにもう一つwebhookエンドポイントを登録し、その宛先をEchobellのチャンネルURLにして、ごく少数のイベントタイプだけを購読し、それぞれに付いている「締め切り」に合わせて通知タイプを選びます。チャージバックと早期不正警告にはタイマーが動いています。更新の決済失敗には、たいてい動いていません。
この記事では、どのStripeイベントが割り込みに値するか、5分ほどでStripeをEchobellにつなぐ方法、各オブジェクトに実在するテンプレートフィールド、そして署名検証を省くことで受け入れるトレードオフを扱います。
本当に割り込みに値するイベント
決済アラートの失敗パターンはいつも同じです。気分がいいので payment_intent.succeeded を購読し、スマホが1日40回震え、6週間後にチャージバックの通知が未読のまま流れていく。出発点を締め切りに変えましょう。8時間気づかなくても何のコストも生まないなら、それは8秒で届く必要がありません。
| イベント | なぜ重要か | 推奨タイプ |
|---|---|---|
charge.dispute.created | 応答できる期間は限られています——通常はカードネットワークにより7〜21日。逃せば自動的に敗北です。 | 電話 |
radar.early_fraud_warning.created | カード発行会社が、その決済は不正かもしれないとStripeに通知した状態です。正式なチャージバックになる前の返金がまだ取れる手であり、その窓は短いです。 | 電話 |
payout.failed | Stripeが集めたお金があなたの銀行に届きません。給与支払いも資金繰りの計算も、いま全部ずれています。 | 電話 |
invoice.payment_failed | 非自発的チャーン。自然に解消することも多いので電話はやりすぎですが、金額の大きいアカウントはその日のうちに見る価値があります。 | 時間指定 |
customer.subscription.deleted | 自発的チャーン。今日知れば十分で、起こされるほどではありません。 | 通常 |
payment_intent.succeeded | 何も壊れていません。他の5つを無視する習慣を作るのが、まさにこのイベントです。 | 通知しない |
これらの段階はEchobellの3つの通知タイプに対応します。通常は普通のプッシュ、時間指定はほとんどの集中モードを突破し、電話は着信として表示されるためおやすみモードでも鳴ります。購読者はチャンネルごとに自分のレベルを選べるので、共同創業者はチャージバックを電話で、サポート担当は同じものをプッシュで受け取れます。
必要なもの
- WorkbenchのWebhooksタブにアクセスできるStripeアカウント
- Echobellのインストール(App Store / Google Play)
- 5分。サーバーもデプロイもコードも不要です——署名検証が欲しい場合を除いて。それは最後の節で扱います。
ステップ1 — イベントタイプごとに1チャンネル
「Stripe」という1つのチャンネルを作って全部そこに流したくなります。やめましょう。チャージバック、請求書、入金はそれぞれ違うオブジェクトを運ぶので、本文テンプレートもダッシュボードのリンクも別物になります——そして、この構成の目的はまさに、何も開かずに通知だけで何が起きたか分かることにあります。
イベント名でチャンネルを作ります:Stripe Disputes。タイトルと本文のテンプレートは、ロック画面で読み切れる形に:
Title: 🔴 Dispute opened — {{data.object.reason}}
Body: Amount: {{data.object.amount}} {{data.object.currency}}
Charge: {{data.object.charge}}
Status: {{data.object.status}}
詳細設定でリンクテンプレートを設定し、通知レコードから正しいページが開くようにします:
https://dashboard.stripe.com/disputes/{{data.object.id}}
そして自分を電話タイプで購読し、チャンネル詳細画面からwebhook URLをコピーします。https://hook.echobell.one/t/<channel-token> のような形です。
ステップ2 — Stripeをそのチャンネルに向ける
StripeダッシュボードでWebhooksタブを開き、イベント宛先を作成します:
Create an event destination をクリックし、Your account を選び、APIバージョンはアカウントの既定のままにします。
イベントタイプはちょうど1つだけ選びます——このチャンネルなら charge.dispute.created。統合が必要とするイベントだけを購読するのはStripe自身の推奨でもあり、ここではテンプレートを正直に保つ効果もあります。届くペイロードがすべて同じ形になるからです。
宛先タイプに Webhook endpoint を選び、EchobellのチャンネルURLを貼り付けます。
保存してから Send test event——あるいはCLIの stripe trigger charge.dispute.created——で、実際に電話が鳴ることを確認します。
作成したチャンネルごとに繰り返します。Stripeは1アカウントあたり16個までwebhookエンドポイントを許可しているので、アラート段階ごとに1つずつでも十分足ります。
ステップ3 — 実際に届くもの
StripeはEventオブジェクトをJSONでPOSTします。Echobellはボディをそのまま読むので、すべてのフィールドがテンプレートと条件からドット記法で参照できます:
{
"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 }
}
}
}
このペイロードで意外に思われる点が3つあります。
金額は最小通貨単位の整数です。 amount が 4900 なら $49.00 です。Echobellのテンプレートは値の埋め込みと比較はしますが算術はしないので、{{data.object.amount}} は 4900 と表示されます。単位を正直にラベル付けする(Amount: 4900 (cents))か、最後の節のフォワーダーで送信前に100で割ってください。
タイムスタンプはUnix秒です。 {{data.object.evidence_details.due_by}} は日付ではなく 1759449600 になります。正確な時刻より締め切りの存在自体が重要なら、テンプレートから外し——チャージバックのページに表示されています——リンクテンプレートに仕事をさせましょう。
フィールド名はオブジェクトごとに違います。 チャージバックは amount、請求書は amount_due・customer_email・attempt_count・hosted_invoice_url、入金は failure_message と arrival_date、早期不正警告は fraud_type・actionable、そして charge が単なる文字列IDです。存在しない変数はエラーではなく空文字列として描画されるため、別のチャンネルからコピーしたテンプレートは静かに壊れます。これが「イベントタイプごとに1チャンネル」の実務的な理由です。
ステップ4 — 意志ではなく条件で絞る
チャンネル条件はテンプレートと同じ式構文を波括弧なしで使い、配信の前に評価されます。
すべてのStripeチャンネルに設定すべきもの:
livemode == true
テストモードのトラフィック——自分で走らせた stripe trigger、同僚がサンドボックスでいじっているもの——はもう手元に届きません。これは配線が動くことを確認した後に追加してください。前ではなく。
請求書失敗のチャンネルでは、しきい値が小口アカウントをあなたの夜から締め出します:
livemode == true && data.object.amount_due > 20000
セント単位で「$200超」と読みます。初回の失敗すべてではなく、本当に止まっているリトライだけを見たいなら:
livemode == true && data.object.attempt_count > 1
もし1つのエンドポイントで複数のイベントタイプを1チャンネルに向けてしまった場合も、条件で分け直せます:
type == "charge.dispute.created" || type == "payout.failed"
ステップ5 — 通知経路を「壊れるもの」の外に置く
ここがテンプレートより価値のある部分です。
本番のwebhookエンドポイントはフルフィルメントが起きる場所です。アクセスを付与し、DBに書き込み、領収書メールを送ります。つまりそれは、アプリが落ちたときに一緒に落ちるエンドポイントでもあります。落ちると、Stripeは指数バックオフで最大3日間リトライし、メールを送ってきます——そして未配信webhookのメールは他のStripeメールと見た目がまったく同じなので、見つかるのは月曜になります。
webhookが静かに死ぬ原因は地味です。Stripeは 3xx リダイレクトを失敗として扱うので、http を https に飛ばし始めたり末尾スラッシュを付け始めたりするエンドポイントはイベントを受け取らなくなります。TLS 1.2以上が必要なので、証明書の期限切れや設定ミスだけで十分です。先週誰かが追加したWAFルールの 403 でも同じです。
Echobellに直接向けた2つ目のエンドポイントは、そのどれとも運命を共にしません。別ホストの別URLで、証明書も別物で、あなたのアプリが倒れている間も鳴り続けます。この規則は一般化できます。何かが壊れたと知らせる経路は、その壊れたものを経由すべきではない。
とはいえ、自分のエンドポイントの失敗も見えてほしいはずです。おかしいと感じたらWorkbenchの Event deliveries タブを確認してください——イベントごとに Delivered・Pending・Failed と、各試行のHTTPステータスが表示されます。Stripeはダッシュボードから15日以内、CLIの stripe events resend なら30日以内のイベントを再送できるので、2週間以内に気づいた穴は埋められます。
Stripe-Signature ヘッダーを誰も検証しないということで、URLの流出はデータ漏洩ではなく「偽アラート製造機」を生みます。リポジトリやスクリーンショットに残さず、漏れたら Reset Token を使ってください。このトレードオフが気になるなら次の節へ。任意 — 先に署名を検証する
Stripeの署名を実際に検証し、金額を通貨らしく整形したいなら、手前に小さなフォワーダーを置きます。次のCloudflare Workerはイベントを検証し、Stripeの要求どおり即座に 200 を返し、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 });
},
};
整形をコード側で済ませたので、向こう側のテンプレートはずっときれいになります:
Title: 💳 Payment failed — {{currency}} {{amount}}
Body: Customer: {{customer}}
Attempt #{{attempt}}
externalLink は特殊変数です。リンクテンプレートが未設定なら、Echobellはこれを通知レコードのリンクとして使うので、ホスト型請求書ページがワンタップで開きます。
このトレードオフの形に注意してください。これはそれ自体が失敗しうるインフラであり、まさにステップ5が警告していたものです。妥当な妥協は、偽アラートが煩わしくなる大量配信のチャンネルでは署名を検証し、チャージバックのチャンネルは直結のままにすること。そこでは誤った着信のコストは一瞬の困惑で、逃したときのコストはチャージバックされた金額です。
この構成で得られないもの
- オンコールのローテーションとエスカレーションはありません。 電話チャンネルを購読している全員が同時に鳴ります。4人なら利点、40人なら問題です。40人ならインシデント管理プラットフォームが要ります。
- 重複排除はありません。 Stripeはイベント順序を保証せず、同じイベントを複数回配信することがあります。1件のチャージバックで2回鳴ることはあり得ます。
- 確認応答はありません。 誰かが見たという記録も、誰も反応しなかったときに次の人へ上げる仕組みもありません。
- 「決済が止まった」アラートはありません。 Stripeは何かが起きたときにイベントを出し、止まったときには出しません。チェックアウトが壊れれば、イベントは一つも発火しません。これには自分側で定期ジョブを走らせ、直近1時間の決済件数がゼロならチャンネルを叩く——cronベースのデッドマンスイッチが必要です。
トラブルシューティング
Stripeでは 200 なのに通知が来ない。 Echobellは配信しない場合でもJSONボディ付きで 200 を返します——Event deliveriesタブでレスポンスボディを見てください。長さが正しいトークンで success: false なら、チャンネルトークンが違います。success が true なら、原因はたいてい条件です。livemode == true は設計どおりテストイベントをすべて止めます。
Stripeが 405 Method Not Allowed を報告する。 チャンネルのPOST Onlyがオンで、何かがGETを送りました。Stripe自身は常にPOSTなので、これはリンクプレビューかブラウザのタブであってStripeではありません。
通知は来るがフィールドが空。 テンプレートが違うオブジェクトを参照しています——請求書チャンネルで {{data.object.amount}} と書いているが、そこでのフィールドは amount_due です。実イベントを1件送り、ダッシュボードで開いてJSONを読んでください。
数週間動いた後に配信が失敗し始めた。 証明書と、URLの手前にあるリダイレクトを確認してください。チャンネルURL直結ではまれですが、自分でデプロイしたフォワーダーなら第一の容疑者です。
よくある質問
チャージバックが起きたときStripeは電話をかけてくれますか?
単体ではできません。Stripeはメール、ダッシュボード、charge.dispute.created イベント、そしてStripe Dashboardアプリを使っていればプッシュで通知します。実際に鳴らすには、そのイベントを購読タイプが電話のチャンネルへルーティングします。
StripeとEchobellをつなぐのにコードは必要ですか?
いいえ。Stripeは公開されたHTTPS URLならどこへでもJSONをPOSTし、EchobellのチャンネルURLはまさにそれです。コードが要るのは、Stripe-Signature ヘッダーを検証したいときと、金額を整形したいときだけです。
サードパーティのwebhook URLをStripeに渡して安全ですか?
意識的なトレードオフです。Stripeが送るペイロードには顧客と決済のメタデータが含まれ、Echobellは生のwebhookペイロードを永続保存しません——描画された通知はあなたの端末に残ります。手放すのは署名検証です。URLを知った人は説得力のある偽アラートを送れます。APIキーと同じ扱いにし、検証したいものにはフォワーダー方式を使ってください。
アラートに $49.00 ではなく 4900 と出るのはなぜ?
Stripeは最小通貨単位の整数で金額を送り、Echobellのテンプレートは算術をしません。テンプレートに単位を書くか、フォワーダーで100で割ってから送ってください。
テストモードのイベントで起こされないようにするには?
チャンネルに条件 livemode == true を追加します。Stripeはサンドボックスと stripe trigger のイベントをすべて livemode: false としてマークします。
共同創業者はシート課金なしで同じアラートを受け取れますか?
はい。チャンネルのリンクを共有すれば、購読者それぞれが自分の通知タイプを選べます。一人はチャージバックを電話で、もう一人は通常プッシュで受け取れ、購読者ごとの課金はありません。
決済成功にもアラートを付けるべき?
短期間だけ、しかも1件1件がまだ「出来事」でいられるくらい事業が小さいうちだけです。成功通知が日常になった瞬間から、それは本当に重要な通知への反応を削り始めます——アラート疲れの中核にある仕組みです。
まとめ
構成の全体は、Echobellチャンネル1つにつきStripeのイベント宛先を1つ、livemode == true の条件を1行、そして電話という段階をタイマー付きのイベントのために取っておく規律です。チャージバックと早期不正警告にはタイマーがあります。$9プランの更新失敗にはありません。それがあるふりをすることが、本当にタイマーの付いた一件を寝過ごす原因になります。
iPhoneにEchobellをダウンロード、またはGoogle Playで入手して、まずチャージバックのチャンネルを作り、本番の何かをこの経路に託す前に stripe trigger charge.dispute.created を1回流してください。