edalzell/mailchimp
A Laravel package that provides a simple, fluent interface to the Mailchimp API for managing audiences, members, and subscriptions. Easily subscribe or unsubscribe users, update member data, and integrate Mailchimp with your Laravel app.
Install and wire up credentials
composer require edalzell/mailchimp
php artisan vendor:publish --tag=mailchimp-config
Statamic addons use package auto-discovery, so there is nothing to register in config/app.php. The one thing that trips people up immediately is the API key: Mailchimp keys are formatted xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-us21, and the us21 suffix is the data center. Most implementations derive the base URL from the key suffix automatically, but confirm this in the config — if there is a separate server_prefix/dc value, it must match the key suffix exactly or every request 404s.
Store the key in .env, never in the config file:
MAILCHIMP_API_KEY=abc123...-us21
MAILCHIMP_LIST_ID=9f2a1b3c4d
Where to look first
README.md — for the supported tag/fieldtype/event surface (this is a small addon; the README is the documentation).src/ServiceProvider.php — shows exactly what is registered: tags, fieldtypes, listeners, bindings. Five minutes here tells you whether it is a form-action addon, a tag addon, or both.config/mailchimp.php — the only configuration contract you need to care about.src/ for the Mailchimp client wrapper — this is what you will end up calling from your own code when the built-in path doesn't fit.First use case: email capture on a Statamic form
The 90% case is "subscribe this person to an audience when they submit a form." Create a Statamic form (or reuse an existing one), add an email field, and configure the addon's listener/action so the submission is forwarded to a list. Submit the form once locally and check the audience in Mailchimp — if the member appears as Pending rather than Subscribed, you have hit double opt-in (see Gotchas), not a bug.
The second use case, and the one that justifies reaching for the package rather than curl: rendering a standalone subscribe form via an Antlers tag, or calling the client directly from a custom listener/controller.
Pattern 1 — Form submission → audience (declarative)
The dominant integration. Wire the addon to a Statamic form so a submission synchronously (or via queue — see below) creates/updates a member in a list. Keep the mapping in config so non-developers can add fields without touching PHP:
email → EMAIL, first_name → FNAME).FNAME in Mailchimp and a form without it produces a 400 that is easy to misread as an auth failure.Pattern 2 — Programmatic subscribe from your own code
When you need conditional logic (only subscribe B2B leads, add tags based on a field), resolve the client out of the container rather than reimplementing HTTP:
use Edalzell\Mailchimp\Mailchimp; // confirm the FQCN in src/
public function handle(array $submission): void
{
app(Mailchimp::class)->subscribe(
listId: config('mailchimp.list_id'),
email: $submission['email'],
mergeFields: [
'FNAME' => $submission['first_name'] ?? '',
'LNAME' => $submission['last_name'] ?? '',
],
tags: ['newsletter', 'site-signup'],
);
}
This is the escape hatch you will use most often. It keeps the API key, base URL, and error handling in the package while your application owns the business rules.
Pattern 3 — Queue the call
Mailchimp's p99 latency is not something you want inside a form POST. Wrap any programmatic call in a job:
dispatch(new SubscribeToMailchimp($email, $mergeFields))->onQueue('marketing');
Statamic form submissions are already a good place to hook an event listener (Statamic\Events\SubmissionCreated) — listen, dispatch a job, return the user to their thank-you page immediately. This also gives you free retries on 429s.
Pattern 4 — Antlers tag for the public subscribe form
Where the addon exposes a tag pair, use it to render the form and handle the POST so you don't hand-roll a route. Test the rendered markup in the browser and with JS disabled — Statamic tag-based forms usually degrade to a plain POST, and a failed POST re-renders the page with an error bag you need to display.
Pattern 5 — Testing
Never let CI hit the live Mailchimp API. Either fake the underlying HTTP client (Http::fake(['api.mailchimp.com/*' => Http::response(['status' => 'subscribed'], 200)])) or bind a stub implementation of the package's client interface in your test case. The latter is cleaner and lets you assert "we tried to subscribe this email with these tags" without coupling to Mailchimp's payload shape.
Double opt-in is on by default in most audiences. A successful API call returns status: pending, not subscribed. Users will report "it says it worked but they're not on the list." Either flip the audience to single opt-in in Mailchimp, or set the status explicitly in your call — but check your consent obligations before choosing.
400 Member Exists is not an error. Re-submitting a known email returns a 400 with title: "Member Exists". Treat it as success in your UI, or use an upsert/PUT-style call so you can update merge fields and tags for returning subscribers. If the addon only exposes POST /members, you will need the programmatic path.
The data center suffix is load-bearing. ...-us21 in the key determines the API host (us21.api.mailchimp.com). A copy-pasted key from a different account, or a key with a trailing newline from a .env paste, produces a confusing 401 that looks like a permissions problem.
Merge tags must already exist in the audience. Mailchimp will not create FNAME on the fly. Add the merge tags in the Mailchimp UI before deploying, and make sure the tag strings match exactly (uppercase, no spaces). A typo silently drops the value or 400s.
Rate limits: 10 concurrent, and 429s include Retry-After. Bulk imports (e.g., a console command looping over users) will hit this fast. Batch in chunks, sleep between calls, or use Mailchimp's batch endpoint. Never run a full-audience sync from a web request.
Never call the API in a page render. If the addon registers an Antlers tag that fetches list data, remember Antlers output can be cached. Either bust the cache deliberately with {{ cache }} / a cache tag, or fetch list metadata into your own cached variable. Mailchimp's "list growth history" style data does not need to be live per request.
Errors are swallowed more often than thrown. Small addons frequently catch the HTTP exception and log a warning, so a broken key can look like "form works, nobody subscribes." Check storage/logs/laravel.log before concluding the addon is misconfigured, and add your own Log::error around the programmatic call path where you control the try/catch.
Extension points worth knowing. Publish the config to override list IDs and field maps. Bind your own implementation of the client class in a service provider if you need retry/backoff or a different HTTP client. Statamic's SubmissionCreated event is the cleanest hook that does not require touching vendor code — prefer it over editing the addon.
Pin your dependency. With 18 stars and a 2026 last-release date, the maintenance surface is small. Commit a specific version constraint in composer.json and re-test after any Statamic major upgrade — addon compatibility with Statamic's form/fieldtype APIs is the usual breakage point, not Mailchimp itself.
How can I help you explore Laravel packages today?