Skip to content

Implementing Paywalls

Learn how to implement paywalls to monetize your content with Sesamy.

Overview

Paywalls control access to premium content by requiring users to purchase or subscribe. Sesamy supports multiple paywall types:

  • Hard Paywall: Full content blocked until purchase
  • Metered Paywall: Limited free articles, then paywall
  • Freemium: Basic content free, premium content paid
  • Dynamic: Conditional access based on user behavior

Basic Implementation

1. Check User Access

Before displaying content, verify the user has access:

typescript
import { SesamyClient } from '@sesamy/client';

const client = new SesamyClient({
  apiKey: process.env.SESAMY_API_KEY,
  vendorId: process.env.SESAMY_VENDOR_ID
});

async function checkContentAccess(contentId: string) {
  try {
    const access = await client.checkAccess({ contentId });
    return access.hasAccess;
  } catch (error) {
    console.error('Error checking access:', error);
    return false;
  }
}
python
from sesamy import SesamyClient

client = SesamyClient(
    api_key=os.environ['SESAMY_API_KEY'],
    vendor_id=os.environ['SESAMY_VENDOR_ID']
)

def check_content_access(content_id):
    try:
        access = client.check_access(content_id=content_id)
        return access.has_access
    except Exception as e:
        print(f'Error checking access: {e}')
        return False

2. Show Content or Paywall

Based on access check, show content or paywall:

typescript
async function displayContent(contentId: string) {
  const hasAccess = await checkContentAccess(contentId);
  
  if (hasAccess) {
    // Show full content
    showPremiumContent(contentId);
  } else {
    // Show paywall
    showPaywall(contentId);
  }
}

3. Create Checkout

When user clicks to purchase, create a checkout session:

typescript
async function purchaseContent(productId: string) {
  try {
    const checkout = await client.createCheckout({
      productId,
      successUrl: `${window.location.origin}/success`,
      cancelUrl: window.location.href
    });
    
    // Redirect to checkout
    window.location.href = checkout.url;
  } catch (error) {
    console.error('Error creating checkout:', error);
  }
}

Paywall Types

Hard Paywall

Block all content until user subscribes:

typescript
function HardPaywall({ contentId }: { contentId: string }) {
  const [hasAccess, setHasAccess] = useState(false);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    checkContentAccess(contentId).then(access => {
      setHasAccess(access);
      setLoading(false);
    });
  }, [contentId]);
  
  if (loading) return <Loading />;
  
  if (!hasAccess) {
    return (
      <div className="paywall">
        <h2>Subscribe to Read</h2>
        <p>Get unlimited access to premium content</p>
        <button onClick={() => purchaseContent('prod_monthly')}>
          Subscribe Now
        </button>
      </div>
    );
  }
  
  return <PremiumContent contentId={contentId} />;
}

Metered Paywall

Allow limited free articles before showing paywall:

typescript
async function checkMeteredAccess(contentId: string) {
  // Check if user has viewed free article limit
  const viewCount = getLocalViewCount();
  const freeArticleLimit = 3;
  
  if (viewCount >= freeArticleLimit) {
    // User hit limit, check subscription
    return await checkContentAccess(contentId);
  }
  
  // Still within free limit
  incrementLocalViewCount();
  return true;
}

function MeteredPaywall({ contentId }: { contentId: string }) {
  const [hasAccess, setHasAccess] = useState(false);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    checkMeteredAccess(contentId).then(access => {
      setHasAccess(access);
      setLoading(false);
    });
  }, [contentId]);
  
  if (loading) return <Loading />;
  
  if (!hasAccess) {
    const remaining = 3 - getLocalViewCount();
    return (
      <div className="paywall">
        <h2>You've Reached Your Limit</h2>
        <p>Subscribe to continue reading</p>
        <p>Free articles remaining: {remaining}</p>
        <button onClick={() => purchaseContent('prod_monthly')}>
          Subscribe Now
        </button>
      </div>
    );
  }
  
  return <PremiumContent contentId={contentId} />;
}

Best Practices

