/ astro-integrations / How to Integrate Mailchimp with Astro: Complete Guide
astro-integrations 11 min read

How to Integrate Mailchimp with Astro: Complete Guide

Step-by-step guide to integrating Mailchimp with your Astro website. Setup, configuration, and best practices.

How to Integrate Mailchimp with Astro: Complete Guide

How to Integrate Mailchimp with Astro: Complete Guide

Mailchimp is one of the most popular email marketing platforms, used by millions of businesses for newsletters, marketing campaigns, and audience management. Integrating Mailchimp with your Astro site lets you add newsletter signup forms, manage subscriber lists, and trigger automated email sequences. The most common use case is capturing email addresses from your website visitors and adding them to a Mailchimp audience for ongoing engagement.

This guide covers connecting Mailchimp to an Astro project using both the Marketing API for server-side integration and the embedded form approach for simpler setups.

Prerequisites

Before starting, you will need:

  • Node.js 22.12.0 or higher installed (Astro 6 requires Node 22.12.0 and up; support for Node 18 and Node 20 was dropped)
  • An existing Astro project (any version for embedded forms; an adapter plus on-demand rendering for the API integration)
  • A Mailchimp account (free plan available at mailchimp.com)
  • A Mailchimp API key (Account > Extras > API Keys)
  • Your Audience ID (also called List ID, found in Audience > Settings > Audience name and defaults)
  • Your server prefix (the usX suffix of your API key, e.g., us21, the same value that appears in your Mailchimp dashboard URL such as https://us21.admin.mailchimp.com/)

This guide was written and verified against Astro 6.4.2, @astrojs/node 10.1.2, and @mailchimp/mailchimp_marketing 3.0.80.

Installation

For the API-based integration, install the Mailchimp Marketing SDK (current version 3.0.80):

npm install @mailchimp/mailchimp_marketing

For the embedded form approach, no packages are needed.

If using the API approach, you need an adapter so Astro can render the subscribe endpoint on demand. Add the Node adapter (current version 10.1.2) with the official command, which installs the package and updates astro.config.mjs in one step:

npx astro add node

The other official adapters are @astrojs/cloudflare, @astrojs/netlify, and @astrojs/vercel. Pick the one that matches where you deploy. They are all added the same way (for example npx astro add netlify).

Configuration

Environment Variables

Add your Mailchimp credentials to .env:

MAILCHIMP_API_KEY=your_api_key_here-us21
MAILCHIMP_SERVER_PREFIX=us21
MAILCHIMP_AUDIENCE_ID=your_audience_id_here

Creating the Mailchimp Client

// src/lib/mailchimp.ts
import mailchimp from '@mailchimp/mailchimp_marketing';

mailchimp.setConfig({
  apiKey: import.meta.env.MAILCHIMP_API_KEY,
  server: import.meta.env.MAILCHIMP_SERVER_PREFIX,
});

export { mailchimp };
export const audienceId = import.meta.env.MAILCHIMP_AUDIENCE_ID;

Astro Config for API Integration

The output: 'hybrid' value that older guides used was removed in Astro 5 and does not exist in Astro 6. Today, static is the default output mode, and you keep your blog pages pre-rendered while opting individual routes into on-demand rendering. So you do not set output: 'server' unless most of your site is dynamic. You only need to register an adapter:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  // output defaults to 'static'; no need to set it
  adapter: node({ mode: 'standalone' }),
});

With the default static output, every page is pre-rendered at build time. To let a single route run server-side at request time, add export const prerender = false at the top of that route (shown in the subscribe endpoint below). This is the modern replacement for the old hybrid mode. The Astro docs advise starting with the default static mode and only switching to output: 'server' once most or all of your pages need on-demand rendering.

Common Patterns

Create an API route that adds subscribers to your Mailchimp audience:

// src/pages/api/subscribe.ts
import type { APIRoute } from 'astro';
import { mailchimp, audienceId } from '../../lib/mailchimp';

// Required when the project uses the default static output.
// This opts the endpoint into on-demand rendering so the
// handler actually runs at request time instead of build time.
export const prerender = false;

