# How to Integrate Replit with Mailchimp

- Tool: Replit
- Difficulty: Intermediate
- Time required: 25 minutes
- Last updated: March 2026

## TL;DR

To integrate Replit with Mailchimp, generate a Mailchimp API key from your account settings, store it in Replit Secrets (lock icon 🔒), and use the Mailchimp Marketing API from your server-side Python or Node.js code to manage lists, add contacts, and trigger email campaigns. Use Autoscale deployment for web-app-driven marketing workflows.

## Why Connect Replit to Mailchimp?

Mailchimp is the dominant email marketing platform for small and medium businesses, with over 11 million users and one of the most comprehensive marketing APIs available. Connecting your Replit app to Mailchimp lets you automate subscriber management, trigger campaigns based on user actions, and sync your app's data with your marketing lists — all without manual CSV imports or copy-paste workflows.

The most common use cases are adding new users to an audience when they sign up in your app, unsubscribing users who opt out, and triggering automated email sequences based on in-app events. The Mailchimp Marketing API v3 covers all of these scenarios with a clean REST interface that is easy to call from any Python or Node.js server.

Replit's Secrets system (lock icon 🔒 in the sidebar) keeps your Mailchimp API key encrypted and out of your codebase. Because the API key grants access to your entire Mailchimp account including all audience lists and campaign data, it should be treated with the same care as a database password. Never commit it to code or include it in client-side JavaScript — always call the Mailchimp API from your server-side backend.

## Before you start

- A Replit account with a Python or Node.js project created
- A Mailchimp account (free tier works for up to 500 contacts)
- At least one Mailchimp Audience (also called a List) already created
- Basic familiarity with REST APIs and HTTP authentication
- Node.js 18+ or Python 3.10+ (both available on Replit by default)

## Step-by-step guide

### 1. Generate a Mailchimp API Key and Find Your Data Center

Log in to your Mailchimp account and click your profile avatar in the bottom-left corner. Select 'Profile' and then navigate to the 'Extras' menu at the top. Click 'API keys'. On the API keys page, click 'Create A Key'. Give it a descriptive name like 'replit-integration' so you can identify it later. The generated key looks like: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6-us6

The last segment after the hyphen (e.g., 'us6', 'us1', 'us14') is your data center prefix. This prefix determines the base URL for all your API requests. For example, if your key ends in -us6, all API calls go to https://us6.api.mailchimp.com/3.0/. Every Mailchimp account is assigned to one data center, and using the wrong server prefix will result in API errors.

You also need your Audience List ID. In Mailchimp, go to Audience > Manage Audience > Settings. Under 'Audience name and defaults', find the Audience ID (a string like 'a1b2c3d4e5'). Copy both the API key and the Audience ID — you will add both to Replit Secrets.

**Expected result:** You have a Mailchimp API key (ending in your data center prefix like -us6) and an Audience ID copied and ready.

### 2. Store Mailchimp Credentials in Replit Secrets

Open your Replit project and click the lock icon 🔒 in the left sidebar to open the Secrets pane. Add the following secrets using the 'Add a new secret' form:

- Key: MAILCHIMP_API_KEY — Value: your full API key including the data center suffix
- Key: MAILCHIMP_SERVER_PREFIX — Value: just the server prefix, e.g., us6
- Key: MAILCHIMP_LIST_ID — Value: your Audience ID

Click 'Add Secret' after each one. Replit encrypts these values with AES-256 encryption at rest and injects them as environment variables at runtime. They are never visible in your file tree or Git history.

Separating the server prefix into its own Secret (rather than parsing it from the API key at runtime) makes the code cleaner and easier to update if you ever need to change accounts. In Python, access these with os.environ['MAILCHIMP_API_KEY']; in Node.js, use process.env.MAILCHIMP_API_KEY. Do not use Deno.env.get() — that is a pattern from Supabase Edge Functions and does not work in Replit's Node.js environment.

**Expected result:** Three Secrets appear in the Replit Secrets pane: MAILCHIMP_API_KEY, MAILCHIMP_SERVER_PREFIX, and MAILCHIMP_LIST_ID.

### 3. Subscribe and Manage Contacts with Python

The Mailchimp Marketing API uses HTTP Basic Authentication where the username is any string (by convention 'anystring') and the password is your API key. The base URL is constructed using your server prefix. No official Python SDK is required — the standard requests library is sufficient.

