# Mailchimp

Source: https://developers.swell.is/app-guides/mailchimp

The Mailchimp app keeps your customers, products, carts and orders in sync with your Mailchimp account, so you can use Mailchimp's abandoned-cart emails, order notifications, product recommendations and purchase-based segments with your store data. When someone unsubscribes, bounces or changes their email address in Mailchimp, their Swell account is updated too.

The app replaces Swell's built-in Mailchimp integration, which adds only the email addresses of opted-in customers to an audience.

## How it works

| When | What the app does |
| --- | --- |
| A customer account is created, or its email, opt-in, name, phone or address changes | Sends the customer to your Mailchimp store and the audience, subscribed if they opted in and unsubscribed if they opted out. |
| A product is created or changes, or a variant changes | Sends the product with its variants, prices, stock, images and link. |
| A cart is created, changes or is marked abandoned | Sends the cart with its items, totals and checkout link, once it has a customer email. |
| A cart becomes an order | Removes the cart from Mailchimp. The cart isn't sent again after that. |
| An order is submitted, paid, refunded, canceled, delivered or edited | Sends the order with its items, totals, discount, and payment and fulfillment status. |
| A customer account is deleted | Archives the audience member. |
| Someone unsubscribes, bounces or changes their address in Mailchimp | Checks the change with Mailchimp, then updates the Swell account's opt-in or email. Live environment only. |
| You run the first sync | Sends your existing records, with Mailchimp's automations paused. |
| Once a day | Checks the Mailchimp store and webhook, retries up to 3 failed records of each type, and ends the first sync once no record is waiting on a first-sync batch. |

## Before you begin

- A Mailchimp account and the audience your customers should join. Use a separate audience for the Swell test environment.
- If Swell's built-in Mailchimp integration is on, turn it off before you turn on the app. Running both sends your customers to Mailchimp twice. When you install the app, Swell lists the built-in integration as a conflict, and **Disable conflicting integration** turns it off. You can also turn it off in **Integrations**. Setup and the app's daily check warn while it's still on.
- The Swell CLI, to run setup right away and to send the records you already have. Install it with `npm install -g @swell/cli` and sign in to your store with `swell login`.
- Room on your Mailchimp plan. Every customer the app sends becomes a contact in your audience, including customers who didn't opt in to marketing.

## Set up

Install Mailchimp from the Swell App Store, and set it up in your test environment before the live one.

**1. Create an API key**

In Mailchimp, go to **Profile > Extras > API keys**, create a key and copy it. Copy the whole key, including the part after the dash, such as `-us14`, which tells the app which Mailchimp server your account is on.

**2. Copy the audience ID**

In Mailchimp, go to **Audience > Settings > Audience name and defaults** and copy the **Audience ID**. Mailchimp ties the app's store to this audience, and it can't be changed later.

**3. Add them to the app**

In Swell, go to **Apps > Mailchimp > Settings**. On the **Credentials** tab, enter the **API key** and **Audience ID**. On the **Ecommerce store** tab, enter a **Store name** and your **Storefront domain**, and change **Store currency** if your store doesn't use USD. On the **Inbound webhooks** tab, enter a random string of 16 characters or more as the **Webhook secret**. Turn on **Enable Mailchimp sync** and click **Save settings**.

In the test environment, also set **Store ID** to a value that differs from live, such as your store ID followed by `-test`. Both environments use your Swell store ID as the Mailchimp store ID by default, and a Mailchimp store belongs to one audience.

**4. Run setup**

Setup checks the API key, creates your store in Mailchimp and registers the webhook that brings unsubscribes back to Swell. The app's daily check, at 06:00 UTC, does the same, so without the CLI, wait for it before the test order. To run setup now, use the Swell CLI:

```sh
swell api post /functions/mailchimp/setup
```

The response lists each step with `ok` and a message. If the webhook step shows a signing secret, paste it into **Webhook signing secret** and save. Mailchimp shows it only once. In the test environment, the webhook step can fail, because Mailchimp's calls reach only the live environment.

**5. Send your existing records**

With the Swell CLI, run the first sync for products, customers and orders, then end it, as described under **First sync**. New records are sent without it.

**6. Place a test order**

