Skip to content
License Server v1.29.0 · released 30 Oct 2026release notes →
Skip to content

Issuing Licenses ​

Most licenses are issued in the console, one at a time, or by a storefront when a sale completes. This page covers the console first, then the same work through the API, for scripts and bulk issuing.

Prerequisites ​

  • In the console: a hosted account, or the self-hosted bundle's console (see Installation).
  • Through the API: your API address and an admin key: on a hosted account, your account's API key from Settings → API access; self-hosted, ADMIN_API_KEY.
  • A plan for your products and tiers: which Product IDs you'll use, and what each tier allows.

Issue a license in the console ​

(Hosted, or the self-hosted bundle's console.)

  1. Go to Licenses and choose Issue license.
  2. Fill in Product ID, Tier and Issued to. All three are required.
  3. Check the expiry. Expires on defaults to a year from today, at 23:59 Eastern time; change Expires on, At or Time zone if you need to. For a license that never expires, tick Perpetual (never expires) instead, which hides the expiry fields (Console 1.14.2 or later; self-hosted, update to the current bundle).
  4. Set Max activations, the number of machines that can hold the license at once. It defaults to 1.
  5. Optionally, set Free inactive machines, Notes and Limits (optional).
  6. Choose Issue license.

The "License issued" page shows the Key, the Product UUID and the customer's Portal link. The portal link is shown only once, so send it to your customer now, with the key. Using the Console describes every field.

Console fields and API fields ​

Each field on the issue form sets one field of POST /v1/issue-license:

Console fieldAPI field
Product IDproduct_id
Tiertier
Issued toissued_to
Perpetual (never expires)No expires_at: a license that never expires
Expires on, At, Time zoneexpires_at, one ISO 8601 date-time. The console converts your time zone
Max activationsmax_activations
Free inactive machineslease_seconds: the number of days × 86400. Never sends nothing, which means no lease
Notesnotes
Limits (optional), Featureslimits: one key per filled-in field, such as users or api_calls_per_day, and features as an array

Edit terms uses POST /v1/admin/update-license-terms: Perpetual (never expires) sends expires_at: null, and Unlimited seats sends max_activations: null.

Find, revoke and change a license in the console ​

(Hosted, or the self-hosted bundle's console.)

Automating this with the API ​

Everything above can be done through the admin API, which is how you issue licenses in bulk or from your own back-office systems. Call it from a server, never from code that ships to customers: the admin key can issue and revoke every license on your account.

Your API address

The examples use https://your-license-api.com as a stand-in for your License Server's API address. Put yours in its place:

  • Hosted account: https://license-cloud-api.casazium.com. The console's Settings → API access → API base URL shows it with /v1 already on the end, https://license-cloud-api.casazium.com/v1, so that value replaces https://your-license-api.com/v1 as a whole: don't add a second /v1. Not license-cloud.casazium.com: that's the console, and an API call sent there gets a JSON 404 naming the right address. Where an example sends $ADMIN_API_KEY, use your account's API key from the same Settings → API access section instead: a hosted account doesn't accept ADMIN_API_KEY.
  • Self-hosted: your own server's address. The self-hosted bundle answers on http://localhost:3001 on the machine running it, so http://localhost:3001/v1 replaces https://your-license-api.com/v1.

Issuing through the API ​

Issue a Simple License ​

tier, product_id, and issued_to are required. expires_at is optional — omit it (or send null) for a license that never expires. It must be a full ISO 8601 date-time (2027-12-31 alone is rejected with a 400), and a date in the past isn't rejected: the license is issued already expired, and verifying or activating it returns 403 License has expired.

bash
curl -X POST https://your-license-api.com/v1/issue-license \
  -H "Authorization: Bearer $ADMIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tier": "pro",
    "product_id": "your-product",
    "issued_to": "customer@company.com",
    "expires_at": "2027-12-31T23:59:59.000Z"
  }'

Response:

json
{
  "key": "7yChyZcfMG23Dx1sjBoLziPFrH4n-6f28",
  "status": "issued",
  "product_uuid": "b3f1c9a0-1234-4abc-9def-e2d4f5a6b7c8",
  "portal_token": "a1d5163d1e1e4cd7e0e46f577dbeb6f2"
}

product_uuid is this product's real, immutable identity - see Product IDs are per-tenant, not global. This response is the natural place to capture it once and hard-code it in your app - also shown on the console's license detail page afterward, if you need it again.

The server doesn't echo back tier, product_id, issued_to, expires_at, limits, max_activations or lease_seconds — if you need those values afterward, keep them in your own records at issuance time, or fetch them later with the admin-only GET /v1/admin/license/:key. The public export (POST /v1/license-export, or the deprecated GET /v1/export-license/:key) returns only tier, product_id, product_uuid, expires_at and limits (plus key and a signature), not issued_to or max_activations.

portal_token is a real credential, not a value to log or store casually — it's the customer's own key into the end-user license portal, returned once, here. Capture it now if you want to hand it to this customer (in whatever delivery email/receipt you already send); it cannot be retrieved again except via POST /v1/admin/reissue-portal-token, which also invalidates the old one.

Issue a License with Usage Limits ​

Create a license with specific usage constraints:

bash
curl -X POST https://your-license-api.com/v1/issue-license \
  -H "Authorization: Bearer $ADMIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tier": "enterprise",
    "product_id": "your-product", 
    "issued_to": "enterprise@company.com",
    "expires_at": "2027-12-31T23:59:59.000Z",
    "limits": {
      "users": 500,
      "api_calls_per_day": 100000,
      "projects": 50
    },
    "max_activations": 10
  }'

