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
-us6talks tous6.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_newapplies 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.
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.
pendingsends Mailchimp's confirmation email and subscribes the customer only when they click. It is the defensible default for anyone who ticked a marketing box.subscribedskips confirmation. Use it only when your own checkout already recorded explicit, provable consent.transactionalstores 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.
/ 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.
| 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 |
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.