Last updated: Jul 25, 2025, 10:08 AM UTC

Webhook and API Key Implementation Guide

Generated: 2025-07-23 20:30 UTC
Status: Implementation Guide
Verified: Based on deployment documentation
Updated: 2025-07-23 - Added hybrid approach details

Overview

This guide details the hybrid implementation for API key generation and email delivery:

  • Free Tier: Direct signup via Cloud Function (no Stripe involvement)
  • Pro Tier: Stripe webhook handler for subscription management

Architecture

Free Tier Flow

graph TD A[User Enters Email] --> B[free-tier-signup Function] B --> C[Validate Email] C --> D[Generate API Key] D --> E[Store in Firestore] D --> F[Send Welcome Email] F --> G[User Receives API Key]

Pro Tier Flow

graph TD A[User Clicks Subscribe] --> B[Stripe Checkout] B --> C[Payment Processed] C --> D[Webhook Event] D --> E[stripe-webhook Function] E --> F[Generate API Key] E --> G[Store in Firestore] E --> H[Send Email] F --> I[User Receives API Key]

Free Tier Implementation

1. Free Tier Signup Function

The free tier signup is handled by a dedicated Cloud Function:

// functions/free-tier-signup/index.js
exports.freeTierSignup = async (req, res) => {
  // CORS handling
  // Email validation
  // Check for existing subscription
  // Generate API key
  // Store in Firestore
  // Send welcome email
  // Return success response
};

Endpoint: https://free-tier-signup-qpg64cvnga-uk.a.run.app

2. Integration with Pricing Page

The pricing page includes an inline form for free tier signup:

<form id="free-tier-signup" onsubmit="return handleFreeSignup(event)">
  <input type="email" name="email" placeholder="Enter your email address" required>
  <button type="submit">Get Your Free API Key →</button>
</form>

Pro Tier Implementation (Stripe Webhook)

1. Function Structure

Create functions/stripe-webhook/index.js:

const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const { Firestore } = require('@google-cloud/firestore');
const crypto = require('crypto');
const { sendWelcomeEmail } = require('../lib/email');

const db = new Firestore();

