license-keys

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Dodo Payments License Keys

Dodo Payments 许可证密钥

License keys authorize access to your digital products. Use them for software licensing, per-seat controls, and gating premium features.

许可证密钥用于授权访问您的数字产品。可将其用于软件许可、按席位控制以及高级功能的权限管控。

Overview

概述

License keys are unique tokens that:
  • Authorize access to software, plugins, CLIs
  • Limit activations per user or device
  • Gate downloads, updates, or premium features
  • Can be linked to subscriptions or one-time purchases

许可证密钥是唯一的令牌,可用于:
  • 授权访问软件、插件、CLI
  • 限制每个用户或设备的激活次数
  • 管控下载、更新或高级功能的访问权限
  • 可与订阅或一次性购买关联

Creating License Keys

创建许可证密钥

In Dashboard

在控制台中操作

  1. Go to Dashboard → License Keys
  2. Click "Create License Key"
  3. Configure settings:
    • Expiry Date: Duration or "no expiry" for perpetual
    • Activation Limit: Max concurrent activations (1, 5, unlimited)
    • Activation Instructions: Steps for customers
  4. Save the license key configuration
  1. 进入控制台 → 许可证密钥
  2. 点击“创建许可证密钥”
  3. 配置设置:
    • 到期日期:有效期,或选择“无到期”以设置为永久有效
    • 激活限制:最大并发激活次数(1、5、无限制)
    • 激活说明:给客户的操作步骤
  4. 保存许可证密钥配置

Auto-Generation on Purchase

购买时自动生成

License keys can be automatically generated when a product is purchased:
  1. Configure your product with license key settings
  2. When purchased, a key is generated and emailed to customer
  3. license_key.created
    webhook is fired

许可证密钥可在产品被购买时自动生成:
  1. 为您的产品配置许可证密钥设置
  2. 购买完成后,系统会生成密钥并通过电子邮件发送给客户
  3. 触发
    license_key.created
    webhook

API Reference

API参考

Public Endpoints (No API Key Required)

公开端点(无需API密钥)

These endpoints can be called directly from client applications:
EndpointDescription
POST /licenses/activate
Activate a license key
POST /licenses/deactivate
Deactivate an instance
POST /licenses/validate
Check if key is valid
这些端点可直接从客户端应用调用:
端点描述
POST /licenses/activate
激活许可证密钥
POST /licenses/deactivate
停用实例
POST /licenses/validate
检查密钥是否有效

Authenticated Endpoints (API Key Required)

认证端点(需API密钥)

EndpointDescription
GET /license_keys
List all license keys
GET /license_keys/:id
Get license key details
PATCH /license_keys/:id
Update license key
GET /license_key_instances
List activation instances

端点描述
GET /license_keys
列出所有许可证密钥
GET /license_keys/:id
获取许可证密钥详情
PATCH /license_keys/:id
更新许可证密钥
GET /license_key_instances
列出激活实例

Implementation Examples

实现示例

Activate a License Key

激活许可证密钥

typescript
import DodoPayments from 'dodopayments';

// No API key needed for public endpoints
const client = new DodoPayments();

async function activateLicense(licenseKey: string, deviceName: string) {
  try {
    const response = await client.licenses.activate({
      license_key: licenseKey,
      name: deviceName, // e.g., "John's MacBook Pro"
    });

    return {
      success: true,
      instanceId: response.id,
      message: 'License activated successfully',
    };
  } catch (error: any) {
    return {
      success: false,
      message: error.message || 'Activation failed',
    };
  }
}
typescript
import DodoPayments from 'dodopayments';

// No API key needed for public endpoints
const client = new DodoPayments();

async function activateLicense(licenseKey: string, deviceName: string) {
  try {
    const response = await client.licenses.activate({
      license_key: licenseKey,
      name: deviceName, // e.g., "John's MacBook Pro"
    });

    return {
      success: true,
      instanceId: response.id,
      message: 'License activated successfully',
    };
  } catch (error: any) {
    return {
      success: false,
      message: error.message || 'Activation failed',
    };
  }
}

