Notion Blog Starter
August 17, 2022
React subscribe form with Tailwind using ConvertKit API
How to make a subscription form with Tailwind and using Convertkit
API subscribe

Here is a video of what we will be building today.

import isValidEmail from 'utils/isValidEmail';
const subscribeConvertkit = async (req, res) => {
const { email } = JSON.parse(req.body);
const isValid = isValidEmail(email);
if (!email) {
return res.status(400).json({ error: 'Email is required.' });
if (!isValid) {
return res.status(400).json({ error: 'Email is invalid.' });
try {
const FORM_ID = process.env.CONVERTKIT_FORM_ID;
const API_KEY = process.env.CONVERTKIT_API_KEY;
const API_URL = process.env.CONVERTKIT_API_URL;
const response = await fetch(`${API_URL}forms/${FORM_ID}/subscribe`, {
body: JSON.stringify({ email, api_key: API_KEY }),
headers: { 'Content-Type': 'application/json' },
method: 'POST'
// something went wrong on the convertkit server
if (response.status >= 400) {
return res
.json({ error: 'There was an error subscribing to the list.' });
// Success
return res.status(201).json({ error: null });
} catch (error) {
return res.status(500).json({ error: error.message || error.toString() });
export default subscribeConvertkit;

AddSubscriber fetcher

For submitting the subscription, let's create a simple handler that returns the result from the API call 🎉

export default async function addSubscriber(email) {
// The location of your API route
const url = '/api/subscribe-convertkit';
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify({ email })
const result = await response.json();
return result;

React Subscribe component

Now we have the API ready and the fetcher ready, so we can get into creating the Subscribe component

import Container from 'components/Container';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import addSubscriber from 'utils/addSubscriber';
import Confetti from './Confetti';
export default function Subscribe() {
const [formState, setFormState] = useState({ state: 'initial', message: '' });
const { register, handleSubmit } = useForm();
const onSubmit = async data => {
setFormState({ state: 'loading', message: null });
const { error } = await addSubscriber(;
if (error) {
return setFormState({ state: 'error', message: error });
return setFormState({
state: 'success',
message: 'Check your email to confirm your subscription'
return (
<div className="py-24 bg-gray-100">
<div className="sm:text-center">
<div className="text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl">
Subscribe to the newsletter
<p className="max-w-2xl mx-auto mt-4 text-lg text-gray-500">
Get emails from me about web development, tech, and early access to new
className="mt-8 sm:mx-auto sm:max-w-lg sm:flex"
<div className="flex-1 min-w-0">
<label htmlFor="cta-email" className="sr-only">
Email address
className="block w-full px-5 py-3 text-base text-gray-900 placeholder-gray-500 border border-transparent rounded-md shadow-sm"
placeholder="Enter your email"
{formState.state === 'success' && (
<div className="my-2 mb-4 ml-2 text-sm font-semibold text-green-700 dark:text-green-500">
{formState.state === 'error' && (
<div className="my-2 mb-4 ml-2 text-sm font-semibold text-red-700 dark:text-red-500">
className="block w-full px-5 py-3 mt-4 text-base font-medium text-white bg-gray-600 border border-transparent rounded-md shadow hover:bg-gray-400 focus:outline-none sm:px-10"

Subscribe to the newsletter
Get emails from me about web development, tech, and early access to new articles.
Be the first to know when the blog is published