Skip to main content

React to a scheduled plan change, and to the change itself

Premium Intermediate

A built-in-membership subscriber can move to another tier from their account page. A move down, or to a tier at the same price, lands at their next renewal, which can be a month or more away. Until then they keep everything their current tier gives them, so there are two moments, and two hooks:

Using the first for provisioning is how a downgrader loses access the day they ask instead of the day they stop paying.

An upgrade has one moment, not two. It is charged the day they ask and lands as soon as the payment succeeds, so the scheduled hook never fires for it — only benecaster_subscription_tier_changed, once the money is in. Nor do any of the benecaster_subscription_cadence_switch* hooks. A listener that waits for a scheduled hook before recording anything misses every upgrade; one that acts on tier_changed is already right for both. To tell an upgrade from a downgrade there, call benecaster_compare_tiers(), never the raw tier_order values.

The subscriber is still on their old tier when the scheduled hook fires. Use $to_tier_slug; looking up their current tier returns the one they’re leaving.

A scheduled move can be withdrawn. The subscriber can cancel it from their account page before it lands, and cancelling their membership withdraws it too. Both fire benecaster_subscription_cadence_switch_cancelled, so anything you record on the scheduled hook is provisional until the move lands or that hook undoes it.

benecaster_subscription_tier_changed isn’t only for this feature. It also fires for every other built-in-membership tier change, such as a plan changed in the Stripe dashboard, and for an admin moving somebody by hand. That makes it the right place for provisioning on a built-in-membership show, as long as the callback is correct whatever caused the change.

It does not fire for a plan change in MemberPress, Paid Memberships Pro, Restrict Content Pro or WooCommerce Subscriptions. Provisioning that must cover those shows too belongs on benecaster_subscription_tier_moved, which fires for every source — see Retag a subscriber in your email platform whenever their tier changes. Never put the same job on both hooks: on a built-in-membership show both fire for one move.

A subscriber can have only one pending change at a time, either a tier move or a billing-period switch, never both. An upgrade that also changes billing period is split: tier_changed fires for the plan when it is paid, then benecaster_subscription_cadence_switch_scheduled for the period, which lands at the renewal.

A move onto the show’s free plan has its own hooks. It fires benecaster_subscription_free_switch_scheduled when they ask and benecaster_subscription_free_switch_landed when it lands, just after benecaster_subscription_tier_changed. The tier-switch and billing-period hooks don’t fire for it. At Stripe’s level the request is cancel_at_period_end, because a free tier has no price to schedule onto, so anything you hang on the Stripe state, or on a Stripe webhook of your own, reports somebody leaving who is staying. They keep their account, their feed URL and the show’s free episodes, and benecaster_subscription_cancelled never fires for them.

A move onto the free plan can be withdrawn too, and it has its own undo hook: benecaster_subscription_free_switch_cancelled fires when the subscriber takes it back or cancels their membership outright instead. A paused membership is not a withdrawal. If the podcaster’s license lapses and billing pauses, the free move stays in place and still lands, and this hook does not fire.

The other direction never reaches a switch hook. A free member who takes out a paid plan is charged straight away through the ordinary signup form, so what fires is benecaster_subscription_activated, with $source 'resubscribe' because their free membership is replaced. Their feed URL stays the same, and benecaster_subscription_tier_changed doesn’t fire, so a listener that provisions only on that hook never hears about the upgrade. The code below handles it.

Code

<?php
// They ASKED to move down, or to a same-priced tier. Nothing has been charged
// and their access has not moved. An upgrade never fires this.
add_action(
    'benecaster_subscription_tier_switch_scheduled',
    function ( $user_id, $show_id, $from_tier_slug, $to_tier_slug, $switch_at ) {
        // Use $to_tier_slug: looking their tier up here returns $from_tier_slug.
        my_note_intent( $user_id, $to_tier_slug, $switch_at );
    },
    10,
    5
);

// They WITHDREW it before it landed (or their membership was cancelled).
add_action(
    'benecaster_subscription_cadence_switch_cancelled',
    function ( $user_id, $show_id, $to_price_id, $reason ) {
        my_clear_intent( $user_id, $show_id );
    },
    10,
    4
);

// It HAPPENED. Their tier, their feed and their badges have moved.
// An upgrade lands here the moment it is paid; anything else at the renewal.
add_action(
    'benecaster_subscription_tier_changed',
    function ( $user_id, $show_id, $old_tier_slug, $new_tier_slug ) {
        my_clear_intent( $user_id, $show_id );
        my_provision_tier_perks( $user_id, $new_tier_slug );
    },
    10,
    4
);

// They chose the FREE plan. Their paid subscription ends at $switch_at
// without charging again. Not a cancellation: they stay a member.
add_action(
    'benecaster_subscription_free_switch_scheduled',
    function ( $user_id, $show_id, $from_tier_slug, $to_tier_slug, $switch_at ) {
        // A win-back email sent here reaches somebody who is still a member.
        my_note_intent( $user_id, $to_tier_slug, $switch_at );
    },
    10,
    5
);

// The free move landed. benecaster_subscription_tier_changed has already
// fired above; this says why. Same feed URL.
add_action(
    'benecaster_subscription_free_switch_landed',
    function ( $user_id, $show_id, $from_tier_slug, $to_tier_slug ) {
        my_move_to_free_segment( $user_id, $from_tier_slug );
    },
    10,
    4
);

// They took the free move back, or cancelled the membership outright
// instead. Undo whatever the free_switch_scheduled callback recorded.
add_action(
    'benecaster_subscription_free_switch_cancelled',
    function ( $user_id, $show_id, $to_tier_slug, $reason ) {
        my_clear_intent( $user_id, $show_id );
    },
    10,
    4
);

// A free member took out a paid plan through signup. No switch hook fires,
// and neither does benecaster_subscription_tier_changed.
add_action(
    'benecaster_subscription_activated',
    function ( $user_id, $show_id, $tier_slug, $source ) {
        my_provision_tier_perks( $user_id, $tier_slug );
    },
    10,
    4
);

View on GitHub →

Hooks Used

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