Skip to main content

benecaster_donation_payment_failed

Action Free

Fires on payment_intent.payment_failed for a Listener Support donation, after the failure has been counted towards the show’s rolling decline rate.

This action is the only way to observe a declined donation. Nothing is written to benecaster_listener_support_donations on a failure, deliberately: that table is money received, and every reader of it — the admin donation list, the Donations dashboard panel, the Analytics Dashboard add-on — would count a failure row as a donation. There is no failure table and none is planned, so if you do not capture it here it is not recoverable later.

It fires only for Listener Support donations. The handler ignores any Stripe intent without a benecaster_show_id in its metadata, so membership and buy-up failures on the same Stripe account do not reach it.

And it fires only when the show’s Stripe webhook is subscribed to payment_intent.payment_failed, not just payment_intent.succeeded. A show missing that event produces no declines, no counters and no alert — silence there is absence of data, not health.

See a declined donation, and tune the decline-rate alert

Free Intermediate

The action is the only way to observe a declined donation. Nothing is written to benecaster_listener_support_donations on a failure, deliberately: that table is money received, and every reader of it — the admin donation list, the Donations dashboard panel, the Analytics Dashboard add-on — would count a failure row as a donation. There is no failure table and none is planned.

It fires only for Listener Support donations — the handler ignores any Stripe intent without a benecaster_show_id, so membership and buy-up failures on the same Stripe account do not reach it. And it fires only when the show’s Stripe webhook is configured, because the event is the signal. A show with no webhook produces no declines, no counters and no alert — silence there is absence of data, not health.

Core counts both outcomes over a rolling 24 hours per show and warns the podcaster in the notification bell at ≥10 attempts and ≥40% declines. Tune it per show with benecaster_donation_decline_alert_thresholds.

Both halves of the threshold matter, and min_attempts is the one people delete. One genuinely expired card on a quiet day is a 100% decline rate. An alert that fires on that is an alert the podcaster learns to ignore, which costs more than it saves — core clamps the floor to a minimum of 1 for exactly that reason.

None of this detects card testing; it raises the cost and reports the shape. The thing that recognises an attack is Stripe Radar’s card-testing rules, on the podcaster’s own Stripe account. Any surface you build on this action should say so rather than reading as the defence.

<?php
/**
 * @param string $decline_code Stripe's `decline_code` when present
 *                             (`stolen_card`, `insufficient_funds`, ...),
 *                             otherwise its generic `code`, else ''.
 */
add_action( 'benecaster_donation_payment_failed', function ( int $show_id, string $intent_id, string $decline_code ): void {
    if ( in_array( $decline_code, [ 'stolen_card', 'lost_card', 'pickup_card' ], true ) ) {
        error_log( "benecaster: hostile decline on show {$show_id} ({$decline_code})" );
    }
}, 10, 3 );

add_filter( 'benecaster_donation_decline_alert_thresholds', function ( array $thresholds, int $show_id ): array {
    // A high-traffic show can afford to alert sooner.
    return 42 === $show_id
        ? [ 'min_attempts' => 25, 'rate' => 0.25 ]
        : $thresholds;
}, 10, 2 );

View on GitHub →

Parameters

Name Type Default Description
$show_id int ID of the show
$payment_intent_id string Stripe PaymentIntent ID, or empty string when the event carried none
$decline_code string Stripe's decline_code when present ('stolen_card', 'insufficient_funds', …), otherwise its generic code, otherwise an empty string.

Need this built rather than just documented? See our services →