Agent skill

Mailchimp Webhooks Skill

Receive and secure Mailchimp webhooks. Use when setting up Mailchimp webhook handlers, responding to Mailchimp's GET URL validation, securing the endpoint with a URL secret, or handling audience events like subscribe, unsubscribe, profile, upemail, cleaned, and campaign.

Install this skill

npx skills add hookdeck/webhook-skills --skill mailchimp-webhooks


When to Use This Skill

  • Setting up Mailchimp webhook handlers
  • How do I respond to Mailchimp's webhook URL validation (the GET request)?
  • How do I secure Mailchimp webhooks (they are not HMAC-signed)?
  • Handling audience events: subscribe, unsubscribe, profile, upemail, cleaned, campaign
  • Parsing Mailchimp's application/x-www-form-urlencoded payloads

Verification (core)

Mailchimp does NOT sign its webhooks — there is no HMAC and no signature header. You secure the endpoint two ways, both described in Mailchimp's sync audience data with webhooks guide:

  1. URL validation (GET): When you save a webhook, Mailchimp sends a GET to the URL to confirm it is reachable. Respond 200 — do not require the secret on GET.
  2. Shared secret (POST): Put an unguessable secret in the webhook URL's query string (e.g. https://your.app/webhooks/mailchimp?secret=…) and compare it on every POST with a timing-safe comparison. Always serve the endpoint over HTTPS.

Payloads are application/x-www-form-urlencoded with a top-level type field and data[...] fields.

Node:

const crypto = require('crypto');

// Timing-safe compare of the ?secret= query param against your stored secret.
function verifyMailchimpSecret(provided, expected) {
  if (!provided || !expected) return false;
  const a = Buffer.from(provided);
  const b = Buffer.from(expected);
  if (a.length !== b.length) return false;      // avoid throw on length mismatch
  return crypto.timingSafeEqual(a, b);
}

Python:

import hmac

# Timing-safe compare of the ?secret= query param against your stored secret.
def verify_mailchimp_secret(provided: str, expected: str) -> bool:
    if not provided or not expected:
        return False
    return hmac.compare_digest(provided, expected)

For complete handlers with GET validation, form parsing, event dispatch, and tests, see:

Common Event Types

Dispatch on the top-level type field.

typeTriggered WhenKey data fields
subscribeA contact joins the audienceid, list_id, email, email_type, merges, ip_opt, ip_signup
unsubscribeA contact leaves the audienceaction (unsub/delete), reason (manual/abuse), id, list_id, email, campaign_id
profileA contact updates their profileid, list_id, email, email_type, merges, ip_opt
upemailA contact changes their email addresslist_id, new_id, new_email, old_email
cleanedAn address is cleaned (bounces/spam)list_id, campaign_id, reason (hard/abuse), email
campaignA campaign finishes sendingid, subject, status, reason, list_id

For full event reference, see Mailchimp's webhook guide.

Environment Variables

# The unguessable secret you append to your webhook URL query string.
# Register the URL as: https://your.app/webhooks/mailchimp?secret=<this value>
MAILCHIMP_WEBHOOK_SECRET=a-long-random-hard-to-guess-string

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 mailchimp --path /webhooks/mailchimp

Reference Materials