# Validate Emails in Mailchimp

URL: https://email-validation.org/docs/guides/mailchimp/
Updated: 2026-11-06

> Documentation index: https://email-validation.org/llms.txt

Check the addresses of a Mailchimp audience and tag each member with the result, so your segments and journeys can
leave out the addresses you should not mail. Three small pieces do the work:

- **A sync command** checks every member of an audience that has no result yet, in one [bulk list](https://email-validation.org/docs/bulk/).
- **A webhook relay** checks each new subscriber within seconds. It signs in to Mailchimp with an API key.
- **An OAuth app** does what the relay does, but signs in to Mailchimp with OAuth and adds the webhooks itself. You
  connect your Mailchimp account on its page instead of copying a key and a webhook address.

Use the sync command for the first clean-up, with either the relay or the app for new subscribers. All of them run in
your own accounts with your own keys: the command with Node, the relay and the app as Cloudflare Workers. The app is
not in the Mailchimp Marketplace yet, so you run it yourself.

## Before you start

- **An API key.** Create one on the [API keys page](https://email-validation.org/app/api-keys/). The relay and the app need the permission
  "Validate addresses"; the sync command also needs **"Bulk lists"**, which is off by default. A
  [daily credit cap](https://email-validation.org/docs/authentication/#limits) limits what a busy audience can spend.
- **Your audience id.** In Mailchimp: **Audience → Settings → Audience name and defaults → Audience ID**, such as
  `a6b5da1054`.
- **Node 22.18 or later** for the sync command, and a **Cloudflare account** for the relay or the app.
- **The files**, which are built from our tested code:
  - [sync.mjs](https://email-validation.org/downloads/mailchimp/sync.mjs): the sync command, one Node module;
  - [relay.js](https://email-validation.org/downloads/mailchimp/relay.js): the webhook relay, one Cloudflare Worker module;
  - [webhook.json](https://email-validation.org/downloads/mailchimp/webhook.json): the relay's webhook settings, for Mailchimp's API;
  - [app.js](https://email-validation.org/downloads/mailchimp/app.js) and [app-wrangler.jsonc](https://email-validation.org/downloads/mailchimp/app-wrangler.jsonc): the
    OAuth app and its Worker settings.

## The tags

| Tag | Meaning |
|---|---|
| `ev:valid`, `ev:invalid`, `ev:catch_all`, `ev:unknown`, `ev:disposable` | the [result status](https://email-validation.org/docs/statuses/); a member has exactly one, and a new check replaces the old one |
| `ev:remove` | the [action](https://email-validation.org/docs/statuses/) is remove: do not mail this member |

Spam traps and complainers are `ev:valid` with `ev:remove`, so build your segment on `ev:remove`: **Tags → contact is
tagged → ev:remove** holds every member you should not mail. Tags need no setup, and subscribers never see them.
Adding a tag starts any Customer Journey that begins with "tag added" for that tag: build journeys on our tags on
purpose, or start them from a segment instead.

## Clean an audience

The sync command needs your Mailchimp API key: **Profile → Extras → API keys → Create A Key**. It ends in your data
center, such as `-us21`, and the command sends requests only to that data center's API. The key can do everything in
your account, so keep it secret.

```bash
export EV_API_KEY=…  MAILCHIMP_API_KEY=…
node sync.mjs --list a6b5da1054 --dry-run    # counts what it would check
node sync.mjs --list a6b5da1054              # checks and tags
node sync.mjs --list a6b5da1054 --suppress   # also archives the members whose action is remove
```

It lists the subscribed members, skips those that already have a status tag other than `ev:unknown` (add
`--revalidate` to check them again), checks the rest in one [bulk list](https://email-validation.org/docs/bulk/) and tags each member. It prints
member ids and counts, never addresses.

If a run stops for any reason, such as no credits or a closed laptop, run the same command again. It keeps the bulk
job's id in a small state file (ids only, no addresses) and resumes that job, so a list is never paid for twice.
Exit code `75` means the job was still running when the command stopped waiting: run it again later.

`--suppress` **archives** each member whose action is remove: the member leaves the audience and stops counting
toward your plan, but keeps its data and history, and you can add it back. Nothing is ever deleted permanently.

Other options: `--status subscribed,pending` for other member statuses, `--region eu` for
[EU data residency](https://email-validation.org/docs/eu-data-residency/), `--concurrency` for Mailchimp writes at once (default 5, at most 10), and
`--wait-minutes` for how long to wait for a bulk list (default 240).

## Check new subscribers with the relay

1. Deploy the relay to your Cloudflare account and give it its secrets:

   ```bash
   npx wrangler deploy relay.js --name ev-mailchimp-relay --compatibility-date 2026-10-01
   npx wrangler secret put EV_API_KEY --name ev-mailchimp-relay
   npx wrangler secret put MAILCHIMP_API_KEY --name ev-mailchimp-relay
   npx wrangler secret put RELAY_SECRET --name ev-mailchimp-relay
   ```

   `RELAY_SECRET` needs at least 32 random letters and digits, for example from `openssl rand -hex 32`. For
   [EU data residency](https://email-validation.org/docs/eu-data-residency/), add `--var EV_REGION:eu` to the deploy command. To check only one audience,
   also set `MAILCHIMP_LIST_ID` to its id.
2. In Mailchimp, open **Audience → Settings → Webhooks → Create New Webhook**:
   - **Callback URL** the relay's address followed by `/` and your `RELAY_SECRET`, such as
     `https://ev-mailchimp-relay.<your-subdomain>.workers.dev/<RELAY_SECRET>`;
   - **Events** Subscribes only; **Sources** all three.

   To create the webhook with Mailchimp's API instead, send `webhook.json`, with your URL filled in, to
   `POST /3.0/lists/<audience id>/webhooks`. If you turn on signing for the webhook, put its signing secret in
   `MAILCHIMP_WEBHOOK_SECRET` and the relay refuses any delivery without a valid signature.
3. Add a test contact with the address `no_mailbox@sandbox.email-validation.org`, a [test address](https://email-validation.org/docs/test-addresses/) that
   costs nothing and receives no mail. Within a few seconds it has the tags `ev:invalid` and `ev:remove`. Then archive
   the test contact, and keep it out of campaigns until then.

The secret in the URL is what proves a delivery comes from your webhook, so anyone who can read that URL can send
deliveries to the relay. Keep request logs that record URLs off for the Worker, and change the secret if it leaks.

## Check new subscribers with the OAuth app

The app is the relay with a sign-in to Mailchimp through OAuth, for one or more Mailchimp accounts. It stores each
account's token encrypted in a KV namespace of your own Cloudflare account, and only you can open its pages.

1. **Register an OAuth app in Mailchimp:** **Profile → Extras → Registered apps → Register an app**. As the redirect
   URI, give your app's address followed by `/oauth/callback`, such as
   `https://ev-mailchimp-app.<your-subdomain>.workers.dev/oauth/callback`. Mailchimp then shows the client id and the
   client secret; copy the secret right away, as it is shown once.
2. **Deploy the app.** In the folder with `app.js` and `app-wrangler.jsonc`:

   ```bash
   npx wrangler kv namespace create ev-mailchimp-tokens
   ```

   Put the id it prints into `app-wrangler.jsonc`, and set `APP_URL` there to your app's address. Then:

   ```bash
   npx wrangler deploy -c app-wrangler.jsonc
   npx wrangler secret put EV_API_KEY -c app-wrangler.jsonc
   npx wrangler secret put MAILCHIMP_CLIENT_ID -c app-wrangler.jsonc
   npx wrangler secret put MAILCHIMP_CLIENT_SECRET -c app-wrangler.jsonc
   npx wrangler secret put APP_SETUP_SECRET -c app-wrangler.jsonc
   npx wrangler secret put TOKEN_ENCRYPTION_KEY -c app-wrangler.jsonc
   ```

   `APP_SETUP_SECRET` opens the app's pages: at least 32 random letters and digits (`openssl rand -hex 32`).
   `TOKEN_ENCRYPTION_KEY` encrypts the stored tokens: 32 random bytes (`openssl rand -base64 32`). Keep it: without it
   the app cannot read its connections, and you connect again. For [EU data residency](https://email-validation.org/docs/eu-data-residency/), set `EV_REGION`
   to `eu` in `app-wrangler.jsonc`.
3. **Connect.** Open the app's address, type the setup secret, choose **Connect a Mailchimp account** and allow
   access in Mailchimp. Back on the app's page, choose **Check new subscribers** next to an audience. The app adds
   the audience's webhook itself, with a secret address of its own.
4. **Check it once** with the test contact `no_mailbox@sandbox.email-validation.org`, as in step 3 of the relay.

**Stop checking** removes an audience's webhook. **Disconnect this account** removes the webhooks and deletes the
token from your KV namespace; Mailchimp keeps the app's access until you remove it in your Mailchimp account too.
The app's pages ask for the setup secret again after an hour.

## Answers and retries

Mailchimp cancels a delivery that takes more than 10 seconds, and delivers a failed one again for about 75 minutes.
The relay and the app answer the same way:

| They answer | When | What to do |
|---|---|---|
| `200` saved | The member was checked and tagged | Nothing |
| `200` ignored or skipped | Not a subscribe event, another audience, the member is gone, or it already has a status tag | Nothing |
| `200` not saved | The check ran, but Mailchimp did not take the tags | Run the sync command to catch up |
| `503` | A rate limit or an outage before the check | Nothing: Mailchimp delivers again |
| `422` | Our API refused the key, there are no credits or the key reached its daily cap; or Mailchimp refused the key or token | Fix the cause, then run the sync command to catch up |
| `404` | The secret in the address is wrong | Check the webhook's address |

Nothing is charged for a delivery that answers `503` or `422`. Before it checks, the relay reads the member and skips
one that already has a status tag, so a repeated delivery costs nothing.

## Costs and limits

- Each checked address is one check. Results with the status `unknown` are free, and so are malformed addresses,
  duplicates in a list and the [test addresses](https://email-validation.org/docs/test-addresses/).
- The relay and the app give a check at most 5 seconds, to answer within Mailchimp's 10.
- Mailchimp allows 10 connections at once per user, across all your apps: keep `--concurrency` at or below that.
- Our own [rate limits](https://email-validation.org/docs/rate-limits/) apply as to any other client.

## Privacy

The pieces send each address only to our API, for the check, and to Mailchimp's API for that member's tags. Their
answers and output never contain an address, a key or a token, and the relay and the app write no logs. The sync
command's state file holds job ids only. The app stores, per connected Mailchimp account, its token, account name and
watched audiences, encrypted, in your own KV namespace; it stores no addresses or results.