Validate a License Key

验证许可证密钥

typescript
import DodoPayments from 'dodopayments';

const client = new DodoPayments();

async function validateLicense(licenseKey: string) {
  try {
    const response = await client.licenses.validate({
      license_key: licenseKey,
    });

    return {
      valid: response.valid,
      activations: response.activations_count,
      maxActivations: response.activations_limit,
      expiresAt: response.expires_at,
    };
  } catch (error) {
    return { valid: false };
  }
}
typescript
import DodoPayments from 'dodopayments';

const client = new DodoPayments();

async function validateLicense(licenseKey: string) {
  try {
    const response = await client.licenses.validate({
      license_key: licenseKey,
    });

    return {
      valid: response.valid,
      activations: response.activations_count,
      maxActivations: response.activations_limit,
      expiresAt: response.expires_at,
    };
  } catch (error) {
    return { valid: false };
  }
}

Deactivate a License

停用许可证

typescript
import DodoPayments from 'dodopayments';

const client = new DodoPayments();

async function deactivateLicense(licenseKey: string, instanceId: string) {
  try {
    await client.licenses.deactivate({
      license_key: licenseKey,
      license_key_instance_id: instanceId,
    });

    return { success: true, message: 'License deactivated' };
  } catch (error: any) {
    return { success: false, message: error.message };
  }
}

typescript
import DodoPayments from 'dodopayments';

const client = new DodoPayments();

async function deactivateLicense(licenseKey: string, instanceId: string) {
  try {
    await client.licenses.deactivate({
      license_key: licenseKey,
      license_key_instance_id: instanceId,
    });

    return { success: true, message: 'License deactivated' };
  } catch (error: any) {
    return { success: false, message: error.message };
  }
}

Desktop App Integration

桌面应用集成

Electron App Example

Electron应用示例

typescript
// main/license.ts
import Store from 'electron-store';
import DodoPayments from 'dodopayments';

const store = new Store();
const client = new DodoPayments();

interface LicenseInfo {
  key: string;
  instanceId: string;
  activatedAt: string;
}

export async function activateLicense(licenseKey: string): Promise<boolean> {
  try {
    // Get device identifier
    const deviceName = `${os.hostname()} - ${os.platform()}`;
    
    const response = await client.licenses.activate({
      license_key: licenseKey,
      name: deviceName,
    });

    // Store license info locally
    const licenseInfo: LicenseInfo = {
      key: licenseKey,
      instanceId: response.id,
      activatedAt: new Date().toISOString(),
    };
    
    store.set('license', licenseInfo);
    return true;
  } catch (error) {
    console.error('Activation failed:', error);
    return false;
  }
}

export async function checkLicense(): Promise<boolean> {
  const license = store.get('license') as LicenseInfo | undefined;
  
  if (!license) {
    return false;
  }

  try {
    const response = await client.licenses.validate({
      license_key: license.key,
    });

    return response.valid;
  } catch (error) {
    // If offline, trust local license (with optional grace period)
    const activatedAt = new Date(license.activatedAt);
    const daysSinceActivation = (Date.now() - activatedAt.getTime()) / (1000 * 60 * 60 * 24);
    
    // Allow 30-day offline grace period
    return daysSinceActivation < 30;
  }
}

export async function deactivateLicense(): Promise<boolean> {
  const license = store.get('license') as LicenseInfo | undefined;
  
  if (!license) {
    return true;
  }

  try {
    await client.licenses.deactivate({
      license_key: license.key,
      license_key_instance_id: license.instanceId,
    });

    store.delete('license');
    return true;
  } catch (error) {
    console.error('Deactivation failed:', error);
    return false;
  }
}
typescript
// main/license.ts
import Store from 'electron-store';
import DodoPayments from 'dodopayments';

const store = new Store();
const client = new DodoPayments();

interface LicenseInfo {
  key: string;
  instanceId: string;
  activatedAt: string;
}

