Skip to main content
This guide shows you how to use Chariow’s License API to implement a paywall in your SaaS application. Whether you’re building with Lovable, Cursor, Bolt, or any AI coding assistant, this guide provides everything you need including ready-to-use AI prompts.
This guide assumes you have a Chariow store with a License type product created. If you haven’t set one up yet, create your license product first.

What is a License Paywall?

A license paywall restricts access to your SaaS application (or specific features) until the user provides a valid license key. This is ideal for:
  • Desktop applications: Electron apps, native software, CLI tools
  • Web applications: SaaS platforms, admin dashboards, premium tools
  • Mobile apps: iOS/Android applications with premium features
  • API access: Gating API usage based on license validity

How It Works

1

Customer Purchases License

Customer buys your license product on Chariow. A unique license key is automatically generated (e.g., ABC-123-XYZ-789).
2

Customer Enters License Key

In your application, the customer enters their license key in a settings or activation screen.
3

Your App Validates the Key

Your application calls the Chariow API to validate the license key and check its status.
4

Access Granted or Denied

Based on the API response (is_active, is_expired), your app grants or denies access.
5

Optional: Activate on Device

For device-limited licenses, activate the license to track and limit device usage.

API Endpoints You’ll Need

EndpointMethodPurpose
/v1/licenses/{key}GETValidate a license key and check status
/v1/licenses/{key}/activatePOSTActivate license on a device
Your API key (sk_live_...) must be kept server-side only. Never expose it in client-side code.

Architecture Patterns

Your backend validates licenses and controls access. Best for web applications.
Server-side license validation flow

Pattern 2: Serverless/Edge Validation

Validate licenses at the edge using serverless functions. Good for static sites.
Serverless license validation flow

Pattern 3: Desktop App with Periodic Validation

Desktop apps validate on startup and periodically. Includes offline grace period.
Desktop app license validation flow with local cache

Implementation Guide

Step 1: Create an API Route for License Validation

Your backend should expose an endpoint that your frontend calls:
// /api/validate-license.js (Next.js API route)
export default async function handler(req, res) {
  const { licenseKey } = req.body;

  if (!licenseKey) {
    return res.status(400).json({ valid: false, error: 'License key required' });
  }

  try {
    const response = await fetch(
      `https://api.chariow.com/v1/licenses/${encodeURIComponent(licenseKey)}`,
      {
        headers: {
          'Authorization': `Bearer ${process.env.CHARIOW_API_KEY}`
        }
      }
    );

    if (!response.ok) {
      return res.status(200).json({ valid: false, error: 'Invalid license key' });
    }

    const { data } = await response.json();

    // Check license validity
    if (!data.is_active) {
      return res.status(200).json({
        valid: false,
        error: 'License is not active'
      });
    }

    if (data.is_expired) {
      return res.status(200).json({
        valid: false,
        error: 'License has expired'
      });
    }

    // License is valid
    return res.status(200).json({
      valid: true,
      license: {
        status: data.status,
        expiresAt: data.expires_at,
        activationsRemaining: data.activations?.remaining
      }
    });

  } catch (error) {
    console.error('License validation error:', error);
    return res.status(500).json({ valid: false, error: 'Validation failed' });
  }
}

Step 2: Create the License Entry Component

// components/LicenseGate.jsx
import { useState, useEffect } from 'react';

export function LicenseGate({ children }) {
  const [isValidated, setIsValidated] = useState(false);
  const [isLoading, setIsLoading] = useState(true);
  const [licenseKey, setLicenseKey] = useState('');
  const [error, setError] = useState('');

  // Check for stored license on mount
  useEffect(() => {
    const storedKey = localStorage.getItem('license_key');
    if (storedKey) {
      validateLicense(storedKey);
    } else {
      setIsLoading(false);
    }
  }, []);

  const validateLicense = async (key) => {
    setIsLoading(true);
    setError('');

    try {
      const response = await fetch('/api/validate-license', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ licenseKey: key })
      });

      const data = await response.json();

      if (data.valid) {
        localStorage.setItem('license_key', key);
        setIsValidated(true);
      } else {
        setError(data.error || 'Invalid license');
        localStorage.removeItem('license_key');
      }
    } catch (err) {
      setError('Failed to validate license');
    } finally {
      setIsLoading(false);
    }
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    validateLicense(licenseKey);
  };

  if (isLoading) {
    return <div className="license-loading">Validating license...</div>;
  }

  if (isValidated) {
    return children;
  }

  return (
    <div className="license-gate">
      <h2>Enter Your License Key</h2>
      <p>Please enter your license key to access the application.</p>

      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={licenseKey}
          onChange={(e) => setLicenseKey(e.target.value)}
          placeholder="ABC-123-XYZ-789"
        />
        <button type="submit">Activate</button>
      </form>

      {error && <p className="error">{error}</p>}

      <p className="help-text">
        Don't have a license? <a href="https://yourstore.chariow.com">Purchase one here</a>
      </p>
    </div>
  );
}

