Newsletter
6 Nov 2026
8 min read

Newsletter Integration: ConvertKit Double Opt-In and Unsubscribe Flow

How SpaceZero integrates with ConvertKit for newsletter management including double opt-in confirmation and unsubscribe handling.

Newsletter Integration with ConvertKit

SpaceZero provides a complete newsletter system via ConvertKit.

Architecture

code

User → Frontend Form → Edge Function → ConvertKit API

Double Opt-In (Automatic)

ConvertKit handles confirmation emails automatically:

  1. User submits email on frontend
  2. Edge function calls ConvertKit /subscribe
  3. ConvertKit sends confirmation email
  4. User clicks link → confirmed in ConvertKit

No additional code needed!

Frontend Form

tsx

// components/ui/newsletter.tsx

const NewsletterForm = () => {

const [email, setEmail] = useState('');

const [loading, setLoading] = useState(false);

const handleSubmit = async (e) => {

const response = await fetch('/api/newsletter', {

method: 'POST',

body: JSON.stringify({ email }),

});

};

return (

<form onSubmit={handleSubmit}>

<Input type="email" value={email} onChange={...} />

<Button type="submit" disabled={loading}>

{loading ? 'Subscribing...' : 'Subscribe'}

</Button>

</form>

);

};

Edge Function

typescript

// functions/api/newsletter.ts

const response = await fetch(

`https://api.convertkit.com/v3/accounts/${accountId}/forms/${formId}/subscribe`,

{

method: 'POST',

headers: { Authorization: `Bearer ${apiKey}` },

body: JSON.stringify({ api_key: apiKey, email }),

}

);

Unsubscribe Endpoint

typescript

// GET /api/newsletter/[email protected]

export const onRequestGet: PagesFunction<Env> = async ({ request, env }) => {

// 1. Find subscriber ID by email

// 2. Call ConvertKit /unsubscribe

// 3. Return success page

};

Add to ConvertKit email templates:

html

<a href="https://your-app.space/api/newsletter/unsubscribe?email={{ subscriber.email }}">

Unsubscribe

</a>

Profile Integration

Newsletter toggle in Profile → Preferences:

tsx

<Toggle

checked={newsletterSubscribed}

onChange={handleNewsletterToggle}

disabled={isGuest || !appConfig.newsletter.enabled}

/>

Syncs to user_profiles.newsletter_subscribed.

Configuration

All app configuration is environment-defined — src/app.ts only reads and normalizes .env:

typescript

// src/app.ts

newsletter: {

enabled: import.meta.env.VITE_NEWSLETTER_ENABLED === 'true',

convertKitFormId: import.meta.env.VITE_CONVERTKIT_FORM_ID || undefined,

description: import.meta.env.VITE_NEWSLETTER_DESCRIPTION || 'Stay updated with new features and releases.',

},

Note: VITE_* variables end up in the browser bundle. Only the form ID is safe to expose — the edge function reads the API key and account ID from server-side secrets, so they never appear in client config.

Server Secrets (Cloudflare Pages)

code

CONVERTKIT_API_KEY=xxx

CONVERTKIT_ACCOUNT_ID=xxx

CONVERTKIT_FORM_ID=xxx

CONVERTKIT_FORM_ID is read server-side from Cloudflare Pages secrets (same value as the client-side VITE_CONVERTKIT_FORM_ID).

Benefits

  • Compliance: Double opt-in = GDPR/CAN-SPAM compliant
  • Deliverability: ConvertKit's infrastructure
  • Management: Tags, segments, automations in ConvertKit
  • Unsubscribe: One-click with confirmation page