How to Integrate ConvertKit with Astro: Complete Guide
Step-by-step guide to integrating ConvertKit with your Astro website. Setup, configuration, and best practices.
How to Integrate ConvertKit with Astro: Complete Guide
ConvertKit (now Kit) is an email marketing platform designed specifically for creators, bloggers, and online businesses. It focuses on subscriber management, automated email sequences, and landing pages. Integrating Kit with your Astro site lets you add newsletter signups, deliver lead magnets, and build email sequences that engage your audience automatically. The API-first approach works particularly well with Astro's on-demand rendering capabilities.
This guide covers connecting Kit to Astro using the REST API for custom signup forms and the embedded script approach for simpler setups.
A note on naming before you start. ConvertKit rebranded to Kit, and the API moved from the legacy api.convertkit.com/v3 host to api.kit.com/v4. The V4 API is the current version and is what this guide uses. V4 API keys are not compatible with V3, authentication moved to an X-Kit-Api-Key header instead of an api_key body field, and several endpoint paths and field names changed. If you find an older tutorial that posts api_key in the JSON body to api.convertkit.com/v3, it is describing the legacy V3 surface.
Prerequisites
You will need:
- Node.js 22.12.0 or newer. Astro 6 requires
node >=22.12.0per the packageenginesfield, so Node 18 will no longer install or run it. - An existing Astro project (any version for embeds, an on-demand adapter for the API integration)
- A Kit account (the free plan covers up to 10,000 subscribers)
- A Kit V4 API key, created in your account under the Developer settings tab at app.kit.com/account_settings/developer_settings
- A Form ID or Tag ID for subscriber targeting
Installation
Kit does not require an official JavaScript SDK for these flows, so you will call the REST API directly with fetch. No additional packages are required for the Kit side.
For the API-based approach you need an adapter so the route can render on demand. In Astro 6 the default output is static, which prerenders everything at build time. Adding an adapter and opting an individual route into on-demand rendering is all you need. The Node adapter is a good default for self-hosting:
npx astro add node
The same command works for other targets if you deploy elsewhere, for example npx astro add vercel, npx astro add netlify, or npx astro add cloudflare. Pick the adapter that matches your host.
Configuration
Environment Variables
Add your Kit credentials to .env:
KIT_API_KEY=your_v4_api_key_here
KIT_FORM_ID=your_default_form_id
The V4 API key authenticates every request through the X-Kit-Api-Key header. There is no separate API secret in V4. The legacy V3 api_key body field and api_secret are not used in V4.
Creating a Kit Utility
In V4 the subscribe endpoint moved from /v3/forms/:id/subscribe to /v4/forms/:id/subscribers, the email field is now email_address instead of email, and the API key travels in a header rather than the request body.
// src/lib/kit.ts
const API_KEY = import.meta.env.KIT_API_KEY;
const BASE_URL = 'https://api.kit.com/v4';
function kitHeaders() {
return {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-Kit-Api-Key': API_KEY,
};
}
export async function subscribeToForm(formId: string, email: string, firstName?: string) {
const response = await fetch(`${BASE_URL}/forms/${formId}/subscribers`, {
method: 'POST',
headers: kitHeaders(),
body: JSON.stringify({
email_address: email,
// first_name lives on the subscriber's fields object in V4
fields: firstName ? { first_name: firstName } : undefined,
}),
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
// V4 returns errors as { errors: string[] }
const message = Array.isArray(data.errors) ? data.errors.join(', ') : 'Subscription failed';
throw new Error(message);
}
return response.json();
}
export async function addTagToSubscriber(tagId: string, subscriberId: string) {
// V4 tags by subscriber id: POST /v4/tags/:tag_id/subscribers/:id
const response = await fetch(`${BASE_URL}/tags/${tagId}/subscribers/${subscriberId}`, {
method: 'POST',
headers: kitHeaders(),
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
const message = Array.isArray(data.errors) ? data.errors.join(', ') : 'Tagging failed';
throw new Error(message);
}
return response.json();
}
Astro Config
output: 'hybrid' was removed in Astro 5, so it is not a valid value in Astro 6. The default output is static. You add an adapter, leave output at its default, and opt the specific API route or page into on-demand rendering with export const prerender = false. Setting output: 'server' is optional and only flips the default so that pages render on demand unless you mark them prerender = true. For a mostly static blog with a single dynamic API route, keep the default.
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
// output: 'static' is the default in Astro 6, so it can be omitted.
adapter: node({ mode: 'standalone' }),
});
Common Patterns
Server-Side Signup API Route
Create an API endpoint that subscribes users to a Kit form. Mark it prerender = false so it runs on demand at request time rather than being captured at build:
// src/pages/api/subscribe.ts
import type { APIRoute } from 'astro';
import { subscribeToForm } from '../../lib/kit';
// Opt this route into on-demand rendering (default output is 'static').
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 formId = import.meta.env.KIT_FORM_ID;
if (!email) {
return new Response(JSON.stringify({ error: 'Email is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
try {
await subscribeToForm(formId, email, firstName);
return new Response(JSON.stringify({ success: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error: any) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
};
Custom Newsletter Signup Component
---
// src/components/Newsletter.astro
interface Props {
heading?: string;
description?: string;
}
const { heading = 'Join the newsletter', description = 'Get weekly tips delivered to your inbox.' } = Astro.props;
---
<div class="newsletter-wrapper">
<h3>{heading}</h3>
<p>{description}</p>
<form id="ck-signup" class="newsletter-form">
<input type="text" name="firstName" placeholder="First name" />
<input type="email" name="email" placeholder="your@email.com" required />
<button type="submit">Subscribe</button>
</form>
<p id="ck-message" style="display: none;"></p>
</div>
<script>
const form = document.getElementById('ck-signup') as HTMLFormElement;
const msg = document.getElementById('ck-message') as HTMLParagraphElement;
form?.addEventListener('submit', async (e) => {
e.preventDefault();
const data = new FormData(form);
try {
const res = await fetch('/api/subscribe', { method: 'POST', body: data });
const result = await res.json();
msg.style.display = 'block';
if (res.ok) {
msg.textContent = 'Check your email to confirm your subscription!';
msg.style.color = 'green';
form.reset();
} else {
msg.textContent = result.error || 'Something went wrong.';
msg.style.color = 'red';
}
} catch {
msg.style.display = 'block';
msg.textContent = 'Network error. Please try again.';
msg.style.color = 'red';
}
});
</script>
Embedded Kit Form (No Adapter Required)
For fully static sites, use Kit's embed script. Get the code from your Kit dashboard under Grow then Landing Pages and Forms, then choose your form and the JavaScript embed option:
---
// src/components/KitEmbed.astro
interface Props {
formId: string;
}
const { formId } = Astro.props;
---
<script async data-uid={formId} src={`https://your-subdomain.kit.com/${formId}/index.js`}></script>
Copy the exact src and data-uid from your dashboard rather than hand building the URL, since the host reflects your account subdomain. Use the component in any page:
<KitEmbed formId="abc123def" />
Tagging Subscribers Based on Content
Add tags to subscribers based on where they signed up. This is useful for segmentation. In V4 the tag endpoint references the subscriber by id, so use the id returned from the form subscribe call rather than passing an email:
// src/pages/api/subscribe-tagged.ts
import type { APIRoute } from 'astro';
import { subscribeToForm, addTagToSubscriber } from '../../lib/kit';
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 tagId = formData.get('tag') as string;
const formId = import.meta.env.KIT_FORM_ID;
try {
const result = await subscribeToForm(formId, email, firstName);
// V4 returns the subscriber object; grab its id for tagging.
const subscriberId = result?.subscriber?.id;
if (tagId && subscriberId) {
await addTagToSubscriber(tagId, String(subscriberId));
}
return new Response(JSON.stringify({ success: true }), { status: 200 });
} catch (error: any) {
return new Response(JSON.stringify({ error: error.message }), { status: 500 });
}
};
Lead Magnet Delivery
Kit can automatically send a lead magnet when someone subscribes to a specific form. Set up the incentive email in the form's settings inside your Kit dashboard. When you subscribe a user to that form via the API, Kit handles the delivery automatically.
Troubleshooting
Invalid email or missing field: Validate the email on the client side before submitting. The required field is email_address, not email, so a request that still sends the old V3 email key will fail validation.
Subscriber added but no confirmation email: Kit sends a confirmation email by default for new subscribers. Check the subscriber's spam folder. Double opt-in behavior is configured per form in the Kit dashboard.
Form ID not found: Make sure the form is published and active in Kit. Archived or draft forms will not accept subscribers. You can find active form IDs in the Kit dashboard URL when editing a form.
Duplicate subscriber handling: Kit handles duplicates gracefully. Adding a subscriber to a form returns a 201 when newly added and a 200 when the subscriber was already on the form, rather than creating a duplicate. This means you do not need to check for existing subscribers before calling the API.
API rate limits: Kit allows no more than 120 requests over a rolling 60 second period for a given API key, and offers 600 requests over the same window for OAuth. For high-traffic sites, throttle submissions or move to OAuth.
Embedded form styling conflicts: Kit's embedded forms come with their own CSS that may conflict with your site's styles. Use a minimal embed style or the API approach for full control over the form design.
Common Errors and Fixes
output: 'hybrid' is an invalid option: This value was removed in Astro 5 and remains invalid in Astro 6. Remove it. The default output is static, and you opt individual routes into on-demand rendering with export const prerender = false. Astro recommends starting with static mode until most or all of your pages need on-demand rendering.
401 Unauthorized from the Kit API: V4 authenticates with the X-Kit-Api-Key request header, not the legacy V3 api_key body field. A request that posts api_key in the JSON body, or hits api.convertkit.com/v3, will not authenticate against V4. Confirm the key was created under the Developer settings tab and is a V4 key, since V4 keys are not compatible with V3.
Errors arrive in an unexpected shape: V4 returns all errors as a JSON object with a single errors attribute that is an array of strings. Code written for V3, which read a single message field, will log undefined. Read data.errors and join the array, as the utility above does.
Astro install or build fails on Node 18 or 20: Astro 6 declares node >=22.12.0 in its engines field. Upgrade Node to 22.12.0 or newer before installing.
API route returns static HTML or 404 at request time: With the default static output an API route is captured at build time unless you mark it on demand. Add export const prerender = false to the route file and make sure an adapter is configured, otherwise on-demand routes have no runtime to execute in.
Adapter version mismatch after upgrading Astro: The Astro 6 adapters pin a peer dependency on Astro 6. For example @astrojs/node 10.x declares a peer of astro ^6.3.0. Re-run the relevant npx astro add command after a major Astro upgrade so the adapter version matches.
Official Docs and Examples
- Kit Developer Documentation, API V4 home: https://developers.kit.com/api-reference/overview
- Kit V4 authentication, the
X-Kit-Api-Keyheader and rate limits: https://developers.kit.com/api-reference/authentication - Kit V3 to V4 upgrade notes, the renamed endpoints and fields: https://developers.kit.com/api-reference/upgrading-to-v4
- Kit add-subscriber-to-form endpoint reference: https://developers.kit.com/api-reference/forms/add-subscriber-to-form-by-email-address
- Astro on-demand rendering guide, output modes and
prerender: https://docs.astro.build/en/guides/on-demand-rendering/ - Astro Node adapter docs: https://docs.astro.build/en/guides/integrations-guide/node/
- Example repo, Astro SSR with the Node adapter (official template): https://github.com/withastro/astro/tree/main/examples/ssr
Conclusion
Kit pairs well with Astro for creator-focused websites and blogs. The V4 REST API gives you full control over form design and subscriber management without depending on embedded scripts. Use the API approach for custom-designed forms that match your site's look, and leverage Kit's tagging system to segment subscribers based on their interests and behavior. For fully static Astro sites, the embedded form script is a quick solution that requires no adapter or on-demand route.
Sources
All versions and facts below were checked on 2026-05-29 against these sources.
- Astro on npm, latest version 6.4.2 and
engines.node >=22.12.0: https://registry.npmjs.org/astro/latest @astrojs/nodeon npm, latest version 10.1.2 with peerastro ^6.3.0: https://registry.npmjs.org/@astrojs/node/latest@astrojs/vercelon npm, latest version 10.0.8: https://registry.npmjs.org/@astrojs/vercel/latest@astrojs/netlifyon npm, latest version 7.0.11: https://registry.npmjs.org/@astrojs/netlify/latest@astrojs/cloudflareon npm, latest version 13.6.0: https://registry.npmjs.org/@astrojs/cloudflare/latest- Astro on-demand rendering guide (default
static, nohybrid,prerenderper page, adapters): https://docs.astro.build/en/guides/on-demand-rendering/ - Kit API V4 authentication (
X-Kit-Api-Keyheader, base URLhttps://api.kit.com/v4, 120 vs 600 request limits, Developer settings tab): https://developers.kit.com/api-reference/authentication - Kit V3 to V4 upgrade reference (base URL change,
/v4/forms/:id/subscribers,email_address, tag endpoint by subscriber id,errorsarray shape): https://developers.kit.com/api-reference/upgrading-to-v4 - Kit add-subscriber-to-form endpoint (
POST /v4/forms/{form_id}/subscribers, bodyemail_address, 200 or 201 responses): https://developers.kit.com/api-reference/forms/add-subscriber-to-form-by-email-address
Related Articles
How to Use Algolia with Astro: Complete Guide
Step-by-step guide to integrating Algolia with your Astro website.
How to Integrate Auth0 with Astro: Complete Guide
Step-by-step guide to integrating Auth0 with your Astro website. Setup, configuration, and best practices.
How to Use AWS Amplify with Astro: Complete Guide
Step-by-step guide to integrating AWS Amplify with your Astro website.