Last updated: Jul 23, 2025, 04:14 PM UTC

Webhook and API Key Implementation Guide

Generated: 2025-07-23 20:30 UTC
Status: Implementation Guide
Verified: Based on deployment documentation

Overview

This guide details how to implement the Stripe webhook handler for automatic API key generation and email delivery when users subscribe to either the Free or Pro tier.

Architecture

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]

Webhook Handler Implementation

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
// Document ID: Stripe Customer ID (cus_xxxxx)
{
  email: "user@example.com",
  stripeCustomerId: "cus_xxxxx",
  subscriptionId: "sub_xxxxx",
  plan: "free" | "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: 50,      // 50 for free, 10000 for pro
    maxFileSize: 5242880,    // 5MB for free, 52428800 for pro (50MB)
    priority: false,         // true for pro
    dashboard: false         // true for pro
  }
}

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://convert-to-markdown.knowcode.tech/xlsx-converter \\
     -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 dashboard access
 Priority processing
 Priority email support
 Advanced API features

Getting Started:
1. View your dashboard: https://convert-to-markdown.knowcode.tech/dashboard
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

1. Create Products in Stripe

# Using Stripe CLI (from deployment scripts)
stripe products create \
  --name="Convert to Markdown API - Free" \
  --description="Free tier with 50 conversions/month" \
  --metadata[plan]="free" \
  --metadata[conversions_limit]="50"

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 Prices

# Free tier - $0/month subscription
stripe prices create \
  --product=prod_free_xxxxx \
  --unit-amount=0 \
  --currency=usd \
  --recurring[interval]=month

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

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

Replace placeholders in docs/pricing.md:

  • YOUR_FREE_TIER_CHECKOUT_LINK → Free tier payment link
  • YOUR_PRO_TIER_CHECKOUT_LINK → Pro tier payment link
  • YOUR_STRIPE_CUSTOMER_PORTAL_LINK → Customer portal URL

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