Create and send custom Mailchimp email campaigns from WordPress

There are lots of Mailchimp + WordPress integrations out there already. But when I wanted to create a customized, automated daily email campaign that would be generated from WordPress content (beyond just a listing of recent posts) and sent to a Mailchimp list, I couldn’t find anything to do just that.

For a little while I used MailChimp’s ability to read RSS feeds and generate campaigns from them, and I built a custom RSS feed on my WordPress site where the latest entry in the feed was the customized content I wanted to be populated into the *|RSSITEM:CONTENT_FULL|* variable to then go out to my subscribers.

But this was unreliable — sometimes the message wouldn’t go out because of caching issues on my end, mysterious days where MailChimp didn’t seem to check the feed, and there were other quirks — and even working with MailChimp support I couldn’t get things to a stable state. I also didn’t like that MailChimp’s system forced me to pick a top-of-the-hour time during the day when the feed would be checked, and if for some reason it was missed, I had to reconfigure the whole campaign to get the message out.

I needed a better way.

The resulting method that’s been working for almost a year now is to initiate and send the Mailchimp campaign directly from within WordPress. I get full control of scheduling, message generation and formatting. I can re-run the campaign send if I need to. And it has worked reliably every day. Here’s how I set it up (with inspiration from this blog post by Isabel Castillo).

MailChimp setup

On the MailChimp side of things, I needed to generate an API key and set up a template to use for the campaigns I would be sending. Note that the custom template method used likely requires a paid MailChimp plan.

API key generation is fast and easy: Account -> Extras -> API keys -> Create a Key.

Template setup is also relatively fast (and was especially so for me because I already had a template in place from my attempts with RSS feed campaign triggering): Campaigns -> Templates -> Create a Template.

The main trick is that after you use whatever fancy tools MailChimp offers you for template creation, you need to then bring that into a new custom template as HTML code (“code your own”), so that you have full control over the contents of the generated messages using the MailChimp API. But I do recommend starting with one of MailChimp’s visually built templates so that you know you’re getting all the basics of what can go into a campaign.

In my case, I only had a few things about the generated campaign I would need to customize with each sending: the subject line, the internal campaign title, and one chunk of content in the middle of the template. So as a part of creating and saving that “code your own” template, I have this section:

  <table align="left" border="0" cellpadding="0" cellspacing="0" style="max-width:100%;min-width:100%;" width="100%" class="mcnTextContentContainer">
	<tbody>
	  <tr>
		<td mc:edit="dailynewscontent" valign="top" class="mcnTextContent dailynewscontent" style="padding-top:0;padding-right:18px;padding-bottom:9px;padding-left:18px;"> </td>
	  </tr>
	</tbody>
  </table>

The key part is the mc:edit="dailynewscontent", which is essentially establishing a variable (I called it “dailynewscontent”, you can call it whatever) you can populate later via API call.

No need to set up an actual campaign or automation on the MailChimp side; that’s all going to be handled in WordPress.

The last things to grab from Mailchimp are the ID of the template you just created (usually visible in the template view/edit URL) and the ID of the audience/list you want to send to (available from Audience -> Manage Audience -> Settings -> Audience name and campaign defaults -> Audience ID).

WordPress functionality

My hope has been to release this functionality as a standard WordPress plugin, but I haven’t had the opportunity to do that yet. So for now, I’m pasting in pieces of code that I use in a custom theme/plugin setup on my site. You can adapt this accordingly to your own site.

I’m using this simple MailChimp API library from Drew McLellan to help. I put it in a lib/ directory and include it like so, as well as creating a helper function to instantiate the API client:

public function load_dependencies() {
	// Load the Mailchimp API Library
	if ( file_exists( get_stylesheet_directory() . '/lib/mailchimp-api/src/MailChimp.php' ) ) {
		require_once get_stylesheet_directory() . '/lib/mailchimp-api/src/MailChimp.php';
	}
}

public function get_mailchimp_api() {
	$mailchimp_api_key = get_option( 'my_mailchimp_token' );

	if ( ! empty( $mailchimp_api_key ) ) {
		return new \DrewM\MailChimp\MailChimp( $mailchimp_api_key );
	} else {
		return false;
	}
}

You can see that I’m getting the MailChimp API key from a WordPress option entry, which elsewhere I’ve configured the WordPress customizer to allow setting as a theme option, for my own convenience. I did the same with the MailChimp audience/list ID.

Here’s the function that actually sends the email out via the MailChimp API:

public function send_mailchimp_email() {

	$error_logfile = get_stylesheet_directory() . '/mailchimp-debug.log';

	$newsletter_last_sent_time = get_transient( 'my_newsletter_last_sent' );
	$current_time              = wp_date( 'U' );

	$mailchimp_list_id = get_option( 'my_newsletter_mailchimp_list_id' );

	if ( empty( $mailchimp_list_id ) ) {
		$error_message = '[' . date( 'F j, Y, g:i a e O' ) . ']' . 'No list ID specified.' . PHP_EOL;
		// @codingStandardsIgnoreLine
		error_log( $error_message, 3, $error_logfile );
		return false;
	}

	if ( ( $current_time - $newsletter_last_sent_time ) < ( 22 * HOUR_IN_SECONDS ) ) {
		$error_message = '[' . date( 'F j, Y, g:i a e O' ) . ']' . 'It has not been long enough since the last newsletter send' . PHP_EOL;
		// @codingStandardsIgnoreLine
		error_log( $error_message, 3, $error_logfile );
		return false;
	}

	// Generate the content we're sending from WordPress
	$newsletter_dynamic_content = $this->generate_newsletter_content();

	if ( 50 > strlen( $newsletter_dynamic_content ) ) {
		$error_message = '[' . date( 'F j, Y, g:i a e O' ) . ']' . 'Newsletter content is not long enough to be valid, exiting' . PHP_EOL;
		// @codingStandardsIgnoreLine
		error_log( $error_message, 3, $error_logfile );
		return false;
	}

	// Connect to Mailchimp
	$mailchimp = $this->get_mailchimp_api();

	// Create a Mailchimp campaign
	$today_string          = wp_date( 'D, d M Y' );
	$today_day_only_string = wp_date( 'l' );

	$campaign_data = array(
		'type'       => 'regular',
		'recipients' => array( 'list_id' => $mailchimp_list_id ),
		'settings'   => array(
			'title'        => 'My Daily Newsletter for ' . $today_string,
			'subject_line' => 'My Daily News Summary for ' . $today_day_only_string,
			'reply_to'     => 'my@emailaddress.com',
			'from_name'    => 'My Site Name',
		),
	);

	$new_campaign = $mailchimp->post( 'campaigns', $campaign_data );

	if ( ! isset( $new_campaign['id'], $new_campaign['status'] ) || ! $mailchimp->success() || ( 'save' !== $new_campaign['status'] ) ) {
		$error_message = '[' . date( 'F j, Y, g:i a e O' ) . ']' . 'Error when creating campaign: ' . $mailchimp->getLastError() . PHP_EOL;
		// @codingStandardsIgnoreLine
		error_log( $error_message, 3, $error_logfile );
		return false;
	}

	// Then, set the campaign content
	$template_content = array(
		'template' => array(
			'id'       => '123456789',
			'sections' => array(
				'dailynewscontent' => $newsletter_dynamic_content,
			),
		),
	);

	$campaign_content_endpoint = 'campaigns/' . $new_campaign['id'] . '/content';

	$updated_campaign = $mailchimp->put( $campaign_content_endpoint, $template_content );

	if ( ! $mailchimp->success() || ! isset( $updated_campaign['html'] ) ) {
		$error_message = '[' . date( 'F j, Y, g:i a e O' ) . ']' . 'Error when adding campaign content: ' . $mailchimp->getLastError() . PHP_EOL;
		// @codingStandardsIgnoreLine
		error_log( $error_message, 3, $error_logfile );
		return false;
	}

	// Then send it
	$sent_campaign = $mailchimp->post( 'campaigns/' . $new_campaign['id'] . '/actions/send' );

	// Set a transient to prevent duplicates, even if the above send failed
	set_transient( 'my_newsletter_last_sent', wp_date( 'U' ) );

	if ( ! $mailchimp->success() ) {
		$error_message = '[' . date( 'F j, Y, g:i a e O' ) . ']' . 'There was a problem sending the campaign: ' . $mailchimp->getLastError() . PHP_EOL;
		// @codingStandardsIgnoreLine
		error_log( $error_message, 3, $error_logfile );
		return false;
	}

	return true;

}

