Zero
Zero
Back

Sending Transactional Email with the Mailchimp API

Almost every production application needs to send transactional email, e.g. for password resets and notifications. This article will walk you through integrating a Next.js web app with the Mailchimp Transactional Email API.

Sam Magura

Sam Magura

Envelopes laid out on a table

There are two main types of automated email: marketing email and transactional email. Most email services, including Mailchimp, offer separate APIs for these two types of email. Here's when you should use each type:

  • Marketing email: This one is pretty self-explanatory. Use a marketing email API when you want to send an email blast to a large group of users. Most marketing email services will provide tools to help you market effectively, like the ability to target emails to certain cohorts of users. Marketing email services allow you to send tens of thousands of emails efficiently and cost-effectively.
  • Transactional email: Use a transactional email API whenever you want to send an email to a single user. You'll use transactional email for things like email verification, password resets, and user-specific notifications like order confirmations. Transactional email services should generally not be used for sending emails in bulk, since they cost more per message than marketing email services and may have stricter rate limits.

In this article, we're going to build a simple Next.js web app that sends email via the Mailchimp Transactional Email API , with the Mailchimp API key stored in the Zero secrets manager. Our app will show a mock user sign-up form. When the user enters their email address and submits the form, the application's backend will use the Mailchimp Transactional API to send a "please verify your email" message to the user.

🔗 The full code for this example is available in the zerosecrets/examples  GitHub repository.

Setting up our Mailchimp Transactional Account

First, we'll sign up for Mailchimp and add a verified domain to send emails from. Then we'll build the Next.js app and integrate it with Zero and the Mailchimp API.

Prerequisites

To send a real email from your application, you'll need the following:

  1. ($7+) A domain name and the ability to add DNS records to it. You can buy a domain name from Google , Namecheap , GoDaddy , and many other services. For my project, I purchased srmagura.de from Amazon Route 53  for $9.
  2. ($20) One block of transactional email from Mailchimp, which will allow you to send 25,000 emails. Mailchimp Transactional does have a free tier, but it only allows you to send emails to your own verified domains. I went ahead and paid the $20 since this seemed easier than setting up an email address on my newly-purchased domain.

Of course, if you would just like to write the code without seeing your email get delivered, you can skip both of these purchases.

Adding a Verified Sending Domain

You can sign up for a free Mailchimp account here . Once you're logged in to Mailchimp, navigate to https://mandrillapp.com/settings . It's a bit confusing — Mailchimp's transactional email service is provided by Mandrill, a company which Mailchimp recently purchased. As of this writing, the UI for configuring your transactional email account is still in the Mandrill app, which is completely separate from Mailchimp's main admin console.

Now that we're in the Mandrill settings dashboard, let's add our domain name as a verified sending domain by selecting "Domains" > "Sending domains" from the top navigation bar. Then enter your domain name in the text box and click "Add".

Adding a sending domain in Mandrill a.k.a. Mailchimp Transactional

At this point, your domain will show up in the Sending Domains table, but you'll see a warning or error in each of the "Verified Domain", "DKIM Settings", and "SPF Settings" columns. You'll need to get a green checkmark in each of these columns before you can send emails. If you're curious, DKIM  and SPF  are email authentication technologies which show recipients that your email did in fact originate from your domain.

This domain has been added to Mandrill but not yet verified

To verify the domain and enable SPF, log in to the service that you bought your domain name from, go to the DNS settings, and create a TXT record. The TXT record should contain two lines, with the first line copied from the "Verified Domain" > "View details" dialog, and the second line copied from the "View SPF settings" dialog. The content of your record will look similar to this:

1
2
mandrill_verify.ygz0T4KFtElZclGcPes3_A
v=spf1 include:spf.mandrillapp.com ?all
shell

Wait around 30 seconds after adding the record and then click "Test DNS Settings". You should now see checkmarks under "Verified Domain" and "SPF Settings".