The most important nuance is how Mailchimp identifies contacts within a list: it uses an MD5 hash of the lowercase email address as the member ID. This hash is used in the URL when updating or deleting a specific subscriber. The code below shows how to subscribe a new contact, update their merge fields (name, custom fields), check their subscription status, and unsubscribe them. All operations use the PATCH method to upsert (create or update) contact records, which prevents duplicate-member errors.

```
import os
import hashlib
import requests
from typing import Optional

API_KEY = os.environ["MAILCHIMP_API_KEY"]
SERVER = os.environ["MAILCHIMP_SERVER_PREFIX"]
LIST_ID = os.environ["MAILCHIMP_LIST_ID"]
BASE_URL = f"https://{SERVER}.api.mailchimp.com/3.0"

# Mailchimp uses Basic Auth: any username + API key as password
AUTH = ("anystring", API_KEY)

def get_subscriber_hash(email: str) -> str:
    """Mailchimp identifies subscribers by MD5 hash of lowercase email."""
    return hashlib.md5(email.lower().encode()).hexdigest()

def subscribe_contact(email: str, first_name: str = "", last_name: str = "") -> dict:
    """Add or update a subscriber in the audience (upsert via PATCH)."""
    subscriber_hash = get_subscriber_hash(email)
    url = f"{BASE_URL}/lists/{LIST_ID}/members/{subscriber_hash}"
    data = {
        "email_address": email,
        "status_if_new": "subscribed",  # Status for new subscribers
        "status": "subscribed",          # Status for existing subscribers
        "merge_fields": {
            "FNAME": first_name,
            "LNAME": last_name
        }
    }
    response = requests.put(url, json=data, auth=AUTH)
    response.raise_for_status()
    return response.json()

def unsubscribe_contact(email: str) -> dict:
    """Unsubscribe a contact (does not delete — preserves history)."""
    subscriber_hash = get_subscriber_hash(email)
    url = f"{BASE_URL}/lists/{LIST_ID}/members/{subscriber_hash}"
    response = requests.patch(url, json={"status": "unsubscribed"}, auth=AUTH)
    response.raise_for_status()
    return response.json()

def get_contact_status(email: str) -> Optional[str]:
    """Check if an email is subscribed, unsubscribed, or not in the list."""
    subscriber_hash = get_subscriber_hash(email)
    url = f"{BASE_URL}/lists/{LIST_ID}/members/{subscriber_hash}"
    response = requests.get(url, auth=AUTH)
    if response.status_code == 404:
        return None  # Not in list
    response.raise_for_status()
    return response.json().get("status")

def add_tag_to_contact(email: str, tag_name: str) -> None:
    """Add a tag to a subscriber for segmentation."""
    subscriber_hash = get_subscriber_hash(email)
    url = f"{BASE_URL}/lists/{LIST_ID}/members/{subscriber_hash}/tags"
    data = {"tags": [{"name": tag_name, "status": "active"}]}
    response = requests.post(url, json=data, auth=AUTH)
    response.raise_for_status()

# Example usage
if __name__ == "__main__":
    result = subscribe_contact("user@example.com", "Jane", "Doe")
    print(f"Subscribed: {result['email_address']} — status: {result['status']}")

    add_tag_to_contact("user@example.com", "early-adopter")
    print("Tag added successfully")

    status = get_contact_status("user@example.com")
    print(f"Current status: {status}")
```

**Expected result:** Running the Python script subscribes a test contact, adds a tag, and prints the subscription status without errors.

### 4. Build a Node.js Integration with Campaign Support

For Node.js projects, use the @mailchimp/mailchimp_marketing npm package — the official SDK maintained by Mailchimp. Install it with 'npm install @mailchimp/mailchimp_marketing'. The SDK wraps the REST API and handles authentication automatically once configured.

The Express server below exposes a POST /subscribe endpoint for adding contacts and a POST /campaign endpoint for creating and sending a campaign to the entire audience. Campaign creation is a two-step process: first create the campaign (which returns a campaign ID), then call the send endpoint. You can also schedule campaigns using the schedule endpoint if you want to deliver at a specific time.

Note that the Mailchimp free tier has restrictions on sending campaigns — you cannot send to more than 500 contacts and monthly send limits apply. Check your plan limits in the Mailchimp billing page before triggering campaign sends programmatically.