export const POST: APIRoute = async ({ request }) => {
  const formData = await request.formData();
  const email = formData.get('email') as string;
  const firstName = formData.get('firstName') as string || '';
  const lastName = formData.get('lastName') as string || '';

  if (!email) {
    return new Response(JSON.stringify({ error: 'Email is required' }), {
      status: 400,
    });
  }

  try {
    await mailchimp.lists.addListMember(audienceId, {
      email_address: email,
      status: 'subscribed', // Use 'pending' for double opt-in
      merge_fields: {
        FNAME: firstName,
        LNAME: lastName,
      },
    });

    return new Response(JSON.stringify({ success: true }), { status: 200 });
  } catch (error: any) {
    const errorBody = error.response?.body;

    if (errorBody?.title === 'Member Exists') {
      return new Response(
        JSON.stringify({ error: 'This email is already subscribed.' }),
        { status: 400 }
      );
    }

    return new Response(
      JSON.stringify({ error: 'Subscription failed. Please try again.' }),
      { status: 500 }
    );
  }
};

Build a newsletter form component:

---
// src/components/NewsletterForm.astro
---

<form id="newsletter-form" method="POST" action="/api/subscribe">
  <h3>Subscribe to our newsletter</h3>
  <input type="text" name="firstName" placeholder="First name" />
  <input type="email" name="email" placeholder="Email address" required />
  <button type="submit">Subscribe</button>
  <p id="form-message" style="display: none;"></p>
</form>

<script>
  const form = document.getElementById('newsletter-form') as HTMLFormElement;
  const message = document.getElementById('form-message') as HTMLParagraphElement;

  form?.addEventListener('submit', async (e) => {
    e.preventDefault();
    const formData = new FormData(form);

    try {
      const response = await fetch('/api/subscribe', {
        method: 'POST',
        body: formData,
      });

      const result = await response.json();
      message.style.display = 'block';

      if (response.ok) {
        message.textContent = 'Successfully subscribed!';
        message.style.color = 'green';
        form.reset();
      } else {
        message.textContent = result.error;
        message.style.color = 'red';
      }
    } catch {
      message.style.display = 'block';
      message.textContent = 'Something went wrong. Please try again.';
      message.style.color = 'red';
    }
  });
</script>

Method 2: Embedded Mailchimp Form

For static Astro sites without SSR, use Mailchimp's embedded form. Get the form HTML from Mailchimp: Audience > Signup forms > Embedded forms.

---
// src/components/MailchimpEmbed.astro
---

<div id="mc_embed_shell">
  <form
    action="https://yourdomain.us21.list-manage.com/subscribe/post?u=YOUR_U_VALUE&id=YOUR_ID_VALUE"
    method="post"
    target="_self"
  >
    <label for="mce-EMAIL">Email Address</label>
    <input type="email" name="EMAIL" id="mce-EMAIL" required />
    <!-- Bot protection -->
    <div style="position: absolute; left: -5000px;" aria-hidden="true">
      <input type="text" name="b_YOUR_U_VALUE_YOUR_ID_VALUE" tabindex="-1" value="" />
    </div>
    <button type="submit">Subscribe</button>
  </form>
</div>

This approach requires no server-side code and works with fully static Astro sites.

Adding Tags to Subscribers

Organize subscribers with tags when they sign up:

// At the top of the file (Astro endpoints are ESM, so use an import,
// not require('crypto') which is CommonJS and will fail here)
import { createHash } from 'node:crypto';

// After adding the member. The subscriber hash is the lowercase email
// run through MD5, which Mailchimp uses to address a member by email.
const subscriberHash = createHash('md5')
  .update(email.toLowerCase())
  .digest('hex');

await mailchimp.lists.updateListMemberTags(audienceId, subscriberHash, {
  tags: [
    { name: 'Website Signup', status: 'active' },
    { name: 'Blog Reader', status: 'active' },
  ],
});

Troubleshooting

"Member Exists" error: The email is already in your audience. The API returns a specific error for this case. Handle it gracefully by showing a friendly message instead of a generic error.

