Integrating Mailchimp with Django

Last updated August 10th, 2022

In this article, we'll look at how to integrate Mailchimp with Django in order to handle newsletter subscriptions and send transactional emails.

Contents

Objectives

By the end of this article, you'll be able to:

  1. Explain the differences between Mailchimp's Marketing and Transactional Email APIs
  2. Integrate Mailchimp's Marketing API with Django
  3. Manage Mailchimp audiences and contacts
  4. Set up and use merge fields to send personalized campaigns
  5. Use Mailchimp's Transactional Email API to send transactional emails

What is Mailchimp?

Mailchimp is a marketing automation platform that allows you to create, send, and analyze email and ad campaigns. Additionally, it allows you to manage contacts, create custom email templates, and generate reports. It's one of the most popular email marketing solutions used by businesses.

Mailchimp offers the following APIs for developers:

  1. Marketing API
  2. Transactional Email API (a.k.a, Mandrill)
  3. Mailchimp Open Commerce

Marketing API vs Transactional Email API

The Marketing and Transactional Email APIs can both be used for sending emails... so what's the difference?

The Marketing API is used for sending bulk emails usually for marketing purposes. Its uses include newsletters, product promotions, and welcome series.

The Transactional Email API, on the other hand, is used for sending emails to a single recipient after they trigger an action. Its uses include account creation emails, order notifications, and password reset emails.

For detailed explanation check out the official documentation.

In the first part of the article, we'll use the Marketing API to create a newsletter. After that, we'll demonstrate how to send emails via the Transactional Email API.

Project Setup

Create a new project directory along with a new Django project called djangomailchimp:

$ mkdir django-mailchimp && cd django-mailchimp
$ python3.10 -m venv env
$ source env/bin/activate

(env)$ pip install django==4.1
(env)$ django-admin startproject djangomailchimp .

Feel free to swap out virtualenv and Pip for Poetry or Pipenv. For more, review Modern Python Environments.

Next, migrate the database:

(env)$ python manage.py migrate

And that's it for the basic setup.

Mailchimp Marketing API

In this section, we'll use the Marketing API to create a newsletter.

With a free Mailchimp account, you can have up to 2,000 contacts and send up to 10,000 emails monthly (max 2,000 daily).

Setup

For organizational purposes let's create a new Django app called marketing:

(env)$ python manage.py startapp marketing

Add the app to the INSTALLED_APPS configuration in settings.py:

# djangomailchimp/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'marketing.apps.MarketingConfig',  # new
]

Next, update the project-level urls.py with the marketing app:

# djangomailchimp/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('marketing/', include('marketing.urls')),  # new
]

Create a urls.py file within the marketing app and populate it:

# marketing/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('', views.subscribe_view, name='subscribe'),
    path('success/', views.subscribe_success_view, name='subscribe-success'),
    path('fail/', views.subscribe_fail_view, name='subscribe-fail'),
    path('unsubscribe/', views.unsubscribe_view, name='unsubscribe'),
    path('unsubscribe/success/', views.unsubscribe_success_view, name='unsubscribe-success'),
    path('unsubscribe/fail/', views.unsubscribe_fail_view, name='unsubscribe-fail'),
]

Take note of the URLs. They should be pretty self-explanatory:

  • The index will display the subscription form.
  • If the user subscribes successfully they'll be redirected to /success, otherwise to /fail.
  • Unsubscribe URLs work in the same way.

Before working on to the views, let's create a new file called forms.py:

# marketing/forms.py

from django import forms


class EmailForm(forms.Form):
    email = forms.EmailField(label='Email', max_length=128)

We created a simple EmailForm that will be used to collect contact data when users subscribe to the newsletter.

Feel free to add optional information that you'd like to collect, like first_name, last_name, address, phone_number, and so on.

Now add the following to views.py:

# marketing/views.py

from django.http import JsonResponse
from django.shortcuts import render, redirect

from djangomailchimp import settings
from marketing.forms import EmailForm


def subscribe_view(request):
    if request.method == 'POST':
        form = EmailForm(request.POST)
        if form.is_valid():
            form_email = form.cleaned_data['email']
            # TODO: use Mailchimp API to subscribe
            return redirect('subscribe-success')

    return render(request, 'subscribe.html', {
        'form': EmailForm(),
    })


def subscribe_success_view(request):
    return render(request, 'message.html', {
        'title': 'Successfully subscribed',
        'message': 'Yay, you have been successfully subscribed to our mailing list.',
    })


def subscribe_fail_view(request):
    return render(request, 'message.html', {
        'title': 'Failed to subscribe',
        'message': 'Oops, something went wrong.',
    })