License Parameters ​

Required Fields ​

FieldTypeDescription
tierstringLicense tier. Not enforced against a fixed list server-side — any non-empty string is accepted — but the published API contract's tier values are free, pro, enterprise
product_idstringProduct identifier — a label scoped to your own tenant, not a global claim; see Product IDs are per-tenant, not global
issued_tostringCustomer identifier (email, company name, etc.)

Optional Fields ​

FieldTypeDescriptionDefault
expires_atstring (ISO 8601)Expiration timestamp — omit or send null for a license that never expiresnull (never expires)
limitsobjectUsage limits — see allowed keys below{} (no limits)
max_activationsnumberMaximum concurrent activations1
lease_secondsnumber or null1.14.0+: free the seat of a machine that hasn't checked in for this many seconds (86400 to 31536000, 1 to 365 days) when another machine needs one — see Seats that free themselvesnull (seats are permanent)
notesstringInternal notesnull

License Limits ​

The limits object's keys are validated against a fixed allowlist — anything else is rejected with 400 {"error": "Unknown limit key: <key>"}:

json
{
  "limits": {
    "users": 100,                  // Maximum users
    "seats": 10,                   // Maximum seats
    "admins": 5,                   // Maximum admin users
    "projects": 5,                 // Number of projects
    "environments": 3,             // Number of environments
    "tenants": 1,                  // Number of tenants
    "api_calls_per_day": 10000,    // API calls per day
    "rate_limit_rps": 100,         // Requests per second
    "concurrent_sessions": 50,     // Concurrent sessions
    "features": ["advanced-reporting", "sso"]   // Feature-flag style gate - array of flag names, not a count
  }
}

You don't need to set all of these — include only the keys relevant to your product. There is no storage/bandwidth-related limit key, and no way to add a custom, arbitrary metric name — metric values used with Usage Tracking must be one of the keys above.

Bulk License Issuance ​

Using a Script ​

For issuing multiple licenses, create a script:

javascript
const axios = require('axios');

class LicenseIssuer {
  constructor(apiUrl, adminKey) {
    this.apiUrl = apiUrl;
    this.headers = {
      'Authorization': `Bearer ${adminKey}`,
      'Content-Type': 'application/json'
    };
  }

