Sync contacts to Mailchimp or Kit
Push Northwind subscribers from a Sheet to your email platform — one source of truth.
Published Aug 16, 2025
Northwind keeps its subscriber list in a Google Sheet — it is where new contacts get added, tidied, and tagged. But the newsletter goes out from Mailchimp, and the two drift apart fast. Someone adds a contact to the sheet, forgets to copy it across, and that person never gets the next campaign. The sheet says one thing, the email platform says another, and nobody is sure which is right.
This script makes the sheet the single source of truth. It reads every contact row and pushes it to Mailchimp’s API, creating new subscribers and updating existing ones in the same call. Run it after editing the sheet — or on a schedule — and Mailchimp always matches it. A Kit (formerly ConvertKit) variant is shown below for teams on that platform.
What you’ll need
- A Google Sheet of contacts with
email,firstName, andlastNamein the first three columns, and a header in row 1. - A Mailchimp account, an API key, and the audience (list) ID you want to sync
into. The key ends in a data-centre suffix like
-us1— that suffix is theMC_DCvalue below. - The API key saved as
MAILCHIMP_KEYin Script Properties — see Store API keys and secrets securely. - For the Kit variant, a Kit API secret saved as
KIT_SECRETand a form ID.
The script (Mailchimp)
// Mailchimp credentials and target audience.
const MC_KEY = PropertiesService.getScriptProperties()
.getProperty('MAILCHIMP_KEY');
const MC_LIST = 'YOUR_LIST_ID'; // the audience to sync contacts into
const MC_DC = 'us1'; // the data-centre suffix from your API key
// The spreadsheet that holds the contacts.
const CONTACTS_SHEET_ID = '1abcSubsId';
/**
* Reads every contact row and upserts it into the Mailchimp audience —
* new emails are added, existing ones are updated.
*/
function syncToMailchimp() {
// 1. Read the sheet and drop the header row.
const [_, ...rows] = SpreadsheetApp.openById(CONTACTS_SHEET_ID)
.getSheets()[0]
.getDataRange()
.getValues();
// 2. Bail out early if there are no contacts.
if (!rows.length) {
Logger.log('No contacts to sync — nothing to do.');
return;
}
let synced = 0;
// 3. Upsert each contact one at a time.
for (const [email, firstName, lastName] of rows) {
if (!email) continue;
// 4. Mailchimp keys members by an MD5 hash of the lowercased email.
// PUT to that hash creates the member if new, updates it if not.
const hash = emailHash(email);
const url = 'https://' + MC_DC + '.api.mailchimp.com/3.0' +
'/lists/' + MC_LIST + '/members/' + hash;
UrlFetchApp.fetch(url, {
method: 'put',
contentType: 'application/json',
headers: {
Authorization: 'Basic ' + Utilities.base64Encode('any:' + MC_KEY),
},
payload: JSON.stringify({
email_address: email,
status_if_new: 'subscribed', // only applied when the member is new
merge_fields: { FNAME: firstName, LNAME: lastName },
}),
muteHttpExceptions: true,
});
synced++;
}
Logger.log('Synced ' + synced + ' contact(s) to Mailchimp.');
}
/**
* Returns the lowercase-email MD5 hash Mailchimp uses to address a member.
* Apps Script's digest returns signed bytes, so each byte is normalised
* back into the 0-255 range before being formatted as hex.
*/
function emailHash(email) {
return Utilities.computeDigest(
Utilities.DigestAlgorithm.MD5,
email.toLowerCase()
)
.map((b) => (b < 0 ? b + 256 : b).toString(16).padStart(2, '0'))
.join('');
}
How it works
syncToMailchimpopens the contacts sheet and uses array destructuring to keep every data row while discarding the header.- If there are no data rows, it logs a message and stops.
- It loops the rows, skipping any with a blank email.
- For each contact it computes the MD5 hash Mailchimp uses as the member ID,
then sends a
PUTto that member URL. APUTis an upsert: if no member with that hash exists, Mailchimp creates one; if it does, Mailchimp updates it.status_if_newonly applies to brand-new members, so re-running the script never resubscribes someone who unsubscribed. emailHashdoes the digest. Apps Script returns signed bytes, so negatives are shifted back into 0-255 before being padded to two hex digits — the small detail that makes the hash match what Mailchimp expects.
Example run
Say the contacts sheet holds:
| firstName | lastName | |
|---|---|---|
| [email protected] | Sam | Carter |
| [email protected] | Dana | Lee |
The first run creates both members in the Mailchimp audience with subscribed
status, and the log reads Synced 2 contact(s) to Mailchimp. If you later fix a
typo in Dana’s surname and run again, the same two PUTs fire — Sam is
unchanged, Dana’s LNAME is updated, and no duplicates appear.
The script (Kit variant)
Kit’s API addresses subscribers by email directly, so no hashing is needed:
const KIT_SECRET = PropertiesService.getScriptProperties()
.getProperty('KIT_SECRET');
const KIT_FORM = 'YOUR_FORM_ID'; // subscribers are added through a form
/** Adds each sheet contact to a Kit form, which creates or updates them. */
function syncToKit() {
const [_, ...rows] = SpreadsheetApp.openById(CONTACTS_SHEET_ID)
.getSheets()[0]
.getDataRange()
.getValues();
if (!rows.length) return;
for (const [email, firstName] of rows) {
if (!email) continue;
const url = 'https://api.convertkit.com/v3/forms/' +
KIT_FORM + '/subscribe';
UrlFetchApp.fetch(url, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify({
api_secret: KIT_SECRET,
email: email,
first_name: firstName,
}),
muteHttpExceptions: true,
});
}
}
Trigger it
If contacts are edited often, run the sync on a schedule:
- In the Apps Script editor, open Triggers (the clock icon).
- Click Add Trigger.
- Choose
syncToMailchimp(orsyncToKit), a Time-driven source, and a Day timer at an off-peak hour.
For occasional edits, skip the trigger and run it by hand from the editor, or add
an onOpen menu item so editors can sync from the sheet itself.
Watch out for
- One API call per contact. A few hundred rows is fine, but a few thousand will be slow and may brush the six-minute execution limit. For large lists, use Mailchimp’s batch endpoint, which takes many members in a single request.
- The script never deletes. Removing a row from the sheet does not unsubscribe that person — Mailchimp still has them. The sync only adds and updates.
status_if_newis deliberate. It means a contact who unsubscribed in Mailchimp stays unsubscribed even though they are still in the sheet. Treat the sheet as the membership list, not the consent record.- The data-centre suffix must be right.
MC_DChas to match the-usXsuffix on your API key, or every request 404s. Read it straight off the key. - Kit and Mailchimp model contacts differently — Kit uses forms and tags rather than audiences. Pick one platform per sheet; do not run both syncs against the same data and expect identical behaviour.
Related
Sync calendar bookings with Calendly
Bridge Google Calendar and Calendly — Northwind bookings on either side appear on both.
Updated Jan 7, 2026
Connect to an air-quality and weather feed
Build a Northwind environmental dashboard — current London AQI plus 5-day forecast.
Updated Dec 30, 2025
Build a podcast and media stats tracker
Pull Northwind's podcast download numbers across platforms into a single sheet.
Updated Dec 10, 2025
Track real-estate listings for new matches
Monitor property feeds for Northwind office hunts — alert when a match appears.
Updated Nov 28, 2025
Translate columns with a translation API
Localise Northwind text in bulk without manual work — via Google Translate or DeepL.
Updated Nov 24, 2025