API key not working: Verify your API key includes the server prefix (e.g., abc123def-us21). The us21 part tells the SDK which Mailchimp data center to connect to. If the server prefix is wrong, all requests will fail.

Subscribers not receiving emails: Check your Mailchimp Audience > All contacts. If the status shows "Cleaned" or "Unsubscribed," the contact cannot receive emails. New subscribers with status: 'pending' need to confirm via the double opt-in email first.

CORS errors with embedded form: Mailchimp's embedded forms submit directly to Mailchimp's servers. If you get CORS issues, make sure the form action URL is exactly as provided by Mailchimp, and the form uses a standard POST submission (not fetch/AJAX).

TypeScript errors with the SDK: The @mailchimp/mailchimp_marketing package has limited TypeScript definitions. You may need to add // @ts-ignore for some methods or create a type declaration file. The API method names follow the documentation at mailchimp.com/developer.

Rate limiting: Mailchimp allows 10 simultaneous connections per API key. For high-traffic signup forms, consider batching requests or implementing a queue.

Common Errors and Fixes

"output: 'hybrid' is an invalid option": The hybrid output mode was removed in Astro 5 and is still gone in Astro 6. If you copied an old config, delete the output: 'hybrid' line. Leave output unset (it defaults to static) and add export const prerender = false to each route you want rendered on demand. Reach for output: 'server' only when most of the site is dynamic.

Endpoint returns the source or a 404 instead of running: A POST handler that gets pre-rendered at build time will not execute at request time. Make sure the route file has export const prerender = false and that an adapter is registered in astro.config.mjs. Without an adapter, Astro has nowhere to run on-demand code and the build will warn or fail.

"Cannot find adapter" or build fails after adding an API route: On-demand rendering requires one of the official adapters (@astrojs/node, @astrojs/cloudflare, @astrojs/netlify, @astrojs/vercel). Run npx astro add node (or your platform's adapter) so the package installs and the config is wired in one step.

require is not defined: Astro endpoints are ES modules. Patterns like require('crypto') throw at runtime. Use import { createHash } from 'node:crypto' instead, as shown in the tags example above.

"Member Exists" error: The email is already in your audience. The API returns this specific error so you can handle it gracefully and show a friendly message rather than a generic failure.

Wrong or missing server prefix: The SDK needs the server value (for example us21) to pick the correct Mailchimp data center. It is the suffix after the dash in your API key and the same prefix you see in your dashboard URL (https://us21.admin.mailchimp.com/). If it is wrong or missing, every request fails.

Subscribers not receiving emails: Check Audience > All contacts. A status of "Cleaned" or "Unsubscribed" means the contact cannot receive email. Members added with status: 'pending' must confirm via the double opt-in email before they are active.

CORS errors with the embedded form: Mailchimp's embedded forms post directly to Mailchimp. Keep the form action URL exactly as Mailchimp provides it and submit with a standard POST, not fetch or AJAX.

Limited TypeScript definitions: The @mailchimp/mailchimp_marketing package ships thin types. You may need a // @ts-ignore on a few methods or a small declaration file. Method names follow the Marketing API reference at mailchimp.com/developer.

Node version mismatch on build: Astro 6 requires Node 22.12.0 or higher. Support for Node 18 and Node 20 was dropped, so building on an older runtime can fail with no obvious cause. Run node -v to check, and pin 22.12.0 in a .nvmrc file.

Official Docs and Examples

Conclusion

Mailchimp integrates with Astro through either the Marketing API for full control or embedded forms for simplicity. The API approach gives you custom form designs, error handling, and the ability to add tags and merge fields programmatically. On Astro 6, keep your pages on the default static output and add export const prerender = false to the subscribe endpoint so it runs on demand behind an adapter, rather than reaching for the removed hybrid mode. Use double opt-in (status: 'pending') for better deliverability and compliance, handle the "Member Exists" case gracefully, and add meaningful tags to segment your audience from the start. For static sites, the embedded form works without any server-side code.

Sources

All versions and recommendations below were checked on 2026-05-29.