```
const express = require('express');
const mailchimp = require('@mailchimp/mailchimp_marketing');
const crypto = require('crypto');

const app = express();
app.use(express.json());

// Configure Mailchimp SDK from Replit Secrets
mailchimp.setConfig({
  apiKey: process.env.MAILCHIMP_API_KEY,
  server: process.env.MAILCHIMP_SERVER_PREFIX
});

const LIST_ID = process.env.MAILCHIMP_LIST_ID;

function getSubscriberHash(email) {
  return crypto.createHash('md5').update(email.toLowerCase()).digest('hex');
}

// Verify API connection on startup
async function checkConnection() {
  try {
    const response = await mailchimp.ping.get();
    console.log('Mailchimp connected:', response.health_status);
  } catch (err) {
    console.error('Mailchimp connection failed:', err.response?.text);
  }
}
checkConnection();

// Subscribe or update a contact
app.post('/subscribe', async (req, res) => {
  const { email, firstName, lastName, tags } = req.body;
  if (!email) return res.status(400).json({ error: 'email is required' });

  const subscriberHash = getSubscriberHash(email);
  try {
    const response = await mailchimp.lists.setListMember(LIST_ID, subscriberHash, {
      email_address: email,
      status_if_new: 'subscribed',
      status: 'subscribed',
      merge_fields: {
        FNAME: firstName || '',
        LNAME: lastName || ''
      },
      tags: tags || []
    });
    res.json({ success: true, id: response.id, status: response.status });
  } catch (err) {
    console.error('Subscribe error:', err.response?.text);
    res.status(500).json({ error: err.message });
  }
});

// Unsubscribe a contact
app.post('/unsubscribe', async (req, res) => {
  const { email } = req.body;
  if (!email) return res.status(400).json({ error: 'email is required' });

  const subscriberHash = getSubscriberHash(email);
  try {
    await mailchimp.lists.updateListMember(LIST_ID, subscriberHash, {
      status: 'unsubscribed'
    });
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Create and send a campaign
app.post('/campaign', async (req, res) => {
  const { subject, previewText, fromName, replyTo, htmlContent } = req.body;
  try {
    // Step 1: Create campaign
    const campaign = await mailchimp.campaigns.create({
      type: 'regular',
      recipients: { list_id: LIST_ID },
      settings: {
        subject_line: subject,
        preview_text: previewText || '',
        from_name: fromName,
        reply_to: replyTo
      }
    });

    // Step 2: Set campaign content
    await mailchimp.campaigns.setContent(campaign.id, {
      html: htmlContent
    });

    // Step 3: Send campaign
    await mailchimp.campaigns.send(campaign.id);
    res.json({ success: true, campaignId: campaign.id });
  } catch (err) {
    console.error('Campaign error:', err.response?.text);
    res.status(500).json({ error: err.message });
  }
});

app.listen(3000, '0.0.0.0', () => {
  console.log('Mailchimp integration server running on port 3000');
});
```

**Expected result:** The server starts, Mailchimp ping responds with 'Everything's Chimpy!', and the /subscribe endpoint adds a test contact to your audience.

### 5. Set Up Mailchimp Webhooks and Deploy

Mailchimp can send webhook events to your Replit app when subscribers update their preferences, unsubscribe, or when campaign events occur. This keeps your database in sync with Mailchimp without polling.

