How to use Mailchimp REST-API with PHP the right way in 2018

Hagen Hübel
6 min readAug 31, 2018

--

In order to use Mailchimp’s Rest API to populate specific custom mailing lists with new members one might run into trouble nowadays. Maybe it has something to do with their GDPR-implementation, maybe not. However, due to a lack of documentation it took me many hours ‘til days to investigate the root cause and how to solve the problem. In this particular case I developed in PHP, but I believe that the actual problem is a more basic one.

The problem is, that while we were able to add members to custom mailing lists or campaigns the way we’ve got used to it over the past ten years, all these newly added members were then marked as dead entries with a so-called “opted out” state. That is, opted out of everything by default:

Having this state on these member records, nothing can be sent out to them. Not even a single email.

When my client was asking me why the member is opted out completely, i only shrugged my shoulders. I really had no clue what was going on here since I have never seen these things before. Okay, to be honest, I had actually no clue in general about Mailchimp and how to use it properly from the perspective of an end-user or in particular a marketeer, but as a developer I am usually able to read API-references and I am almost always able to implement those specifications. Moreover, I have worked with Mailchimp’s API some years ago (2014–2016) in two different projects without these complications.

Let’s dive into the details:

Since there will always be a slightly danger to get opted out of Mailchimp of behalf of Mailchimp itself without any further notice, I’ve built an abstraction layer for being able to switch to another solution provider at any time without having any impact to our application.

In order to deal with Mailchimp official Rest-API, I decided to use for DrewM\MailChimp\MailChimp. It calls itself the “Super-simple, minimum abstraction MailChimp API v3 wrapper” and I can confirm their slogan.

My method to add a subscriber to a specific Mailchimp list looks as follows:

/**
* Add subscriber to a specific list
*
*
@param string $email
*
@param string $listId
*
@param array $mergeFields
*
@return array
*/
public function addSubscriber($email, $listId, array $mergeFields = [])
{

$data = [
'email_address' => $email,
'status' => 'subscribed',
];

if(!empty($mergeFields)) {
$data['merge_fields'] = $mergeFields;
}

$result = $this->mailchimp->post('lists/' . $listId . '/members', $data);

// ATTENTION: sometimes, Mailchimp API returns array for success, sometimes an object

if (is_object($result)) {
if (!empty($result->id)) {
return [
'state' => 'success',
'id' => $result->id,
];
}
}

if (is_array($result)) {
if (!empty($result['id'])) {
return [
'state' => 'success',
'id' => $result['id'],
];
}
if (isset($result['title']) && strtolower($result['title']) === 'member exists') {
return [
'state' => 'success',
'reason' => 'already exists',
];
}
return [
'state' => 'error',
'reason' => isset($result['detail']) ? $result['detail'] : 'There is an unknown error occurred. Please try again.',
];
}

return [
'state' => 'error',
'reason' => 'There is an unknown error occurred. Please try again!'
];

}

Since I stumbled over different result sets, which are sometimes (and in a seemingly random manner) either an array or sometimes an object, I took these impacts into account and prepared the code for such circumstances.

But besides checking for valid results, this code is following their official API: it simply makes a POST-request to the endpoint “/lists/$listId/members” with a few properties.

At the end of the day, what we get here is: opted out users! Nobody will tell you that all these submitted users are dead entries due to the opted-out state here!

Where the heck do these “opted out”-features come from?

First I’ve studied their official Rest-API references several times and found out the property “interests”. But, to make it short: it won’t help here.
The API-reference is also lacking here and expects further knowledge how Mailchimp works in general and what is meant by “interests” in particular. But this doesn’t matter anyways for our case.

To narrow down our main issue and to learn their internal datasets I requested a record of a newly created list-member with the following API-call:

