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
Pro Tier Flow
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:
- Go to Payment Links
- Create new payment link
- Select the appropriate price
- Configure:
- Collect email: Required
- Allow promotion codes: Optional
- After payment: Show success page
- Metadata: Add
plan: free
orplan: 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
- API Key Storage: Never store plain text API keys
- Webhook Validation: Always verify Stripe signatures
- Rate Limiting: Implement per-IP rate limits for public endpoints
- CORS: Configure appropriate CORS policies
- 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
- Replace placeholder URLs in pricing page
- Deploy webhook function
- Configure Stripe webhook endpoint
- Test end-to-end flow
- Monitor first real subscriptions
Questions? See the main deployment documentation or contact lindsay@knowcode.tech