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.
Integration method
You connect Replit to Mailchimp by generating an API key in your Mailchimp account, saving it to Replit Secrets, and calling the Mailchimp Marketing API from your server-side Python or Node.js code. All API requests are authenticated with the API key in the Authorization header using HTTP Basic Auth format. The Mailchimp API server prefix (e.g., us1, us6) is embedded in the API key and determines the base URL for all requests.
Prerequisites
- 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
Generate a Mailchimp API Key and Find Your Data Center
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.
Pro tip: Create a separate API key for each project instead of reusing one key across multiple apps. This lets you revoke access to a specific integration without affecting others.
Expected result: You have a Mailchimp API key (ending in your data center prefix like -us6) and an Audience ID copied and ready.
Store Mailchimp Credentials in Replit Secrets
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.
Pro tip: If you are building for multiple Mailchimp audiences, store each List ID as a separate Secret with a descriptive name: MAILCHIMP_NEWSLETTER_LIST_ID, MAILCHIMP_CUSTOMERS_LIST_ID, etc.
Expected result: Three Secrets appear in the Replit Secrets pane: MAILCHIMP_API_KEY, MAILCHIMP_SERVER_PREFIX, and MAILCHIMP_LIST_ID.
Subscribe and Manage Contacts with Python
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.
1import os2import hashlib3import requests4from typing import Optional56API_KEY = os.environ["MAILCHIMP_API_KEY"]7SERVER = os.environ["MAILCHIMP_SERVER_PREFIX"]8LIST_ID = os.environ["MAILCHIMP_LIST_ID"]9BASE_URL = f"https://{SERVER}.api.mailchimp.com/3.0"1011# Mailchimp uses Basic Auth: any username + API key as password12AUTH = ("anystring", API_KEY)1314def get_subscriber_hash(email: str) -> str:15 """Mailchimp identifies subscribers by MD5 hash of lowercase email."""16 return hashlib.md5(email.lower().encode()).hexdigest()1718def subscribe_contact(email: str, first_name: str = "", last_name: str = "") -> dict:19 """Add or update a subscriber in the audience (upsert via PATCH)."""20 subscriber_hash = get_subscriber_hash(email)21 url = f"{BASE_URL}/lists/{LIST_ID}/members/{subscriber_hash}"22 data = {23 "email_address": email,24 "status_if_new": "subscribed", # Status for new subscribers25 "status": "subscribed", # Status for existing subscribers26 "merge_fields": {27 "FNAME": first_name,28 "LNAME": last_name29 }30 }31 response = requests.put(url, json=data, auth=AUTH)32 response.raise_for_status()33 return response.json()3435def unsubscribe_contact(email: str) -> dict:36 """Unsubscribe a contact (does not delete β preserves history)."""37 subscriber_hash = get_subscriber_hash(email)38 url = f"{BASE_URL}/lists/{LIST_ID}/members/{subscriber_hash}"39 response = requests.patch(url, json={"status": "unsubscribed"}, auth=AUTH)40 response.raise_for_status()41 return response.json()4243def get_contact_status(email: str) -> Optional[str]:44 """Check if an email is subscribed, unsubscribed, or not in the list."""45 subscriber_hash = get_subscriber_hash(email)46 url = f"{BASE_URL}/lists/{LIST_ID}/members/{subscriber_hash}"47 response = requests.get(url, auth=AUTH)48 if response.status_code == 404:49 return None # Not in list50 response.raise_for_status()51 return response.json().get("status")5253def add_tag_to_contact(email: str, tag_name: str) -> None:54 """Add a tag to a subscriber for segmentation."""55 subscriber_hash = get_subscriber_hash(email)56 url = f"{BASE_URL}/lists/{LIST_ID}/members/{subscriber_hash}/tags"57 data = {"tags": [{"name": tag_name, "status": "active"}]}58 response = requests.post(url, json=data, auth=AUTH)59 response.raise_for_status()6061# Example usage62if __name__ == "__main__":63 result = subscribe_contact("user@example.com", "Jane", "Doe")64 print(f"Subscribed: {result['email_address']} β status: {result['status']}")6566 add_tag_to_contact("user@example.com", "early-adopter")67 print("Tag added successfully")6869 status = get_contact_status("user@example.com")70 print(f"Current status: {status}")Pro tip: Use 'status_if_new': 'pending' instead of 'subscribed' if you want Mailchimp to send a double opt-in confirmation email to new subscribers. This is required in some jurisdictions for GDPR compliance.
Expected result: Running the Python script subscribes a test contact, adds a tag, and prints the subscription status without errors.
Build a Node.js Integration with Campaign Support
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.
1const express = require('express');2const mailchimp = require('@mailchimp/mailchimp_marketing');3const crypto = require('crypto');45const app = express();6app.use(express.json());78// Configure Mailchimp SDK from Replit Secrets9mailchimp.setConfig({10 apiKey: process.env.MAILCHIMP_API_KEY,11 server: process.env.MAILCHIMP_SERVER_PREFIX12});1314const LIST_ID = process.env.MAILCHIMP_LIST_ID;1516function getSubscriberHash(email) {17 return crypto.createHash('md5').update(email.toLowerCase()).digest('hex');18}1920// Verify API connection on startup21async function checkConnection() {22 try {23 const response = await mailchimp.ping.get();24 console.log('Mailchimp connected:', response.health_status);25 } catch (err) {26 console.error('Mailchimp connection failed:', err.response?.text);27 }28}29checkConnection();3031// Subscribe or update a contact32app.post('/subscribe', async (req, res) => {33 const { email, firstName, lastName, tags } = req.body;34 if (!email) return res.status(400).json({ error: 'email is required' });3536 const subscriberHash = getSubscriberHash(email);37 try {38 const response = await mailchimp.lists.setListMember(LIST_ID, subscriberHash, {39 email_address: email,40 status_if_new: 'subscribed',41 status: 'subscribed',42 merge_fields: {43 FNAME: firstName || '',44 LNAME: lastName || ''45 },46 tags: tags || []47 });48 res.json({ success: true, id: response.id, status: response.status });49 } catch (err) {50 console.error('Subscribe error:', err.response?.text);51 res.status(500).json({ error: err.message });52 }53});5455// Unsubscribe a contact56app.post('/unsubscribe', async (req, res) => {57 const { email } = req.body;58 if (!email) return res.status(400).json({ error: 'email is required' });5960 const subscriberHash = getSubscriberHash(email);61 try {62 await mailchimp.lists.updateListMember(LIST_ID, subscriberHash, {63 status: 'unsubscribed'64 });65 res.json({ success: true });66 } catch (err) {67 res.status(500).json({ error: err.message });68 }69});7071// Create and send a campaign72app.post('/campaign', async (req, res) => {73 const { subject, previewText, fromName, replyTo, htmlContent } = req.body;74 try {75 // Step 1: Create campaign76 const campaign = await mailchimp.campaigns.create({77 type: 'regular',78 recipients: { list_id: LIST_ID },79 settings: {80 subject_line: subject,81 preview_text: previewText || '',82 from_name: fromName,83 reply_to: replyTo84 }85 });8687 // Step 2: Set campaign content88 await mailchimp.campaigns.setContent(campaign.id, {89 html: htmlContent90 });9192 // Step 3: Send campaign93 await mailchimp.campaigns.send(campaign.id);94 res.json({ success: true, campaignId: campaign.id });95 } catch (err) {96 console.error('Campaign error:', err.response?.text);97 res.status(500).json({ error: err.message });98 }99});100101app.listen(3000, '0.0.0.0', () => {102 console.log('Mailchimp integration server running on port 3000');103});Pro tip: Use 'status_if_new': 'pending' for subscriber endpoints that handle public sign-up forms where double opt-in is required. Use 'subscribed' only for admin/server-side imports where consent is already confirmed.
Expected result: The server starts, Mailchimp ping responds with 'Everything's Chimpy!', and the /subscribe endpoint adds a test contact to your audience.
Set Up Mailchimp Webhooks and Deploy
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.
1# Flask webhook receiver for Mailchimp events2from flask import Flask, request, jsonify3import os45app = Flask(__name__)67@app.route('/mailchimp/webhook', methods=['GET', 'POST'])8def mailchimp_webhook():9 # Mailchimp sends a GET request to verify the endpoint10 if request.method == 'GET':11 return 'OK', 2001213 # POST contains the event data14 data = request.form # Mailchimp sends form-encoded data, not JSON15 event_type = data.get('type')16 email = data.get('data[email]')17 list_id = data.get('data[list_id]')1819 print(f"Mailchimp webhook: {event_type} for {email}")2021 if event_type == 'unsubscribe':22 # Update your database to mark user as unsubscribed23 # update_user_subscription_status(email, subscribed=False)24 print(f"User unsubscribed: {email}")25 elif event_type == 'subscribe':26 print(f"User subscribed: {email}")27 elif event_type == 'profile':28 new_email = data.get('data[new_email]')29 print(f"Profile updated: {email} -> {new_email}")3031 return jsonify({'status': 'received'}), 2003233if __name__ == '__main__':34 app.run(host='0.0.0.0', port=3000)Pro tip: Mailchimp sends webhook data as application/x-www-form-urlencoded (not JSON). In Flask use request.form; in Express use express.urlencoded() middleware to parse the body.
Expected result: Mailchimp confirms the webhook URL is live, and your app logs incoming events when subscribers update their preferences.
Common 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.
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.
Copy this prompt to try it in Replit
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.
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.
Copy this prompt to try it in Replit
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.
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.
Copy this prompt to try it in Replit
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://'.
1# Python: correct URL construction2server = os.environ["MAILCHIMP_SERVER_PREFIX"] # e.g., 'us6'3base_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.
1# Check status before subscribing2status = get_contact_status(email)3if status == 'unsubscribed':4 print('User must re-subscribe via opt-in form')5else:6 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.
1// Express β add urlencoded middleware2app.use(express.urlencoded({ extended: true }));34app.post('/mailchimp/webhook', (req, res) => {5 const eventType = req.body['type'];6 const email = req.body['data[email]'];7 res.json({ received: true });8});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.
Alternatives
SendGrid has native Replit Agent integration and excels at transactional email (receipts, password resets) rather than marketing campaigns, making it a simpler choice for basic email sending.
Mailgun is a developer-focused transactional email API that is more affordable for high-volume sending, better suited for automated emails than for managing marketing lists.
Constant Contact has a simpler API with fewer features than Mailchimp, making it a good choice for non-technical users who prefer its drag-and-drop campaign builder.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation