DEV Community

Brad
Brad

Posted on • Edited on

Adding a Paywall to Your Flask App in Under 2 Hours (With LemonSqueezy)

I added a $9/month paywall to my Flask app last week. Here's the exact implementation — from zero to paid subscriptions in under 2 hours.

No Stripe. No US entity. No complex OAuth. Just LemonSqueezy + a webhook.

Why LemonSqueezy Over Stripe

If you're building something small and don't have a registered business:

  • Stripe requires a business entity or operates under complicated personal account rules
  • LemonSqueezy is a Merchant of Record — they handle sales tax, compliance, and entity requirements. You just get a payout
  • Setup is 30 minutes, not 3 weeks
  • No KYC headaches for sub-$1k/month volumes

Trade-off: 5% + $0.50 per transaction vs Stripe's 2.9% + $0.30. Worth it to ship today instead of waiting for Stripe Atlas.

The Architecture

User visits app → 
  hits paywall (>20 results) → 
    clicks "Upgrade" → 
      LemonSqueezy checkout → 
        webhook fires → 
          Flask grants access → 
            user sees full results
Enter fullscreen mode Exit fullscreen mode

Implementation

1. LemonSqueezy Setup (15 min)

  1. Create account at lemonsqueezy.com
  2. Add a product → Subscription → $9/month
  3. Go to Settings → Webhooks → add https://yourapp.com/webhook/payment
  4. Note your product ID and webhook signing secret

2. Flask Paywall Gate

from flask import Flask, session, request, redirect
import hmac, hashlib

app = Flask(__name__)
app.secret_key = 'your-secret-key'

# Simulate your data endpoint
@app.route('/search')
def search():
    results = get_all_results()  # your data function

    if not session.get('pro'):
        # Free tier: first 20 results + upgrade CTA
        limited = results[:20]
        return render_template('results.html', 
                               results=limited,
                               show_upgrade=True,
                               upgrade_url='https://your-store.lemonsqueezy.com/checkout/buy/YOUR_PRODUCT_ID')

    # Pro tier: full results
    return render_template('results.html', results=results)
Enter fullscreen mode Exit fullscreen mode

3. Webhook Handler

@app.route('/webhook/payment', methods=['POST'])
def payment_webhook():
    # Verify signature
    signature = request.headers.get('X-Signature')
    secret = 'your-webhook-signing-secret'

    computed = hmac.new(
        secret.encode(),
        request.data,
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(computed, signature or ''):
        return 'Invalid signature', 401

    data = request.json
    event_type = data.get('meta', {}).get('event_name')

    if event_type == 'order_created':
        # Store the customer email as pro
        email = data['data']['attributes']['user_email']
        # In production: store in DB, not session
        # For MVP: use a simple set
        pro_emails.add(email)
        return 'OK'

    return 'OK'
Enter fullscreen mode Exit fullscreen mode

4. Activate Page

After checkout, LemonSqueezy redirects to your success URL. Add a page to activate the session:

@app.route('/activate')
def activate():
    email = request.args.get('email')

    if email in pro_emails:
        session['pro'] = True
        session['email'] = email
        return redirect('/')

    return 'Activation failed. Please contact support.', 400
Enter fullscreen mode Exit fullscreen mode

Set your LemonSqueezy product's "Redirect URL" to https://yourapp.com/activate?email={email}.

The Result

This is running live at HN Startup Hunter:

  • Free: 20 startup contacts from HN hiring threads
  • Pro ($9/month): 200+ contacts, CSV export, email-only filter
  • Checkout: hosted by LemonSqueezy, works out of the box

Total implementation time: ~90 minutes including CSS for the upgrade banner.

What I'd Do Differently

  1. Store pro status in a DB, not a set — the in-memory set resets on app restart. Use SQLite or Redis.
  2. Add email verification on activate — currently trusting the email param; add a signed token.
  3. Handle subscription cancellations — webhook also fires subscription_cancelled. Remove from pro set.

Cost Structure

At $9/month with LemonSqueezy's 5% + $0.50 take:

  • You keep: $9 × 0.95 − $0.50 = $8.05 per subscriber
  • Break-even with Stripe (assuming Stripe Atlas $500): 500/0.58 = ~862 subscribers

For <100 subscribers, LemonSqueezy is cheaper when you account for time to register an entity.


I'm building HN Startup Hunter in public. Free tool to search 6 months of HN hiring threads by tech stack — useful for job seekers and founders sourcing B2B leads. Pro tier is $9/month.

Have questions about the webhook implementation? Drop them in the comments.


🔧 **Found this useful?* I build custom HN lead reports (20–50 companies with verified emails, tech stacks, 24h delivery) → Order done-for-you lead report — $75 | Got a workflow to automate? → 1-Hour Python Automation Audit — $39*

Top comments (0)