splitforms.com
All articles/ INTEGRATIONS9 MIN READPublished May 11, 2026

How to Send Contact Form Submissions to Mailchimp 2026

Send contact form submissions directly to Mailchimp in 2026 — list subscriber creation, custom merge fields, double opt-in flow, and a no-code path.

✶ Written by
splitforms.com / blog

Founder of splitforms — the form backend API for developers. Writes about form UX, anti-spam, and shipping web apps without backend code.

The actual problem with Mailchimp's embedded form

Most tutorials tell you to copy Mailchimp's embed code into your contact page and call it done. That works, but it ships three problems straight to production: bloated client-side JavaScript, almost zero spam protection, and a brittle lock-in where Mailchimp owns the submission before anything else can see it. If a bot fills out the embed, the bot is in your audience and counts against your contact limit.

The cleaner pattern is to treat Mailchimp as a destination, not a form provider. Your HTML form posts to a form backend you control (splitforms), which validates, filters spam, emails you, AND forwards the cleaned submission to Mailchimp as a new subscriber. The contact form stays plain HTML — no jQuery, no tracker, no script tags.

This guide walks through both directions: a code path using the Mailchimp Marketing API v3.0 directly, and a no-code path using Zapier as the middleman. Both end with subscribers landing in the right audience, with the right merge fields, the right tags, and an audit trail of consent.

If you've never wired splitforms to an external service before, the foundation post is send form data to a webhook — that's the underlying delivery primitive both paths in this article build on.

Prerequisites (5 minutes of setup)

