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:
- Explain the differences between Mailchimp's Marketing and Transactional Email APIs
- Integrate Mailchimp's Marketing API with Django
- Manage Mailchimp audiences and contacts
- Set up and use merge fields to send personalized campaigns
- 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:
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:
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":
Then click "Extras" > "API keys":
Lastly, click on "Create A Key" to generate an API key:
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
.
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":
On the right side of the screen, copy the "Audience ID":
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:
subscribed
- immediately adds the contactpending
- 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.
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/
:
Next, open your Mailchimp dashboard and navigate to "Audience" > "All contacts". You should be able to see your first newsletter 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:
- Obtained the user's email via the form.
- Used
md5
to hash the user's email and to generate a subscriber hash. The hash allows us to manipulate the user data. - Created a dictionary called
member_update
with all the data we want to change. In our case, we just changed the status. - 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":
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": ""
},
...
}
--
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):
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":
After the key gets generated, copy it:
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".
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:
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:
- Created a dictionary that contains all the email information.
- 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.