> ## Documentation Index
> Fetch the complete documentation index at: https://chariow.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# SaaS License Integration

> Complete guide to implementing license-based paywalls in your SaaS application using Chariow API with AI coding tools

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.

<Note>
  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](https://app.chariow.com) first.
</Note>

## 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

<Steps>
  <Step title="Customer Purchases License">
    Customer buys your license product on Chariow. A unique license key is automatically generated (e.g., `ABC-123-XYZ-789`).
  </Step>

  <Step title="Customer Enters License Key">
    In your application, the customer enters their license key in a settings or activation screen.
  </Step>

  <Step title="Your App Validates the Key">
    Your application calls the Chariow API to validate the license key and check its status.
  </Step>

  <Step title="Access Granted or Denied">
    Based on the API response (`is_active`, `is_expired`), your app grants or denies access.
  </Step>

  <Step title="Optional: Activate on Device">
    For device-limited licenses, activate the license to track and limit device usage.
  </Step>
</Steps>

## API Endpoints You'll Need

| Endpoint                      | Method | Purpose                                 |
| ----------------------------- | ------ | --------------------------------------- |
| `/v1/licenses/{key}`          | GET    | Validate a license key and check status |
| `/v1/licenses/{key}/activate` | POST   | Activate license on a device            |

<Info>
  Your API key (`sk_live_...`) must be kept **server-side only**. Never expose it in client-side code.
</Info>

## Architecture Patterns

### Pattern 1: Server-Side Validation (Recommended)

Your backend validates licenses and controls access. Best for web applications.

<Frame>
  <img src="https://mintcdn.com/chariow/H6ouyZI6trlStJ7k/images/license-validation-flow.svg?fit=max&auto=format&n=H6ouyZI6trlStJ7k&q=85&s=9c0bba4afbf73ddbb3137308c2f510bb" alt="Server-side license validation flow" width="800" height="380" data-path="images/license-validation-flow.svg" />
</Frame>

### Pattern 2: Serverless/Edge Validation

Validate licenses at the edge using serverless functions. Good for static sites.

<Frame>
  <img src="https://mintcdn.com/chariow/H6ouyZI6trlStJ7k/images/static-site-flow.svg?fit=max&auto=format&n=H6ouyZI6trlStJ7k&q=85&s=1a4bf98c1412aeb5743121c9367868fb" alt="Serverless license validation flow" width="700" height="160" data-path="images/static-site-flow.svg" />
</Frame>

### Pattern 3: Desktop App with Periodic Validation

Desktop apps validate on startup and periodically. Includes offline grace period.

<Frame>
  <img src="https://mintcdn.com/chariow/H6ouyZI6trlStJ7k/images/desktop-app-flow.svg?fit=max&auto=format&n=H6ouyZI6trlStJ7k&q=85&s=c5db70877e81f619cd881fa9bf45bb44" alt="Desktop app license validation flow with local cache" width="500" height="280" data-path="images/desktop-app-flow.svg" />
</Frame>

## Implementation Guide

### Step 1: Create an API Route for License Validation

Your backend should expose an endpoint that your frontend calls:

```javascript theme={null}
// /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

```jsx theme={null}
// 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

```jsx theme={null}
// 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:

```javascript theme={null}
// /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)

```text theme={null}
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

```text theme={null}
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

```text theme={null}
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

```text theme={null}
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

```text theme={null}
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

```text theme={null}
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

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

### 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

| Scenario                 | Expected Behaviour                               |
| ------------------------ | ------------------------------------------------ |
| Valid, active license    | Access granted                                   |
| Invalid license key      | Show "Invalid license" error                     |
| Expired license          | Show "License expired" message with renewal link |
| Revoked license          | Show "License revoked" - contact support         |
| Network error            | Show cached result or offline warning            |
| Activation limit reached | Show "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:

<CodeGroup>
  ```typescript app/api/license/validate/route.ts theme={null}
  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 }
      );
    }
  }
  ```

  ```typescript hooks/useLicense.ts theme={null}
  'use client';

  import { useState, useEffect, useCallback } from 'react';

  interface LicenseState {
    isLoading: boolean;
    isValid: boolean;
    error: string | null;
    expiresAt: string | null;
  }

  export function useLicense() {
    const [state, setState] = useState<LicenseState>({
      isLoading: true,
      isValid: false,
      error: null,
      expiresAt: null
    });

    const validate = useCallback(async (key: string): Promise<boolean> => {
      setState(s => ({ ...s, isLoading: true, error: null }));

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

        const data = await res.json();

        if (data.valid) {
          localStorage.setItem('license_key', key);
          setState({
            isLoading: false,
            isValid: true,
            error: null,
            expiresAt: data.license?.expiresAt
          });
          return true;
        } else {
          localStorage.removeItem('license_key');
          setState({
            isLoading: false,
            isValid: false,
            error: data.error,
            expiresAt: null
          });
          return false;
        }
      } catch {
        setState(s => ({
          ...s,
          isLoading: false,
          error: 'Validation failed'
        }));
        return false;
      }
    }, []);

    const logout = useCallback(() => {
      localStorage.removeItem('license_key');
      setState({
        isLoading: false,
        isValid: false,
        error: null,
        expiresAt: null
      });
    }, []);

    useEffect(() => {
      const storedKey = localStorage.getItem('license_key');
      if (storedKey) {
        validate(storedKey);
      } else {
        setState(s => ({ ...s, isLoading: false }));
      }
    }, [validate]);

    return { ...state, validate, logout };
  }
  ```

  ```tsx components/LicenseGate.tsx theme={null}
  'use client';

  import { useState } from 'react';
  import { useLicense } from '@/hooks/useLicense';

  export function LicenseGate({ children }: { children: React.ReactNode }) {
    const { isLoading, isValid, error, validate } = useLicense();
    const [keyInput, setKeyInput] = useState('');

    if (isLoading) {
      return (
        <div className="flex items-center justify-center min-h-screen">
          <p>Validating license...</p>
        </div>
      );
    }

    if (isValid) {
      return <>{children}</>;
    }

    return (
      <div className="flex flex-col items-center justify-center min-h-screen p-4">
        <div className="w-full max-w-md space-y-6">
          <div className="text-center">
            <h1 className="text-2xl font-bold">Enter Your License Key</h1>
            <p className="text-gray-600 mt-2">
              Please enter your license key to access the application.
            </p>
          </div>

          <form
            onSubmit={(e) => {
              e.preventDefault();
              validate(keyInput);
            }}
            className="space-y-4"
          >
            <input
              type="text"
              value={keyInput}
              onChange={(e) => setKeyInput(e.target.value)}
              placeholder="ABC-123-XYZ-789"
              className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
            />
            <button
              type="submit"
              className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
            >
              Activate License
            </button>
          </form>

          {error && (
            <p className="text-red-600 text-center">{error}</p>
          )}

          <p className="text-center text-gray-500 text-sm">
            Don't have a license?{' '}
            <a href="https://yourstore.chariow.com" className="text-blue-600 hover:underline">
              Purchase one here
            </a>
          </p>
        </div>
      </div>
    );
  }
  ```
</CodeGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Licenses Guide" icon="key" href="/en/guides/licenses">
    Learn more about license management
  </Card>

  <Card title="Licenses API" icon="code" href="/api-reference/licenses/list-licenses">
    View the complete License API reference
  </Card>

  <Card title="Pulses (Webhooks)" icon="bolt" href="/en/guides/pulses">
    Get notified when licenses are purchased
  </Card>

  <Card title="Best Practices" icon="star" href="/en/guides/best-practices">
    Security and integration best practices
  </Card>
</CardGroup>
