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 / Leaky Paywall

Sesamy provides built-in server-side metered paywall support via tallies. This tracks free article views per user (or IP for anonymous users) and enforces limits without relying on client-side storage.

The simplest approach is to use content.hasAccess() with a <sesamy-article> element that has a paywall-url attribute. The SDK handles the entire flow automatically:

html
<sesamy-article
  item-src="https://example.com/articles/my-article"
  publisher-content-id="article-123"
  paywall-url="https://api.sesamy.com/paywall/paywalls/acme/pw_abc123.json"
  access-level="entitlement"
>
  <h1>Premium Article</h1>
  <p>Article content here...</p>
</sesamy-article>

<script type="module">
  window.addEventListener('sesamyJsReady', async () => {
    // Checks entitlements first, then falls back to leaky paywall
    const access = await window.sesamy.content.hasAccess();

    if (access) {
      console.log('User has access');
    } else {
      console.log('Show paywall');
    }
  });
</script>

Using the API directly

For lower-level control, call the leaky paywall endpoint directly:

typescript
const result = await window.sesamy.paywalls.checkAccess('pw_abc123', {
  publisherContentId: 'article-123',
  url: 'https://example.com/articles/my-article', // optional — enables signed links
});

if (result.status === 'allowed') {
  // User has free reads remaining
  console.log('Remaining:', result.remaining);
  // If url was provided, result.signedLink contains a signed URL
  if (result.signedLink) {
    window.location.href = result.signedLink;
  }
} else {
  // result.status === 'paywall' — free limit reached
  console.log('Paywall config:', result.paywall);
  showPaywall(result.paywall);
}

REST API

bash
# Check leaky paywall access
curl -X POST https://api.sesamy.com/paywalls/{paywallId}/access \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer {token}" \
  -d '{
    "publisherContentId": "article-123",
    "url": "https://example.com/articles/my-article"
  }'

Response (allowed):

json
{
  "status": "allowed",
  "signedLink": "https://example.com/articles/my-article?sesamy_token=eyJ...",
  "remaining": 2
}

Response (blocked):

json
{
  "status": "paywall",
  "remaining": 0,
  "paywall": {
    "id": "pw_abc123",
    "vendorId": "acme",
    "subscriptions": [...]
  }
}

How it works

  1. Each paywall has a configurable free article limit (e.g., 3 articles per month)
  2. Article views are tracked server-side using tallies, keyed by user ID (or IP for anonymous users)
  3. Unique article tracking prevents re-reading the same article from consuming a free read
  4. Tallies expire after a configurable period (e.g., 30 days)
  5. When the limit is reached, the API returns the paywall configuration for display

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.