Synchronize Audience Data with Webhooks
At a glance
Webhooks are a helpful tool that you can use to collect information about audience changes in Mailchimp as they happen. By entering a valid URL that’s set up to accept HTTP POST requests, you can receive updates on subscriptions, changed email addresses, campaign sending, and more.
You can use webhooks to:
Alert your application when a campaign has finished sending
Keep your client’s profile data in sync with your own database
Detect when an email address starts bouncing
For the purposes of this guide, we run a messaging app for vegetarians called Chatatouille. We use Mailchimp for our marketing emails, and we want to keep our application’s database in sync with our Mailchimp audience data. We’ll create a webhook that updates our database every time a user subscribes or unsubscribes from our mailing list.
What you’ll need
A Mailchimp account
An audience you would like to use a webhook with
A callback URL for your application that can accept HTTP POST requests
Set up your callback URL
In order to create our webhook, we need to provide a callback URL that accepts HTTP POST requests. In the sample code below, you’ll see an example of what your application code might look like, but if you just want to test out a webhook to see what the payloads look like without setting up and deploying the webhook handler in your application, you can also use a service like RequestBin to stand up a callback URL without deploying anything.
When a webhook triggers based on your configured settings, Mailchimp sends an HTTP POST request to the URL you specified. If the URL is unavailable or takes more than 10 seconds to respond, the request is canceled and the system will try again later. Retries happen at increasing intervals over the course of 75 minutes. Excessive or unresponsive webhook requests may be dropped or disabled at Mailchimp’s discretion.
Note: Mailchimp strongly recommends using an HTTPS URL as your webhook callback URL. You can further increase the security of your webhook setup by using a URL containing a hard-to-guess secret and by checking this secret in your callback code.
Create a new webhook
There are two ways to set up our webhook: in the Mailchimp web app, or through the API. In this guide, we’ll walk through setting it up using the Mailchimp app.
To create a webhook:
Log into Mailchimp and navigate to Audience
Select the audience you want to work with in the the Current Audience dropdown
Click the Manage Audience dropdown button and select Settings
On the Settings page, click Webhooks
Click the Create New Webhook button
In the Callback URL field, add the URL of the integration or application where you want to send webhook requests—this URL will receive data about your Mailchimp audience
Select the boxes next to each update type to choose the events that will trigger your webhook—in this guide, we’ll choose Subscribes and Unsubscribes
Click Save to save your new webhook
After you click Save, Mailchimp displays a signing secret for your webhook.
Signature verification is optional — if you don't plan to verify incoming deliveries, you can safely dismiss this dialog and skip the Verifying Webhook Signatures section below.
If you do want to verify signatures, copy the secret now and store it somewhere secure (for example, in your application's environment variables or a secrets manager). The secret is shown exactly once and cannot be retrieved later. If you dismiss without saving it, you'll need to delete the webhook and create a new one to get a new secret.
The webhook will now notify your application of subscribe and unsubscribe events as they occur.
API Creation
You can also create list webhooks programmatically via the API. The create response includes a signing_secret field with the one-time plaintext value — the same "shown exactly once" contract applies. See the Add Webhook API reference for request/response details.
Handling the webhook response in your application
Now that we have the webhook set up to alert us to changes in subscription status, we need to handle the callback data in our application. (This code should be accessible via the webhook URL you set up previously.)
The body of the webhook request is sent as application/x-www-form-urlencoded data. The Subscribes event, ingested and parsed as JSON, will look something like this:
Webhook response: Subscribes
{
"type": "subscribe",
"fired_at": "2009-03-26 21:35:57",
"data": {
"id": "8a25ff1d98",
"list_id": "a6b5da1054",
"email": "api@mailchimp.com",
"email_type": "html",
"ip_opt": "10.20.10.30",
"ip_signup": "10.20.10.30",
"merges": {
"EMAIL": "api@mailchimp.com",
"FNAME": "Mailchimp",
"LNAME": "API",
"INTERESTS": "Group1,Group2"
}
}
}The body of the webhook request for the Unsubscribes event, ingested and parsed as JSON, will look something like this:
Webhook response: Unsubscribes
{
"type": "unsubscribe",
"fired_at": "2009-03-26 21:40:57",
"data": {
"action": "unsub",
"reason": "manual",
"id": "8a25ff1d98",
"list_id": "a6b5da1054",
"email": "api+unsub@mailchimp.com",
"email_type": "html",
"ip_opt": "10.20.10.30",
"campaign_id": "cb398d21d2",
"merges": {
"EMAIL": "api+unsub@mailchimp.com",
"FNAME": "Mailchimp",
"LNAME": "API",
"INTERESTS": "Group1,Group2"
}
}
}Now that we know what the webhook data will look like, we’ll write the code that will handle it on our server. For the purposes of this example, imagine that fakeDB is a package with functions we use to interact with Chatatouille’s database.
The code could look something like this:
Handling the webhook response in your app
const express = require("express");
const bodyParser = require("body-parser");
// this is a stand-in for the code you'd use to write to your own database
const fakeDB = require("fakeDB");
const app = express();
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }));
app.post("/", (req, res) => {
const { type, data } = req.body;
if (type === "subscribe") {
fakeDB.subscribeUser(data);
} else if (type === "unsubscribe") {
fakeDB.unsubscribeUser(data.id);
}
});
app.listen(port, () =>
console.log(`Listening at http://localhost:3000`)
);Test the webhook
Now that we have the code set up, we’ll test it using the Mailchimp app:
Navigate to Audience and select the audience you’d like to add a test member to
In the Add Contacts dropdown, select Add a Subscriber
Fill in the input fields with information for your test email account
Check the checkbox for “This person gave me permission to email them” if you’d like to skip the step of confirming the subscription manually
Hit Subscribe
Your server should receive the webhook request and your database should be updated accordingly
If everything’s working, your webhook is good to go!
Verifying webhook signatures
When you enable HMAC signing on a webhook, Mailchimp includes a signature in every delivery so your endpoint can confirm the request came from Mailchimp and hasn't been tampered with in transit.
How it works
When you create a signed webhook, Mailchimp generates a signing secret and returns it in the API response. Copy and store this value securely — it is shown exactly once and cannot be retrieved later. If you lose it, delete and recreate the webhook.
For every delivery, Mailchimp computes
HMAC-SHA256(key=signing_secret, message="{timestamp}.{raw_body}")where{timestamp}is a Unix timestamp (seconds) and{raw_body}is the exact bytes of the request body.Mailchimp sends the result in the
X-Mailchimp-Signatureheader in the formatt={timestamp},v1={hex_signature}.Your endpoint reconstructs the same signed string, recomputes the HMAC, and compares it to the value in the header using a timing-safe comparison function.
Reject any delivery where the signatures don't match, or where the timestamp is more than 5 minutes old (to prevent replay attacks).
The header format is: X-Mailchimp-Signature: t=1718000000,v1=a3f2c1...
Note: Always verify the signature against the raw, unmodified request body. Parsing the body as JSON or URL-decoding it before verification may change the bytes and cause a mismatch.
Use a constant-time comparison function (e.g., hash_equals, hmac.compare_digest, crypto.timingSafeEqual). A standard equality check leaks timing information that can be exploited.
Verification examples
<?php
function verifyMailchimpWebhook(
string $signingSecret,
string $signatureHeader,
string $rawBody,
int $toleranceSeconds = 300
): void {
if (!preg_match('/\bt=(\d+)\b/', $signatureHeader, $tsMatch)
|| !preg_match('/\bv1=([0-9a-f]{64})\b/', $signatureHeader, $sigMatch)) {
throw new RuntimeException('Missing or malformed X-Mailchimp-Signature header.');
}
$timestamp = (int) $tsMatch[1];
$receivedSig = $sigMatch[1];
if (abs(time() - $timestamp) > $toleranceSeconds) {
throw new RuntimeException('Webhook timestamp is outside the tolerance window.');
}
$expectedSig = hash_hmac('sha256', $timestamp . '.' . $rawBody, $signingSecret);
if (!hash_equals($expectedSig, $receivedSig)) {
throw new RuntimeException('Webhook signature verification failed.');
}
}
$signingSecret = getenv('MAILCHIMP_WEBHOOK_SECRET');
$signatureHeader = $_SERVER['HTTP_X_MAILCHIMP_SIGNATURE'] ?? '';
$rawBody = file_get_contents('php://input');
try {
verifyMailchimpWebhook($signingSecret, $signatureHeader, $rawBody);
} catch (RuntimeException $e) {
http_response_code(400);
exit;
}
$event = json_decode($rawBody, true);