How to add a waitlist to your Astro site
A complete guide to adding a waitlist to an Astro site using the Enlist API: server endpoint and client-side form with position display.
Astro server endpoints handle POST requests on the server, making them the right place to call the Enlist API without exposing your key.
1. Get an API key
Create an account and add to your .env:
ENLIST_API_KEY=en_live_your_key_here
WAITLIST_ID=your_waitlist_id_hereMake sure your Astro project is using SSR or hybrid mode. Server endpoints require a server runtime. Add the adapter for your deployment target (Vercel, Netlify, Node, etc.).
2. Create the API endpoint
import type { APIRoute } from 'astro';
export const POST: APIRoute = async ({ request, url }) => {
const body = await request.json().catch(() => null);
if (!body?.email) {
return new Response(JSON.stringify({ error: 'Email is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
const utmSource = url.searchParams.get('utm_source') ?? undefined;
const res = await fetch(
`https://api.enlist.dev/v1/waitlists/${import.meta.env.WAITLIST_ID}/signups`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${import.meta.env.ENLIST_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: body.email, utm_source: utmSource }),
},
);
if (!res.ok) {
const data = await res.json().catch(() => ({}));
return new Response(JSON.stringify({ error: data.error?.message ?? 'Something went wrong' }), {
status: res.status,
headers: { 'Content-Type': 'application/json' },
});
}
const { position, total } = await res.json();
return new Response(JSON.stringify({ position, total }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
};3. Create the form component
Since Astro components don't have client-side reactivity by default, use a <script> tag or an island (React, Svelte, Vue). Here's a vanilla JS approach. No framework required:
<div id="waitlist-wrapper">
<form id="waitlist-form">
<input id="email-input" name="email" type="email" required placeholder="you@example.com" />
<button type="submit">Join waitlist</button>
</form>
<p id="waitlist-result" hidden></p>
<p id="waitlist-error" role="alert" hidden></p>
</div>
<script>
const form = document.getElementById('waitlist-form') as HTMLFormElement;
const result = document.getElementById('waitlist-result') as HTMLParagraphElement;
const errorEl = document.getElementById('waitlist-error') as HTMLParagraphElement;
const button = form.querySelector('button') as HTMLButtonElement;
form.addEventListener('submit', async (e) => {
e.preventDefault();
const email = new FormData(form).get('email') as string;
button.disabled = true;
button.textContent = 'Joining…';
errorEl.hidden = true;
try {
const res = await fetch('/api/waitlist', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const data = await res.json();
if (!res.ok) {
errorEl.textContent = data.error ?? 'Something went wrong';
errorEl.hidden = false;
button.disabled = false;
button.textContent = 'Join waitlist';
return;
}
form.hidden = true;
result.textContent = `You're #${data.position} of ${data.total} on the waitlist.`;
result.hidden = false;
} catch {
errorEl.textContent = 'Network error. Please try again.';
errorEl.hidden = false;
button.disabled = false;
button.textContent = 'Join waitlist';
}
});
</script>4. Use it
---
import WaitlistForm from '../components/WaitlistForm.astro';
---
<html>
<body>
<h1>Coming soon</h1>
<WaitlistForm />
</body>
</html>Notes
Astro's import.meta.env variables without the PUBLIC_ prefix are server-only, so they're never bundled into client JavaScript. The API key stays on the server.
If you prefer a React or Svelte island for the form, the approach is the same as those framework guides: point fetch at /api/waitlist and handle the response states in your component.