Step 3: Wrap Your App with the License Gate

// App.jsx
import { LicenseGate } from './components/LicenseGate';
import { Dashboard } from './components/Dashboard';

function App() {
  return (
    <LicenseGate>
      <Dashboard />
    </LicenseGate>
  );
}

Device Activation (Optional)

If your license has limited activations, activate on the device:
// /api/activate-license.js
export default async function handler(req, res) {
  const { licenseKey, deviceId } = req.body;

  const response = await fetch(
    `https://api.chariow.com/v1/licenses/${encodeURIComponent(licenseKey)}/activate`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.CHARIOW_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        device_identifier: deviceId
      })
    }
  );

  const data = await response.json();

  if (!response.ok) {
    return res.status(400).json({
      success: false,
      error: data.message
    });
  }

  return res.status(200).json({
    success: true,
    activationsRemaining: data.data.activations?.remaining
  });
}

AI Prompts for Implementation

Use these prompts with Lovable, Cursor, Bolt, or any AI coding assistant to implement the license paywall quickly.

Prompt 1: Basic License Paywall (React + Next.js)

Create a license paywall system for my Next.js application with these requirements:

1. Create an API route at /api/validate-license that:
   - Accepts POST requests with { licenseKey: string }
   - Calls Chariow API: GET https://api.chariow.com/v1/licenses/{licenseKey}
   - Uses Bearer token authentication with CHARIOW_API_KEY environment variable
   - Returns { valid: true/false, error?: string, license?: object }
   - Check is_active and is_expired fields from the response

2. Create a LicenseGate component that:
   - Shows a license key input form when not validated
   - Stores valid license key in localStorage
   - Checks localStorage on mount to auto-validate returning users
   - Renders children only when license is valid
   - Shows loading state during validation
   - Displays error messages for invalid licenses

3. Style the license form with Tailwind CSS:
   - Centre the form on the page
   - Use clean, professional styling
   - Include a link to purchase a license

Environment variable needed: CHARIOW_API_KEY (store API key)

Prompt 2: License Paywall with Device Activation

Extend the license paywall system with device activation:

1. Generate a unique device identifier:
   - For web apps: combine user agent + screen resolution + timezone
   - Hash the result to create a consistent device ID
   - Store the device ID in localStorage

2. Create /api/activate-license endpoint:
   - Accepts POST with { licenseKey, deviceId }
   - Calls Chariow API: POST https://api.chariow.com/v1/licenses/{key}/activate
   - Body: { device_identifier: deviceId }
   - Handle "Activation limit reached" error gracefully

3. Update LicenseGate component:
   - After validating license, call activate endpoint
   - Show remaining activations count to user
   - Handle activation errors with user-friendly messages

4. Add a "Manage Devices" section showing:
   - Current activation count
   - Maximum activations allowed
   - Option to deactivate current device

Prompt 3: Feature-Based Licensing

Implement feature-gated licensing where different license tiers unlock different features:

1. Create a useLicense hook that:
   - Validates license on app load
   - Returns { isValid, tier, features, expiresAt }
   - Tier is determined by the product name or metadata from Chariow

2. Create a FeatureGate component:
   - Accepts requiredTier prop (e.g., "pro", "enterprise")
   - Shows upgrade prompt if user's tier is insufficient
   - Renders children if tier requirement is met

3. Example usage:
   <FeatureGate requiredTier="pro">
     <AdvancedAnalytics />
   </FeatureGate>

4. Create an upgrade modal that:
   - Shows current vs required tier
   - Links to Chariow checkout for the upgrade product
   - Includes feature comparison

Prompt 4: Offline-Capable License Validation

Add offline support to the license system:

