This recipe describes Mailchimp’s data model and event types based on the platform’s general public documentation. Mailchimp evolves its API over time — confirm against Mailchimp’s current docs before relying on specific field names. The Virtuous-side mapping is the durable part of the recipe.
Architecture
Three workers:- Inbound from Mailchimp captures subscriber events (subscribe, update profile, unsubscribe, bounce) and writes them to the outbound queue.
- Submitter drains the queue into Virtuous as Contact Transactions.
- Tag sync propagates Virtuous-side tag changes back to Mailchimp list membership.
Field mapping
Mailchimp subscriber → Virtuous Contact
| Virtuous field | Source from Mailchimp |
|---|---|
referenceSource | The literal string "Mailchimp" |
referenceId | The Mailchimp Subscriber Hash (or the Mailchimp ID — both are stable per subscriber) |
firstName / lastName | Mailchimp merge fields FNAME / LNAME |
email (with emailType: "Home Email") | The subscriber’s email address — Mailchimp’s primary key |
phone (with phoneType: "Mobile Phone") | Mailchimp merge field PHONE if configured |
address1 / city / state / postal | Mailchimp merge field ADDRESS (if structured) |
contactType | Default to "Household" unless the customer’s Mailchimp audience captures organization data |
Mailchimp Audience and Tags → Virtuous Tags or Custom Fields
The most important architectural decision in a Mailchimp-Virtuous integration: how to represent Mailchimp’s list and tag structure in Virtuous. Mailchimp organizes subscribers into:| Mailchimp concept | Description |
|---|---|
| Audience (list) | The top-level grouping. A single subscriber belongs to one Audience but can be in many. |
| Group | Within an Audience, optional categorization (e.g., “Newsletter Type” with values “Weekly,” “Monthly”). |
| Tag | Within an Audience, free-form labels applied to subscribers. |
| Mailchimp | Virtuous mapping | Notes |
|---|---|---|
| Audience membership | Virtuous Tag (e.g., “Mailchimp: Newsletter Audience”) | One tag per Audience the customer maintains. |
| Mailchimp Tag | Virtuous Tag (with prefix to disambiguate) | Use a consistent prefix like "MC:" to keep them visually distinct. |
| Mailchimp Group | Virtuous Custom Field | Groups are categorical; a custom field with the same option values is the natural fit. |
Step 1: receive Mailchimp webhook events
Mailchimp delivers events to a webhook endpoint you register in their dashboard. The events relevant to Virtuous sync:| Mailchimp event | Trigger | Virtuous action |
|---|---|---|
subscribe | New email signup | Create or update Contact with the new email |
unsubscribe | Subscriber unsubscribed | Update Contact: set isOptedIn: false on the email; optionally add a tag |
profile | Subscriber updated merge fields | Update Contact with new field values |
upemail | Subscriber changed their email | Update Contact’s primary email |
cleaned | Email bounced or was removed for hygiene reasons | Update Contact: mark email as invalid; surface for cleanup |
JavaScript
- Form-urlencoded body, not JSON. Mailchimp uses URL-encoded webhook bodies; standard JSON parsing middleware won’t work.
- Multi-tenant resolution at the queue boundary. The
list_idfrom Mailchimp maps to a specific customer’s Audience. Resolvecustomer_idat queue insertion so downstream workers always know which Virtuous organization to target.
Step 2: submit subscriber events to Virtuous
The submitter worker drains the queue, mapping Mailchimp events to Virtuous Contact Transactions:JavaScript
subscribe and profile events both use Contact Transaction — Virtuous’s matching algorithm handles whether to create a new Contact or merge into an existing one. See Create a Contact.
Handling unsubscribes
When a subscriber unsubscribes in Mailchimp, the Virtuous Contact should be updated to reflect that the email is no longer opted-in:JavaScript
isOptedIn field on the ContactMethod is Virtuous’s signal that the email is suppressed from outreach. The customer’s team uses this to filter out unsubscribed addresses when sending appeals through Virtuous.
Handling bounced or cleaned emails
When Mailchimp removes an email for hygiene reasons (hard bounce, abuse complaint, repeated soft bounces), surface this to the customer’s team:JavaScript
Step 3: propagate Virtuous tag changes back to Mailchimp
The inbound direction: when Virtuous changes a Contact’s tags or opt-in status, those changes should flow back to Mailchimp’s list membership. Subscribe to thecontactUpdate event in Virtuous (see Webhooks Overview). On each event:
- Check whether the Contact has a Mailchimp
referenceId(i.e., your integration manages this Contact). - Compare the current Virtuous tags with the last known state in your sync database.
- For any change, call the Mailchimp API to update the subscriber’s audience membership or tags.
JavaScript
- Filters to Mailchimp-prefixed tags only. A Virtuous tag like
"Major Donor"doesn’t belong on a Mailchimp list — only tags that originated from or are meant for Mailchimp should round-trip back. - Diffs against last known state. Without the diff, every
contactUpdateevent would re-send every tag to Mailchimp, multiplying API calls.
Avoiding sync loops
The same sync-loop defense from Build a Two-Way Sync applies. When your Mailchimp inbound handler applies a tag to a Virtuous Contact, the resultingcontactUpdate event will fire — your outbound handler must recognize that the change originated from Mailchimp and suppress the round-trip.
The pattern: store the timestamp of the last Mailchimp-originated update on each Contact, and in the outbound handler, ignore Virtuous events that arrived within a short window after a Mailchimp inbound update.
Step 4: opt-in handling
Mailchimp distinguishes between several opt-in states:| Mailchimp status | Meaning | Virtuous mapping |
|---|---|---|
subscribed | Subscriber confirmed and active | isOptedIn: true on the email ContactMethod |
unsubscribed | Subscriber opted out | isOptedIn: false on the email ContactMethod |
pending | Subscriber signed up but hasn’t confirmed (double opt-in) | Hold sync until confirmation — don’t create the Virtuous Contact yet |
cleaned | Email removed for hygiene | isOptedIn: false + ContactNote |
transactional | Receives transactional emails only, not marketing | isOptedIn: false if your customer uses Virtuous for marketing only |
pending state is the most often-missed: a subscriber in double-opt-in flow has provided their email but hasn’t yet clicked the confirmation link. Creating a Virtuous Contact at that moment means non-confirmed subscribers count toward the customer’s metrics. Most customers prefer to wait for confirmation.
Confirmation gating
JavaScript
subscribe event with status: subscribed), the Contact Transaction is submitted.
Step 5: bulk import on initial connection
When a customer first connects their Mailchimp account, you typically need to bulk-load their existing subscribers into Virtuous. The pattern is the same as the Import Historical Gifts recipe but applied to Contacts:Export all subscribers from Mailchimp
Use Mailchimp’s export API to pull every subscriber across all the customer’s Audiences. Capture merge fields, tags, and Audience membership.
Insert each subscriber into your sync queue
Treat the bulk export as a series of synthetic
subscribe events. Each subscriber becomes a queued Contact Transaction.Throttle the submission rate
Bulk imports can hit the 1,500/hour Virtuous rate limit immediately if not paced. Throttle the submitter to leave headroom for live events arriving during the import.
POST /api/Contact/Query to page through Contacts and call Mailchimp’s bulk-subscribe API.
Common edge cases
A subscriber changes their email
Mailchimp’supemail event signals an email change. The pattern:
- Find the existing Virtuous Contact by Mailchimp
referenceId. - Update the email ContactMethod’s
valueto the new address. - Preserve the rest of the Contact’s data.
GET-then-PUT pattern — see Update a Contact: change a primary email.
A subscriber exists in Mailchimp but not in Virtuous
This is the normal first-time case. The Contact Transaction creates a new Contact.A Contact exists in Virtuous but not in Mailchimp
If your integration is the source of truth for the customer’s email list, you may want to push these Contacts into Mailchimp. If Mailchimp is the source of truth, leave them alone — Mailchimp may not be the right channel for those donors. Document the source-of-truth model with the customer before deployment.A subscriber is in multiple Mailchimp Audiences
Mailchimp allows the same email across multiple Audiences. The Virtuous Contact should reflect all of them — typically as multiple tags rather than multiple Contacts. The matching algorithm will merge them naturally if you submit Contact Transactions for each Audience with the same email.Production readiness checklist
- Mailchimp webhook signature verification implemented per Mailchimp’s documentation.
- The Mailchimp Subscriber Hash (or ID) is used as the Virtuous
referenceId— stable across email changes. pendingsubscribers are held out of sync until confirmation.cleanedandunsubscribeevents setisOptedIn: falseon the email ContactMethod.- Email-only Tags (Mailchimp-derived) are prefixed for visual distinction from Virtuous-native tags.
- Sync-loop defense in place: tags originated by Mailchimp are not echoed back via the outbound direction.
- Multi-tenant credentials and Audience-to-customer mapping in place.
- Bulk-import path throttles to stay within the 1,500/hour Virtuous rate limit.
- Reconciliation pulls modified Contacts periodically to catch missed webhooks.
Where to go next
Constant Contact to Virtuous CRM
The same architecture applied to Constant Contact — different source platform, same Virtuous-side patterns.
Build a Nightly Data Sync
For ESPs without webhook support, the batched alternative to event-driven sync.
Build a Two-Way Sync
The general two-way sync architecture this recipe instantiates.
Stripe to Virtuous CRM
A donation-side companion recipe with the same architectural patterns.