def unsubscribe_view(request):
    if request.method == 'POST':
        form = EmailForm(request.POST)
        if form.is_valid():
            form_email = form.cleaned_data['email']
            # TODO: use Mailchimp API to unsubscribe
            return redirect('unsubscribe-success')

    return render(request, 'unsubscribe.html', {
        'form': EmailForm(),
    })


def unsubscribe_success_view(request):
    return render(request, 'message.html', {
        'title': 'Successfully unsubscribed',
        'message': 'You have been successfully unsubscribed from our mailing list.',
    })


def unsubscribe_fail_view(request):
    return render(request, 'message.html', {
        'title': 'Failed to unsubscribe',
        'message': 'Oops, something went wrong.',
    })

Next, let's accompany these views with the HTML templates. Create a "templates" folder in your root directory. Then add the following files...

subscribe.html:

<!-- templates/subscribe.html -->

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Django + Mailchimp</title>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" crossorigin="anonymous">
        <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js" crossorigin="anonymous"></script>
    </head>
    <body>
        <div class="container mt-5">
            <h1>Subscribe</h1>
            <p>Enter your email address to subscribe to our mailing list.</p>
            <form method="post">
                {% csrf_token %}
                {{ form.as_p }}
                <button type="submit" class="btn btn-primary">Subscribe</button>
            </form>
            <p class="mt-2"><a href="{% url "unsubscribe" %}">Unsubscribe form</a></p>
        </div>
    </body>
</html>

unsubscribe.html:

<!-- templates/unsubscribe.html -->

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Django + Mailchimp</title>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" crossorigin="anonymous">
        <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js" crossorigin="anonymous"></script>
    </head>
    <body>
        <div class="container mt-5">
            <h1>Unsubscribe</h1>
            <p>Enter your email address to unsubscribe from our mailing list.</p>
            <form method="post">
                {% csrf_token %}
                {{ form.as_p }}
                <button type="submit" class="btn btn-danger">Unsubscribe</button>
            </form>
            <p class="mt-2"><a href="{% url "subscribe" %}">Subscribe form</a></p>
        </div>
    </body>
</html>

message.html:

<!-- templates/message.html -->

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Django + Mailchimp</title>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" crossorigin="anonymous">
        <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js" crossorigin="anonymous"></script>
    </head>
    <body>
        <div class="container mt-5">
            <h1>{{ title }}</h1>
            <p>{{ message }}</p>
        </div>
    </body>
</html>

Make sure to update the settings.py file so Django knows to look for a "templates" folder:

# djangomailchimp/settings.py


TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': ['templates'], # new
        ...

Finally, run the runserver command to start Django's local web server:

(env)$ python manage.py runserver

Navigate to http://localhost:8000/marketing/ in your browser. You should see the subscription form:

Django + Mailchimp Marketing Index

By clicking on the "Unsubscribe form" anchor, you'll be redirected to the unsubscribe form.

Great! We're done with the Django setup.

Add Mailchimp Marketing Client

First, install the mailchimp-marketing package, the official client library for the Mailchimp Marketing API:

(env)$ pip install mailchimp-marketing==3.0.75

Next, we need to create/obtain an API key.

If you don't already have a Mailchimp account, go ahead and sign up.

After you've logged into your Mailchimp account, navigate to Account API Keys by clicking the link or by clicking on your account (bottom left) > "Account & billing":

Mailchimp API Keys 1

Then click "Extras" > "API keys":

Mailchimp API Keys 2

Lastly, click on "Create A Key" to generate an API key:

Mailchimp API Key Create

After you've got your API key, copy it.

To use the Marketing API, we also need to know our region. The easiest way to figure it out is by looking at the start of the Mailchimp URL.

For example:

https://us8.admin.mailchimp.com/
        ^^^

In my case, the region is: us8.

Mailchimp API Key Copy

Store the Mailchimp API key and the region at the bottom of settings.py like so:

# djangomailchimp/settings.py

MAILCHIMP_API_KEY = '<your mailchimp api key>'
MAILCHIMP_REGION = '<your mailchimp region>'

Next, we'll initialize the Marketing API client in marketing/views.py and create an endpoint that pings the API:

# marketing/views.py

mailchimp = Client()
mailchimp.set_config({
  'api_key': settings.MAILCHIMP_API_KEY,
  'server': settings.MAILCHIMP_REGION,
})


def mailchimp_ping_view(request):
    response = mailchimp.ping.get()
    return JsonResponse(response)

Don't forget to import Client at the top of the file:

from mailchimp_marketing import Client

Register the newly created endpoint in marketing/urls.py:

# marketing/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('ping/', views.mailchimp_ping_view),  # new
    path('', views.subscribe_view, name='subscribe'),
    path('success/', views.subscribe_success_view, name='subscribe-success'),
    path('fail/', views.subscribe_fail_view, name='subscribe-fail'),
    path('unsubscribe/', views.unsubscribe_view, name='unsubscribe'),
    path('unsubscribe/success/', views.unsubscribe_success_view, name='unsubscribe-success'),
    path('unsubscribe/fail/', views.unsubscribe_fail_view, name='unsubscribe-fail'),
]

Run the server again and visit http://localhost:8000/marketing/ping/ to see if you can ping the API.

{
    "health_status": "Everything's Chimpy!"
}

If you see the message above then everything works as expected.

Create an Audience

To create a newsletter, we'll need to use an audience. An audience (or a list) is a list of contacts to which you can send campaign emails.

Mailchimp's free plan only allows you to have one (default) audience. If you have a paid plan, feel free to create an audience specifically for this demo newsletter. You can create one via the web interface or programmatically.

Navigate to your Mailchimp dashboard. Under "Audience", click on "All contacts". Then click on "Settings" > "Audience name and defaults":

Mailchimp - All contacts

On the right side of the screen, copy the "Audience ID":

Mailchimp - audience

Paste it at the end of settings.py, under the API key and region:

# djangomailchimp/settings.py

MAILCHIMP_API_KEY = '<your mailchimp api key>'
MAILCHIMP_REGION = '<your mailchimp region>'
MAILCHIMP_MARKETING_AUDIENCE_ID = '<your mailchimp audience id>'  # new

In the next section, we'll add users to the audience.

Subscribe View

Head over to marketing/views.py and replace the subscribe_view with the following code:

# marketing/views.py

def subscribe_view(request):
    if request.method == 'POST':
        form = EmailForm(request.POST)
        if form.is_valid():
            try:
                form_email = form.cleaned_data['email']
                member_info = {
                    'email_address': form_email,
                    'status': 'subscribed',
                }
                response = mailchimp.lists.add_list_member(
                    settings.MAILCHIMP_MARKETING_AUDIENCE_ID,
                    member_info,
                )
                logger.info(f'API call successful: {response}')
                return redirect('subscribe-success')

            except ApiClientError as error:
                logger.error(f'An exception occurred: {error.text}')
                return redirect('subscribe-fail')

    return render(request, 'subscribe.html', {
        'form': EmailForm(),
    })

Don't forget to import ApiClientError like so:

from mailchimp_marketing.api_client import ApiClientError

Also, add the logger:

import logging


logger = logging.getLogger(__name__)

So, we first created a dictionary that contains all the user information we want to store. The dictionary needs to contain email and status. There are multiple status types that we can use, but most importantly:

  1. subscribed - immediately adds the contact
  2. pending - user will receive a verification email before being added as a contact

If we want to append additional user information, we have to use merge fields. Merge fields later allow us to send personalized emails. They're composed of a name and a type (e.g., text, number, address).

The default merge fields are: ADDRESS, BIRTHDAY, FNAME, LNAME, PHONE.

If you want to use custom merge fields you can add them using the Mailchimp dashboard or via the Marketing API.

Let's hardcode the user's first name and last name to member_info:

member_info = {
    'email_address': form_email,
    'status': 'subscribed',
    'merge_fields': {
      'FNAME': 'Elliot',
      'LNAME': 'Alderson',
    }
}

When sending campaigns, you can access the merge fields, via placeholders -- e.g., *|FNAME|* will be replaced with Elliot.

If you added additional fields to the EmailForm in the first step, feel free to append them as merge fields as shown in the example.

More information about the merge fields can be found in the official docs.

lists.add_list_member() API reference

And we are done with the subscribe view. Let's test it.

Run the server and navigate to http://localhost:8000/marketing/. Then, use the subscribe form to subscribe. You should get redirected to success/:

Successfully subscribed

Next, open your Mailchimp dashboard and navigate to "Audience" > "All contacts". You should be able to see your first newsletter subscriber:

Mailchimp - All contacts new subscriber

Unsubscribe View

To enable the unsubscribe functionality, replace the unsubscribe_view with the following code:

# marketing/views.py

