Skip to main content
This recipe walks through building a Mailchimp-to-Virtuous integration: receiving subscriber events from Mailchimp, mapping subscribers to Virtuous Contacts, propagating list membership as Tags or custom field values, and keeping email preferences synchronized in both directions. The Virtuous-side patterns are stable across all email-platform integrations — the same architecture applies almost identically to Constant Contact and other ESPs. The differences are mostly on the source-platform side: event names, API shapes, and segmentation models.
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.
The architecture parallels Stripe to Virtuous — the source-platform-specific receivers differ, but the Virtuous-side patterns are identical.

Field mapping

Mailchimp subscriber → Virtuous Contact

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: The recommended mapping into Virtuous:
Confirm the mapping with the customer’s marketing team before implementation. Some customers prefer Mailchimp-derived data to flow into Virtuous custom fields instead of tags for cleaner segmentation. The architectural pattern is the same either way — just substitute “custom field write” for “tag update” in the code.

Step 1: receive Mailchimp webhook events

Mailchimp delivers events to a webhook endpoint you register in their dashboard. The events relevant to Virtuous sync: A representative webhook handler:
JavaScript
Two things this gets right:
  • 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_id from Mailchimp maps to a specific customer’s Audience. Resolve customer_id at 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
The 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
The 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.
Always use the GET-then-PUT pattern when updating a Contact’s opt-in status — see Update a Contact. A partial PUT could clear other fields you didn’t intend to modify.

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
The ContactNote gives the customer’s team context for the opt-out — distinguishing “donor chose to unsubscribe” from “email address is broken.”

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 the contactUpdate event in Virtuous (see Webhooks Overview). On each event:
  1. Check whether the Contact has a Mailchimp referenceId (i.e., your integration manages this Contact).
  2. Compare the current Virtuous tags with the last known state in your sync database.
  3. For any change, call the Mailchimp API to update the subscriber’s audience membership or tags.
JavaScript
Two things this gets right:
  • 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 contactUpdate event 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 resulting contactUpdate 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: The 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
When the subscriber confirms (Mailchimp fires a separate 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:
1

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.
2

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.
3

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.
4

Reconcile after the import

Confirm the count of synced Contacts matches the count of exported subscribers. Investigate any that failed to sync.
The reverse direction — bulk import of Virtuous Contacts into Mailchimp — is less common but follows the same pattern. Use POST /api/Contact/Query to page through Contacts and call Mailchimp’s bulk-subscribe API.

Common edge cases

A subscriber changes their email

Mailchimp’s upemail event signals an email change. The pattern:
  1. Find the existing Virtuous Contact by Mailchimp referenceId.
  2. Update the email ContactMethod’s value to the new address.
  3. Preserve the rest of the Contact’s data.
Use the 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.
  • pending subscribers are held out of sync until confirmation.
  • cleaned and unsubscribe events set isOptedIn: false on 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.
Last modified on May 21, 2026