Back to blog

Mailchimp Integration: Auto-Subscribe Form Respondents

Manuk Minasyan · · 8 min read

A newsletter signup form goes live on a Tuesday. By Friday it has eighty-three new email addresses, every one of them sitting in a form_submissions row in your Laravel database. The next campaign sends on Monday — to the audience that existed last month, because nobody got around to exporting the new addresses and importing them into Mailchimp.

This is the gap a mailchimp laravel form integration exists to close. The submission hits your Laravel app, a queued listener turns around and POSTs the address to your Mailchimp audience, and the next campaign sends to a list that includes Tuesday's signups by Tuesday afternoon. No export. No CSV. No "I'll do it later" Slack message from marketing on Sunday night.

The wiring is fifteen lines of PHP. The interesting part of this post is not those lines. It is the one-character decision — subscribed versus pending — that determines whether your form is GDPR-compliant for European subscribers, and the 400 response that breaks the naive version the first time somebody fills out the form twice.

The Mailchimp API basics

Mailchimp's REST API is the Marketing API, which is what the old "API v3" got renamed to a few years back. The resource you want is /lists/{list_id}/members. A list and an audience are the same thing — Mailchimp renamed it in the UI without renaming it in the API, so the URL keeps the older noun.

The thing that catches new integrators is the base URL. Mailchimp shards customers across data centers, and the data center is baked into your API key. An API key that ends in -us10 means your account lives on the us10 data center, and every request goes to https://us10.api.mailchimp.com/3.0/. Hit api.mailchimp.com without the prefix and you get a 401 that does not tell you why. Parse the suffix off the API key once at boot and store it alongside the token.

Authentication is Basic with any username and the API key as the password — Mailchimp ignores the username field entirely.

Setting up an API key and finding your audience ID

In Mailchimp, open Account & billing → Extras → API keys → Create A Key. Name it "Laravel form sync" so you can revoke it later without guessing which one it is. Copy the key — you only see it once. Drop it into .env as MAILCHIMP_API_KEY, and surface a derived MAILCHIMP_DC value (everything after the last - in the key) in config/services.php.

Finding the audience ID trips up everybody on their first attempt because the obvious place to look — the browser URL while viewing the audience — does not contain it. The audience ID lives at Audience → Settings → Audience name and defaults, near the bottom of the page, labelled "Audience ID". It is a ten-character alphanumeric string. Store it as MAILCHIMP_AUDIENCE_ID.

If you have multiple audiences for different products, give each its own env var rather than passing the ID around at runtime — explicit beats clever when somebody emails the wrong list six months later.

How to auto-subscribe Laravel form respondents to Mailchimp

Auto-subscribing a Laravel form respondent to Mailchimp means listening for the FormSubmitted event, then making one POST request from a queued listener to https://{dc}.api.mailchimp.com/3.0/lists/{audienceId}/members with the email address and a status flag. Four steps:

  1. Create the audience in Mailchimp and copy its Audience ID.
  2. Create a Marketing API key and parse the data-center suffix off the end.
  3. Register a queued FormSubmitted listener in your Laravel app.
  4. Tag the new member with the form's origin so segmentation works later.

The listener body:

namespace App\Listeners;

use FilaForms\Core\Events\FormSubmitted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Http;

class SubscribeSubmissionToMailchimp implements ShouldQueue
{
    public function handle(FormSubmitted $event): void
    {
        $data = $event->submission->data;

        Http::withBasicAuth('anystring', config('services.mailchimp.api_key'))
            ->post(sprintf(
                'https://%s.api.mailchimp.com/3.0/lists/%s/members',
                config('services.mailchimp.dc'),
                config('services.mailchimp.audience_id'),
            ), [
                'email_address' => $data['email'],
                'status'        => 'pending',
                'tags'          => ['newsletter'],
                'merge_fields'  => [
                    'FNAME' => $data['first_name'] ?? '',
                    'LNAME' => $data['last_name']  ?? '',
                ],
            ])
            ->throw();
    }
}