Before you write a single line of code, get these four things lined up:

  1. A splitforms access key. Sign up at splitforms.com/login — it's free for 1,000 submissions/month, no card required. Copy the auto-generated access key from your dashboard.
  2. A Mailchimp account. The Free tier is fine. Inside Mailchimp, click your profile icon → Account & billing → Extras → API keys → Create A Key. Copy that key somewhere safe (you'll only see it once).
  3. A Mailchimp audience (list) ID. Audience → Settings → Audience name and defaults. The list ID is on the right side — a 10-character alphanumeric string like a1b2c3d4e5. Copy it.
  4. Your Mailchimp data center. Look at your API key: it ends with -us21 or similar. That suffix (e.g. us21) is your data center prefix — you'll need it for the API hostname.

Optional but recommended: open Audience → Settings → Audience fields and *|MERGE|* tags, then add or confirm the merge fields you want to capture. EMAIL, FNAME, LNAME are there by default. Add PHONE, COMPANY, or anything else your form collects so the mapping is clean.

Path 1: splitforms webhook to the Mailchimp API (the code path)

This is the path most developers should take. It costs nothing extra, you control every byte that hits Mailchimp, and you can layer in transformations (lowercase the email, normalize phone format, derive tags from referrer) that no-code tools struggle with.

The architecture is three hops:

  1. Visitor submits HTML form → https://splitforms.com/api/submit
  2. splitforms validates, sends you the notification email, then fires a webhook to your server
  3. Your server receives the JSON payload and PUTs it to https://<dc>.api.mailchimp.com/3.0/lists/{list_id}/members/{subscriber_hash}

You only need one new endpoint on a server you already control — a single function in a Next.js route handler, an Express endpoint, a Cloudflare Worker, anything that can receive POST and call fetch. The total code is around 30 lines.

Step 1: the HTML form

This is the entire frontend. Plain HTML, no Mailchimp script, no React state required (though it works fine inside any framework — drop the same field names into a Next.js form backend or React form integration without changing the shape).

<form action="https://splitforms.com/api/submit" method="POST">
  <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />

  <label>First name
    <input type="text"  name="fname"   required />
  </label>
  <label>Last name
    <input type="text"  name="lname"   required />
  </label>
  <label>Email
    <input type="email" name="email"   required />
  </label>
  <label>Message
    <textarea           name="message" required></textarea>
  </label>

  <label>
    <input type="checkbox" name="gdpr_consent" value="yes" required />
    I agree to receive marketing emails. You can unsubscribe at any time.
  </label>

  <!-- honeypot -->
  <input type="checkbox" name="botcheck" style="display:none" tabindex="-1" />

  <button type="submit">Subscribe</button>
</form>

The gdpr_consent checkbox is the consent record. The hidden botcheck is the honeypot — splitforms drops any submission where it's checked, which kills most form-stuffing bots without a CAPTCHA. For a deeper take on bot defense, see honeypot vs reCAPTCHA.

Path 1, the webhook + Mailchimp API call

Step 2: configure the splitforms webhook

In the splitforms dashboard, open your form, click IntegrationsWebhooksAdd webhook. Paste the URL of the server endpoint you're about to write — for example https://yourdomain.com/api/mailchimp-sync. Save the shared secret splitforms generates; you'll use it to verify the HMAC signature on incoming requests.

When a submission comes in, splitforms POSTs JSON to your URL. The payload looks like this:

{
  "form_id": "frm_8xZ...",
  "submitted_at": "2026-05-10T14:22:31Z",
  "data": {
    "fname": "Ada",
    "lname": "Lovelace",
    "email": "ada@example.com",
    "message": "Loved your post about webhooks.",
    "gdpr_consent": "yes"
  },
  "meta": {
    "ip": "203.0.113.10",
    "user_agent": "Mozilla/5.0 ...",
    "referrer": "https://yoursite.com/pricing"
  }
}

Your endpoint reads data.email, splits the name into FNAME/LNAME, derives a tag from the referrer (e.g. anyone from /pricing gets a pricing-page tag), and forwards everything to Mailchimp.

Step 3: call the Mailchimp API

Mailchimp's Marketing API v3.0 uses HTTP Basic auth: any username plus your API key as the password. The endpoint to upsert a contact is PUT /lists/{list_id}/members/{subscriber_hash}, where subscriber_hash is the lowercase MD5 of the email. Using PUT (not POST) makes the operation idempotent — resubmissions update tags and merge fields instead of throwing Member Exists.

Here's the raw curl version so you can sanity-check the API works before wiring it up. Replace us21 with your data center, LIST_ID with your audience ID, and API_KEY with your key.

SUBSCRIBER_HASH=$(printf "ada@example.com" | md5sum | awk '{print $1}')

curl -X PUT \
  "https://us21.api.mailchimp.com/3.0/lists/LIST_ID/members/$SUBSCRIBER_HASH" \
  -u "anystring:API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email_address": "ada@example.com",
    "status_if_new": "pending",
    "merge_fields": {
      "FNAME": "Ada",
      "LNAME": "Lovelace"
    },
    "tags": ["contact-form", "pricing-page"]
  }'

Note status_if_new: "pending" — that's how you ask Mailchimp to send the double opt-in confirmation email for new contacts. If you want single opt-in instead, change it to "subscribed". For an existing contact, neither field changes their status; they stay whatever they already were. That's the whole reason PUT is safer than POST here.

Step 4: the server endpoint in JavaScript

Wire it all up. This is a Next.js route handler — adapt the framework wrapper to whatever you use, but the body is portable.

// app/api/mailchimp-sync/route.ts
import crypto from "node:crypto";

const DC = process.env.MAILCHIMP_DC!;          // e.g. "us21"
const LIST_ID = process.env.MAILCHIMP_LIST_ID!;
const API_KEY = process.env.MAILCHIMP_API_KEY!;
const WEBHOOK_SECRET = process.env.SPLITFORMS_WEBHOOK_SECRET!;