Place a test order as a customer who accepts email marketing at checkout. On the customer's **Mailchimp** tab, **Sync status** shows **Synced** and **Audience status** shows **Subscribed**. The order's **Mailchimp** tab shows **Synced**. In Mailchimp, the customer is in your audience.

**7. Go live**

Settings are saved separately for each environment. In the live environment, repeat steps 3 to 5 with your live audience, leave **Store ID** empty, and add `--live` to each CLI command.

Unsubscribes, bounces and email changes made in Mailchimp reach Swell only in the live environment. Mailchimp calls a public address on your store, and Swell serves it from the live environment only.

## Using Mailchimp

### Customers and opt-ins

Each customer account becomes a customer in your Mailchimp store, with their name, company and address, and a member of your audience, with their first name, last name and phone number. A customer is sent again when their email, opt-in, name, phone or addresses change. The account updates Swell makes with each order don't send them again.

- A new customer who opted in to marketing is added to the audience as subscribed. A new customer whose opt-in was never set is added as transactional: their purchases are in Mailchimp, but they aren't subscribed to marketing.
- When a customer's opt-in is off in Swell, the app unsubscribes the member when it sends the customer, including a new customer and a member who signed up through a Mailchimp form. To stop this, turn off **Push opt-in changes to Mailchimp**.
- With **Push opt-in changes to Mailchimp** on, when a customer turns their opt-in on in Swell, the member is subscribed, even if they were transactional or had unsubscribed. A member who unsubscribed through Mailchimp can't be subscribed again by an app, so the app sets them to pending, and Mailchimp emails them to confirm. With the setting off, turning the opt-in on leaves an existing member's status as it is.
- When a customer's email changes in Swell, the app changes the existing member's address instead of adding a second member.
- Customers who existed before the app was installed are sent by the first sync or **Re-sync on save**. Until then, their changes aren't sent, except turning their opt-in on, which adds them while **Push opt-in changes to Mailchimp** is on and **Push records to Mailchimp** is set to **Automatically, as events fire**.

### Products

Each product is sent with its name, description, type, brand, images, link and variants. Each variant has its name, SKU, price, stock level and link. The price is the sale price while the product or variant is on sale. Variants of inactive products are hidden in Mailchimp. A product without variants is sent with one variant that has the product's ID.

New products are sent when they're created. Edits to products already in Mailchimp are sent, and stock adjustments and variant changes send the product even if it wasn't in Mailchimp yet. Product links are your **Storefront domain** followed by `/products/` and the product's slug.

Mailchimp rejects a cart or order that contains a product it doesn't have, so send your products first.

### Carts and abandoned-cart emails

A cart is sent with its items, totals, currency, customer and checkout link, which is the link in Mailchimp's abandoned-cart emails. Mailchimp needs a customer email, so a cart is sent only when it has a customer account with an email address and at least one product.

- With **Which carts to sync** set to **All active carts**, a cart is sent when it's created, and its later changes are sent too. A cart that has no customer account at that point, such as a guest's cart, is sent when Swell marks it abandoned.
- With **Only carts Swell has marked abandoned**, carts are sent only when Swell marks them abandoned. Mailchimp starts its abandoned-cart timer when the cart arrives, so the emails go out later.
- When a cart becomes an order, the app removes it from Mailchimp, whatever **Sync deletions** is set to, so the customer doesn't get an abandoned-cart email for it. The cart's **Mailchimp** tab then shows **Removed from Mailchimp**.
- A cart that has become an order is never sent again. When a later change, the daily check or **Re-sync on save** reaches it, the app removes it from Mailchimp instead, and the first sync leaves it out.

### Orders

An order is sent when it's submitted, with its items, totals, tax, shipping, discount, shipping and billing addresses, customer, link and the ID of the cart it came from. When it's paid, canceled or delivered, or its items, totals, refunds, coupon or addresses change, it's sent again. Mailchimp's order notifications are based on the status the app sends:

| In Swell | Sent to Mailchimp |
| --- | --- |
| Canceled | Financial status cancelled, with the cancellation date, which cancels the order in Mailchimp |
| Refunded in full | Financial status refunded |
| Partly refunded | Financial status partially_refunded |
| Paid | Financial status paid |
| Not paid | Financial status pending |
| Delivered | Fulfillment status fulfilled |
| Partly delivered | Fulfillment status partially_fulfilled |
| Not delivered | Fulfillment status unfulfilled |

An order with a coupon code and a discount is sent with the code as a promo, for the order's whole discount amount. Order links are your **Storefront domain** followed by `/account/orders/` and the order ID. Only orders already in Mailchimp get these updates, so send older orders with the first sync.

### Unsubscribes and changes from Mailchimp

Setup registers a webhook on your audience, so Mailchimp tells the app when a member unsubscribes, is cleaned after bouncing, changes their email address or edits their profile. Changes made by the member or by your Mailchimp users are sent. Changes made through Mailchimp's API, including the app's own, aren't.

Swell drops the query string from Mailchimp's calls, and with it the webhook secret, so the app doesn't trust the call itself. Before it changes an account, it reads the member from Mailchimp with your API key and acts only on what Mailchimp reports:

- An unsubscribe turns off the account's marketing opt-in if Mailchimp reports the member as unsubscribed.
- A cleaned address turns off the opt-in if Mailchimp reports the member as cleaned.
- An email change replaces the account's email if the new address is a member and the old one isn't any more.
- A profile edit doesn't change the account. Names and addresses in Swell stay as they are.

The account is found by its email, or by the address the app last sent for it, shown as **Subscribed as**. Calls for another audience, or for an address without a Swell account, are ignored. The account's **Mailchimp** tab shows the audience status and the last change from Mailchimp. With **Write unsubscribes back to Swell** off, the tab is still updated, but the account's opt-in and email aren't changed.

These changes reach only the live environment. In the test environment, changes made in Mailchimp don't reach Swell.

### First sync

The first sync sends the records you already have. Run it with the Swell CLI after setup, one type at a time: products first, then customers, then orders.

```sh
swell api post /functions/mailchimp/backfill --body '{"collection":"products"}'
swell api post /functions/mailchimp/backfill --body '{"collection":"accounts"}'
swell api post /functions/mailchimp/backfill --body '{"collection":"orders"}'
```

Each call sends up to 10 records that haven't been sent, oldest first. Repeat the same call until the response has `"has_more": false`. In the live environment, add `--live`. A record that fails is marked **Error**, with its error in the response and on its **Mailchimp** tab, and later calls move on past it. Once you've fixed the cause, send the failed records again with `{"collection":"products","sync_status":"error"}`, using that type.

Each call marks your Mailchimp store as syncing, which pauses Mailchimp's automations, so customers don't get order notifications or abandoned-cart emails for old orders and carts. Automations stay paused for new orders and carts too, until the sync ends. If the app can't pause automations, the call sends nothing and returns an error. To end the sync:

```sh
swell api post /functions/mailchimp/backfill --body '{"action":"finish"}'
```

Running setup again also ends it, and so does the daily check once no record is waiting on a first-sync batch. The next first-sync call pauses automations again. End the sync yourself when the last type is done, so automations aren't paused until the next daily check.

- The cart sync, `{"collection":"carts"}`, sends every cart that hasn't been sent, however old, and leaves out carts that became orders.
- For a large catalog, add `"mode":"batch"` to the body. Each call then sends 25 records through Mailchimp's batch API and returns a `batch_id`. To check a batch, send `{"collection":"products","batch_id":"<batch_id>"}` and repeat until `has_more` is false. When the batch is finished, its records show **Synced**, or all of them show **Error** if any failed, because the app can't read which ones. To find them, send `{"collection":"products","sync_status":"error"}` with that type, without batch mode.
- While **Push opt-in changes to Mailchimp** is on, the customer sync in batch mode leaves out customers whose opt-in is off, for direct mode to unsubscribe. Once no other customers are left, the response gives their number as `opted_out_remaining`. Send them with `{"collection":"accounts"}`, without batch mode, repeated until `has_more` is false.

### Deleted accounts and records

When a customer account is deleted in Swell, the app archives the audience member, if **Archive audience member when an account is deleted** is on. You can restore an archived member in Mailchimp. The Mailchimp customer and their orders stay, so your reports keep the revenue.