def unsubscribe_view(request):
    if request.method == 'POST':
        form = EmailForm(request.POST)
        if form.is_valid():
            try:
                form_email = form.cleaned_data['email']
                form_email_hash = hashlib.md5(form_email.encode('utf-8').lower()).hexdigest()
                member_update = {
                    'status': 'unsubscribed',
                }
                response = mailchimp.lists.update_list_member(
                    settings.MAILCHIMP_MARKETING_AUDIENCE_ID,
                    form_email_hash,
                    member_update,
                )
                logger.info(f'API call successful: {response}')
                return redirect('unsubscribe-success')

            except ApiClientError as error:
                logger.error(f'An exception occurred: {error.text}')
                return redirect('unsubscribe-fail')

    return render(request, 'unsubscribe.html', {
        'form': EmailForm(),
    })

Here, we:

  1. Obtained the user's email via the form.
  2. Used md5 to hash the user's email and to generate a subscriber hash. The hash allows us to manipulate the user data.
  3. Created a dictionary called member_update with all the data we want to change. In our case, we just changed the status.
  4. Passed all this data to lists.update_list_member() -- and voila!

You can use the same method to change other user data, like merge fields.

Add the import:

import hashlib

Let's test if everything works.

Run the server again, navigate to http://localhost:8000/marketing/unsubscribe/ and use the form to unsubscribe. Open "All contacts" and you'll notice that the status changed from "Subscribed" to "Unsubscribed":

Mailchimp - Unsubscribe status

lists.update_list_member() API reference

Fetch Subscription Information

Here's a code example of how to fetch a user's subscription status:

member_email = '[email protected]'
member_email_hash = hashlib.md5(member_email.encode('utf-8').lower()).hexdigest()

try:
    response = mailchimp.lists.get_list_member(
        settings.MAILCHIMP_MARKETING_AUDIENCE_ID,
        member_email_hash
    )
    print(f'API call successful: {response}')
except ApiClientError as error:
    print(f'An exception occurred: {error.text}')

As we saw in the previous section, whenever we want to fetch/modify some user, we need to hash their email and feed it to the API.

The response will look something like this:

{
   "id": "f4ce663018fefacfe5c327869be7485d",
   "email_address": "[email protected]",
   "unique_email_id": "ec7bdadf19",
   "contact_id": "67851f34b33195292b2977590007e965",
   "full_name": "Elliot Alderson",
   "web_id": 585506089,
   "email_type": "html",
   "status": "unsubscribed",
   "unsubscribe_reason": "N/A (Unsubscribed by admin)",
   "consents_to_one_to_one_messaging": true,
   "merge_fields": {
      "FNAME": "Elliot",
      "LNAME": "Alderson",
      "ADDRESS": "",
      "PHONE": "",
      "BIRTHDAY": ""
   },

   ...

}

lists.get_list_member() API reference

--

Our newsletter is more or less done now. We created an audience and enabled subscribe and unsubscribe functionality. Now, the only thing left to do is to get a few actual users and start sending campaign emails!

Mailchimp Transactional API

In this section, we'll demonstrate how you can use the Mailchimp Transactional Email API to send transactional emails.

With a free Mailchimp Transactional account/Mandrill account, you can send up to 500 test emails. The test emails can only be sent to verified domains, though.

To use the Mailchimp Transactional Email API, you'll need to own a domain name and have access to its advanced DNS settings.

Setup

For organizational purposes, let's create another Django app called transactional:

(env)$ python manage.py startapp transactional

Add the app to the INSTALLED_APPS configuration in settings.py:

# djangomailchimp/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'marketing.apps.MarketingConfig',
    'transactional.apps.TransactionalConfig',  # new
]

Update the project-level urls.py with the transactional app:

# djangomailchimp/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('marketing/', include('marketing.urls')),
    path('transactional/', include('transactional.urls')),  # new
]

Now, create a urls.py file within the transactional app:

# transactional/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('send/', views.send_view, name='mailchimp-send'),
]

Next, create a send_view in transactional/views.py:

# transactional/views.py

from django.http import JsonResponse


def send_view(request):
    return JsonResponse({
        'detail': 'This view is going to send an email.',
    })

Run the server and navigate to http://localhost:8000/transactional/send/. You should get this response:

{
    "detail": "This view is going to send an email."
}

Add Transactional Email Client

Next, let's install the mailchimp-transactional package:

pip install mailchimp-transactional==1.0.47

This package makes it easy to send requests to the Transactional Email API.

If you don't already have a Mailchimp account, go ahead and sign up.

We now need to create a new transactional API key.

Log in to your Mailchimp account, go to "Automations" > "Transactional Email" and then press "Launch" (top right of the window):

Mailchimp Transactional Email

After that, you'll be redirected to Mandrill where you'll be asked you if you want to log in with your account. Press "Log in using Mailchimp".