export async function POST(req: Request) {
  // 1. Verify splitforms signature
  const sig = req.headers.get("x-splitforms-signature");
  const body = await req.text();
  const expected = crypto
    .createHmac("sha256", WEBHOOK_SECRET)
    .update(body)
    .digest("hex");
  if (sig !== expected) {
    return new Response("bad signature", { status: 401 });
  }

  // 2. Parse the submission
  const { data, meta } = JSON.parse(body);
  if (data.gdpr_consent !== "yes") {
    return new Response("no consent", { status: 200 });
  }

  // 3. Derive subscriber hash + tags
  const email = String(data.email).toLowerCase().trim();
  const hash = crypto.createHash("md5").update(email).digest("hex");
  const tags = ["contact-form"];
  if (meta?.referrer?.includes("/pricing")) tags.push("pricing-page");

  // 4. PUT to Mailchimp
  const res = await fetch(
    `https://${DC}.api.mailchimp.com/3.0/lists/${LIST_ID}/members/${hash}`,
    {
      method: "PUT",
      headers: {
        "Authorization": "Basic " + Buffer.from("any:" + API_KEY).toString("base64"),
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        email_address: email,
        status_if_new: "pending",  // double opt-in
        merge_fields: {
          FNAME: data.fname ?? "",
          LNAME: data.lname ?? "",
        },
        tags,
      }),
    }
  );

  if (!res.ok) {
    console.error("mailchimp error", res.status, await res.text());
    return new Response("mailchimp failed", { status: 502 });
  }
  return new Response("ok", { status: 200 });
}

Set the four env vars in your hosting platform (Vercel, Netlify, Render, Fly, wherever) and deploy. The next test submission will land in Mailchimp with FNAME, LNAME, and the right tags, pending confirmation.

Merge field mapping and tag-based segmentation

Merge fields and tags solve different problems. Merge fields are personalization variables — they go into the email template as *|FNAME|* and render as the contact's first name. Tags are segmentation labels — they live on the contact, can be added/removed at any time, and drive who gets which automation.

The mapping pattern is straightforward:

HTML form fieldMailchimp merge tagNotes
name="fname"FNAMEDefault merge field, always present
name="lname"LNAMEDefault merge field, always present
name="email"EMAILPrimary identifier; goes in email_address
name="phone"PHONEAdd manually in audience settings
name="company"COMPANYAdd manually in audience settings

For tags, the rule of thumb is: derive them on the server, not in the form. The form can't be trusted to label itself honestly, but your server can read the referrer, the IP's country, the time of day, the UTM parameters, and tag accordingly. Common tag patterns: source-{referrer-path}, campaign-{utm_campaign}, geo-{country}. Mailchimp Automations can then trigger on any tag — for example, "send the SaaS onboarding sequence only to contacts tagged pricing-page."

Double opt-in and GDPR consent (the legal bit)

If any portion of your traffic is in the EU, UK, Switzerland, or California, you need real consent records. The GDPR's practical test (Article 7) is: can you prove the contact actively, specifically agreed to marketing? A pre-checked box doesn't count. A buried "by submitting you agree" line doesn't count. A separately-checked, plain-language consent box does.

The flow that survives an audit:

  1. Unchecked consent checkbox in the HTML form, with clear text. Make it required so the form won't submit without it.
  2. Server-side check: refuse to forward to Mailchimp unless gdpr_consent === "yes" (the code in section above does this).
  3. Use status_if_new: "pending" so Mailchimp sends its confirmation email. The click on that email is the second consent signal — recorded in Mailchimp with timestamp and IP.
  4. Enable GDPR fields on the audience (Audience → Settings → Audience fields → GDPR fields). Pass marketing_permissions on the API call to record which specific permissions the contact granted.

With double opt-in plus GDPR fields, every contact in your audience has two timestamps of consent (the form submission and the email click) plus an explicit permission record. That's the package most DPOs will sign off on.

One nuance: even without GDPR fields, the status: "pending" trick alone gives you a defensible record for soft opt-in markets (US, Canada, AU). It also reduces spam-trap addresses in your list, which protects your sender reputation.

Path 2: splitforms to Zapier to Mailchimp (the no-code path)

