If you're building a product or content site, here's the hard truth: your subscriber list is more valuable than your product features.
I know, I know. You want to build cool features. You want to perfect your UI. You want to add that fancy animation everyone's doing. But none of that matters if you don't have an audience to show it to.
That's why the first thing I built for KuduTek—before perfecting the blog, before adding advanced features, before anything else—was a robust newsletter subscription system powered by ConvertKit.
Here's why I made that choice, and the technical reality of implementing it with Claude Code in about 4-5 hours.
Why Newsletter First?
The Subscriber-to-Customer Path
Every successful indie maker I've studied follows the same pattern:
- Build an audience (newsletter subscribers)
- Provide value (free content, insights, tips)
- Understand their problems (through engagement)
- Build solutions (products they actually want)
- Convert to customers (easy sale to warm audience)
Skip step 1, and you're building in a vacuum. You're guessing what people want. You're launching to crickets.
A subscriber list is:
- Direct line to interested people
- Feedback channel for ideas
- Built-in launch audience
- Owned platform (not algorithm-dependent)
I've seen too many solo founders spend months building perfect products that nobody buys. They forgot to build an audience first.
The Math is Simple
Let's say you have two scenarios:
Scenario A: Build First
- Spend 6 months building the perfect product
- Launch to 0 subscribers
- Hope Twitter/Reddit/HN picks it up
- Maybe get 100 signups
- Convert 2% → 2 customers
Scenario B: Audience First
- Spend 1 month building MVP
- Spend 5 months writing, sharing, growing list to 1,000 subscribers
- Launch to warm audience
- Get 200 signups from email alone
- Convert 5% → 10 customers
Which would you choose?
Why ConvertKit?
I evaluated several email service providers. Here's my thinking:
The Contenders
Mailchimp - The default choice
- ✅ Free tier up to 500 subscribers
- ✅ Familiar interface
- ❌ Expensive scaling ($20/mo for 500, $50/mo for 2,500)
- ❌ Limited automation on free tier
- ❌ Clunky API
Substack - Writer-focused
- ✅ Dead simple for writers
- ✅ Built-in audience discovery
- ❌ Don't own your subscribers
- ❌ Limited control over branding
- ❌ Takes 10% of paid subscriptions
ConvertKit - Creator-focused
- ✅ Free up to 1,000 subscribers
- ✅ Built for creators and indie makers
- ✅ Powerful automation and tagging
- ✅ Great API documentation
- ✅ No feature restrictions on free tier
- ❌ Costs more at scale ($29/mo for 1,000+)
Why ConvertKit Won
1. The Creator Focus
ConvertKit was built specifically for people like me—solo creators, indie makers, people building an audience. The interface makes sense. The features align with how creators actually work.
2. Tagging and Segmentation
This was the killer feature. ConvertKit's tagging system lets me:
- Track where subscribers came from (
source:hero,source:footer, etc.) - Identify intent (
consulting-prospect,needs-help) - Segment by interest (
excel-user,builder) - Personalize content based on tags
Mailchimp's segments felt like an afterthought. ConvertKit's tags are first-class citizens.
3. The Free Tier
1,000 subscribers with FULL features? That's enough runway to validate my audience-building strategy before spending a dime.
Most competitors either:
- Limit features on free tier
- Cap at 500 subscribers
- Charge for basic automation
4. The API
I knew I'd be integrating email signup throughout the site—homepage hero, footer, dedicated newsletter page, contact form. I needed a robust API.
ConvertKit's API is:
- Well documented
- RESTful
- Supports both public and secret keys (security++)
- Has endpoints for everything (subscribers, tags, custom fields, automation)
The Technical Implementation
Okay, let's get into the weeds. Building a newsletter system sounds simple: "Just add an email form!" But if you want it done right—with proper tracking, segmentation, and user experience—there's a lot more to it.
Total implementation time: 4-5 hours (with Claude Code doing the heavy lifting)
The Architecture
I built a progressive signup strategy with two tiers:
Tier 1: Simple Signup (Homepage hero, Footer)
<EmailSignup variant="hero" source="hero" />
- Collect: Email + First Name
- Tag:
source:heroorsource:footer - Show: Profile completion modal after subscription
- Why: Lower barrier = higher conversion
Tier 2: Extended Signup (Newsletter page, Contact page)
<EnhancedEmailSignup
source="newsletter"
title="Join the Newsletter"
description="Weekly insights on building products..."
/>
- Collect: All 5 fields (email, firstName, lastName, company, interests)
- Tags:
source:newsletter+ profile tags - No modal needed
- Why: High-intent pages can ask for more
The API Routes
I built two Next.js API routes:
/api/subscribe - Initial subscription
export async function POST(request: NextRequest) {
const { email, firstName, source, ...optionalFields } = await request.json()
// 1. Validate input
// 2. Subscribe to ConvertKit
// 3. Determine tags based on data provided
// 4. Apply tags to subscriber
// 5. Return subscriber ID for modal
return NextResponse.json({
success: true,
subscriberId: result.id,
tags: appliedTags
})
}
/api/update-profile - Profile completion from modal
export async function POST(request: NextRequest) {
const { subscriberId, lastName, company, interests } = await request.json()
// 1. Update custom fields in ConvertKit
// 2. Apply additional tags (profile-completed, has-company, needs-help)
return NextResponse.json({
success: true,
tags: appliedTags
})
}
The Tagging System
This is where it gets interesting. Tags are the backbone of email segmentation, but they're tricky to implement correctly.
Automatic Tags Applied:
export function determineTagsToApply(params, source) {
const tags = []
// Source tracking (always applied)
tags.push(`source:${source}`)
// Profile completeness
if (params.lastName || params.company || params.interests) {
tags.push('profile-completed')
}
// B2B indicator
if (params.company && params.company.length >= 2) {
tags.push('has-company')
}
// Consulting prospect
if (params.interests && params.interests.length >= 10) {
tags.push('needs-help')
}
return tags
}
Why These Tags Matter:
source:hero- Highest converting page gets analysishas-company- B2B leads get case studies, not tipsneeds-help- Consulting prospects get solution contentprofile-completed- Higher engagement = different nurture
The Problems We Hit
Here's where reality diverged from plan. What should have been a 2-hour implementation turned into 5 hours of debugging. Welcome to API integration.
Problem #1: The Missing Function Call
What happened:
We wrote a beautiful addTagsToSubscriber() function. We determined tags. We passed tags around. But tags weren't being applied to subscribers.
Why: We never actually called the function. Classic developer moment—you write the code but forget to execute it.
The fix:
// After successful subscription
if (tags.length > 0 && subscriberId) {
await addTagsToSubscriber(subscriberId, tags, email)
}
Lesson: Just because you wrote the code doesn't mean it runs. Always trace execution flow.
Problem #2: API Key vs. API Secret
What happened:
ConvertKit returned: { error: "Authorization Failed", message: "API Key not valid" }
Why: ConvertKit uses two different authentication methods:
api_key(public) - For form submissionsapi_secret(private) - For backend operations (tagging, updates)
We were using the public api_key for tag operations. Wrong key, wrong permissions.
The fix:
// Wrong
body: JSON.stringify({
api_key: config.apiSecret, // ❌
tag: { name: tagName }
})
// Right
body: JSON.stringify({
api_secret: config.apiSecret, // ✅
tag: { name: tagName }
})
Lesson: Read the docs carefully. API authentication isn't always obvious.
Problem #3: Tag Duplication
What happened: Every subscription tried to CREATE a new tag. But most tags already existed. ConvertKit returned:
{ "error": "Unprocessable Entity", "message": "Tag already exists" }
This meant:
- Tag creation failed
- Couldn't get tag ID
- Subscriber never got tagged
The fix: We implemented a three-step smart tag management system:
// Step 1: Fetch all existing tags
async function getAllTags() {
const response = await fetch(
`${CONVERTKIT_API_BASE}/tags?api_secret=${config.apiSecret}`
)
return response.json().tags
}
// Step 2: Check if tag exists, create if needed
async function getOrCreateTag(tagName, existingTags) {
const existing = existingTags.find(t => t.name === tagName)
if (existing) return existing.id
// Create new tag only if needed
const response = await fetch(`${CONVERTKIT_API_BASE}/tags`, {
method: 'POST',
body: JSON.stringify({
api_secret: config.apiSecret,
tag: { name: tagName }
})
})
return response.json().tag.id
}
// Step 3: Subscribe user to tag
await fetch(`${CONVERTKIT_API_BASE}/tags/${tagId}/subscribe`, {
method: 'POST',
body: JSON.stringify({
api_secret: config.apiSecret,
email: subscriberEmail
})
})
Why this approach:
- Only creates tags when needed (99% of subscriptions use existing tags)
- One extra API call (fetch tags) is negligible
- Handles edge cases gracefully
- Scalable—can add new tags dynamically
Alternative approaches we considered:
- Try/create, catch errors - Messy error handling
- Pre-create all tags, hardcode IDs - Not scalable
- Always create, ignore errors - Silent failures
Lesson: Idempotency matters. Design for resources that already exist.
Problem #4: The Email Field Requirement
What happened:
Even though we sent subscriber_id, ConvertKit still returned:
{ "error": "Missing parameter", "message": "Subscriber email is required" }
Why:
The API documentation said subscriber_id OR email, but reality was: email is actually required.
The fix:
export async function addTagsToSubscriber(
subscriberId: number,
tags: string[],
email?: string // Added email parameter
) {
const requestBody = {
api_secret: config.apiSecret,
email: email, // Always include if available
subscriber_id: subscriberId // Backup
}
}
Lesson: API documentation doesn't always match reality. Trust error messages over docs.
The User Experience Flow
Let's walk through what a subscriber actually experiences:
Flow 1: Homepage Hero Subscription
- User lands on homepage
- Sees hero: "Shipping Ideas. Sharing the Journey."
- Enters email + first name
- Clicks "Subscribe"
- Backend:
- Creates subscriber in ConvertKit
- Applies
source:herotag - Returns subscriber ID
- Frontend:
- Shows success message
- Opens profile completion modal
- "You're Subscribed! 🎉 Want to personalize your experience?"
- User can:
- Fill out lastName, company, interests (+ more tags)
- Or click "Skip for Now"
- Done. Subscriber is in the system.
Flow 2: Newsletter Page Subscription
- User navigates to
/newsletter - Sees extended form with all fields
- Fills out email, firstName, lastName, company, interests
- Clicks "Subscribe"
- Backend:
- Creates subscriber
- Applies
source:newsletter,profile-completed,has-company,needs-helptags
- Frontend:
- Shows success message
- No modal (already got all the data)
- Done.
Performance Optimization
After we got everything working, we had console logs everywhere:
🏷️ [ConvertKit] determineTagsToApply called: {...}
✅ [ConvertKit] Added source tag: source:hero
🔍 [ConvertKit] hasProfileData check: {...}
📋 [ConvertKit] Fetching all existing tags...
✅ [ConvertKit] Found 15 existing tags
🔗 [ConvertKit] Subscribing to tag: source:hero ID: 13357731
Great for debugging. Terrible for production.
What we removed:
- All emoji-prefixed debug logs
- Step-by-step progress logs
- Verbose API response logs
What we kept:
- Error logs (
console.error()) for failures - Critical success logs for debugging real issues
Why it matters:
- Logging has performance overhead
- Clutters server logs
- Can leak sensitive data
Production code should be quiet and professional.
The Results
After 4-5 hours of development and debugging:
✅ Fully functional newsletter system
- Simple signup (email + name)
- Extended signup (all 5 fields)
- Profile completion modal
- Automatic tagging
✅ Smart attribution
- Know exactly where subscribers come from
- Track which pages convert best
- Identify high-intent signups
✅ Segmentation ready
- Tag-based email sequences
- Personalized content
- Lead qualification
✅ Production ready
- Clean code
- Error handling
- Performance optimized
Lessons Learned
1. Build Audience Infrastructure First
Don't wait until you have "enough content" or "enough features." Build the newsletter system NOW. Every visitor could be a subscriber.
2. Choose Tools Built for Your Use Case
ConvertKit is built for creators. It shows. The features, the pricing, the API—everything aligns with how solo makers work.
3. Progressive Profiling Works
Don't ask for everything upfront. Get the email, celebrate the subscription, then ask for more. Conversion rates prove it works.
4. Tag Everything
Source tracking, intent signals, engagement markers—tags are your segmentation superpower. Use them aggressively.
5. API Integration Takes Longer Than You Think
What looks like 2 hours on paper becomes 5 hours in reality. Authentication quirks, documentation gaps, edge cases—they all add up.
6. Claude Code Accelerates Development
Writing this from scratch would have taken me 2-3 days. Claude Code handled:
- API integration patterns
- Error handling
- TypeScript types
- React component structure
- Testing scenarios
Total time: 5 hours. That's a 4-5x productivity multiplier.
What's Next?
Now that the infrastructure is in place, I can:
✅ Set up automated sequences
- Welcome series for new subscribers
- Different flows based on tags
- Nurture sequences for
consulting-prospecttags
✅ Build the contact form integration
- Add newsletter opt-in checkbox to contact form
- Tag as
source:contact+consulting-prospect - Build list from high-intent traffic
✅ Create content based on tags
excel-user→ Excel tips and XLNavigator contentbuilder→ Product development insightshas-company→ B2B case studies
✅ Analyze conversion rates
- Which sources convert best?
- Which pages need optimization?
- What content drives subscriptions?
The Bottom Line
If you're building a product, content site, or any kind of digital business: build your newsletter system first.
Not after you have content. Not after you perfect the design. First.
Every day without a newsletter system is a day of lost subscribers. Every visitor who leaves without subscribing is opportunity wasted.
ConvertKit gave me the tools. Claude Code gave me the speed. Now I have a system that:
- Captures every interested visitor
- Tags them intelligently
- Segments for personalization
- Builds an asset I own
In 5 hours, I built something that will compound for years. That's the best investment I could make.
Your move: What are you waiting for? Build the newsletter system. Start growing your audience. Everything else can wait.
Want to see the technical implementation in detail? Check out the GitHub repository or reach out if you're building something similar.