Skip to main content
Back to Blog
Tutorials
3 min read
November 10, 2024

How to Add Newsletter Signup with ConvertKit in Next.js

Integrate ConvertKit newsletter signup into your Next.js site with a custom form, API route, and success handling.

Ryel Banfield

Founder & Lead Developer

Email newsletters drive repeat traffic and customer loyalty. Here is how to integrate ConvertKit.

Step 1: Get Your ConvertKit API Key

  1. Go to ConvertKit Settings > Advanced > API
  2. Copy your API Key and API Secret
  3. Note your Form ID from the form you want subscribers added to
CONVERTKIT_API_KEY=your_api_key
CONVERTKIT_FORM_ID=your_form_id

Step 2: API Route

// app/api/newsletter/route.ts
import { NextResponse } from "next/server";

const CONVERTKIT_API_KEY = process.env.CONVERTKIT_API_KEY!;
const CONVERTKIT_FORM_ID = process.env.CONVERTKIT_FORM_ID!;

export async function POST(req: Request) {
  const { email, firstName } = await req.json();

  if (!email || !email.includes("@")) {
    return NextResponse.json(
      { error: "Valid email is required" },
      { status: 400 }
    );
  }

  const res = await fetch(
    `https://api.convertkit.com/v3/forms/${CONVERTKIT_FORM_ID}/subscribe`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        api_key: CONVERTKIT_API_KEY,
        email,
        first_name: firstName || undefined,
      }),
    }
  );

  if (!res.ok) {
    const error = await res.json();
    return NextResponse.json(
      { error: error.message || "Subscription failed" },
      { status: 500 }
    );
  }

  return NextResponse.json({ success: true });
}

Step 3: Newsletter Form Component

"use client";

import { useState, useTransition } from "react";
import { Mail } from "lucide-react";

export function NewsletterForm() {
  const [email, setEmail] = useState("");
  const [firstName, setFirstName] = useState("");
  const [status, setStatus] = useState<"idle" | "success" | "error">("idle");
  const [isPending, startTransition] = useTransition();

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setStatus("idle");

    startTransition(async () => {
      const res = await fetch("/api/newsletter", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email, firstName }),
      });

      if (res.ok) {
        setStatus("success");
        setEmail("");
        setFirstName("");
      } else {
        setStatus("error");
      }
    });
  }

  if (status === "success") {
    return (
      <div className="rounded-xl border border-green-200 bg-green-50 p-6 text-center dark:border-green-800 dark:bg-green-950">
        <p className="font-semibold text-green-800 dark:text-green-200">
          You are subscribed!
        </p>
        <p className="mt-1 text-sm text-green-600 dark:text-green-400">
          Check your email to confirm your subscription.
        </p>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-3">
      <div className="flex gap-2">
        <input
          type="text"
          value={firstName}
          onChange={(e) => setFirstName(e.target.value)}
          placeholder="First name"
          className="w-32 rounded-lg border px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900"
        />
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="you@example.com"
          required
          className="flex-1 rounded-lg border px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900"
        />
      </div>
      <button
        type="submit"
        disabled={isPending}
        className="flex w-full items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
      >
        <Mail className="h-4 w-4" />
        {isPending ? "Subscribing..." : "Subscribe"}
      </button>
      {status === "error" && (
        <p className="text-sm text-red-500">
          Something went wrong. Please try again.
        </p>
      )}
    </form>
  );
}

Step 4: Inline Blog Newsletter Section

export function BlogNewsletter() {
  return (
    <section className="my-12 rounded-2xl border bg-gray-50 p-8 dark:border-gray-800 dark:bg-gray-900">
      <div className="mx-auto max-w-md text-center">
        <h3 className="text-xl font-bold">Stay Updated</h3>
        <p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
          Get the latest web development tips and insights delivered to your
          inbox. No spam, unsubscribe anytime.
        </p>
        <div className="mt-4">
          <NewsletterForm />
        </div>
      </div>
    </section>
  );
}

Step 5: Footer Newsletter Variant

export function FooterNewsletter() {
  return (
    <div className="max-w-sm">
      <h4 className="font-semibold">Newsletter</h4>
      <p className="mt-1 text-sm text-gray-400">
        Weekly insights on web development and design.
      </p>
      <div className="mt-3">
        <NewsletterForm />
      </div>
    </div>
  );
}

Step 6: Alternative β€” Mailchimp Integration

// app/api/newsletter/route.ts (Mailchimp version)
import { NextResponse } from "next/server";

const API_KEY = process.env.MAILCHIMP_API_KEY!;
const LIST_ID = process.env.MAILCHIMP_LIST_ID!;
const DC = API_KEY.split("-").pop(); // Data center from API key

export async function POST(req: Request) {
  const { email, firstName } = await req.json();

  if (!email || !email.includes("@")) {
    return NextResponse.json(
      { error: "Valid email required" },
      { status: 400 }
    );
  }

  const res = await fetch(
    `https://${DC}.api.mailchimp.com/3.0/lists/${LIST_ID}/members`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        email_address: email,
        status: "pending", // Double opt-in
        merge_fields: { FNAME: firstName || "" },
      }),
    }
  );

  if (!res.ok) {
    const error = await res.json();
    if (error.title === "Member Exists") {
      return NextResponse.json({ success: true }); // Already subscribed
    }
    return NextResponse.json({ error: "Subscription failed" }, { status: 500 });
  }

  return NextResponse.json({ success: true });
}

Need Email Marketing Integration?

We build websites with newsletter signups, email automation, and lead capture forms. Contact us to discuss your project.

newsletterConvertKitemail marketingNext.jstutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles