Email newsletters drive repeat traffic and customer loyalty. Here is how to integrate ConvertKit.
Step 1: Get Your ConvertKit API Key
- Go to ConvertKit Settings > Advanced > API
- Copy your API Key and API Secret
- 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.