With **Sync deletions** on, deleting a product, cart or order in Swell deletes it from Mailchimp. Mailchimp can't undo this, and it removes the record from your Mailchimp reports. **Sync deletions** is off at first.

### Sync status and retries

Each customer, product, cart and order has a **Mailchimp** tab with its **Sync status**, its Mailchimp ID, when it last synced and the **Last error**. Customers also show their **Audience status**.

| Sync status | Meaning |
| --- | --- |
| Pending | Sent in a first-sync batch that hasn't been checked yet. |
| Synced | Accepted by Mailchimp. |
| Error | The last attempt failed. The reason is in Last error. |
| Skipped | Mailchimp can't accept the record, for example a cart without a customer email, or an account without an email. The reason is in Last error. |
| Canceled | Removed from Mailchimp, such as a cart that became an order, which shows Removed from Mailchimp. A cart that became an order before it was sent shows this too. An order canceled in Swell stays in Mailchimp as canceled and shows Synced. |

- To send a record again, turn on **Re-sync on save** on its **Mailchimp** tab and save. The switch is shown while **Enable Mailchimp sync** is on, and it also sends records that were never sent.
- To send every record with an error again, send `{"collection":"orders","sync_status":"error"}` to the first sync, with the type you need, and repeat it until `has_more` is false. Each call takes the records tried longest ago first. If every record in a full call fails again, the response has `has_more` false, so fix the cause before you repeat it. Like the first sync, this pauses Mailchimp's automations until the sync ends.
- If Mailchimp is busy, doesn't answer or has a server error, Swell runs the sync again later. Other errors, such as a rejected field, are saved as **Error** and aren't retried by Swell.
- Once a day, the app sends up to 3 records of each type with **Error** or **Pending** again, those tried longest ago first.

### In the dashboard

| Where | What it's for |
| --- | --- |
| Apps > Mailchimp > Settings | The API key and audience, the Mailchimp store, what syncs, and the inbound webhook. |
| Integrations | Swell's built-in Mailchimp integration. Turn it off while the app is on. |
| A record's Mailchimp tab | On customers, products, carts and orders: the sync status, Mailchimp ID, last sync, last error and Re-sync on save. Customers also show the audience status, the address last sent and the last change from Mailchimp. |
| The customer, product, cart and order lists | A Mailchimp column with each record's sync status, and the Mailchimp errors and Not synced to Mailchimp tabs. |
| Developer > Console > Logs | The app's errors and warnings, including why a change from Mailchimp wasn't applied. |

## Settings

Settings are in **Apps > Mailchimp > Settings** and are saved separately for each environment.

