WP Webhooks / Blog / Integrations

Adding WooCommerce Customers to Mailchimp Through the API

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.

9 min 2026-09-17
#mailchimp#woocommerce#api

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 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 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}, 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, so that addresses do not leak into URLs and server logs. Lowercase first: [email protected] and [email protected] 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, 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 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.

/ 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: 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.

ConcernHand-rolled wp_remote_requestWebhook Actions
Full catalogue and cart syncNot this either. Use the official pluginNot what it does. It delivers events; it does not mirror a product catalogue
Where the call runsInside checkout unless you build a queueQueued in its own table, delivered in the background
A 429 or 5xx from MailchimpLost unless you wrote retry logicRetried with exponential backoff, five attempts by default
A 400 from a bad merge fieldInvisibleMarked permanently failed, with the response body in the delivery log
The API keyA constant in wp-config.phpAn encrypted Bearer or Basic credential, redacted in every log
Member URL per customerString concatenationA URL template resolved from the payload, using the email form the endpoint accepts
Constant fields such as status_if_newWritten in your arrayA short pre-dispatch snippet; field mapping moves values and cannot invent one
The tag call after the upsertA second request in the same functionA 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.

/ 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, 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 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.

/ 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 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 on 2026-09-19: 200,000+ active installations, version 6.2, author Mailchimp.
² Mailchimp API fundamentals, Throttling and Stream timeouts: 10 simultaneous connections, 429 on exceeding it, and a 120-second timeout on API calls.
FAQ

Things engineers always ask.

Don't see yours? Open an issue on GitHub or check the full reference in the API docs.

Is there an official Mailchimp plugin for WooCommerce? +
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.
Which Mailchimp endpoint adds or updates a subscriber? +
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.
What does status_if_new do in the Mailchimp API? +
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.
What is the Mailchimp API rate limit? +
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.
How do I add tags when upserting a Mailchimp member? +
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.
Ready

Your next automation is
one sentence away.

$ wp plugin install flowsystems-webhook-actions --activate