  async issueLicense(licenseData) {
    try {
      const response = await axios.post(
        `${this.apiUrl}/issue-license`,
        licenseData,
        { headers: this.headers }
      );
      return response.data;
    } catch (error) {
      const wrapped = new Error(`Failed to issue license: ${error.response?.data?.error || error.message}`);
      // Keep the HTTP status (undefined for a network error) and Retry-After
      // so callers like issueLicenseWithRetry() below can act on them.
      wrapped.status = error.response?.status;
      wrapped.retryAfterSeconds = Number(error.response?.headers?.['retry-after']) || undefined;
      throw wrapped;
    }
  }

  async issueBulkLicenses(licenses) {
    const results = [];
    
    for (const licenseData of licenses) {
      try {
        const license = await this.issueLicense(licenseData);
        results.push({ success: true, license, data: licenseData });
        console.log(`✓ Issued license for ${licenseData.issued_to}: ${license.key}`);
      } catch (error) {
        results.push({ success: false, error: error.message, data: licenseData });
        console.error(`✗ Failed to issue license for ${licenseData.issued_to}: ${error.message}`);
      }
    }
    
    return results;
  }
}

// Usage
const issuer = new LicenseIssuer('https://your-license-api.com/v1', process.env.ADMIN_API_KEY);

const licenses = [
  {
    tier: 'free',
    product_id: 'your-product',
    issued_to: 'customer1@company.com',
    expires_at: '2027-12-31T23:59:59.000Z'
  },
  {
    tier: 'pro', 
    product_id: 'your-product',
    issued_to: 'customer2@company.com',
    expires_at: '2027-12-31T23:59:59.000Z',
    limits: { users: 50 }
  }
  // Add more licenses...
];

issuer.issueBulkLicenses(licenses)
  .then(results => {
    const successful = results.filter(r => r.success).length;
    const failed = results.filter(r => !r.success).length;
    console.log(`\nCompleted: ${successful} successful, ${failed} failed`);
  });

CSV Import Script ​

Import licenses from a CSV file:

python
import csv
import requests
import os
from datetime import datetime, timezone

class BulkLicenseIssuer:
    def __init__(self, api_url, admin_key):
        self.api_url = api_url
        self.headers = {
            'Authorization': f'Bearer {admin_key}',
            'Content-Type': 'application/json'
        }
    
    def issue_license(self, license_data):
        response = requests.post(
            f'{self.api_url}/issue-license',
            json=license_data,
            headers=self.headers
        )
        response.raise_for_status()
        return response.json()
    
    def import_from_csv(self, csv_file_path):
        results = []
        
        with open(csv_file_path, 'r') as file:
            reader = csv.DictReader(file)
            
            for row in reader:
                try:
                    # Convert CSV row to license data
                    license_data = {
                        'tier': row['tier'],
                        'product_id': row['product_id'],
                        'issued_to': row['issued_to']
                    }
                    
                    # expires_at is optional -- omit or leave blank in the
                    # CSV for a license that never expires
                    if row.get('expires_at'):
                        license_data['expires_at'] = row['expires_at']

                    if row.get('max_activations'):
                        license_data['max_activations'] = int(row['max_activations'])
                    
                    # Add limits if present -- keys must come from the
                    # server's fixed allowlist (see the License Limits
                    # section above)
                    limits = {}
                    for key in ['users', 'api_calls_per_day', 'projects']:
                        if row.get(key):
                            limits[key] = int(row[key])
                    if limits:
                        license_data['limits'] = limits
                    
                    # Issue the license
                    license = self.issue_license(license_data)
                    results.append({
                        'success': True,
                        'license': license,
                        'row': row
                    })
                    print(f"✓ Issued license for {row['issued_to']}: {license['key']}")
                    
                except Exception as error:
                    results.append({
                        'success': False,
                        'error': str(error),
                        'row': row
                    })
                    print(f"✗ Failed to issue license for {row['issued_to']}: {error}")
        
        return results

# Usage
issuer = BulkLicenseIssuer(
    'https://your-license-api.com/v1',
    os.getenv('ADMIN_API_KEY')
)

# Example CSV format (expires_at may be left blank for a license that never expires):
# tier,product_id,issued_to,expires_at,max_activations,users,api_calls_per_day
# free,my-product,user1@company.com,2027-12-31T23:59:59.000Z,1,10,1000
# pro,my-product,user2@company.com,2027-12-31T23:59:59.000Z,3,50,10000