If you don't want to maintain a server endpoint, route through Zapier (or Make / Pipedream — same idea). The cost is one Zap and ~100 tasks/month, which fits inside the free Zapier tier for most contact forms.

  1. In Zapier, click Create Zap. Choose Webhooks by Zapier as the trigger, event Catch Hook. Zapier gives you a unique webhook URL.
  2. In splitforms, dashboard → Integrations → Webhooks → Add webhook. Paste the Zapier URL. Save.
  3. Submit your form once with test data. Zapier will catch the payload and let you map the fields visually.
  4. Add a Zapier action: MailchimpAdd/Update Subscriber. Connect your Mailchimp account, pick the audience, map email → Email, fname → FNAME, lname → LNAME.
  5. Under Double Opt-In, set to Yes. Under Tags, type contact-form (and any others you want).
  6. Turn the Zap on.

That's it. Five minutes, no code, no servers, no API keys stored on your machine. The trade-off vs path 1: Zapier charges per task once you exceed the free tier, you can't do complex transformations without a Code by Zapier step (which itself counts as a task), and you have one more vendor in the chain that can go down.

For most freelancers and small business owners, this is the right call. For agencies running dozens of client forms or anyone doing >500 submissions/month per form, path 1 pays for itself the first month.

Testing and verifying the integration

Once either path is deployed, run through this checklist:

  1. Submit a real (but disposable) email from the form on your live site.
  2. Check your inbox — splitforms notification email should arrive within 5 seconds.
  3. Check Mailchimp → Audience → All contacts. The new contact should appear with status Pending (double opt-in) or Subscribed (single).
  4. Confirm FNAME and LNAME are populated. Click the contact to see the full profile.
  5. Check the Tags tab — your derived tags should be attached.
  6. If pending: confirm the Mailchimp opt-in email arrived at the test inbox, click the link, refresh the contact in Mailchimp, status should flip to Subscribed.
  7. Submit the same email a second time with different name fields. With PUT/upsert, the contact should update in place — not error, not duplicate.

Common failure modes and where to look: 401 from Mailchimp means wrong API key or wrong data center prefix in the hostname. 404 means wrong list ID. 400 with "invalid resource" usually means a merge field name in the payload that doesn't exist on the audience — go add it or remove it from the JSON. If splitforms isn't firing the webhook at all, check Allowed Domains in security settings (localhost gets blocked unless you allow it) and check the form's webhook log in the dashboard.

What splitforms adds on top of raw Mailchimp

The whole reason this two-step architecture wins over the embedded Mailchimp form is what splitforms layers in between:

  • AI spam classification on every submission, free, before anything reaches Mailchimp. Bots can't pollute your audience.
  • Honeypot field built in — the botcheck input in the example HTML drops obvious bots without a CAPTCHA.
  • Email notification to you on every submission, in parallel with the webhook. You always know a real human just submitted, independent of whether Mailchimp accepts the contact.
  • Free webhooks — most form backends charge $10+/month for webhooks. splitforms includes them on the 1,000/month free tier.
  • Submission archive in the dashboard. If Mailchimp drops the contact for any reason, the original submission is still in your splitforms dashboard.
  • Fan-out to multiple destinations. Add a second webhook to fire the same submission into Slack, Discord, Airtable, or your own CRM. The form HTML stays unchanged.

splitforms pricing as of 2026-05: free up to 1,000 submissions/month, Pro at $5/mo for 5,000, and a $59 4-year plan for high-volume users. Compared to bolting Mailchimp's embed onto a paid form provider, this stack is usually cheaper and always more flexible. See best free form backend services 2026 for the broader landscape, or browse the full blog.

Next steps and references

FAQ

Do I need a paid Mailchimp account to receive form submissions via API?