I know there are some rough spots and DRY violations in there, please be kind. But here’s essentially what it does:

  • Setup and variable fetching
  • Checks to make sure the basics are in place (list ID, that a campaign hasn’t gone out in the last day or so)
  • Generates the HTML content from another function in the class, generate_newsletter_content()
  • Checks to make sure there’s a reasonable amount of content that was generated
  • Connects to the API
  • Sets the properties of a new campaign
  • Sets the content of the campaign, including the list ID and the desired value of the mc:edit variable we defined above.
  • Create the campaign and check for errors
  • Send the campaign and check for errors

That’s the bulk of the work right there.

All put together, here’s roughly what my custom plugin class looks like:

<?php

class Newsletter {

	public function __construct() {
		$this->load_dependencies();

		add_action( 'init', array( $this, 'init' ), 1 );
	}

	public function init() {
		// set up cron schedule
		if ( ! wp_next_scheduled( 'newsletter_event_hook' ) ) {
			$site_timezone_string = get_option( 'timezone_string' );
			wp_schedule_event( strtotime( 'Tomorrow 5 AM ' . $site_timezone_string ), 'daily', 'newsletter_event_hook' );
		}

		add_action( 'newsletter_event_hook', array( $this, 'send_mailchimp_email' ) );
	}

	public function load_dependencies() {
		...
	}

	public function get_mailchimp_api() {
		...
	}

	public function send_mailchimp_email() {
		...
	}

	public function generate_newsletter_content() {
		...
	}

}

$newsletter = new Newsletter();

Then I just include that plugin file from somewhere else in the main logic (maybe functions.php) and I’m all set.

As a further convenience, in a separate class where I define some WP-CLI commands, I create some commands that allow me to manually initiate the sending of the campaign, or to just see the contents of the email first:

/**
 * Run the Mailchimp campaign
 *
 * @subcommand send-daily-newsletter
 */
public function send_daily_newsletter() {
	$newsletter = new Newsletter();

	$result = $newsletter->send_mailchimp_email();

	if ( $result ) {
		WP_CLI::success( 'I think I did something good.' );
	} else {
		WP_CLI::error( 'Something did not work.' );
	}
}

/**
 * Display the Mailchimp campaign
 *
 * @subcommand show-daily-newsletter
 */
public function show_daily_newsletter() {
	$newsletter = new Newsletter();

	$result = $newsletter->generate_newsletter_content();

	if ( $result ) {
		WP_CLI::log( $result );
	} else {
		WP_CLI::error( 'Something did not work.' );
	}
}

Again, you can adapt that for your needs accordingly.

Wrap Up

The end result is a MailChimp campaign generated on my specified schedule (every day at 5AM in this case) with my custom content. No worries about RSS feed fetching, and no bloat from more complicated newsletter plugins or other integrations.

If you’re curious about the site where this runs, it’s a local news aggregation tool I created for the community where I live, and I blogged about it here.

I hope this helps. If you have suggestions for improvement or ideas about ways to solve this kind of issue, please share in the comments!

Updated Dec 25 to note that the custom Mailchimp template mentioned in this article requires a paid plan.

Published by

Chris Hardie

I'm a deep generalist, currently focused on software engineering + writing + the open web.

2 thoughts on “Create and send custom Mailchimp email campaigns from WordPress”

  1. Chris, thanks for writing this, totally saved me a ton of time. The only thing I would add is for someone starting out and on a free MC account you’ll need to upgrade in order to add the template. For some reason their editor will allow you to add the mc:edit tags in the drag and drop editor and it stores them, but it parses them out when the form is actually used. I couldn’t figure out why it wasn’t working until I exported the template only to realize the mc:edit tags had been stripped.

Leave a Reply

Your email address will not be published. Required fields are marked *