Send reminders to MailChimp list members who did not confirm double opt-in

Activate your pending list subscriptions!

MailChimp is a great starting point to handle your newsletters, some basic marketing automation and even more advanced topics like segmenting based on e-commerce data.

Unfortunately, the statistics regarding your list are not that transparant. It’s easy to get the amount of subscriptions on your list, but how many customers are unregistered, cleaned (bounced emails) or pending?

Pending means that the customer originally registered for your newsletter, but afterwards never confirmed the double opt-in request. The request might have ended in their spam folder, or they simply forgot. Anyhow, it’s a lost subscription.

Often websites offer their customers a — let’s say — 10% discount code when registering for the newsletter. This coupon code is only sent out as soon as the customer confirms the double opt-in e-mail, but in the scenario above this will never happen. Result: unhappy customer.

MailChimp has a lot of Automation options, but unfortunately none of them offer the mechanism to remind customers about their pending subscription. It can only done by hand, customer after customer. Luckily the MailChimp API offers a solution.

The example below will show you how to find out:

  • The amount of customers per status (so you know how many are pending)
  • A way to send a reminder to customers that are pending
  • Extend that filter to only remind customers that registered more than a month ago

I’ll be using PHP & a open-source library installable through Composer. I’ll also supply links to the appropriate MailChimp API documentation, so it should be fairly portable to other programming languages.

Installation

So, let’s kick off installing a lightweight MailChimp API v3 client:

composer require pacely/mailchimp-apiv3

It offers support for Laravel Service Providers & Facades out of the box.

MailChimp offers 4 statuses for list subscriptions: subscribed, unsubscribed, cleaned & pending. When supplying e-commerce data to the API, a fifth ‘transactional’ status might be available. I’ll skip this last status in this example.

Total customers per status

First we would like to know the amount of customers per status, using the /lists API endpoint:

// Adjust to your wishes
$apiKey = '{yourApiKey}';
$listId = '{yourListIdHere}';
$statusesToCheck = ['subscribed', 'unsubscribed', 'cleaned', 'pending'];

// Get data
$mc = new \Mailchimp\Mailchimp($apiKey);
$count = [];
foreach($statuses as $status) {
$result = $mc->get('/lists/' . $listId . '/members', [
'status' => $status,
]);
/** @var \Illuminate\Support\Collection $result */
$count[$status] = $result->get('total_items');
}
print_r($count);

Chances are high at least one customer has the status ‘pending’.

Remind the customer about pending status

The trick now basically is to loop over all the customers with status ‘pending’ and submit them to the API with the same status. MailChimp will send out the opt-in e-mail again and your customers might notice the reminder. Again, we use the /lists API endpoint.

// Loop over 'pending' list members in small batches of 50 members
$offset = 0;
$perRun = 50;
do {
/** @var \Illuminate\Support\Collection $result */
$result = $mc->get('/lists/' . $listId . '/members', [
'count' => $perRun,
'offset' => $offset,
'status' => 'pending',
]);

// Only get members of result object
$members = new \Illuminate\Support\Collection($result->get('members'));
    // Increment offset for next run
$offset += $perRun;

// For each result, we now submit the 'pending' request
// And after each request we take a small break
$members->each(function (stdClass $member) use ($mc, $listId) {
$url = '/lists/' . $listId . '/members/' . $member->id;
$mc->put($url, [
'status' => 'pending',
]);

sleep(1); // Sleep 1 second to not overload the API
});
} while ($members->count() === $perRun);

In steps:

  • Get list members of the specific list in small chunks of 50 entries
  • For each member, a PUT request is sent to the API setting the status to ‘pending’ (pending -> pending)
  • Sleep 1 second not to hit API rate limits
  • Stop the loop as soon as the API returned less than 50 entries

Note that you can also supply merge fields in the PUT request. This might be the correct moment to send the customer a new discount code, as the old discount code might have expired.

Filter on subscription date

Optional it might be required to filter based on registration dates. Unfortunately the API doesn’t allow to filter only for members that registered before a certain date. You probably don’t want to remind customers that registered today for example. Fortunately, the API does return the registration date in the result object, so we will perform our filtering using PHP. It’s suboptimal, but there seems no other way.

First, define the date you would like to filter on:

// Define a filter date
$timezone = new \DateTimeZone('Europe/Amsterdam');
$filterDate = new \DateTime();
$filterDate->setTimezone($timezone)->modify('-1 month');

And then filter the $members array based on your wishes:

// First we filter results, then we process them as done before
$members->filter(function (stdClass $member) use ($filterDate, $timezone) {
$signupDate = \DateTime::createFromFormat(DateTime::ISO8601, $member->timestamp_signup);
if (!$signupDate) { // It happens that it's not set I noticed..
return false;
}

$signupDate->setTimezone($timezone);
if ($signupDate > $filterDate) {
return false;
}

return true;
})->each(function (stdClass $member) use ($listId, $mc) {
$url = '/lists/' . $listId . '/members/' . $member->id;
$mc->put($url, [
'status' => 'pending',
]);

sleep(1); // Sleep 1 second to not overload the API
]));
});

Batch Operations

Worth noting is that the MailChimp API also offers a method to handle requests in batches. According to your list size and the amount of PUT request you might need to do, it might be interesting to save up all the PUT requests in a queue and send them to MailChimp at once using Batch Operations.

To speed up, an example using Batch Operations might look like:

// Loop over 'pending' entries in small batches of 50 members
$offset = 0;
$perRun = 50;
$batch = new \Illuminate\Support\Collection();
do {
/** @var \Illuminate\Support\Collection $result */
$result = $mc->get('/lists/' . $listId . '/members', [
'count' => $perRun,
'offset' => $offset,
'status' => 'pending',
]);

// Only get members of result object
$members = new \Illuminate\Support\Collection($result->get('members'));
    // Increment offset for next run
$offset += $perRun;

// For each result, we add it to the batch operation collection
$members->each(function (stdClass $member) use ($listId, $batch) {
$batch->push(new \ArrayObject([
'method' => 'PUT',
'path' => '/lists/' . $listId . '/members/' . $member->id,
'body' => json_encode([
'status' => 'pending',
]),
]));
});
} while ($listMembers->count() === $perRun);

// If batch items, send them at once
if (!$batch->isEmpty()) {
// Send operations
$result = $mc->post('/batches', [
'operations' => $batch->toArray()
]);

// Ask for updates until its completed
while ($result->get('status') !== 'finished') {
sleep(5); // Wait 5 seconds between status updates
$result = $this->mc->get('/batches/' . $operation->get('id'));
}

// Show result
// Use 'response_body_url' to parse response
print_r($res);
}

There are several flavours possible how to handle the processing, but I hope this example gives you an impression how to get your amount of ‘pending’ customers after sending them a reminder.