---
title: "WooCommerce Mailchimp Integration: API, Upsert, Limits"
description: "A WooCommerce Mailchimp integration on the Marketing API: the data-centre host, the PUT upsert with status_if_new, tags as a second call, the 10-connection cap."
url: "https://wpwebhooks.org/blog/woocommerce-mailchimp-integration/"
date: "2026-09-17"
---

# WooCommerce Mailchimp Integration: API, Upsert, Limits

**TL;DR:** A WooCommerce Mailchimp integration on the API is one idempotent call, and one decision about consent that the API will not make for you.

-   The base URL carries your **data centre**, taken from the suffix of the API key: a key ending `-us6` talks to `us6.api.mailchimp.com`.
-   Use **PUT** on the member, not POST. PUT adds or updates; POST fails on the second order from the same customer.
-   `status_if_new` applies only to addresses Mailchimp has never seen, which is what keeps an upsert from resubscribing someone who left.
-   The limit is **10 simultaneous connections** per user, not requests per second. Slow calls use it up faster than frequent ones.

/ Official

## Is there an official Mailchimp plugin for WooCommerce?

Yes, and for a standard store it is the right answer. [Mailchimp for WooCommerce](https://wordpress.org/plugins/mailchimp-for-woocommerce/) is published by Mailchimp, has 200,000+ active installs, and syncs customers, orders, products and carts into Mailchimp's e-commerce data model, which is what powers abandoned-cart emails, product recommendations and revenue reporting.¹

The API route earns its place in narrower situations. You want one event, such as a completed order for a particular product, to tag a contact, without syncing the whole catalogue. The audience is fed by more than the shop: a membership plugin, a booking form, a custom post type. You need control over which customers are sent at all. Or you already run a delivery queue and would rather add one destination than another plugin with its own background jobs. If none of those apply, install the official plugin and stop here.

/ Auth

## How do you authenticate to the Mailchimp Marketing API?

With an API key, sent either as HTTP Basic or as a bearer token. The [API fundamentals](https://mailchimp.com/developer/marketing/docs/fundamentals/) accept both: Basic with any string as the username and the key as the password, or `Authorization: Bearer` followed by the key.

The part that breaks copied snippets is the host. The root URL is `https://<dc>.api.mailchimp.com/3.0/`, where `<dc>` is the data centre your account lives in, and it is the suffix after the dash in the key. A key ending `-us6` only works against `us6.api.mailchimp.com`. Hard-code the host from a tutorial and the first request fails against an account in another data centre.

The key is also a person. Mailchimp ties API access to the user who created the key and to that user's role: remove the user from the account and the key is revoked with them. Create the key from an account that will outlive staff changes.

/ Upsert

## Which endpoint adds a WooCommerce customer without creating duplicates?

[PUT /lists/{list\_id}/members/{subscriber\_hash}](https://mailchimp.com/developer/marketing/api/list-members/add-or-update-list-member/), the add-or-update call. POST to the collection only adds, so the second order from the same customer is rejected as an existing member and your log fills with 400s that mean nothing is wrong.

The `subscriber_hash` is the MD5 hash of the lowercase email address. Mailchimp uses the hash, per its [parameters documentation](https://mailchimp.com/developer/marketing/docs/methods-parameters/), so that addresses do not leak into URLs and server logs. Lowercase first: `Anna@Example.com` and `anna@example.com` are one member and two different hashes. The endpoint also accepts the plain email address or the contact id in that position, which matters if the tool building the URL cannot compute a hash.

Two body fields are required: `email_address` and `status_if_new`. Both are used only when the address is not already in the audience.

FIG 01 — Order to audience: consent decides the status, PUT makes it idempotent, tags are a second call

PHP — upsert the customer, then tag them

```
add_action( 'woocommerce_checkout_order_created', function ( $order ) {
    $email = strtolower( trim( $order->get_billing_email() ) );
    if ( ! $email ) {
        return;
    }

    // Your checkout's marketing checkbox. No tick, no marketing status.
    $opted_in = 'yes' === $order->get_meta( '_marketing_opt_in' );

    // In production: hand this to a queue instead of calling inline.
    mc_sync_member( $email, $order, $opted_in );
} );

function mc_sync_member( $email, $order, $opted_in ) {
    $key  = MAILCHIMP_API_KEY;                       // abc123...-us6
    $dc   = substr( $key, strrpos( $key, '-' ) + 1 );  // us6
    $base = 'https://' . $dc . '.api.mailchimp.com/3.0/lists/' . MAILCHIMP_LIST_ID;
    $hash = md5( $email );
    $auth = [ 'Authorization' => 'Bearer ' . $key, 'Content-Type' => 'application/json' ];

    $res = wp_remote_request( $base . '/members/' . $hash, [
        'method'  => 'PUT',
        'timeout' => 10,
        'headers' => $auth,
        'body'    => wp_json_encode( [
            'email_address' => $email,
            // Only read for a brand-new address. Never touches an existing status.
            'status_if_new' => $opted_in ? 'pending' : 'transactional',
            'merge_fields'  => [
                'FNAME' => $order->get_billing_first_name(),
                'LNAME' => $order->get_billing_last_name(),
            ],
        ] ),
    ] );

    if ( is_wp_error( $res ) || 200 !== wp_remote_retrieve_response_code( $res ) ) {
        return $res; // let the queue decide whether to retry
    }

    // Tags are NOT a field on the PUT body. They have their own endpoint.
    return wp_remote_post( $base . '/members/' . $hash . '/tags', [
        'timeout' => 10,
        'headers' => $auth,
        'body'    => wp_json_encode( [
            'tags' => [ [ 'name' => 'customer', 'status' => 'active' ] ],
        ] ),
    ] );
}
```

The tag call is the detail most tutorials get wrong. The create endpoint accepts a `tags` array; the add-or-update endpoint does not. Tags on an upsert go through [POST /lists/{list\_id}/members/{subscriber\_hash}/tags](https://mailchimp.com/developer/marketing/api/list-member-tags/add-or-remove-member-tags/), where each tag is a name plus `active` or `inactive`, and a tag that does not exist yet is created. It answers `204` with no body, so check the status code, not the JSON.

Merge fields are keyed by merge tag, not by label: `FNAME`, not "First Name". A required merge field you do not send fails the whole call unless you pass `skip_merge_validation=true` as a query parameter. The [merge fields guide](https://mailchimp.com/developer/marketing/docs/merge-fields/) lists the types.

/ Consent

## Which status should a new customer get?

The API offers five: `subscribed`, `unsubscribed`, `cleaned`, `pending` and `transactional`. Three of them are decisions you make at checkout.

-   **`pending`** sends Mailchimp's confirmation email and subscribes the customer only when they click. It is the defensible default for anyone who ticked a marketing box.
-   **`subscribed`** skips confirmation. Use it only when your own checkout already recorded explicit, provable consent.
-   **`transactional`** stores the contact without marketing permission. That is the honest status for a customer who bought something and did not opt in.

The design that keeps you out of trouble is in the field name. `status_if_new` is read only when the address is new. A customer who unsubscribed last year and orders again today matches an existing member, so their status is left alone. Send `status` instead and you overwrite it on every order, which is how a store quietly resubscribes people who asked to leave.

![Cyberpunk illustration of a steaming galley line under a rank of mint heat lamps, every station taken by a cook bent over it, while a figure with a chrome forearm is forced back off the line, gripping the overhead rail with boots skidding on wet tile.](https://wpwebhooks.org/blog/woocommerce-mailchimp-integration/og_image.jpg)

/ Limits

## What are the Mailchimp API rate limits?

Concurrency, not frequency. The Marketing API allows **10 simultaneous connections** per user and answers the eleventh with `429 TooManyRequests`.² Three properties of that limit are easy to miss, all stated in the [error glossary](https://mailchimp.com/developer/marketing/docs/errors/): it is per user, not per API key or per client, so every integration on the account shares it; how long a request runs decides how many you can make; and a request that timed out on your side may still be running on Mailchimp's, holding a slot you think is free.

The arithmetic is worth doing once. Each new customer costs two calls, the PUT and the tag POST, made in sequence, so one customer holds one connection at a time.

-   At a typical 400 ms per call: 2 × 0.4 s = **0.8 s per customer**.
-   Ten connections in parallel: 10 ÷ 0.8 s = **12.5 customers per second** before the first 429.
-   During a slow patch at 3 s per call: 10 ÷ 6 s = **1.7 customers per second**, a seven-fold drop with no change on your side.

No ordinary store sells 12 orders a second. A flash sale plus a historical import running on the same account does reach it, and because calls also carry a 120-second server timeout, a stalled batch can pin all ten connections for two minutes. At exceptionally high volume Mailchimp notes you may receive a 429 or a 403 with no JSON body at all, so parse defensively.

| Concern | Hand-rolled wp\_remote\_request | Webhook Actions |
| --- | --- | --- |
| Full catalogue and cart sync | Not this either. Use the official plugin | Not what it does. It delivers events; it does not mirror a product catalogue |
| Where the call runs | Inside checkout unless you build a queue | Queued in its own table, delivered in the background |
| A 429 or 5xx from Mailchimp | Lost unless you wrote retry logic | Retried with exponential backoff, five attempts by default |
| A 400 from a bad merge field | Invisible | Marked permanently failed, with the response body in the delivery log |
| The API key | A constant in wp-config.php | An encrypted Bearer or Basic credential, redacted in every log |
| Member URL per customer | String concatenation | A URL template resolved from the payload, using the email form the endpoint accepts |
| Constant fields such as status\_if\_new | Written in your array | A short pre-dispatch snippet; field mapping moves values and cannot invent one |
| The tag call after the upsert | A second request in the same function | A chained webhook that fires on the upsert's 2xx |

try\_it

Seeing it run beats reading about it. The live preview boots a throwaway WordPress with Webhook Actions already installed and demo deliveries sitting in the log — no signup, nothing left on your machine afterwards.

[Try the live preview →](https://playground.wordpress.net/?blueprint-url=https://wpwebhooks.org/blueprint.json) [Install plugin](https://downloads.wordpress.org/plugin/flowsystems-webhook-actions.zip)

/ Queue

## Should the Mailchimp call run inside checkout?

No. `woocommerce_checkout_order_created` fires while the customer is waiting for the order-received page, and two sequential calls to a third party put their latency, and their outages, inside your conversion funnel. A ten-second timeout on each is twenty seconds of spinner on the worst day, which is also your busiest day. The [hook itself is documented here](https://wpwebhooks.org/blog/woocommerce-checkout-order-created/), including what it passes and when it does not fire.

Record the intent when the hook fires and let a background worker make the calls. The PUT is idempotent, so a retry after a timeout is safe: the same address lands on the same member. The tag call is idempotent as well, since setting an active tag active again changes nothing. That combination is rare among marketing APIs and it is the reason a plain retry policy is enough here; no deduplication table is needed. If you are choosing retry intervals, [the backoff article](https://wpwebhooks.org/blog/webhook-retry-policy-exponential-backoff/) covers the schedule and the dead-letter state.

Treat status codes differently. A 429 and any 5xx are worth retrying. A 400 with _Invalid Resource_ is a payload you built wrong, a 401 is a revoked key, and a 403 means the key's owner lost access or the plan does not include the feature. Retrying those burns connections and changes nothing, which is how [integrations fail quietly for weeks](https://wpwebhooks.org/blog/why-wordpress-webhooks-silently-fail-in-production/).

/ Exposure

## What does Mailchimp not protect you from?

**Your consent logic.** The API accepts `subscribed` for any address you send. Whether the customer agreed is a fact only your checkout knows, and the legal exposure for getting it wrong is yours.

**Audience size is billing.** Mailchimp plans are priced by contacts. Pushing every guest checkout into the audience grows the bill whether or not those people ever receive a campaign, so decide deliberately which customers you send, and check how your plan counts transactional and unsubscribed contacts before you ship.

**A shared connection pool.** The ten connections belong to the user, and the official plugin, a form plugin and your own code all draw from the same ten if they share a key owner. A sync that starves is usually being starved by another integration.

**Order data is a separate model.** Adding a member does not give Mailchimp revenue. Purchase-based segments and automations read from the e-commerce endpoints, and [adding an order](https://mailchimp.com/developer/marketing/api/ecommerce-orders/add-order/) requires an id, a customer, a currency code, an order total and line items that each reference a product and a variant that already exist in a store you created through the API. That is a catalogue sync, and it is the point at which the official plugin is less work than your own code.

/Footnotes

¹ Install count and publisher read from the [WordPress.org plugin page](https://wordpress.org/plugins/mailchimp-for-woocommerce/) on 2026-09-19: 200,000+ active installations, version 6.2, author Mailchimp.

² Mailchimp [API fundamentals](https://mailchimp.com/developer/marketing/docs/fundamentals/), Throttling and Stream timeouts: 10 simultaneous connections, 429 on exceeding it, and a 120-second timeout on API calls.

## Structured data

```json
{"@context":"https://schema.org","@type":"Article","headline":"WooCommerce Mailchimp Integration: API, Upsert, Limits","description":"A WooCommerce Mailchimp integration on the Marketing API: the data-centre host, the PUT upsert with status_if_new, tags as a second call, the 10-connection cap.","datePublished":"2026-09-17","dateModified":"2026-09-17","author":{"@type":"Person","name":"Mateusz Skorupa","url":"https://wpwebhooks.org/about/"},"publisher":{"@type":"Organization","name":"WP Webhooks","url":"https://wpwebhooks.org"},"url":"https://wpwebhooks.org/blog/woocommerce-mailchimp-integration/","image":{"@type":"ImageObject","url":"https://wpwebhooks.org/blog/woocommerce-mailchimp-integration/og_image.jpg","width":1200,"height":630,"caption":"Cyberpunk illustration of a steaming galley line under a rank of mint heat lamps, every station taken by a cook bent over it, while a figure with a chrome forearm is forced back off the line, gripping the overhead rail with boots skidding on wet tile."},"keywords":["woocommerce mailchimp integration","mailchimp woocommerce integration","woocommerce mailchimp","integrate mailchimp with woocommerce","mailchimp for woocommerce plugin","mailchimp api wordpress"]}

{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"WP Webhooks","item":"https://wpwebhooks.org/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://wpwebhooks.org/blog/"},{"@type":"ListItem","position":3,"name":"WooCommerce Mailchimp Integration: API, Upsert, Limits","item":"https://wpwebhooks.org/blog/woocommerce-mailchimp-integration/"}]}

{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Is there an official Mailchimp plugin for WooCommerce?","acceptedAnswer":{"@type":"Answer","text":"Yes. Mailchimp for WooCommerce is published by Mailchimp on WordPress.org with more than 200,000 active installs. It syncs customers, orders, products and carts into Mailchimp e-commerce data, which powers abandoned-cart emails and revenue reporting. The API route suits narrower jobs, such as tagging a contact on one specific event."}},{"@type":"Question","name":"Which Mailchimp endpoint adds or updates a subscriber?","acceptedAnswer":{"@type":"Answer","text":"PUT /lists/{list_id}/members/{subscriber_hash}. It adds the member when the address is new and updates it otherwise, so repeated orders from one customer never fail. The subscriber hash is the MD5 hash of the lowercase email address; the endpoint also accepts the email address or the contact id in its place."}},{"@type":"Question","name":"What does status_if_new do in the Mailchimp API?","acceptedAnswer":{"@type":"Answer","text":"It sets the status only when the email address is not already in the audience. An existing member keeps their current status, so a customer who unsubscribed is not resubscribed when they place another order. The allowed values are subscribed, unsubscribed, cleaned, pending and transactional."}},{"@type":"Question","name":"What is the Mailchimp API rate limit?","acceptedAnswer":{"@type":"Answer","text":"Ten simultaneous connections per user. It is a concurrency limit, not a requests-per-second limit, and it is shared by every API key and integration belonging to that user. The eleventh concurrent request receives 429 TooManyRequests. API calls also have a 120-second timeout, and a request that timed out on your side can still hold a connection."}},{"@type":"Question","name":"How do I add tags when upserting a Mailchimp member?","acceptedAnswer":{"@type":"Answer","text":"With a second call. The add-or-update PUT endpoint has no tags field. POST to /lists/{list_id}/members/{subscriber_hash}/tags with an array of objects, each holding a tag name and a status of active or inactive. A tag that does not exist is created, and the endpoint answers 204 with no body."}}]}

{"@context":"https://schema.org","@type":"ImageObject","contentUrl":"https://wpwebhooks.org/diagrams/woocommerce-mailchimp-integration.png","caption":"FIG 01 — Order to audience: consent decides the status, PUT makes it idempotent, tags are a second call","description":"WooCommerce fires the checkout order created hook and the delivery is queued instead of running inside checkout. The worker lowercases the billing email and computes its MD5 hash. It reads whether the customer opted in to marketing: opted-in customers get the status pending, which triggers a confirmation email, and everyone else gets transactional. The worker sends a PUT to the member endpoint with status_if_new, which Mailchimp reads only for an address it has never seen, so an existing unsubscribed member is left untouched. On a 200 the worker posts to the member tags endpoint, which answers 204. A 429, meaning more than ten simultaneous connections, or a 5xx is retried later. A 400 or 401 is not retried.","encodingFormat":"image/png","creator":{"@type":"Organization","name":"WP Webhooks","url":"https://wpwebhooks.org/"},"copyrightHolder":{"@type":"Organization","name":"Flow Systems","url":"https://flowsystems.pl/"},"copyrightNotice":"© Flow Systems","creditText":"WP Webhooks","license":"https://creativecommons.org/licenses/by/4.0/","acquireLicensePage":"https://wpwebhooks.org/image-license/"}
```