To set up a webhook in Mailchimp, go to Audience > Manage Audience > Settings > Webhooks. Click 'Create New Webhook' and enter your deployed Replit URL plus the webhook path (e.g., https://your-app.replit.app/mailchimp/webhook). Select the event types you want to receive: subscribes, unsubscribes, profile updates, and email changed are the most useful for keeping a database in sync.

Webhooks only work with a deployed URL — not a development URL. Click 'Deploy' in Replit and choose Autoscale for a web app that also serves a frontend. Autoscale handles webhook delivery reliably because Mailchimp retries failed webhooks several times. Choose Reserved VM if your app primarily processes webhooks and cannot tolerate cold-start delays.

Mailchimp does not sign its webhook payloads with a secret (unlike Stripe), so it sends a GET request to verify the URL is live before activating the webhook. Your endpoint must respond to both GET and POST requests.

```
# Flask webhook receiver for Mailchimp events
from flask import Flask, request, jsonify
import os

app = Flask(__name__)

@app.route('/mailchimp/webhook', methods=['GET', 'POST'])
def mailchimp_webhook():
    # Mailchimp sends a GET request to verify the endpoint
    if request.method == 'GET':
        return 'OK', 200

    # POST contains the event data
    data = request.form  # Mailchimp sends form-encoded data, not JSON
    event_type = data.get('type')
    email = data.get('data[email]')
    list_id = data.get('data[list_id]')

    print(f"Mailchimp webhook: {event_type} for {email}")

    if event_type == 'unsubscribe':
        # Update your database to mark user as unsubscribed
        # update_user_subscription_status(email, subscribed=False)
        print(f"User unsubscribed: {email}")
    elif event_type == 'subscribe':
        print(f"User subscribed: {email}")
    elif event_type == 'profile':
        new_email = data.get('data[new_email]')
        print(f"Profile updated: {email} -> {new_email}")

    return jsonify({'status': 'received'}), 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=3000)
```

**Expected result:** Mailchimp confirms the webhook URL is live, and your app logs incoming events when subscribers update their preferences.

## Best practices

- Always store your Mailchimp API key in Replit Secrets (lock icon 🔒) — never in source code, .env files committed to Git, or client-side JavaScript.
- Use a separate Mailchimp API key per project so you can revoke access to one integration without affecting others.
- Use 'status_if_new': 'pending' for public-facing sign-up forms to trigger double opt-in confirmation emails and maintain GDPR/CAN-SPAM compliance.
- Hash email addresses with MD5 (lowercase) before using them in Mailchimp API URLs — the subscriber hash is required for all per-member operations.
- Handle the 'Member Exists' error explicitly: check a contact's current status before attempting to subscribe them, since re-subscribing unsubscribed contacts requires user action.
- Deploy your app before registering Mailchimp webhooks — use your stable replit.app URL, not the temporary development URL.
- Choose Autoscale deployment for apps that handle marketing events and subscriptions; choose Reserved VM only if you need zero cold-start latency for webhook processing.
- Monitor your Mailchimp campaign send rates and API rate limits — the free tier allows 500 contacts and 1,000 sends per month, which can be exhausted quickly by automated workflows.

## Use cases

### Automatic Subscriber Sync on Registration

When a new user registers in your Replit web app, automatically add them to a Mailchimp audience so they receive your onboarding email sequence. The integration runs server-side so users never see the Mailchimp API key, and the subscription is recorded in both your database and Mailchimp simultaneously.

Prompt example:

```
Build a Flask registration endpoint that creates a user in a PostgreSQL database and simultaneously subscribes them to a Mailchimp audience using the MAILCHIMP_API_KEY and MAILCHIMP_LIST_ID from Replit Secrets.
```

### Marketing Campaign Automation

A Replit app triggers a targeted Mailchimp campaign when a specific product event occurs — for example, sending a re-engagement email to users who have not logged in for 30 days. The app queries the database for inactive users and creates a Mailchimp segment before triggering the campaign send.

Prompt example:

```
Write a Python script that queries a PostgreSQL database for users inactive for 30+ days, creates a Mailchimp segment with those email addresses, and triggers a campaign send to that segment using the Mailchimp API.
```

### Webhook-Driven Unsubscribe Handler

Receive Mailchimp webhook events in your Replit app to keep your database in sync when users unsubscribe, update their email, or mark a message as spam. This prevents sending emails to contacts who have opted out and maintains CAN-SPAM/GDPR compliance.

Prompt example:

```
Create an Express endpoint at /mailchimp/webhook that receives Mailchimp list update events, verifies the webhook secret, and updates the user's email preferences in the database when an unsubscribe event is received.
```

## Troubleshooting

### API error 401 — 'Your API key may be invalid, or you've attempted to access the wrong datacenter'

Cause: The server prefix in the API URL does not match the data center in your API key, or the API key is invalid. Every Mailchimp API call must use the base URL matching your account's data center (e.g., https://us6.api.mailchimp.com/3.0/).

Solution: Check the last segment of your API key after the hyphen — that is your server prefix (e.g., 'us6'). Make sure MAILCHIMP_SERVER_PREFIX in Replit Secrets matches exactly. If you are building the URL manually, use f'https://{server}.api.mailchimp.com/3.0/' where server is the prefix without 'https://'.

```
# Python: correct URL construction
server = os.environ["MAILCHIMP_SERVER_PREFIX"]  # e.g., 'us6'
base_url = f"https://{server}.api.mailchimp.com/3.0"
```

### API error 400 — 'Member Exists' when trying to subscribe a contact

Cause: The contact is already in the audience with a different status (unsubscribed, archived, or cleaned). Mailchimp does not allow re-subscribing an unsubscribed contact via a normal PUT request — this violates CAN-SPAM compliance.

Solution: Use the PATCH method to update the existing member's status rather than trying to create a new member. Check the current status first and handle the 'unsubscribed' case differently — you may need to inform the user they must re-subscribe through the Mailchimp opt-in form rather than being re-added programmatically.

```
# Check status before subscribing
status = get_contact_status(email)
if status == 'unsubscribed':
    print('User must re-subscribe via opt-in form')
else:
    subscribe_contact(email, first_name, last_name)
```

### Webhook endpoint receives no events from Mailchimp

Cause: The webhook is configured against a development Replit URL which goes offline when you close the browser tab, or the webhook URL was registered before the app was deployed.

Solution: Deploy your Replit app first to get a stable URL at https://your-app.replit.app. Then update the webhook URL in Mailchimp (Audience > Settings > Webhooks) to point to the deployed URL. Mailchimp will send a GET request to verify — make sure your Flask/Express endpoint responds with 200 to GET requests.

### Mailchimp webhook body is empty or parsing fails

Cause: Mailchimp sends webhook payloads as application/x-www-form-urlencoded, not JSON. If your server only parses JSON bodies, the webhook fields will all be missing.

Solution: In Flask, read fields from request.form instead of request.json. In Express, add express.urlencoded({ extended: true }) middleware before your webhook route.

```
// Express — add urlencoded middleware
app.use(express.urlencoded({ extended: true }));

app.post('/mailchimp/webhook', (req, res) => {
  const eventType = req.body['type'];
  const email = req.body['data[email]'];
  res.json({ received: true });
});
```

## Frequently asked questions

### How do I store my Mailchimp API key in Replit?

Click the lock icon 🔒 in the left sidebar of your Replit project to open the Secrets pane. Add MAILCHIMP_API_KEY with your full API key (including the data center suffix like -us6), MAILCHIMP_SERVER_PREFIX with just the prefix (e.g., us6), and MAILCHIMP_LIST_ID with your audience ID. Access them in code with os.environ['MAILCHIMP_API_KEY'] in Python or process.env.MAILCHIMP_API_KEY in Node.js.

### Can I use Mailchimp with Replit on the free tier?

Yes. Mailchimp's free tier allows up to 500 contacts and 1,000 email sends per month, which is enough for development and small projects. The Mailchimp Marketing API is available on all plans including free. Replit's free tier supports outbound API calls without restriction, though you will need Replit's paid plan for always-on deployments needed for webhook reception.

### How do I find my Mailchimp Audience ID (List ID)?

In Mailchimp, navigate to Audience > Manage Audience > Settings. Under 'Audience name and defaults', look for the 'Audience ID' field — it is a 10-character alphanumeric string like 'a1b2c3d4e5'. Copy this value and store it as MAILCHIMP_LIST_ID in Replit Secrets.

### Why does my Mailchimp API call fail with a 401 error?

A 401 error from the Mailchimp API usually means the server prefix in the URL does not match your API key's data center. The API key ends with a hyphen and a prefix like -us6. Your API URL must start with https://us6.api.mailchimp.com/3.0/ (using the matching prefix). Check that MAILCHIMP_SERVER_PREFIX in Replit Secrets matches the suffix of your API key exactly.

### Does Replit work with Mailchimp webhooks?

Yes, but webhooks require a deployed app with a stable URL. Development Replit URLs are temporary and go offline when you close the browser. Deploy your app using Autoscale (for web apps) or Reserved VM (for always-on webhook processing) to get a stable https://your-app.replit.app URL. Register this deployed URL in Mailchimp under Audience > Settings > Webhooks.

### Can I re-subscribe a user who has unsubscribed from Mailchimp?

No — Mailchimp's compliance rules prevent re-subscribing a user who has explicitly unsubscribed via API. Doing so would violate CAN-SPAM and GDPR regulations. The user must re-subscribe themselves by filling in an opt-in form. If your app receives a 400 'Member Exists' error with a status of 'unsubscribed', you should inform the user they need to opt back in through your sign-up form.

---

Source: https://www.rapidevelopers.com/replit-integration/mailchimp
© RapidDev — https://www.rapidevelopers.com/replit-integration/mailchimp
