Integrating a Payment Gateway with Milo Subscriptions

This guide is for developers who want an existing or custom WooCommerce payment gateway to handle subscription renewals and payment-method changes through Milo Subscriptions. Milo Payments uses these same hooks internally; the Any Gateway add-on is what unlocks non-Milo Payments gateways for subscription checkout.

Renewals

When a renewal is due, Milo Subscriptions creates a renewal order and looks up the subscription’s gateway. To charge the stored payment token yourself, hook the per-gateway action:

add_action(
    'milo_subscriptions_scheduled_payment_my_gateway',
    function ( $amount, $renewal_order, $subscription ) {
        // Charge $amount off-session using your stored token / customer id.
        // On success, mark the order paid:
        $renewal_order->payment_complete( $transaction_id );
        // On failure, leave the order unpaid; Milo records the failed renewal.
    },
    10,
    3
);

If your gateway has no dedicated handler but supports tokenization, Milo falls back to a best-effort token charge. If it can do neither, the renewal becomes a manual renewal (the subscription goes on hold and the customer is invoiced).

Change payment method

By default the My Account change-payment flow updates the WooCommerce payment token and the subscription’s payment method. That is enough for gateways whose recurring charge reads the standard WC token, but many gateways store their own recurring source (for example the official Stripe plugin charges _stripe_source_id) and must establish an off-session mandate when the card changes. Take over the flow with these hooks.

Declare that your gateway handles its own change-payment

add_filter(
    'milo_subscriptions_gateway_handles_change_payment',
    function ( $handles, $gateway_id, $subscription ) {
        return 'my_gateway' === $gateway_id ? true : $handles;
    },
    10,
    3
);

Render and process your own form

add_action(
    'milo_subscriptions_change_payment_form_my_gateway',
    function ( $subscription ) {
        // Output your secure card field / hosted form and handle the POST.
        // Establish the off-session mandate, then store the new source on the
        // subscription and call $subscription->save().
    }
);

Resolve a gateway for subscriptions with no stored method

If a subscription has an empty payment method (for example after a manual import), return the gateway that should own its change-payment flow:

add_filter(
    'milo_subscriptions_change_payment_gateway',
    function ( $gateway_id, $subscription ) {
        return $gateway_id ?: 'my_gateway';
    },
    10,
    2
);

Veto an unsafe token swap

For token-based gateways, the generic flow lets you block a naive token change that was never validated for off-session use, so the next renewal cannot be declined for a missing SCA mandate. Return true (or an error string) to block; leave it false if your gateway establishes the mandate at save time.

add_filter(
    'milo_subscriptions_block_unvalidated_token_change',
    function ( $block, $token, $subscription ) {
        if ( 'my_gateway' !== $token->get_gateway_id() ) {
            return $block;
        }
        return token_has_off_session_mandate( $token )
            ? false
            : __( 'Please re-confirm this card before using it for recurring payments.', 'my-plugin' );
    },
    10,
    3
);

Migration

When subscriptions are imported, the gateway each one renews through is chosen by the migration mapping. By default source gateways are remapped to Milo Payments; with the Any Gateway add-on they keep their existing gateway. See Migration and Payment Gateways for the milo_subscriptions_migrate_payment_method filter and the keep-source behavior.

Did this page miss something?

Tell us and we rewrite it, usually the same day.

Send a note