No. Mailchimp's Free plan (up to 500 contacts as of 2026-05) supports the full Marketing API v3.0, including the list members endpoint used in this guide. You get a real API key, can create audiences, define merge fields, and add subscribers programmatically. The paid tiers add automations, advanced segmentation, and more contacts — none of which are required just to capture form submissions. You only need to upgrade when your audience size crosses the free cap or you want to fire automations on tag changes.

What's the difference between double opt-in and single opt-in here?

Single opt-in means the address goes straight to status `subscribed` and starts receiving campaigns immediately. Double opt-in sets the address to `pending` and Mailchimp sends a confirmation email; the contact only becomes `subscribed` after they click the link. For EU/UK traffic under GDPR, double opt-in is the safer default because it documents explicit consent. In the API you pick by sending `status: "subscribed"` or `status: "pending"` in the POST body to `/lists/{list_id}/members`.

How do I avoid 400 errors when the same email submits twice?

Mailchimp's `POST /lists/{list_id}/members` endpoint rejects duplicates with a 400 `Member Exists` error. The fix is to use the upsert endpoint instead: `PUT /lists/{list_id}/members/{subscriber_hash}` where `subscriber_hash` is the MD5 of the lowercased email. This creates the contact if missing and updates merge fields/tags if it already exists. The example curl block in this guide uses the PUT version so resubmissions update tags cleanly instead of throwing.

Can I send tags and merge fields in the same request?

Yes. The members payload accepts both `merge_fields` (an object keyed by tag name like FNAME, LNAME, PHONE) and `tags` (an array of strings). Tags are how you segment later — for example you could pass `["contact-form", "pricing-page"]` so a Mailchimp automation only fires for contacts who came from your pricing page. Merge fields are personalization variables you can drop into email templates with `*|FNAME|*` syntax. The example payload below sets both at once.

Will my Mailchimp API key be exposed if I call the API from the browser?

Yes — and that's why you don't. Mailchimp API keys grant full account access (you can delete audiences, export contacts, change billing). Never put one in client-side JavaScript or a static HTML form. The two safe patterns are: (1) submit to splitforms first, then fire a server-side webhook that calls Mailchimp with the key stored in your backend env vars, or (2) use Zapier/Make as the broker so the key lives in their vault. The webhook pattern in section 4 follows pattern (1).

Why use splitforms in front of Mailchimp instead of Mailchimp's embedded form?

Three concrete reasons. First, Mailchimp's embed scripts are heavy (jQuery + their tracker) and slow your Lighthouse score. Second, the embed offers almost no spam protection — bots love Mailchimp signup forms. splitforms runs AI spam classification and a honeypot before the submission ever reaches Mailchimp, so your audience stays clean. Third, splitforms gives you the raw submission (email confirmation, Slack ping, webhook to your CRM) on top of the Mailchimp sync — you're not locked into one destination.

How does GDPR consent work with the Mailchimp API?

Mailchimp supports GDPR fields on each list. When you create the audience, enable GDPR fields in settings, then pass `marketing_permissions` in the POST body — an array of `{marketing_permission_id, enabled: true}` objects. You'll get the `marketing_permission_id` values back from `GET /lists/{list_id}` once GDPR is enabled. Combine this with `status: "pending"` (double opt-in) and a clearly worded consent checkbox in your form HTML, and you have a defensible record of consent under GDPR Article 7.

What if I want both Mailchimp and a notification email at the same time?

That's the default splitforms behavior. Every submission emails you (the form owner) at the address on your account, and the webhook fires in parallel. So when a visitor submits, you get an inbox notification AND Mailchimp gets the subscriber AND any other webhook (Slack, Discord, CRM) fires too — all from one form POST. No additional config; just add the Mailchimp webhook and the email sends keep working alongside it.

About the author
✻ ✻ ✻

Get your free contact form API key in 60 seconds.

1,000 free form submissions per month. No credit card. No SDK, no PHP, no plugin. Drop one POST endpoint in your form and submissions land in your inbox.

Generate access key →Read the docs
v0.1 · founders pricing locked in · early access open