results = issuer.import_from_csv('licenses.csv')

successful = len([r for r in results if r['success']])
failed = len([r for r in results if not r['success']])
print(f"\nCompleted: {successful} successful, {failed} failed")

Viewing, revoking and reactivating through the API ​

View License Details ​

Fetch a license's full record, with your API key:

bash
curl https://your-license-api.com/v1/admin/license/7yChyZcfMG23Dx1sjBoLziPFrH4n-6f28 \
  -H "Authorization: Bearer $ADMIN_API_KEY"

It returns the license's status, tier, product, issued_to, dates, limits and usage, max_activations, lease_seconds and notes: see GET /v1/admin/license/:key. The public export (POST /v1/license-export, or the deprecated GET /v1/export-license/:key) is a different thing: a smaller, signed shape for offline verification, without issued_to or max_activations.

Revoke a License ​

Temporarily disable a license:

bash
curl -X POST https://your-license-api.com/v1/revoke-license \
  -H "Authorization: Bearer $ADMIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "7yChyZcfMG23Dx1sjBoLziPFrH4n-6f28",
    "revoked": true
  }'

Reactivate a License ​

Re-enable a revoked license. A revoked license that has also passed its expires_at can't be reactivated this way — the server returns 403 {"error": "License has expired"}; extend it with POST /v1/admin/update-license-terms first.

bash
curl -X POST https://your-license-api.com/v1/revoke-license \
  -H "Authorization: Bearer $ADMIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "7yChyZcfMG23Dx1sjBoLziPFrH4n-6f28", 
    "revoked": false
  }'

Best Practices ​

License Key Security ​

  • Never log license keys in plain text
  • Store admin API keys securely
  • Use HTTPS for all license operations
  • Implement audit logging for license operations

License Organization ​

  • Use consistent product_id naming
  • Include meaningful customer identifiers in issued_to
  • Document your licensing tiers and limits
  • Track license issuance in your customer database

Bulk Operations ​

  • Process licenses in batches to respect rate limits
  • Implement retry logic for failed operations
  • Log all operations for audit purposes
  • Validate data before issuing licenses

Monitoring ​

  • Track license issuance rates
  • Monitor for unusual patterns
  • Set up alerts for failed operations
  • Regular audits of active licenses

Troubleshooting ​

Common Issues ​

403 Unauthorized:

bash
# Verify your admin API key
curl -H "Authorization: Bearer $ADMIN_API_KEY" \
     https://your-license-api.com/v1/admin/stats

Rate Limit Exceeded:

  • Implement exponential backoff
  • Batch operations appropriately
  • Consider requesting rate limit increases

Invalid Parameters:

  • Check required fields are present
  • Validate date formats (ISO 8601)
  • Ensure limit values are whole numbers, 0 or more (features is an array of strings)

Error Handling Example ​

Uses the LicenseIssuer from Using a Script above, which keeps the HTTP status on error.status:

javascript
async function issueLicenseWithRetry(licenseData, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const license = await issuer.issueLicense(licenseData);
      return { success: true, license };
    } catch (error) {
      // Retry only a 429 or a 503 (the server answered and issued
      // nothing). Not a network error, a timeout or a 502/504 from a
      // proxy: the license may have been issued with the response lost,
      // and a retry would issue a second one - check list-licenses first.
      // Not other 4xx errors: the request itself is wrong.
      const retryable = error.status === 429 || error.status === 503;
      if (!retryable || attempt === maxRetries) {
        return { success: false, error: error.message };
      }

      // A 429 means waiting for the window to reset: Retry-After seconds,
      // up to 15 minutes. That's fine in a batch job like this one.
      const delay = error.status === 429
        ? (error.retryAfterSeconds ?? 60) * 1000
        : 1000 * Math.pow(2, attempt);
      console.log(`HTTP ${error.status}, retrying in ${delay}ms (${attempt}/${maxRetries})...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

Next Steps ​