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

Drop-in paywall component

Most publishers do not need to build paywall UI by hand. The sesamy-paywall web component renders a complete paywall from your paywall settings and handles product selection and checkout for you. Use this guide when you need a fully custom implementation.

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
}

The sesamy_token query parameter is an ES256 JWT signed by Sesamy. sesamy-js consumes it automatically, but if you need to validate it on your own backend, see Token Validation.

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.