exports.stripeWebhook = async (req, res) => {
  // Verify webhook signature
  const sig = req.headers['stripe-signature'];
  let event;

  try {
    event = stripe.webhooks.constructEvent(
      req.rawBody,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    console.error('Webhook signature verification failed:', err.message);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Handle the event
  switch (event.type) {
    case 'checkout.session.completed':
      await handleCheckoutComplete(event.data.object);
      break;
    case 'customer.subscription.updated':
      await handleSubscriptionUpdate(event.data.object);
      break;
    case 'customer.subscription.deleted':
      await handleSubscriptionCanceled(event.data.object);
      break;
    default:
      console.log(`Unhandled event type ${event.type}`);
  }

  res.json({ received: true });
};

2. API Key Generation

function generateApiKey() {
  // Format: ctm_live_[32 random characters]
  const randomBytes = crypto.randomBytes(32).toString('hex');
  return `ctm_live_${randomBytes}`;
}

function hashApiKey(apiKey) {
  // Store only hashed version in database
  return crypto.createHash('sha256').update(apiKey).digest('hex');
}

3. Checkout Session Handler

async function handleCheckoutComplete(session) {
  const { customer_email, customer, subscription, metadata } = session;
  
  // Generate API key
  const apiKey = generateApiKey();
  const hashedApiKey = hashApiKey(apiKey);
  
  // Determine plan from metadata or price
  const plan = metadata.plan || 'free'; // Set in Stripe product metadata
  
  // Create user record
  const userDoc = {
    email: customer_email,
    stripeCustomerId: customer,
    subscriptionId: subscription,
    plan: plan,
    status: 'active',
    apiKeyHash: hashedApiKey,
    monthlyUsage: 0,
    createdAt: new Date(),
    updatedAt: new Date()
  };
  
  // Save to Firestore
  await db.collection('subscriptions').doc(customer).set(userDoc);
  
  // Send welcome email with API key
  await sendWelcomeEmail(customer_email, apiKey, plan);
  
  console.log(`New ${plan} subscription created for ${customer_email}`);
}

4. Firestore Schema

// Collection: subscriptions

// Free Tier Document (Document ID: email)
{
  email: "user@example.com",
  stripeCustomerId: null,      // No Stripe for free tier
  subscriptionId: null,        // No subscription ID
  plan: "free",
  status: "active",
  apiKeyHash: "sha256_hash_of_api_key",
  monthlyUsage: 0,
  usageResetDate: "2025-02-01T00:00:00Z",
  createdAt: "2025-01-23T20:30:00Z",
  updatedAt: "2025-01-23T20:30:00Z",
  canceledAt: null,
  features: {
    maxConversions: 50,
    maxFileSize: 5242880,    // 5MB
    priority: false,
    usageApiAccess: false
  }
}

// Pro Tier Document (Document ID: Stripe Customer ID cus_xxxxx)
{
  email: "user@example.com",
  stripeCustomerId: "cus_xxxxx",
  subscriptionId: "sub_xxxxx",
  plan: "pro",
  status: "active" | "canceled" | "past_due",
  apiKeyHash: "sha256_hash_of_api_key",
  monthlyUsage: 0,
  usageResetDate: "2025-02-01T00:00:00Z",
  createdAt: "2025-01-23T20:30:00Z",
  updatedAt: "2025-01-23T20:30:00Z",
  canceledAt: null,
  features: {
    maxConversions: 10000,
    maxFileSize: 52428800,   // 50MB
    priority: true,
    usageApiAccess: true
  }
}

Email Templates

Welcome Email (Free Tier)

async function sendWelcomeEmailFree(email, apiKey) {
  const template = `
Subject: Welcome to Convert To Markdown - Your API Key Inside! 🎉

Hi there!

Welcome to Convert To Markdown! Your free account is now active.

🔑 Your API Key:
${apiKey}

⚠️ Important: Save this key securely - we can't retrieve it for you!

What's included in your free plan:
✅ 50 conversions per month
✅ 5MB max file size
✅ All file formats supported
✅ Basic email support

Getting Started:
1. Add your API key to requests: x-api-key: ${apiKey}
2. Try your first conversion:
   
   curl -X POST https://api.convert-to-markdown.knowcode.tech/v1/convert/excel-to-json \\
     -H "x-api-key: ${apiKey}" \\
     -F "file=@your-file.xlsx"

Need help? Check our docs: https://convert-to-markdown.knowcode.tech/api

Happy converting!
The Convert To Markdown Team
`;
  
  // Send using Gmail API
  await sendEmail(email, template);
}

Welcome Email (Pro Tier)

async function sendWelcomeEmailPro(email, apiKey) {
  const template = `
Subject: Welcome to Convert To Markdown Pro! 🚀

Hi there!

Welcome to Convert To Markdown Pro! Your account is ready for serious document processing.

🔑 Your API Key:
${apiKey}

⚠️ Important: Save this key securely - we can't retrieve it for you!

Your Pro benefits:
🚀 10,000 conversions per month (200x more!)
📁 50MB max file size (10x larger!)
📊 Usage API access
⚡ Priority processing
🎯 Priority email support
📊 Usage API access
🔐 Advanced API features

Getting Started:
1. Access usage data via API: https://convert-to-markdown.knowcode.tech/api#usage-api
2. Add your API key to requests: x-api-key: ${apiKey}
3. Try a large file conversion!

Manage your subscription anytime:
https://billing.stripe.com/p/login/${CUSTOMER_PORTAL_ID}

Questions? Reply to this email for priority support.

Welcome aboard!
The Convert To Markdown Team
`;
  
  await sendEmail(email, template);
}

Stripe Configuration (Pro Tier Only)

1. Create Product in Stripe

# Using Stripe CLI (from deployment scripts)
stripe products create \
  --name="Convert to Markdown API - Pro" \
  --description="Pro tier with 10,000 conversions/month" \
  --metadata[plan]="pro" \
  --metadata[conversions_limit]="10000"

2. Create Price

# Pro tier - $10/month subscription  
stripe prices create \
  --product=prod_pro_xxxxx \
  --unit-amount=1000 \
  --currency=usd \
  --recurring[interval]=month

Note: Free tier does not use Stripe - it's handled directly through the free-tier-signup Cloud Function.

3. Create Checkout Links

In Stripe Dashboard:

  1. Go to Payment Links
  2. Create new payment link
  3. Select the appropriate price
  4. Configure:
    • Collect email: Required
    • Allow promotion codes: Optional
    • After payment: Show success page
    • Metadata: Add plan: free or plan: pro

4. Update Pricing Page

The pricing page has been updated with:

  • Free Tier: Inline email signup form (no Stripe link needed)
  • Pro Tier: Stripe payment link already configured
  • Customer Portal: Link for managing subscriptions

API Key Validation

In your conversion functions, validate API keys:

async function validateApiKey(apiKey) {
  if (!apiKey || !apiKey.startsWith('ctm_live_')) {
    return { valid: false, error: 'Invalid API key format' };
  }
  
  const hashedKey = hashApiKey(apiKey);
  
  // Find subscription by API key hash
  const snapshot = await db.collection('subscriptions')
    .where('apiKeyHash', '==', hashedKey)
    .where('status', '==', 'active')
    .limit(1)
    .get();
  
  if (snapshot.empty) {
    return { valid: false, error: 'Invalid or inactive API key' };
  }
  
  const subscription = snapshot.docs[0].data();
  const docId = snapshot.docs[0].id;
  
  // Check usage limits
  const limits = {
    free: { conversions: 50, fileSize: 5 * 1024 * 1024 },
    pro: { conversions: 10000, fileSize: 50 * 1024 * 1024 }
  };
  
  const planLimits = limits[subscription.plan];
  
  if (subscription.monthlyUsage >= planLimits.conversions) {
    return { valid: false, error: 'Monthly conversion limit reached' };
  }
  
  return {
    valid: true,
    subscription: subscription,
    docId: docId,
    limits: planLimits
  };
}

Monthly Usage Reset

Create a Cloud Scheduler job to reset usage:

// functions/reset-usage/index.js
exports.resetMonthlyUsage = async (req, res) => {
  const batch = db.batch();
  
  const snapshot = await db.collection('subscriptions')
    .where('status', '==', 'active')
    .get();
  
  snapshot.forEach(doc => {
    batch.update(doc.ref, {
      monthlyUsage: 0,
      usageResetDate: new Date()
    });
  });
  
  await batch.commit();
  
  console.log(`Reset usage for ${snapshot.size} active subscriptions`);
  res.json({ reset: snapshot.size });
};

Testing

1. Test Webhook Locally

# Forward Stripe events to local function
stripe listen --forward-to localhost:8080/stripe-webhook

# Trigger test event
stripe trigger checkout.session.completed

2. Test API Key Validation

// Test valid key
const result = await validateApiKey('ctm_live_test123...');
console.log(result); // { valid: true, subscription: {...} }

// Test invalid key
const invalid = await validateApiKey('invalid_key');
console.log(invalid); // { valid: false, error: 'Invalid API key format' }

3. Integration Test

# 1. Create test subscription
# 2. Verify webhook received
# 3. Check Firestore for user record
# 4. Verify email sent
# 5. Test API key works
# 6. Verify usage tracking

Security Considerations

  1. API Key Storage: Never store plain text API keys
  2. Webhook Validation: Always verify Stripe signatures
  3. Rate Limiting: Implement per-IP rate limits for public endpoints
  4. CORS: Configure appropriate CORS policies
  5. Encryption: Use HTTPS for all endpoints

Monitoring

Track these metrics:

  • Webhook success/failure rate
  • API key validation performance
  • Email delivery success
  • Usage patterns by plan
  • Conversion success rates

Next Steps

  1. Replace placeholder URLs in pricing page
  2. Deploy webhook function
  3. Configure Stripe webhook endpoint
  4. Test end-to-end flow
  5. Monitor first real subscriptions

Questions? See the main deployment documentation or contact lindsay@knowcode.tech