export async function activateLicense(licenseKey: string): Promise<boolean> {
  try {
    // Get device identifier
    const deviceName = `${os.hostname()} - ${os.platform()}`;
    
    const response = await client.licenses.activate({
      license_key: licenseKey,
      name: deviceName,
    });

    // Store license info locally
    const licenseInfo: LicenseInfo = {
      key: licenseKey,
      instanceId: response.id,
      activatedAt: new Date().toISOString(),
    };
    
    store.set('license', licenseInfo);
    return true;
  } catch (error) {
    console.error('Activation failed:', error);
    return false;
  }
}

export async function checkLicense(): Promise<boolean> {
  const license = store.get('license') as LicenseInfo | undefined;
  
  if (!license) {
    return false;
  }

  try {
    const response = await client.licenses.validate({
      license_key: license.key,
    });

    return response.valid;
  } catch (error) {
    // If offline, trust local license (with optional grace period)
    const activatedAt = new Date(license.activatedAt);
    const daysSinceActivation = (Date.now() - activatedAt.getTime()) / (1000 * 60 * 60 * 24);
    
    // Allow 30-day offline grace period
    return daysSinceActivation < 30;
  }
}

export async function deactivateLicense(): Promise<boolean> {
  const license = store.get('license') as LicenseInfo | undefined;
  
  if (!license) {
    return true;
  }

  try {
    await client.licenses.deactivate({
      license_key: license.key,
      license_key_instance_id: license.instanceId,
    });

    store.delete('license');
    return true;
  } catch (error) {
    console.error('Deactivation failed:', error);
    return false;
  }
}

React Component for License Input

用于许可证输入的React组件

tsx
// components/LicenseActivation.tsx
import { useState } from 'react';

interface Props {
  onActivated: () => void;
}

