License Activation API

Protect your commercial plugins with BeatConnect’s activation system. Generate license keys, control activations per machine, and validate licenses in real-time.

Overview

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│    Customer     │     │   Your Plugin   │     │   BeatConnect   │
│   Purchases     │────▶│   Requests      │────▶│   Validates     │
│   Plugin        │     │   Activation    │     │   License       │
└─────────────────┘     └─────────────────┘     └─────────────────┘
                               │                        │
                               │                        │
                               ▼                        ▼
                        ┌─────────────────┐     ┌─────────────────┐
                        │   Activation    │     │   Database      │
                        │   Confirmed     │◀────│   Updated       │
                        └─────────────────┘     └─────────────────┘

Key Concepts

TermDescription
Activation CodeThe license key given to customers (UUID format)
ActivationA specific machine using the license
Machine IDUnique identifier for a computer
Max ActivationsHow many machines can use one license

Quick Start

1. Enable Activation in Build

{
  "flags": {
    "enableActivationKeys": true
  }
}

2. Generate Test Keys

curl -X POST "https://api.beatconnect.com/functions/v1/plugin-activation/generate" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "project_id": "your-project-id",
    "max_activations": 2,
    "notes": "Development testing"
  }'

Response:

{
  "code": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "key_type": "project_test",
  "max_activations": 2,
  "created_at": "2025-01-15T10:30:00Z"
}

3. Integrate in Your Plugin

See Plugin Integration below.

API Endpoints

Activate License

POST /plugin-activation/activate

Activate a license on a new machine.

curl -X POST "https://api.beatconnect.com/functions/v1/plugin-activation/activate" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "machine_id": "unique-machine-identifier"
  }'

Success Response (200):

{
  "success": true,
  "activations": 1,
  "max_activations": 2,
  "remaining": 1
}

Error Responses:

StatusErrorDescription
400invalid_codeCode format invalid
404code_not_foundCode doesn’t exist
403code_revokedLicense was revoked
403max_activations_reachedNo more slots available

Deactivate License

POST /plugin-activation/deactivate

Free up an activation slot.

curl -X POST "https://api.beatconnect.com/functions/v1/plugin-activation/deactivate" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "machine_id": "unique-machine-identifier"
  }'

Success Response (200):

{
  "success": true,
  "activations": 0,
  "max_activations": 2,
  "message": "Machine deactivated successfully"
}

Validate License

POST /plugin-activation/validate

Check if a license is valid on this machine. Lightweight, designed for frequent calls.

curl -X POST "https://api.beatconnect.com/functions/v1/plugin-activation/validate" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "machine_id": "unique-machine-identifier"
  }'

Success Response (200):

{
  "valid": true
}

Invalid Response (200):

{
  "valid": false,
  "reason": "not_activated"
}

Possible reasons:

  • not_activated — Machine not activated for this code
  • code_revoked — License was revoked
  • code_not_found — Code doesn’t exist
  • deactivated — Previously activated but now deactivated

Check License Status

GET /plugin-activation/status/:code

Public endpoint to check license status (no auth required).

curl "https://api.beatconnect.com/functions/v1/plugin-activation/status/a1b2c3d4-e5f6-7890-abcd-ef1234567890"

Response (200):

{
  "activations": 1,
  "max_activations": 2,
  "available": 1,
  "is_revoked": false
}

Generate Keys (Authenticated)

POST /plugin-activation/generate

Generate test/admin keys for your project.

curl -X POST "https://api.beatconnect.com/functions/v1/plugin-activation/generate" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "project_id": "your-project-id",
    "max_activations": 2,
    "notes": "Beta tester - John"
  }'

Limits:

  • Standard creators: 3 test keys per project
  • Admins: Unlimited

List Project Keys (Authenticated)

GET /plugin-activation/project/:projectId/keys

View all test keys for a project.

curl "https://api.beatconnect.com/functions/v1/plugin-activation/project/YOUR_PROJECT_ID/keys" \
  -H "Authorization: Bearer YOUR_TOKEN"

Response:

{
  "keys": [
    {
      "code": "a1b2c3d4-...",
      "key_type": "project_test",
      "max_activations": 2,
      "current_activations": 1,
      "is_revoked": false,
      "notes": "Beta tester",
      "created_at": "2025-01-15T10:30:00Z"
    }
  ]
}