public function optInListMember($listId, $memberHash)
{
$result = $this->mailchimp->get("lists/$listId/members/$memberHash");

$memberHash is the id-value that we’ll receive from the POST-request after we have submitted the member to a specific list:

$data = $this->newsletterUtils->addSubscriber($customer->getEmail(), $listId, $mergeFields);

if (isset($data['id'])) {
$memberHash = $data['id'];

The response of our GET-request looks as follows:

array(22) {
["id"]=>
string(32) "e7f13f927bxxxxxxxxx36a70290775b35"
["email_address"]=>
string(25) "tester18@xxxxxxxx.com"
["unique_email_id"]=>
string(10) "b6c108xxxx"
["email_type"]=>
string(4) "html"
["status"]=>
string(10) "subscribed"
["merge_fields"]=>
array(4) {
["FNAME"]=>
string(4) "Test"
["LNAME"]=>
string(6) "Tester"
["ADDRESS"]=>
string(0) ""
["PHONE"]=>
string(0) ""
}
["stats"]=>
array(2) {
["avg_open_rate"]=>
int(0)
["avg_click_rate"]=>
int(0)
}
["ip_signup"]=>
string(0) ""
["timestamp_signup"]=>
string(0) ""
["ip_opt"]=>
string(13) "xxx.2x2.74.xxx"
["timestamp_opt"]=>
string(25) "2018-08-31T21:07:49+00:00"
["member_rating"]=>
int(2)
["last_changed"]=>
string(25) "2018-08-31T21:07:49+00:00"
["language"]=>
string(0) ""
["vip"]=>
bool(false)
["email_client"]=>
string(0) ""
["location"]=>
array(6) {
["latitude"]=>
int(0)
["longitude"]=>
int(0)
["gmtoff"]=>
int(0)
["dstoff"]=>
int(0)
["country_code"]=>
string(0) ""
["timezone"]=>
string(0) ""
}
["marketing_permissions"]=>
array(3) {
[0]=>
array(3) {
["marketing_permission_id"]=>
string(10) "d2f2xxxx0e0"
["text"]=>
string(5) "Email"
["enabled"]=>
bool(false)
}
[1]=>
array(3) {
["marketing_permission_id"]=>
string(10) "296exxxxd"
["text"]=>
string(11) "Direct Mail"
["enabled"]=>
bool(false)
}
[2]=>
array(3) {
["marketing_permission_id"]=>
string(10) "3d8xxxxb3"
["text"]=>
string(29) "Customized Online Advertising"
["enabled"]=>
bool(false)
}
}

Besides all these already known or expected keys something new should get our attention: “marketing_permissions”.

Do you remember our first result?

When we compare the labels in quotation marks in the screenshot, we’ll find their corresponding counterparts also in our API-response before. All these “marketing_permissions”-items from the API-response are the key to our success to post members on Mailchimp-list that will be opted in then!

But there is still something to be considered: every single entry in the marketing_permissons-array contains a value to an unknown “marketing_permission_id”, which will be first created after the initial POST-request, when a user was first sent to the mailing-list via API.

Therefore we won’t be able to opt-in the user included into our first API-call right away. Instead, after we’ve posted the user to the mailing-list, we are required to fetch its data again per its member-hash to be able to retrieve the ID’s of each marketing_permissions-record:

public function optInListMember($listId, $memberHash) {
$result = $this->mailchimp->get("lists/$listId/members/$memberHash");

if(isset($result['marketing_permissions'])) {
$perms = $result['marketing_permissions'];

$data = ['marketing_permissions' => []];
foreach($perms as $perm) {
$data['marketing_permissions'][] = [
'marketing_permission_id' => $perm['marketing_permission_id'],
'enabled' => true
];
}

$this->mailchimp->patch('lists/' . $listId . '/members/' . $memberHash, $data);
}
}

This way we can send then another API-request to Mailchimp and thus enable all these options, that were disabled per default but will be enabled afterwards.

We’ll get this now:

The member was opted-in.

Challenge overcome, mission accomplished :)

Another important aspect to consider is to store the $memberHash in the user-table in our database! This way we are always able to refetch any states whether the user has opted out somewhere or not and to implement an unsubscribe-solution by ourselves as well.

--

--