export function LicenseActivation({ onActivated }: Props) {
  const [licenseKey, setLicenseKey] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const handleActivate = async () => {
    setLoading(true);
    setError(null);

    try {
      // Call main process (Electron IPC)
      const success = await window.electronAPI.activateLicense(licenseKey);
      
      if (success) {
        onActivated();
      } else {
        setError('Invalid license key. Please check and try again.');
      }
    } catch (err) {
      setError('Activation failed. Please try again.');
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="license-form">
      <h2>Activate Your License</h2>
      <p>Enter your license key to unlock all features.</p>
      
      <input
        type="text"
        value={licenseKey}
        onChange={(e) => setLicenseKey(e.target.value)}
        placeholder="XXXX-XXXX-XXXX-XXXX"
        disabled={loading}
      />
      
      {error && <p className="error">{error}</p>}
      
      <button onClick={handleActivate} disabled={loading || !licenseKey}>
        {loading ? 'Activating...' : 'Activate License'}
      </button>
      
      <a href="https://yoursite.com/purchase" target="_blank">
        Don't have a license? Purchase here
      </a>
    </div>
  );
}

tsx
// components/LicenseActivation.tsx
import { useState } from 'react';

interface Props {
  onActivated: () => void;
}

export function LicenseActivation({ onActivated }: Props) {
  const [licenseKey, setLicenseKey] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const handleActivate = async () => {
    setLoading(true);
    setError(null);

    try {
      // Call main process (Electron IPC)
      const success = await window.electronAPI.activateLicense(licenseKey);
      
      if (success) {
        onActivated();
      } else {
        setError('Invalid license key. Please check and try again.');
      }
    } catch (err) {
      setError('Activation failed. Please try again.');
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="license-form">
      <h2>Activate Your License</h2>
      <p>Enter your license key to unlock all features.</p>
      
      <input
        type="text"
        value={licenseKey}
        onChange={(e) => setLicenseKey(e.target.value)}
        placeholder="XXXX-XXXX-XXXX-XXXX"
        disabled={loading}
      />
      
      {error && <p className="error">{error}</p>}
      
      <button onClick={handleActivate} disabled={loading || !licenseKey}>
        {loading ? 'Activating...' : 'Activate License'}
      </button>
      
      <a href="https://yoursite.com/purchase" target="_blank">
        Don't have a license? Purchase here
      </a>
    </div>
  );
}

CLI Tool Integration

CLI工具集成

Node.js CLI Example

Node.js CLI示例

typescript
// src/license.ts
import Conf from 'conf';
import DodoPayments from 'dodopayments';
import { machineIdSync } from 'node-machine-id';

const config = new Conf({ projectName: 'your-cli' });
const client = new DodoPayments();

export async function activate(licenseKey: string): Promise<void> {
  const machineId = machineIdSync();
  const deviceName = `CLI - ${process.platform} - ${machineId.substring(0, 8)}`;

  try {
    const response = await client.licenses.activate({
      license_key: licenseKey,
      name: deviceName,
    });

    config.set('license', {
      key: licenseKey,
      instanceId: response.id,
      machineId,
    });

    console.log('License activated successfully!');
  } catch (error: any) {
    if (error.status === 400) {
      console.error('Invalid license key.');
    } else if (error.status === 403) {
      console.error('Activation limit reached. Deactivate another device first.');
    } else {
      console.error('Activation failed:', error.message);
    }
    process.exit(1);
  }
}

export async function checkLicense(): Promise<boolean> {
  const license = config.get('license') as any;

  if (!license) {
    return false;
  }

  try {
    const response = await client.licenses.validate({
      license_key: license.key,
    });

    return response.valid;
  } catch {
    return false;
  }
}

export async function deactivate(): Promise<void> {
  const license = config.get('license') as any;

  if (!license) {
    console.log('No active license found.');
    return;
  }

  try {
    await client.licenses.deactivate({
      license_key: license.key,
      license_key_instance_id: license.instanceId,
    });

    config.delete('license');
    console.log('License deactivated.');
  } catch (error: any) {
    console.error('Deactivation failed:', error.message);
  }
}

// Middleware to check license before commands
export function requireLicense() {
  return async () => {
    const valid = await checkLicense();
    if (!valid) {
      console.error('This command requires a valid license.');
      console.error('Run: your-cli activate <license-key>');
      process.exit(1);
    }
  };
}
typescript
// src/license.ts
import Conf from 'conf';
import DodoPayments from 'dodopayments';
import { machineIdSync } from 'node-machine-id';

const config = new Conf({ projectName: 'your-cli' });
const client = new DodoPayments();

export async function activate(licenseKey: string): Promise<void> {
  const machineId = machineIdSync();
  const deviceName = `CLI - ${process.platform} - ${machineId.substring(0, 8)}`;

  try {
    const response = await client.licenses.activate({
      license_key: licenseKey,
      name: deviceName,
    });

    config.set('license', {
      key: licenseKey,
      instanceId: response.id,
      machineId,
    });

    console.log('License activated successfully!');
  } catch (error: any) {
    if (error.status === 400) {
      console.error('Invalid license key.');
    } else if (error.status === 403) {
      console.error('Activation limit reached. Deactivate another device first.');
    } else {
      console.error('Activation failed:', error.message);
    }
    process.exit(1);
  }
}

export async function checkLicense(): Promise<boolean> {
  const license = config.get('license') as any;

  if (!license) {
    return false;
  }

  try {
    const response = await client.licenses.validate({
      license_key: license.key,
    });

    return response.valid;
  } catch {
    return false;
  }
}

export async function deactivate(): Promise<void> {
  const license = config.get('license') as any;

  if (!license) {
    console.log('No active license found.');
    return;
  }

  try {
    await client.licenses.deactivate({
      license_key: license.key,
      license_key_instance_id: license.instanceId,
    });

    config.delete('license');
    console.log('License deactivated.');
  } catch (error: any) {
    console.error('Deactivation failed:', error.message);
  }
}

// Middleware to check license before commands
export function requireLicense() {
  return async () => {
    const valid = await checkLicense();
    if (!valid) {
      console.error('This command requires a valid license.');
      console.error('Run: your-cli activate <license-key>');
      process.exit(1);
    }
  };
}

CLI Commands

CLI命令

typescript
// src/cli.ts
import { Command } from 'commander';
import { activate, deactivate, checkLicense, requireLicense } from './license';

const program = new Command();

program
  .command('activate <license-key>')
  .description('Activate your license')
  .action(activate);

program
  .command('deactivate')
  .description('Deactivate license on this device')
  .action(deactivate);

program
  .command('status')
  .description('Check license status')
  .action(async () => {
    const valid = await checkLicense();
    console.log(valid ? 'License: Active' : 'License: Not activated');
  });

// Protected command example
program
  .command('generate')
  .description('Generate something (requires license)')
  .hook('preAction', requireLicense())
  .action(async () => {
    // Premium feature
  });

program.parse();

typescript
// src/cli.ts
import { Command } from 'commander';
import { activate, deactivate, checkLicense, requireLicense } from './license';

const program = new Command();

program
  .command('activate <license-key>')
  .description('Activate your license')
  .action(activate);

program
  .command('deactivate')
  .description('Deactivate license on this device')
  .action(deactivate);

program
  .command('status')
  .description('Check license status')
  .action(async () => {
    const valid = await checkLicense();
    console.log(valid ? 'License: Active' : 'License: Not activated');
  });

// Protected command example
program
  .command('generate')
  .description('Generate something (requires license)')
  .hook('preAction', requireLicense())
  .action(async () => {
    // Premium feature
  });

program.parse();

Webhook Integration

Webhook集成

Handle License Key Creation

处理许可证密钥创建

typescript
// app/api/webhooks/dodo/route.ts
export async function POST(req: NextRequest) {
  const event = await req.json();

  if (event.type === 'license_key.created') {
    const { id, key, product_id, customer_id, expires_at } = event.data;

    // Store in your database
    await prisma.license.create({
      data: {
        externalId: id,
        key: key,
        productId: product_id,
        customerId: customer_id,
        expiresAt: expires_at ? new Date(expires_at) : null,
        status: 'active',
      },
    });

    // Optional: Send custom email with activation instructions
    await sendLicenseEmail(customer_id, key, product_id);
  }

  return NextResponse.json({ received: true });
}

typescript
// app/api/webhooks/dodo/route.ts
export async function POST(req: NextRequest) {
  const event = await req.json();

  if (event.type === 'license_key.created') {
    const { id, key, product_id, customer_id, expires_at } = event.data;

    // Store in your database
    await prisma.license.create({
      data: {
        externalId: id,
        key: key,
        productId: product_id,
        customerId: customer_id,
        expiresAt: expires_at ? new Date(expires_at) : null,
        status: 'active',
      },
    });

    // Optional: Send custom email with activation instructions
    await sendLicenseEmail(customer_id, key, product_id);
  }

  return NextResponse.json({ received: true });
}

Server-Side Validation

服务器端验证

For sensitive operations, validate server-side with your API key:
typescript
// app/api/validate-license/route.ts
import { NextRequest, NextResponse } from 'next/server';
import DodoPayments from 'dodopayments';

const client = new DodoPayments({
  bearerToken: process.env.DODO_PAYMENTS_API_KEY!,
});

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

  try {
    // Get detailed license info (requires API key)
    const licenses = await client.licenseKeys.list({
      license_key: licenseKey,
    });

    if (licenses.items.length === 0) {
      return NextResponse.json({ valid: false, error: 'License not found' });
    }

    const license = licenses.items[0];

    // Check various conditions
    const valid = 
      license.status === 'active' &&
      (!license.expires_at || new Date(license.expires_at) > new Date());

    return NextResponse.json({
      valid,
      status: license.status,
      activationsUsed: license.activations_count,
      activationsLimit: license.activations_limit,
      expiresAt: license.expires_at,
    });
  } catch (error: any) {
    return NextResponse.json({ valid: false, error: error.message }, { status: 500 });
  }
}

对于敏感操作,请使用您的API密钥在服务器端进行验证:
typescript
// app/api/validate-license/route.ts
import { NextRequest, NextResponse } from 'next/server';
import DodoPayments from 'dodopayments';

const client = new DodoPayments({
  bearerToken: process.env.DODO_PAYMENTS_API_KEY!,
});

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

  try {
    // Get detailed license info (requires API key)
    const licenses = await client.licenseKeys.list({
      license_key: licenseKey,
    });

    if (licenses.items.length === 0) {
      return NextResponse.json({ valid: false, error: 'License not found' });
    }

    const license = licenses.items[0];

    // Check various conditions
    const valid = 
      license.status === 'active' &&
      (!license.expires_at || new Date(license.expires_at) > new Date());

    return NextResponse.json({
      valid,
      status: license.status,
      activationsUsed: license.activations_count,
      activationsLimit: license.activations_limit,
      expiresAt: license.expires_at,
    });
  } catch (error: any) {
    return NextResponse.json({ valid: false, error: error.message }, { status: 500 });
  }
}