Plugin Integration

C++ Integration (JUCE)

Machine ID Generation

String generateMachineId()
{
    // Combine hardware identifiers for uniqueness
    String systemId;
 
    #if JUCE_MAC
    // Use Mac's hardware UUID
    systemId = SystemStats::getMachineIdentifiers().joinIntoString("-");
    #elif JUCE_WINDOWS
    // Use Windows machine GUID + volume serial
    systemId = SystemStats::getMachineIdentifiers().joinIntoString("-");
    #endif
 
    // Hash for privacy
    return SHA256(systemId.toUTF8()).toHexString();
}

Activation Manager Class

class ActivationManager
{
public:
    ActivationManager() : machineId(generateMachineId()) {}
 
    // Activate with a license code
    Result activate(const String& code)
    {
        URL url("https://api.beatconnect.com/functions/v1/plugin-activation/activate");
 
        DynamicObject::Ptr json = new DynamicObject();
        json->setProperty("code", code);
        json->setProperty("machine_id", machineId);
 
        auto response = url.withPOSTData(JSON::toString(json))
                           .withExtraHeaders("Content-Type: application/json")
                           .readEntireTextStream();
 
        auto result = JSON::parse(response);
 
        if (result.getProperty("success", false))
        {
            saveActivationCode(code);
            return Result::ok();
        }
 
        return Result::fail(result.getProperty("error", "Activation failed").toString());
    }
 
    // Validate current activation
    bool validate()
    {
        String code = loadActivationCode();
        if (code.isEmpty()) return false;
 
        URL url("https://api.beatconnect.com/functions/v1/plugin-activation/validate");
 
        DynamicObject::Ptr json = new DynamicObject();
        json->setProperty("code", code);
        json->setProperty("machine_id", machineId);
 
        auto response = url.withPOSTData(JSON::toString(json))
                           .withExtraHeaders("Content-Type: application/json")
                           .readEntireTextStream();
 
        auto result = JSON::parse(response);
        return result.getProperty("valid", false);
    }
 
    // Deactivate from this machine
    bool deactivate()
    {
        String code = loadActivationCode();
        if (code.isEmpty()) return false;
 
        URL url("https://api.beatconnect.com/functions/v1/plugin-activation/deactivate");
 
        DynamicObject::Ptr json = new DynamicObject();
        json->setProperty("code", code);
        json->setProperty("machine_id", machineId);
 
        auto response = url.withPOSTData(JSON::toString(json))
                           .withExtraHeaders("Content-Type: application/json")
                           .readEntireTextStream();
 
        auto result = JSON::parse(response);
        if (result.getProperty("success", false))
        {
            clearActivationCode();
            return true;
        }
        return false;
    }
 
private:
    String machineId;
 
    void saveActivationCode(const String& code)
    {
        // Save to user's app data folder
        auto file = File::getSpecialLocation(File::userApplicationDataDirectory)
                        .getChildFile("YourCompany/YourPlugin/license.dat");
        file.create();
        file.replaceWithText(code);
    }
 
    String loadActivationCode()
    {
        auto file = File::getSpecialLocation(File::userApplicationDataDirectory)
                        .getChildFile("YourCompany/YourPlugin/license.dat");
        return file.existsAsFile() ? file.loadFileAsString().trim() : String();
    }
 
    void clearActivationCode()
    {
        auto file = File::getSpecialLocation(File::userApplicationDataDirectory)
                        .getChildFile("YourCompany/YourPlugin/license.dat");
        file.deleteFile();
    }
};

Using in Your Plugin

class MyPluginProcessor : public AudioProcessor
{
public:
    MyPluginProcessor()
    {
        // Check activation on load
        if (!activationManager.validate())
        {
            isActivated = false;
            // Show activation dialog or limit functionality
        }
        else
        {
            isActivated = true;
        }
    }
 
    void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) override
    {
        if (!isActivated)
        {
            // Demo mode: silence after 30 seconds
            if (demoTimer.getMillisecondCounter() > 30000)
            {
                buffer.clear();
                return;
            }
        }
 
        // Normal processing...
    }
 