To enable DKIM, follow the instructions shown in the "View DKIM settings" dialog. This time, you'll create a TXT record at mandrill._domainkey.your-domain.com (replace your-domain.com with your actual domain name). Some DNS providers might require you to escape the semicolons in the TXT record's content with backslashes. After adding the record, click "Test DNS Settings" again and you should see a green check for DKIM.

For reference, here are the DNS records I added to my domain in the Amazon Route 53 console:

An example of the DNS records required to verify your domain with Mailchimp Transactional

Creating a Mailchimp API Key

Once you have set up a verified sending domain, the rest of the integration with Mailchimp Transactional will be a breeze. The next step is to create an API key.

First, let's log in to Zero and create a new project to house the Mailchimp API key. Upon creating the project, you'll be presented with a Zero token which you should save in a safe location on your PC. Back in the Zero web console, click the "Create new secret" button and select Mailchimp from the dropdown.

Adding a Mailchimp secret in the Zero web console

Now return to https://mandrillapp.com/settings , click the "Add API key" button, and copy-paste the API key into the "Create new secret" dialog in Zero.

Writing the Next.js Web App

With all of the Mailchimp setup behind us, it's time to create a simple Next.js 13 web application that uses the Mailchimp Transactional Email API to send a demo email. Our app will display a mock user signup form with an email address input. When the user clicks the "Sign up" button, the frontend will pass the email address to our backend API which will send a "Please verify your email address" email via the Mailchimp Transactional API.

To bootstrap a new Next.js project using TypeScript and the experimental app directory, run:

1
npx create-next-app@latest mailchimp-webapp --ts --experimental-app
shell

Building the Frontend

Now let's open app/page.tsx  and create a simple form that looks like this:

The mock user signup form in our Next.js app

The React code for the form should look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const [email, setEmail] = useState('')

/* ... */

return (
  <form onSubmit={onSubmit}>
    <div className="form-group">
      <label htmlFor="emailInput">Email address</label>
      <input
        id="emailInput"
        name="email"
        type="email"
        required
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
    </div>

    <button type="submit">Sign up</button>
  </form>
)
typescript

Remember to place the 'use client' directive at the top of the file to tell Next.js that this is a Client Component, not a Server Component!

When the user clicks "Sign up", the form will invoke the onSubmit function which calls our backend API with the email address that was entered by the user:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const [submitted, setSubmitted] = useState(false)

async function onSubmit(e: React.FormEvent<HTMLFormElement>): Promise<void> {
  e.preventDefault()
  setSubmitted(false)

  try {
    const response = await fetch('/api/signUp', {
      method: 'POST',
      body: JSON.stringify({email}),
      headers: {
        'Content-Type': 'application/json',
      },
    })

    if (!response.ok) {
      throw new Error(`Received an error HTTP status code: ${response.status}.`)
    }

    setSubmitted(true)
  } catch (e) {
    setSubmitted(false)
    console.error(e)
  }
}
typescript

Building the Backend

If you click the "Sign up" button right now, you'll get a 404 error back from the server because the /api/signUp API method doesn't exist yet — let's add it now. We can make a new Next.js API route by creating the file pages/api/signUp.ts :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import type {NextApiRequest, NextApiResponse} from 'next'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') {
    throw new Error('Only POST is allowed.')
  }

  const {email} = req.body

  if (!email) {
    throw new Error('Failed to get the email address from the request.')
  }

  // TODO Use the Mailchimp API to send an email

  res.status(200).send('')
}
typescript

Right now, our API method isn't very interesting. To send an email, we'll need get a Mailchimp API client from the @mailchimp/mailchimp_transactional  library. You can install this library along with the Zero TypeScript SDK  by running

1
npm install @mailchimp/mailchimp_transactional @types/mailchimp__mailchimp_transactional @zerosecrets/zero
shell