Best Practices

最佳实践

1. Keep Limits Clear

1. 明确限制条件

Choose sensible defaults for expiry and activations based on your product type.
根据您的产品类型选择合理的有效期和激活次数默认值。

2. Guide Users

2. 引导用户

Provide precise activation instructions:
  • "Paste the key in Settings → License"
  • "Run:
    mycli activate <key>
    "
  • Include self-serve documentation links
提供清晰的激活说明:
  • "将密钥粘贴到设置 → 许可证中"
  • "运行:
    mycli activate <key>
    "
  • 包含自助文档链接

3. Validate Server-Side

3. 服务器端验证

For critical access control, always validate on your server before granting access.
对于关键的访问控制,在授予权限前务必在服务器端进行验证。

4. Handle Offline Gracefully

4. 优雅处理离线场景

Allow a grace period for offline use in desktop/CLI apps.
在桌面/CLI应用中允许一段离线宽限期。

5. Monitor Events

5. 监控事件

Use webhooks to detect abuse patterns and automate revocations.
使用webhook检测滥用模式并自动执行吊销操作。

6. Provide Easy Deactivation

6. 提供便捷的停用功能

Let users deactivate devices themselves to manage their activation slots.

让用户可以自行停用设备,以管理他们的激活名额。

Common Patterns