private:
    ActivationManager activationManager;
    bool isActivated = false;
    Time demoTimer;
};

Offline Activation

For users without internet access:

// Generate offline activation request
String generateOfflineRequest(const String& code)
{
    DynamicObject::Ptr request = new DynamicObject();
    request->setProperty("code", code);
    request->setProperty("machine_id", machineId);
    request->setProperty("timestamp", Time::getCurrentTime().toISO8601(true));
 
    // User sends this to support
    return Base64::toBase64(JSON::toString(request));
}
 
// Apply offline activation response
bool applyOfflineActivation(const String& response)
{
    // Response contains signed activation token
    auto decoded = Base64::fromBase64(response);
    auto json = JSON::parse(decoded);
 
    // Verify signature and save
    if (verifySignature(json))
    {
        saveActivationCode(json.getProperty("code", "").toString());
        return true;
    }
    return false;
}

Best Practices

Do

  1. Cache validation results — Don’t validate on every process block
  2. Handle offline gracefully — Allow temporary offline use
  3. Provide clear error messages — Help users understand activation issues
  4. Allow deactivation — Let users move licenses between machines
  5. Use secure storage — Don’t store codes in plain text

Don’t

  1. Don’t validate too frequently — Once per session or hour is enough
  2. Don’t block the audio thread — Do network calls on a background thread
  3. Don’t be overly restrictive — Reasonable limits keep customers happy
  4. Don’t rely solely on activation — Determined crackers will bypass it

Demo Mode Implementation

enum class LicenseState
{
    NotChecked,
    Activated,
    DemoMode,
    DemoExpired
};
 
class DemoManager
{
public:
    static constexpr int DEMO_DAYS = 14;
    static constexpr int DEMO_SECONDS_PER_SESSION = 30 * 60; // 30 minutes
 
    LicenseState getState()
    {
        if (isActivated()) return LicenseState::Activated;
 
        auto firstRun = getFirstRunDate();
        auto daysSinceInstall = (Time::getCurrentTime() - firstRun).inDays();
 
        if (daysSinceInstall > DEMO_DAYS)
            return LicenseState::DemoExpired;
 
        return LicenseState::DemoMode;
    }
 
    int getDaysRemaining()
    {
        auto firstRun = getFirstRunDate();
        auto daysSinceInstall = (Time::getCurrentTime() - firstRun).inDays();
        return jmax(0, DEMO_DAYS - (int)daysSinceInstall);
    }
};

Purchase Integration

When a customer purchases through BeatConnect:

  1. Order created → Activation code automatically generated
  2. Customer receives → Code in purchase confirmation email
  3. Plugin activation → Customer enters code in your plugin
  4. License tracked → Activations managed in your dashboard

Webhook for Sales

{
  "event": "purchase.completed",
  "data": {
    "order_id": "order_123",
    "customer_email": "customer@example.com",
    "product_id": "your-plugin-id",
    "activation_code": "a1b2c3d4-..."
  }
}

Management Dashboard

In Creator Portal → Your Product → Licenses:

  • View all activation codes
  • See activation count per code
  • Revoke compromised licenses
  • Generate promotional codes
  • Export license data

Troubleshooting

”Activation failed”

Common causes:

  1. Network blocked — Check firewall settings
  2. Invalid code — Verify code format (UUID)
  3. Max activations reached — User needs to deactivate another machine

”Validation failed after working”

Common causes:

  1. License revoked — Check dashboard
  2. Machine ID changed — Major hardware change
  3. API timeout — Network issues

Machine ID Changing

If machine ID changes (hardware upgrade), user can:

  1. Deactivate from old machine ID (if they saved it)
  2. Contact support to reset activations
  3. Use offline activation process

Security Considerations

Rate Limiting

All endpoints are rate-limited:

  • Activate: 10 requests/minute per IP
  • Validate: 60 requests/minute per IP
  • Generate: 10 requests/hour per user

Code Format

Activation codes use UUID v4:

  • 36 characters with hyphens
  • Example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
  • ~5.3 × 10^36 possible combinations

Revocation

Revoked codes:

  • Cannot be activated
  • Existing activations are invalidated
  • Validation returns code_revoked

Next Steps