1. Cache license validation result locally:
   - Store validation timestamp
   - Store license expiry date
   - Store full license object

2. Implement validation logic:
   - If online: validate with Chariow API
   - If offline: check cached validation (valid for 7 days)
   - If cache expired: show offline warning but allow limited access

3. Add periodic re-validation:
   - Re-validate every 24 hours when online
   - Update cache on successful validation
   - Handle network errors gracefully

4. Show license status indicator:
   - Green: validated online today
   - Yellow: using cached validation
   - Red: cache expired, needs connection

Prompt 5: Complete Electron App Implementation

Create a license system for an Electron desktop app:

1. Main process license manager:
   - Store license in electron-store (encrypted)
   - Validate on app launch
   - Re-validate every 24 hours
   - Send validation status to renderer via IPC

2. Preload script:
   - Expose safe license APIs to renderer
   - licenseAPI.validate(key)
   - licenseAPI.getStatus()
   - licenseAPI.activate()

3. Renderer license UI:
   - License entry screen on first launch
   - License status in settings
   - "Manage Activations" option

4. Device identifier:
   - Use machine-id package for consistent device ID
   - Pass to Chariow activation endpoint

5. Offline handling:
   - Cache validation for 7 days
   - Show "Offline Mode" indicator
   - Block app after cache expires

Prompt 6: Supabase Integration

Integrate license validation with Supabase authentication:

1. Create a Supabase edge function at /validate-license:
   - Verify user is authenticated
   - Store validated licenses in a 'user_licenses' table
   - Link license to user ID

2. Database schema:
   CREATE TABLE user_licenses (
     id UUID PRIMARY KEY,
     user_id UUID REFERENCES auth.users,
     license_key TEXT NOT NULL,
     validated_at TIMESTAMP,
     expires_at TIMESTAMP,
     status TEXT
   );

3. React hook useUserLicense:
   - Check if current user has valid license
   - Sync license status on login
   - Handle multi-device scenarios

4. Row Level Security:
   - Users can only see their own licenses
   - Enable realtime subscriptions for status updates

Security Best Practices

Never expose your Chariow API key in client-side code. Always validate licenses through your backend.

Do’s

  • Store API keys server-side in environment variables
  • Validate on the backend before granting access
  • Cache validation results to reduce API calls
  • Implement rate limiting on your validation endpoint
  • Use HTTPS for all API communications
  • Log validation attempts for security auditing

Don’ts

  • Don’t trust client-side validation alone - it can be bypassed
  • Don’t store the full license object in accessible localStorage
  • Don’t skip validation for “trusted” users
  • Don’t hardcode license keys in your application
  • Don’t expose detailed error messages that could help attackers

Testing Your Integration

Test Scenarios

ScenarioExpected Behaviour
Valid, active licenseAccess granted
Invalid license keyShow “Invalid license” error
Expired licenseShow “License expired” message with renewal link
Revoked licenseShow “License revoked” - contact support
Network errorShow cached result or offline warning
Activation limit reachedShow “Too many devices” with management options

Test License Keys

During development, create test products in your Chariow store with:
  • Free test licenses for development
  • Short expiry periods to test expiration handling
  • Limited activations to test device limits

Complete Example: Next.js App Router

Here’s a complete implementation using Next.js App Router:
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const { licenseKey } = await request.json();

  if (!licenseKey) {
    return NextResponse.json(
      { valid: false, error: 'License key required' },
      { status: 400 }
    );
  }

  try {
    const response = await fetch(
      `https://api.chariow.com/v1/licenses/${encodeURIComponent(licenseKey)}`,
      {
        headers: {
          'Authorization': `Bearer ${process.env.CHARIOW_API_KEY}`
        }
      }
    );

    if (!response.ok) {
      return NextResponse.json({ valid: false, error: 'Invalid license key' });
    }

    const { data } = await response.json();

    if (!data.is_active) {
      return NextResponse.json({ valid: false, error: 'License is not active' });
    }

    if (data.is_expired) {
      return NextResponse.json({ valid: false, error: 'License has expired' });
    }

    return NextResponse.json({
      valid: true,
      license: {
        status: data.status,
        expiresAt: data.expires_at,
        activationsRemaining: data.activations?.remaining
      }
    });
  } catch (error) {
    return NextResponse.json(
      { valid: false, error: 'Validation failed' },
      { status: 500 }
    );
  }
}

Next Steps