Now we can write a function in the src/util directory that fetches the Mailchimp API key from Zero and uses it to instantiate the Mailchimp API client:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import Mailchimp from '@mailchimp/mailchimp_transactional'
import {zero} from '@zerosecrets/zero'

let mailchimpClient: Mailchimp.ApiClient | undefined

export async function getMailchimpClient(): Promise<Mailchimp.ApiClient> {
  // Reuse the same Mailchimp client if one has already been created, so that we
  // don't call Zero on every request
  if (mailchimpClient) {
    return mailchimpClient
  }

  if (!process.env.ZERO_TOKEN) {
    throw new Error('Did you forget to set the ZERO_TOKEN environment variable?')
  }

  const secrets = await zero({
    token: process.env.ZERO_TOKEN,
    pick: ['mailchimp'],
  }).fetch()

  if (!secrets.mailchimp) {
    throw new Error('Did not receive an API key for Mailchimp.')
  }

  mailchimpClient = Mailchimp(secrets.mailchimp.transactional_api_key)

  return mailchimpClient
}
typescript

If you've been following along with our other blog posts, this function should look very familiar!

Next, let's return to api/signUp.ts and replace the // TODO with the code for sending an email:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const mailchimpClient = await getMailchimpClient()

// You can use this to test that communication with Mailchimp is working:

// const response = await mailchimpClient.users.ping();

const response = await mailchimpClient.messages.send({
  message: {
    from_email: 'demo@YOUR-DOMAIN.com', // TODO Replace YOUR-DOMAIN.com with your verified sending domain
    subject: '[Demo] Please verify your email address',
    text: 'If this was a real app, there would be a link right here ;)',
    to: [
      {
        email,
        type: 'to',
      },
    ],
  },
})

console.log('Mailchimp responded with:')
console.log(response)
typescript

At this point, the code is complete. Let's run the app to see if it works. Remember to pass your Zero token in as an environment variable, like this:

1
ZERO_TOKEN=YOUR-ZERO-TOKEN npm run dev
shell

Once the app launches, enter your real email address into the text input and click "Sign up". Then check the terminal where you ran npm run dev to see the response that Mailchimp returned. If it worked, the response will look like this:

1
2
3
4
5
6
7
8
9
;[
  {
    email: 'YOUR-EMAIL@gmail.com',
    status: 'sent',
    _id: '6c18a64de59345e6919ec1c5c6a17718',
    reject_reason: null,
    queued_reason: null,
  },
]
javascript

If you check your inbox, you should see the email! If it's not there even though Mailchimp returned a status of sent, check you spam folder.

The test email in my Gmail inbox

Troubleshooting

Here are some of the reject_reasons Mailchimp may return, and how to fix them:

  • reject_reason: 'unsigned' — you tried to send an email from a domain that has not been verified with Mailchimp. Refer to the "Adding a Verified Sending Domain" section of this article.
  • reject_reason: 'recipient-domain-mismatch' — you are using the free demo version of Mailchimp Transactional and the domain of the recipient email address did not match the domain of the sender email address. (The demo plan requires that the domain names match.) To send an email to a recipient on a different domain, upgrade your Mailchimp Transactional account to a paid plan by purchasing one block of transactional emails.

Next Steps

Congratulations on integrating your Next.js application with Mailchimp Transactional Email and Zero! At this point, you can apply the coding patterns shown in this article to send transactional email from your real application.

One thing we didn't account for in the demo project which you should handle in a production application is transient failures when attempting to send email. If the Mailchimp API has an outage or your server experiences a blip in the network, it's important to ensure your transactional emails are still delivered.

The most common way to build this resiliency into your application is, when you need to send an email (e.g. in response to an API call from the frontend), add the email to a queue instead of sending it straightaway. Then, you can write a background processor which listens for messages on the queue and sends the emails. If sending the email fails, the background processor can re-enqueue the message with a delay. This approach should eliminate (or at least greatly reduce) the number of transactional emails that get lost. See our earlier article on the Work Queue pattern for details.

Additional Resources