常见模式

Feature Gating

功能管控

typescript
async function canAccessFeature(feature: string, licenseKey: string) {
  const { valid } = await validateLicense(licenseKey);
  
  if (!valid) return false;

  // Map features to license tiers
  const featureTiers = {
    'basic-export': ['starter', 'pro', 'enterprise'],
    'advanced-export': ['pro', 'enterprise'],
    'api-access': ['enterprise'],
  };

  const license = await getLicenseDetails(licenseKey);
  return featureTiers[feature]?.includes(license.tier);
}
typescript
async function canAccessFeature(feature: string, licenseKey: string) {
  const { valid } = await validateLicense(licenseKey);
  
  if (!valid) return false;

  // Map features to license tiers
  const featureTiers = {
    'basic-export': ['starter', 'pro', 'enterprise'],
    'advanced-export': ['pro', 'enterprise'],
    'api-access': ['enterprise'],
  };

  const license = await getLicenseDetails(licenseKey);
  return featureTiers[feature]?.includes(license.tier);
}

Subscription-Linked Licenses

关联订阅的许可证

When license is linked to a subscription:
typescript
// Handle subscription.cancelled webhook
if (event.type === 'subscription.cancelled') {
  const { customer_id } = event.data;
  
  // Disable associated license keys
  const licenses = await client.licenseKeys.list({ customer_id });
  
  for (const license of licenses.items) {
    await client.licenseKeys.update(license.id, {
      status: 'disabled',
    });
  }
}

当许可证与订阅关联时:
typescript
// Handle subscription.cancelled webhook
if (event.type === 'subscription.cancelled') {
  const { customer_id } = event.data;
  
  // Disable associated license keys
  const licenses = await client.licenseKeys.list({ customer_id });
  
  for (const license of licenses.items) {
    await client.licenseKeys.update(license.id, {
      status: 'disabled',
    });
  }
}

Resources

资源