Churn is always an issue for subscription based businesses. Involuntary churn, churn caused by failed payments, is a quicker issue to solve than voluntary churn, churn caused by cancellations. Services like Churn Buster help solve involuntary churn by recovering failed payments. Recoveries are made through payment retries, as well as emails to the customer requesting an update to their payment info.
WooCommerce Subscriptions is the most common way to implement subscription payments on WordPress. Churn Buster doesn’t have an available integration with WooCommerce Subscriptions, but you can use their API to integrate it yourself. This guide will walk you through the required steps so that you can get started with Churn Buster more quickly. Read the whole guide first to gain an understanding, then go back through and implement.
All of this will take place in your functions.php file. Make sure to backup your site before editing the functions.php file. If you’re not comfortable editing your functions.php file, send an email to dylan [at] fetch.software and I can do it for you.
We’ll be using Actions to trigger a function that will send a request to Churn Buster’s API. Each request will have a different purpose:
- to notify of a failed payment
- to notify of a successful payment
- to notify of a cancellation.
Note: The last type of Churn Buster API request is to notify of a change in a customer’s payment information. WooCommerce does not have an Action that triggers upon a change in a customer’s payment information, so we won’t be implementing that request. Churn Buster is able to function without it.
Failed Renewal Payment
Let’s start with notification of failed payment. The Action that we’ll be using to trigger that request is “woocommerce_subscription_renewal_payment_failed”. It will trigger a callback function that we will name “subscription_renewal_payment_failed_callback”. Here’s the code to implement that Action:
add_action('woocommerce_subscription_renewal_payment_failed',
'subscription_renewal_payment_failed_callback', 9, 2);
Here’s the code for the callback function:
function subscription_renewal_payment_failed_callback( $subscription, $new_status) {
//error_log( current_action() . " Executing", 0);
$url = "https://api.churnbuster.io/v1/failed_payments";
$auth = churn_buster_auth();
$body_array = create_churn_buster_body_array_payment( $subscription );
$result = make_churn_buster_api_call( $url, $auth, $body_array );
/*error_log("Subscription Payment Failed Callback Result - "
. print_r($result,1) . " - " . print_r($body_array,1), 0);*/
}
Notice that the callback function is calling other functions that we have yet to implement, I’ll walk you through the execution. The other 2 callback functions that we’ll implement will be very similar.
First, the url for the relevant API endpoint is stored in a variable. Then we store our API credentials in a variable using the following function:
function churn_buster_auth() {
return base64_encode( "Your Account ID" . ":" . "Your API Key" );
}
Then we create the body of our request using the following function:
function create_churn_buster_body_array_payment( $subscription ) {
return $body_array = array(
"payment" => array(
"source" => "in_house",
"source_id" => $subscription->get_id(),
"amount_in_cents" => round( 100 * $subscription->get_total() ),
"currency" => "USD"
),
"customer" => array(
"source" => "in_house",
"source_id" => $subscription->get_customer_id(),
"email" => $subscription->get_billing_email(),
"properties" => array(
"first_name" => $subscription->get_billing_first_name(),
"last_name" => $subscription->get_billing_last_name()
),
),
);
}
The function gets passed the $subscription object, then uses the object’s getter functions to put the subscription information into the json object that the API needs in order to properly handle the request.
The url, credentials, and request body are then passed to a function which makes the API call:
function make_churn_buster_api_call( $url, $auth, $body_array ) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,$url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER,
array("Authorization: Basic " . $auth,
"Content-Type: application/json"));
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body_array));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
curl_close ($ch);
return $result;
}
This function uses curl to create a POST request with the necessary headers and then send it to Churn Buster. WordPress has its own built in POST request function, wp_remote_post, but I was unable to use it to properly authenticate with the API. Curl worked for me. Some hosts cannot use curl, but since it worked for me I stuck with it.
That completes the payment failed notification. Churn Buster will now be notified of failed subscription renewal payments. We have 2 more notifications to implement.
Successful Renewal Payment
Next we’ll implement the subscription payment complete notification. It’s nearly identical to the subscription payment failed notification.
The Action that we’ll be using to trigger that request is “woocommerce_subscription_renewal_payment_complete”. It will trigger a callback function that we will name “subscription_renewal_complete_callback”. Here’s the code to implement that Action:
add_action('woocommerce_subscription_renewal_payment_complete',
'subscription_renewal_complete_callback', 9, 2);
Here’s the code for the callback function:
function subscription_renewal_complete_callback( $subscription, $last_order ) {
//error_log( current_action() . " Executing", 0);
$url = "https://api.churnbuster.io/v1/successful_payments";
$auth = churn_buster_auth();
$body_array = create_churn_buster_body_array_payment( $subscription );
$result = make_churn_buster_api_call( $url, $auth, $body_array );
/*error_log("Subscription Renewal Complete Callback Result - "
. print_r($result,1) . " - " . print_r($body_array,1), 0);*/
}
The only difference from the payment failed callback are the url and the second parameter. Everything else is the same. Since we’ve already gone over that functionality, we can move on to the last request type.
Subscription Cancelled
Lastly, we’ll implement the cancellation notification. It’s slightly different than the other two. The Action that we’ll be using to trigger that request is “woocommerce_subscription_status_cancelled”. It will trigger a callback function that we will name “subscription_cancelled_callback”. Here’s the code to implement that Action:
add_action('woocommerce_subscription_status_cancelled',
'subscription_cancelled_callback', 9, 1);
Here’s the code for the callback function:
function subscription_cancelled_callback( $subscription ) {
//error_log( current_action() . " Executing", 0);
$url = "https://api.churnbuster.io/v1/cancellations";
$auth = churn_buster_auth();
$body_array = create_churn_buster_body_array_cancellation( $subscription );
$result = make_churn_buster_api_call( $url, $auth, $body_array );
/*error_log("Subscription Cancelled Callback Result - "
. print_r($result,1) . " - " . print_r($body_array,1), 0);*/
}
Notice that this function uses a different function to create the body of the request. This endpoint takes a different format of json object, so we use a different function to create it:
function create_churn_buster_body_array_cancellation( $subscription ) {
return $body_array = array(
"subscription" => array(
"source" => "in_house",
"source_id" => $subscription->get_id()
),
"customer" => array(
"source" => "in_house",
"source_id" => $subscription->get_customer_id(),
"email" => $subscription->get_billing_email(),
"properties" => array(
"first_name" => $subscription->get_billing_first_name(),
"last_name" => $subscription->get_billing_last_name()
),
),
);
}
For this request, amount and currency are not required, and the name of the first array is changed from “payment” to “subscription”.
Note: You’ll notice there’s a lot of duplicate code between these three callback functions. It is possible to handle all three situations with one callback function. The WordPress function current_action() will print the name of the action that called the callback function, so you can set the correct url, and use the correct function to create the request body. I think it’s best practice to reduce the number of paths through a function when possible to make the function more testable. Even though we did not write tests for these functions, I like to use best practices whenever possible.
Subscription Retry Rules
Lastly, we’ll change WooCommerce’s payment retry rules to Churn Buster’s initial recommendation using the following code:
function churn_buster_retry_rules( $default_retry_rules_array ) {
return array(
array(
'retry_after_interval' => 2 * DAY_IN_SECONDS,
'email_template_customer' => '',
'email_template_admin' => '',
'status_to_apply_to_order' => 'pending',
'status_to_apply_to_subscription' => 'active',
),
array(
'retry_after_interval' => 4 * DAY_IN_SECONDS,
'email_template_customer' => '',
'email_template_admin' => '',
'status_to_apply_to_order' => 'pending',
'status_to_apply_to_subscription' => 'active',
),
array(
'retry_after_interval' => 7 * DAY_IN_SECONDS,
'email_template_customer' => '',
'email_template_admin' => '',
'status_to_apply_to_order' => 'pending',
'status_to_apply_to_subscription' => 'on-hold',
),
array(
'retry_after_interval' => 6 * DAY_IN_SECONDS,
'email_template_customer' => '',
'email_template_admin' => '',
'status_to_apply_to_order' => 'pending',
'status_to_apply_to_subscription' => 'on-hold',
),
array(
'retry_after_interval' => 5 * DAY_IN_SECONDS,
'email_template_customer' => '',
'email_template_admin' => '',
'status_to_apply_to_order' => 'pending',
'status_to_apply_to_subscription' => 'on-hold',
),
array(
'retry_after_interval' => 4 * DAY_IN_SECONDS,
'email_template_customer' => '',
'email_template_admin' => '',
'status_to_apply_to_order' => 'pending',
'status_to_apply_to_subscription' => 'on-hold',
),
);
}
add_filter( 'wcs_default_retry_rules', 'churn_buster_retry_rules' );
Note that this function also deactivate’s WooCommerce’s payment failed emails. This is necessary because Churn Buster will be sending its own emails to customers who fail payments.
Put It All Together
Now that we’ve gone through, here is all of the code together for you to copy and paste into your functions.php file:
/*
* Churn Buster Integration
*/
function create_churn_buster_body_array_payment( $subscription ) {
return $body_array = array(
"payment" => array(
"source" => "in_house",
"source_id" => $subscription->get_id(),
"amount_in_cents" => round( 100 * $subscription->get_total() ),
"currency" => "USD"
),
"customer" => array(
"source" => "in_house",
"source_id" => $subscription->get_customer_id(),
"email" => $subscription->get_billing_email(),
"properties" => array(
"first_name" => $subscription->get_billing_first_name(),
"last_name" => $subscription->get_billing_last_name()
),
),
);
}
function create_churn_buster_body_array_cancellation( $subscription ) {
return $body_array = array(
"subscription" => array(
"source" => "in_house",
"source_id" => $subscription->get_id()
),
"customer" => array(
"source" => "in_house",
"source_id" => $subscription->get_customer_id(),
"email" => $subscription->get_billing_email(),
"properties" => array(
"first_name" => $subscription->get_billing_first_name(),
"last_name" => $subscription->get_billing_last_name()
),
),
);
}
function make_churn_buster_api_call( $url, $auth, $body_array ) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,$url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER,
array("Authorization: Basic " . $auth,
"Content-Type: application/json"));
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body_array));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
curl_close ($ch);
return $result;
}
function churn_buster_auth() {
return base64_encode( "Your Account ID" . ":" . "Your API Key" );
}
function subscription_renewal_payment_failed_callback( $subscription, $new_status) {
//error_log( current_action() . " Executing", 0);
$url = "https://api.churnbuster.io/v1/failed_payments";
$auth = churn_buster_auth();
$body_array = create_churn_buster_body_array_payment( $subscription );
$result = make_churn_buster_api_call( $url, $auth, $body_array );
/*error_log("Subscription Payment Failed Callback Result - "
. print_r($result,1) . " - " . print_r($body_array,1), 0);*/
}
function subscription_renewal_complete_callback( $subscription, $last_order ) {
//error_log( current_action() . " Executing", 0);
$url = "https://api.churnbuster.io/v1/successful_payments";
$auth = churn_buster_auth();
$body_array = create_churn_buster_body_array_payment( $subscription );
$result = make_churn_buster_api_call( $url, $auth, $body_array );
/*error_log("Subscription Renewal Complete Callback Result - "
. print_r($result,1) . " - " . print_r($body_array,1), 0);*/
}
function subscription_cancelled_callback( $subscription ) {
//error_log( current_action() . " Executing", 0);
$url = "https://api.churnbuster.io/v1/cancellations";
$auth = churn_buster_auth();
$body_array = create_churn_buster_body_array_cancellation( $subscription );
$result = make_churn_buster_api_call( $url, $auth, $body_array );
/*error_log("Subscription Cancelled Callback Result - "
. print_r($result,1) . " - " . print_r($body_array,1), 0);*/
}
add_action('woocommerce_subscription_renewal_payment_failed',
'subscription_renewal_payment_failed_callback', 9, 2);
add_action('woocommerce_subscription_renewal_payment_complete',
'subscription_renewal_complete_callback', 9, 2);
add_action('woocommerce_subscription_status_cancelled',
'subscription_cancelled_callback', 9, 1);
/**
* Add custom subscription retry rules
*/
function churn_buster_retry_rules( $default_retry_rules_array ) {
return array(
array(
'retry_after_interval' => 2 * DAY_IN_SECONDS,
'email_template_customer' => '',
'email_template_admin' => '',
'status_to_apply_to_order' => 'pending',
'status_to_apply_to_subscription' => 'active',
),
array(
'retry_after_interval' => 4 * DAY_IN_SECONDS,
'email_template_customer' => '',
'email_template_admin' => '',
'status_to_apply_to_order' => 'pending',
'status_to_apply_to_subscription' => 'active',
),
array(
'retry_after_interval' => 7 * DAY_IN_SECONDS,
'email_template_customer' => '',
'email_template_admin' => '',
'status_to_apply_to_order' => 'pending',
'status_to_apply_to_subscription' => 'on-hold',
),
array(
'retry_after_interval' => 6 * DAY_IN_SECONDS,
'email_template_customer' => '',
'email_template_admin' => '',
'status_to_apply_to_order' => 'pending',
'status_to_apply_to_subscription' => 'on-hold',
),
array(
'retry_after_interval' => 5 * DAY_IN_SECONDS,
'email_template_customer' => '',
'email_template_admin' => '',
'status_to_apply_to_order' => 'pending',
'status_to_apply_to_subscription' => 'on-hold',
),
array(
'retry_after_interval' => 4 * DAY_IN_SECONDS,
'email_template_customer' => '',
'email_template_admin' => '',
'status_to_apply_to_order' => 'pending',
'status_to_apply_to_subscription' => 'on-hold',
),
);
}
add_filter( 'wcs_default_retry_rules', 'churn_buster_retry_rules' );
Conclusion
Remember to insert your test API key and account id into the churn_buster_auth() function. When you’re ready to go live you’ll switch the test key to your live key.
Each of the callback functions has debug statements that are commented out. As long as your debug log is active you can remove the ‘/*’ and ‘*/’ to get some information for debugging purposes. Once you have everything working, add the ‘/*’ and ‘*/’ back to clean up your debug log.
I hope you find that helpful. I’m always happy to learn, so if you have any recommendations to improve this implementation feel free to comment. If I missed anything, or there’s something that you don’t understand, please comment with that information as well.
If all of that seems too complicated, send an email to dylan [at] fetch.software and I can do it for you.