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 False2. 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:
Test with Different User States:
- Anonymous users
- Logged-in users without subscription
- Active subscribers
- Expired subscriptions
Test Payment Flow:
- Successful payment
- Canceled payment
- Failed payment
Test Edge Cases:
- Network errors
- API timeouts
- Invalid content IDs
Next Steps
- Subscriptions Guide - Manage subscriptions
- Webhooks Guide - Handle payment events
- API Reference - Explore API endpoints