Then navigate to "Settings" and click on "Add API key":

Mandrill Settings

After the key gets generated, copy it:

Mandrill API Key

Store the generated key at the bottom of settings.py like so:

# djangomailchimp/settings.py

MAILCHIMP_MARKETING_API_KEY = '<your mailchimp marketing api key>'
MAILCHIMP_MARKETING_REGION = '<your mailchimp marketing region>'
MAILCHIMP_MARKETING_AUDIENCE_ID = '<your mailchimp audience id>'

MAILCHIMP_TRANSACTIONAL_API_KEY = '<your mailchimp transactional api key>'  # new

Next, go back to the Mandrill settings, click on "Domains" and then "Sending domains".

Mandrill Sending Domains

Add your domain and verify the ownership either via a TXT record or email. After you verify the domain ownership, you should also add TXT records for DKIM and SPF settings. Next, click on the "Test DNS Settings" to see if everything works:

Mandrill All Good

There should be only green ticks.

Next, let's initialize the Mailchimp Transactional Email client in transactional/views.py and create an endpoint to test if we can ping the API successfully:

import mailchimp_transactional
from django.http import JsonResponse
from mailchimp_transactional.api_client import ApiClientError

from djangomailchimp import settings

mailchimp = mailchimp_transactional.Client(
    api_key=settings.MAILCHIMP_TRANSACTIONAL_API_KEY,
)


def mailchimp_transactional_ping_view(request):
    try:
        mailchimp.users.ping()
        return JsonResponse({
            'detail': 'Everything is working fine',
        })
    except ApiClientError as error:
        return JsonResponse({
            'detail': 'Something went wrong',
            'error': error.text,
        })

Make sure not to forget any imports.

Register the newly created URL in urls.py:

# transactional/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('ping/', views.mailchimp_transactional_ping_view),  # new
    path('send/', views.send_view, name='mailchimp-send'),
]

Run the server again and visit http://localhost:8000/transactional/ping/ to see if the ping goes through.

{
    "detail": "Everything is working fine"
}

If you see the message above then everything works as expected.

Send a Transactional Email

Replace our send_view with the following code:

def send_view(request):
    message = {
        'from_email': '<YOUR_SENDER_EMAIL>',
        'subject': 'My First Email',
        'text': 'Hey there, this email has been sent via Mailchimp Transactional API.',
        'to': [
            {
                'email': '<YOUR_RECIPIENT_EMAIL>',
                'type': 'to'
            },
        ]
    }
    try:
        response = mailchimp.messages.send({
            'message': message,
        })
        return JsonResponse({
            'detail': 'Email has been sent',
            'response': response,
        })
    except ApiClientError as error:
        return JsonResponse({
            'detail': 'Something went wrong',
            'error': error.text,
        })

Make sure to replace <YOUR_SENDER_EMAIL> and <YOUR_RECIPIENT_EMAIL> with your actual email addresses. Also feel free to change the subject and/or text.

Here, we:

  1. Created a dictionary that contains all the email information.
  2. Passed the newly created dictionary to mailchimp.messages.send.

You should always wrap it in a try/except in case something goes wrong.

If you're on a free Mailchimp plan and your emails are rejected with 'reject_reason': 'recipient-domain-mismatch', you're probably trying to send an email to an unverified domain. If you only have one domain verified, you can only send emails to that domain.

Run the server and visit http://localhost:8000/transactional/send/. If everything goes well, you should see the following response:

{
    "detail": "Email has been sent"
}

Next, check your inbox and the email should be there.

This was just a demonstration of how to send transactional emails. In real-world applications, you can use this code to send emails for password resets and email verification, etc.

Conclusion

In the article, we looked at how to leverage Mailchimp's Marketing and Transactional Email APIs. We created a simple newsletter and learned how to send transactional emails. You should now have a decent understanding of how the Mailchimp APIs work and how other API endpoints could be implemented in your application.

Nik Tomazic

Nik Tomazic

Nik is a software developer from Slovenia. He's interested in object-oriented programming and web development. He likes learning new things and accepting new challenges. When he's not coding, Nik's either swimming or watching movies.

Share this tutorial

Featured Course

Test-Driven Development with Django, Django REST Framework, and Docker

In this course, you'll learn how to set up a development environment with Docker in order to build and deploy a RESTful API powered by Python, Django, and Django REST Framework.

Featured Course

Test-Driven Development with Django, Django REST Framework, and Docker

In this course, you'll learn how to set up a development environment with Docker in order to build and deploy a RESTful API powered by Python, Django, and Django REST Framework.