| Setting | Description | Default |
| --- | --- | --- |
| Enable Mailchimp sync | Turns the app on. While it's off, nothing is sent to Mailchimp, the first sync and the daily check don't run, and changes from Mailchimp are ignored. | Off |
| API key | From Mailchimp > Profile > Extras > API keys. The whole key, including the ending such as -us14. Masked on the settings page. Required. | None |
| Audience ID | From Mailchimp > Audience > Settings > Audience name and defaults. Can't be changed after setup creates the Mailchimp store. Required. | None |
| API base URL override | Leave empty. The app gets the Mailchimp address from the API key. | None |
| Store ID | The ID of the store the app creates in Mailchimp. Use a different value in the test environment. Changing it later leaves the records already sent in the old Mailchimp store. | Your Swell store ID |
| Store name | Shown in Mailchimp's reports. | Your Swell store ID |
| Store currency | The 3-letter currency code of the Mailchimp store. Carts and orders are sent in their own currency. | USD |
| Storefront domain | Your storefront's domain, such as shop.example.com, used for product and order links, and for checkout links when Swell has none. | Your store's URL |
| Push records to Mailchimp | Automatically, as events fire, or Only when re-synced manually or backfilled. With the second, a record is sent for the first time only by the first sync or Re-sync on save. Records already in Mailchimp still get their changes while Sync edits is on, including order status and opt-in changes, deletions are still sent, and carts are still removed when they become orders. | Automatically, as events fire |
| Sync customer accounts | Sends customers and audience members. | On |
| Sync products | Sends products and their variants. Carts and orders need their products in Mailchimp. | On |
| Sync carts | Sends carts for abandoned-cart emails, and removes them when they become orders. | On |
| Which carts to sync | All active carts, or Only carts Swell has marked abandoned, which sends abandoned-cart emails later. | All active carts |
| Sync orders | Sends orders. | On |
| Sync new records | Sends customers, products and carts when they're created, and orders when they're submitted. | On |
| Sync edits | Sends changes to records already in Mailchimp, including order payments, cancellations and deliveries, and sends carts when Swell marks them abandoned. | On |
| Sync deletions | Deletes products, carts and orders from Mailchimp when they're deleted in Swell. Mailchimp can't undo this. Carts that become orders are removed whatever this is set to. | Off |
| Push opt-in changes to Mailchimp | Unsubscribes the audience member when the customer's opt-in is off in Swell, and subscribes them when the opt-in is turned on, adding a customer who wasn't sent yet unless Push records to Mailchimp is set to Only when re-synced manually or backfilled. An opt-in change is an edit, so it also needs Sync edits. While this is off, opt-in changes leave an existing member's status as it is. | On |
| Archive audience member when an account is deleted | Archives the member when the customer account is deleted in Swell. The Mailchimp customer and orders stay. | On |
| Webhook secret | Any random string of 16 characters or more. Setup registers the Mailchimp webhook only when it's set. Run setup again after changing it. | None |
| Webhook signing secret | Optional. Paste the signing secret if setup shows one. Swell doesn't pass Mailchimp's original request to the app, so a signature that doesn't match is only logged. | None |
| Write unsubscribes back to Swell | Lets unsubscribes, cleaned addresses and email changes from Mailchimp change the Swell account's opt-in and email. | On |
| Callback URL override | Leave empty. Another address for Mailchimp to call, for development. | None |
| App ObjectId override | Leave empty unless setup says it couldn't determine the app's public callback URL. | None |
| Accept test webhook payloads | Leave off. Lets the webhook accept JSON test payloads sent with the Swell CLI. | Off |

## Limitations

- The first sync, and setup before the daily check, run only from the Swell CLI. The app has no buttons for them in the dashboard.
- Unsubscribes, bounces and email changes made in Mailchimp reach only the live environment.
- Subscribes in Mailchimp aren't sent to Swell, so a customer who subscribes through a Mailchimp form keeps their Swell opt-in. Changes made through Mailchimp's API, for example by another integration, aren't sent either.
- Every customer the app sends is a contact in your audience, whether they opted in or not.
- Deleting a customer account archives the audience member. The Mailchimp customer and their orders stay.
- Orders are sent without the Mailchimp campaign that led to them, so Mailchimp doesn't credit revenue to individual campaigns or automations.
- Mailchimp promo codes and promo rules, campaign webhooks and Mailchimp's site tracking aren't included. The coupon code is sent on each order.
- Batch mode can't show which records failed, and leaves customers whose opt-in is off to direct mode.
- Swell gives each function call 10 seconds, so the first sync sends 10 records per call, or 25 in batch mode, and the daily check retries 3 records of each type.

## Troubleshooting

Errors are shown in **Last error** on a record's **Mailchimp** tab, and each list's **Mailchimp errors** tab shows the records that failed. Setup shows its problems in its response. The app also logs errors and warnings. In the Swell dashboard, the logs are under **Developer > Console > Logs**.

To see the app's state without changing anything, run `swell api get /functions/mailchimp/setup`, with `--live` in the live environment. It reports the settings, whether Mailchimp accepts the API key, the webhook, and whether the built-in integration is on.

### Nothing reaches Mailchimp

Check that **Enable Mailchimp sync** and the type's sync setting are on, and that **Push records to Mailchimp** is set to **Automatically, as events fire**. Records that existed before the app was installed are sent only by the first sync or **Re-sync on save**.

### Orders or carts fail with a product error

The error includes "this usually means a line item references a product that has never been pushed to Mailchimp". Run the first sync for products, then send the order again with **Re-sync on save**, or let the daily check retry it.

### Mailchimp doesn't send order notifications or abandoned-cart emails

If you ran the first sync, end it with `{"action":"finish"}`. Until it ends, Mailchimp's automations stay paused. Check that the automation is turned on in Mailchimp. A guest's cart is sent only once Swell marks it abandoned, and carts without a customer email aren't sent.