1. Optimize User Experience

  • Show preview or teaser of premium content
  • Clear pricing and value proposition
  • Easy subscription process
  • Remember user choice (cookies/local storage)

2. Handle Edge Cases

typescript
async function robustAccessCheck(contentId: string) {
  try {
    const access = await client.checkAccess({ contentId });
    return access.hasAccess;
  } catch (error) {
    if (error instanceof AuthenticationError) {
      // Prompt user to log in
      redirectToLogin();
      return false;
    } else if (error instanceof SesamyError) {
      // Log error and fail open (show content) or closed (show paywall)
      console.error('Access check failed:', error);
      return false; // or true, depending on your policy
    }
    throw error;
  }
}

3. Cache Access Decisions

typescript
const accessCache = new Map<string, { hasAccess: boolean; expires: number }>();

async function cachedAccessCheck(contentId: string) {
  const cached = accessCache.get(contentId);
  const now = Date.now();
  
  if (cached && cached.expires > now) {
    return cached.hasAccess;
  }
  
  const hasAccess = await checkContentAccess(contentId);
  
  // Cache for 5 minutes
  accessCache.set(contentId, {
    hasAccess,
    expires: now + 5 * 60 * 1000
  });
  
  return hasAccess;
}

4. Track Conversions

typescript
async function trackPaywallView(contentId: string) {
  // Track when paywall is shown
  analytics.track('paywall_viewed', {
    content_id: contentId,
    timestamp: new Date().toISOString()
  });
}

async function trackConversion(productId: string) {
  // Track when user subscribes
  analytics.track('subscription_started', {
    product_id: productId,
    timestamp: new Date().toISOString()
  });
}

Complete Example

Here's a complete React example:

typescript
import { useState, useEffect } from 'react';
import { SesamyClient } from '@sesamy/client';

const client = new SesamyClient({
  apiKey: process.env.NEXT_PUBLIC_SESAMY_API_KEY,
  vendorId: process.env.NEXT_PUBLIC_SESAMY_VENDOR_ID
});

interface ArticleProps {
  contentId: string;
  productId: string;
}

export function Article({ contentId, productId }: ArticleProps) {
  const [hasAccess, setHasAccess] = useState(false);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  
  useEffect(() => {
    checkAccess();
  }, [contentId]);
  
  async function checkAccess() {
    try {
      setLoading(true);
      const access = await client.checkAccess({ contentId });
      setHasAccess(access.hasAccess);
    } catch (err) {
      setError('Failed to check access');
      console.error(err);
    } finally {
      setLoading(false);
    }
  }
  
  async function handleSubscribe() {
    try {
      const checkout = await client.createCheckout({
        productId,
        successUrl: `${window.location.origin}/success`,
        cancelUrl: window.location.href
      });
      window.location.href = checkout.url;
    } catch (err) {
      console.error('Failed to create checkout:', err);
    }
  }
  
  if (loading) {
    return <div>Loading...</div>;
  }
  
  if (error) {
    return <div>Error: {error}</div>;
  }
  
  if (!hasAccess) {
    return (
      <div className="paywall">
        <div className="paywall-content">
          <h2>Premium Content</h2>
          <p>Subscribe to access this article and thousands more</p>
          <div className="pricing">
            <div className="price">$9.99/month</div>
            <button onClick={handleSubscribe} className="subscribe-btn">
              Subscribe Now
            </button>
          </div>
          <ul className="features">
            <li>Unlimited articles</li>
            <li>Ad-free experience</li>
            <li>Early access to content</li>
            <li>Cancel anytime</li>
          </ul>
        </div>
      </div>
    );
  }
  
  return (
    <article>
      {/* Premium content here */}
      <h1>Premium Article</h1>
      <p>Full premium content...</p>
    </article>
  );
}

Testing Paywalls

Test your paywall implementation:

  1. Test with Different User States:

    • Anonymous users
    • Logged-in users without subscription
    • Active subscribers
    • Expired subscriptions
  2. Test Payment Flow:

    • Successful payment
    • Canceled payment
    • Failed payment
  3. Test Edge Cases:

    • Network errors
    • API timeouts
    • Invalid content IDs

Next Steps

Released under the MIT License.