ShouldQueue is non-negotiable. The form response should not block on a Mailchimp round-trip, and the queue gives you free retry on transient failures. The listener uses the same primitive — a FormSubmitted event handler — that powers the outgoing webhook plumbing under every other integration in this series. The Mailchimp filament pattern is no different from the Slack, Notion, or HubSpot one: one listener, one HTTP call, one decision about what to send.

Double opt-in: the EU question

The status field on the member-create request takes one of two values that matter for this conversation. subscribed adds the email to the audience immediately, ready to receive the next campaign. pending adds the email in a pending state and tells Mailchimp to send a confirmation email; the address becomes subscribed only after the recipient clicks the link.

For European subscribers under GDPR, pending is the safer default. GDPR requires explicit, unambiguous consent for marketing email, and a form-tick combined with a server-side flip to subscribed does not, on its own, prove the recipient consented — somebody else could have typed their address into the form. Double opt-in proves the address holder clicked the confirmation link, which is the chain of evidence regulators ask for.

Pick subscribed only when you have a specific reason — a transactional list where the recipient already has a relationship with you and the messages are operational, or a US-only audience where CAN-SPAM is the regime and explicit opt-in is not the rule. For anything that could touch an EU address, default to pending and treat the extra confirmation step as a feature, not a tax. The wider picture lives in the GDPR checklist for form data.

Tags and segments

The tags array on the create request is the cleanest way to keep one Mailchimp audience and still send the right campaign to the right segment. Tag every new subscriber with the form they came from — newsletter, lead-magnet-ebook, webinar-q3, waitlist — and Mailchimp lets you segment a campaign to "subscribers tagged waitlist" in two clicks. Audience-per-form, the alternative pattern, splits your reporting and doubles your Mailchimp bill because each audience is billed separately.

A waitlist form is the obvious case. You collect addresses for months, you launch, and you want the launch sequence to go to the people who waited. Tag every signup from the waitlist form pattern with waitlist and the launch-day campaign is a saved segment, not a CSV juggling act.

Edge cases

Re-submitting the same email returns a 400 with Member Exists. Mailchimp does not treat the create endpoint as an upsert; you have to ask for upsert explicitly. The upsert endpoint is PUT /lists/{listId}/members/{subscriberHash} where subscriberHash is the MD5 of the lowercased email address. Swap the POST for the PUT and catch nothing — same body, same response shape, and an existing member gets their tags merged instead of crashing your listener.

The Marketing API rate limit is ten simultaneous connections per user account and returns 429 when you exceed it. A queue worker with sensible concurrency rarely trips it; a webinar that drops two thousand signups in a minute will. The queue retries handle 429 with backoff if your job is marked tries=3 and uses Laravel's default exponential backoff.

What we got wrong

We shipped with status: subscribed for every signup because the Mailchimp quickstart example uses it and the first ten signups were US-based testers, so confirmation friction felt like a tax we could skip. The audience grew. Open rates were healthy. Nothing rang an alarm.

Two months in, a German subscriber filed a GDPR complaint. They had never explicitly opted in. The address had ended up on the form because a colleague used their laptop to download our lead magnet, the form pre-filled the email field from a browser autofill, and our system had treated that submission as consent. Under GDPR, it wasn't. We had no chain of evidence — no confirmation click, no IP-timestamped opt-in record — because we had skipped the step that creates one. We switched the listener to status: pending the same afternoon, exported the existing audience, and re-sent confirmation emails to every European address. About forty percent re-confirmed. The other sixty percent were the right people to lose. Lesson: assume double opt-in is required unless you have a specific reason not to.

Try it

If your newsletter, lead magnet, or launch waitlist lives behind a Laravel form, the fifteen-line listener above closes the gap between submission and Mailchimp audience by the end of the next queue cycle. Set status to pending for anything that could touch an EU address, tag every signup with its origin, and the Monday-morning CSV export goes away.

You can set up FilaForms here. The pattern in this post rides on the webhook delivery primitive every Connect-series integration uses — one queued listener, one HTTP call, one flag that decides whether your form is GDPR-compliant.

Related posts