### Unsubscribes in Mailchimp don't reach Swell

They reach only the live environment. In live, check that **Webhook secret** is set and that setup ran with `--live`. The setup report's `webhooks` section shows whether the webhook is registered. Check that **Write unsubscribes back to Swell** is on, and that the customer's Swell email matches the Mailchimp address. The logs say why a change wasn't applied, for example when Mailchimp doesn't report the member as unsubscribed.

### A customer is unsubscribed in Mailchimp but didn't unsubscribe

Their marketing opt-in is off in Swell. With **Push opt-in changes to Mailchimp** on, the app unsubscribes the member whenever it sends a customer whose opt-in is off.

### A customer is pending in Mailchimp

The customer turned their opt-in on in Swell after unsubscribing through Mailchimp. Mailchimp emailed them to confirm, and they're subscribed once they do.

### Product links in emails lead to a missing page

Check **Storefront domain**. Product links are the domain followed by `/products/` and the product's slug, so they work only if your storefront serves products at that path.

### "Mailchimp API key has no datacenter suffix"

Paste the whole API key, including the part after the dash, such as `-us14`.

### "Mailchimp rejected the API key"

The key is wrong or was deleted in Mailchimp, or Mailchimp didn't answer in time. Run setup again, and if the message stays, create a new key and enter it in the app settings.

### "already exists but is bound to audience"

A Mailchimp store with this **Store ID** already belongs to another audience, often the one your other environment uses. Set a different **Store ID**, or enter that store's audience in **Audience ID**, and run setup again.

### "No webhook secret is set, so no webhook was registered"

Enter a **Webhook secret** of 16 characters or more, save, and run setup again.

### "Could not determine this app’s public callback URL"

Setup couldn't work out the address Mailchimp should call, so it didn't register the webhook. Email support@swell.is for the app's ID, enter it in **App ObjectId override**, and run setup again.

### "Record has no customer account with an email address, and Mailchimp requires one"

The cart or order is **Skipped** because Mailchimp needs a customer email. A guest's cart is sent once it has a customer account with an email address and Swell marks it abandoned.

### "Swell's built-in Mailchimp integration is still on"

Turn off the built-in Mailchimp integration in **Integrations**. Running both sends your customers to Mailchimp twice.

### "Mailchimp sync is turned off in app settings"

The first sync runs only while **Enable Mailchimp sync** is on. Turn it on and save.

### "Could not pause Mailchimp's automations for the backfill, so nothing was sent"

The first sync couldn't mark your Mailchimp store as syncing, so it sent nothing. This usually means setup hasn't created the Mailchimp store yet. Run setup, then repeat the call.

### "Every record in this page failed again"

Every record in a re-send with `sync_status` failed again, so the response has `has_more` false. Fix the cause shown in each record's error, then repeat the call. More records with that status may be waiting.

### A batch finished with errored operations

Mailchimp doesn't tell the app which records failed, so all of the batch's records show **Error**. To see each failing record and its error, send `{"collection":"products","sync_status":"error"}` with that type, without `"mode":"batch"`.

## Uninstalling

Uninstalling the app stops syncing with Mailchimp and the daily check, and removes the **Mailchimp** tabs, columns and list tabs from your records. Uninstalling applies to the environment you are in, so remove the app from the test and live environments separately. The app's settings and the sync details saved on your records are kept, and come back if you reinstall the app.

The app can't clean up Mailchimp when it's uninstalled. Your Mailchimp store and its customers, products, carts and orders stay, and so do your audience members and the webhook on your audience. Carts already in Mailchimp aren't removed when they become orders, so turn off Mailchimp's abandoned-cart emails before you uninstall. If a first sync is still running, end it first, or Mailchimp's automations stay paused. To stop Mailchimp calling your store, delete the webhook in your audience's settings in Mailchimp.

To use the built-in integration again, turn it on in **Integrations**.

## For developers

| Function | Runs on | What it does |
| --- | --- | --- |
| accounts-sync | account.created, account.updated and account.deleted | Upserts the ecommerce customer and the audience member, applies opt-ins and opt-outs, and archives the member of a deleted account. |
| products-sync | product.created, product.updated, product.deleted, product.stock_adjusted and product.variant.updated | Upserts the product with all its variants, or deletes it when Sync deletions is on. |
| carts-sync | cart.created, cart.updated, cart.abandoned, cart.converted and cart.deleted | Creates or updates the cart, and deletes it on conversion, or on deletion when Sync deletions is on. A cart with an order_id is never sent: the push deletes it if it was sent and otherwise records it as canceled. |
| orders-sync | order.submitted, order.updated, order.paid, order.canceled, order.delivered and order.deleted (order.created is ignored) | Upserts the order with its financial and fulfillment status, or deletes it when Sync deletions is on. |
| mailchimp-webhook | A public GET and POST route, called by Mailchimp | Confirms each audience change with Mailchimp, then writes email_optin or email on the account. |
| setup | A private GET and POST route | GET reports the state. POST checks the key, creates or updates the Mailchimp store and registers the webhook. |
| backfill | A private POST route | Sends records directly or through Mailchimp's batch API, checks batches and ends the first sync. |
| reconcile | Daily, cron 0 6 * * * | Repairs the store and webhook, retries up to 3 error or pending records per collection, and turns off is_syncing once no record in an enabled collection is pending. |

The app stores its fields under `$app.mailchimp` on accounts, products, carts and orders: `sync_status`, `remote_key`, `remote_id`, `batch_id`, `last_synced_at`, `last_error` and `resync_requested`. Accounts also have `remote_email`, `list_status`, `last_webhook_at`, `last_webhook_event` and `last_webhook_fired_at`, and products have `variant_count`.

`remote_key` is always the Swell record ID, and the app uses it as the Mailchimp ID, so a repeated push updates instead of duplicating. Customers, products, orders and members are written with PUT, except opt-outs and email changes, which PATCH the member and so don't create one. Carts have no PUT in Mailchimp's API, so `remote_key` decides between POST and PATCH. Members are addressed by their lowercased email. Mailchimp calls time out after 2 to 4 seconds. A 429 is waited out once for up to 2 seconds. Timeouts, 429s and 5xx errors are thrown so the platform redelivers the event, and other 4xx errors are recorded on the record and not retried.

The webhook is registered for `unsubscribe`, `cleaned`, `upemail` and `profile` events, from the `user` and `admin` sources only. Its URL is `https://<store-id>.swell.store/functions/<app ObjectId>/mailchimp-webhook?secret=<webhook secret>`, built from the app's ObjectId, which setup finds in `/:functions`. The gateway passes a POST's body without its query string, and `req.rawBody` isn't Mailchimp's original bytes, so neither the secret nor the signature can authenticate a delivery. The app reads the member from Mailchimp before acting, rejects a secret that's present and wrong with a 401, and dedupes on `fired_at` and the event type. The only fields it writes outside `$app.mailchimp` are `email_optin` and `email` on accounts, always together with `last_webhook_at`, which accounts-sync uses to ignore its own write.

The backfill route takes `collection` (`accounts`, `products`, `carts` or `orders`), `mode: "batch"`, `batch_id` to check a batch, `record_id` to send one record without pausing automations, `sync_status` to resend records with that status, least recently updated first, `limit` in batch mode, and `action: "finish"`. Every call that sends records without `record_id` sets the Mailchimp store's `is_syncing` to true first, and answers 502 without sending if it can't. Without `sync_status`, it selects records with no `remote_key` whose `sync_status` isn't `skipped`, `canceled` or `error`. Carts with an `order_id` are never selected. In batch mode with **Push opt-in changes to Mailchimp** on, accounts with `email_optin: false` are left for direct mode and counted in `opted_out_remaining`, and each queued account records `remote_email`.

```sh
swell api get /functions/mailchimp/setup
swell api post /functions/mailchimp/backfill --body '{"collection":"orders","record_id":"<order ID>"}'
swell api post /functions/mailchimp/backfill --body '{"collection":"orders","sync_status":"error"}'
```

## Support

Swell builds and maintains the Mailchimp app. For help with the app, email support@swell.is. For questions about your Mailchimp account, audience or automations, contact Mailchimp.
