Skip to content

Products and Variants

Manage products, variants, and discount codes. All endpoints require adminSecret or a scoped apiKey.


Product Lifecycle

Products have a status field representing their lifecycle:

Status Description Shown in default listProducts
live Active for sale, full management Yes
unlisted Removed from sale, existing licenses still work No
archived Discontinued, read-only No

Draft products

draft products exist only on cglounge and are never synced to the license server until approved.

The active: boolean field is kept for backward compatibility and auto-derives from status: archivedfalse, otherwise true. Setting status on write also updates active.


Create Product

POST /createProduct

Creates a new product. Slugs must be unique across the system.

Request

Field Type Required Default Description
name string yes Human-readable product name
slug string yes URL-safe unique identifier (also the product ID)
creatorId string yes (admin) Owner of the product. API key users inherit their scoped creatorId.
status string no "live" "live", "unlisted", or "archived"

Response

{
  "success": true,
  "productId": "abc123",
  "status": "live"
}

Errors

Code Reason
400 Missing name, slug, or creatorId
403 API key user cannot create products for another creator
409 A product with this slug already exists
curl -X POST https://us-central1-cg-license-server.cloudfunctions.net/createProduct \
  -H "Content-Type: application/json" \
  -d '{
    "adminSecret": "YOUR_SECRET",
    "name": "My Plugin",
    "slug": "my-plugin",
    "creatorId": "creator_01",
    "status": "live"
  }'
const res = await fetch('.../createProduct', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    apiKey: 'YOUR_API_KEY',
    name: 'My Plugin',
    slug: 'my-plugin',
  }),
});
const { productId } = await res.json();

List Products

GET/POST /listProducts

Returns products owned by the authenticated caller. By default, only live products are returned so creator tools can auto-populate a clean list without showing unlisted or archived products.

Request

Field Type Required Default Description
status string no "live" Comma-separated statuses to include (e.g. "live,unlisted")
includeAll boolean no false Return products of any status (overrides status)
creatorId string no Filter by creator (admin only)

Response

{
  "success": true,
  "products": [
    {
      "productId": "abc123",
      "name": "My Plugin",
      "slug": "my-plugin",
      "creatorId": "creator_01",
      "active": true,
      "status": "live",
      "createdAt": "2025-01-15T10:00:00Z",
      "updatedAt": "2025-02-01T12:00:00Z"
    }
  ],
  "count": 1
}

The status field is always present in responses. Legacy products that predate the status feature have it derived from active (true"live", false"archived").

# Default: live only
curl -X POST https://us-central1-cg-license-server.cloudfunctions.net/listProducts \
  -H "Content-Type: application/json" \
  -d '{"apiKey": "YOUR_API_KEY"}'

# Live + unlisted (manage legacy licenses)
curl "https://us-central1-cg-license-server.cloudfunctions.net/listProducts?apiKey=YOUR_KEY&status=live,unlisted"

# Everything (admin debug view)
curl "https://us-central1-cg-license-server.cloudfunctions.net/listProducts?apiKey=YOUR_KEY&includeAll=true"
const res = await fetch('.../listProducts', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    apiKey: 'YOUR_API_KEY',
    status: 'live,unlisted',
  }),
});
const { products } = await res.json();

Update Product

POST /updateProduct

Updates product fields. Use status to change lifecycle state (e.g. unlist when removed from sale, archive when discontinued). creatorId reassignment is admin-only.

Request

Field Type Required Description
productId string yes Product to update
name string no New display name
status string no "live", "unlisted", or "archived" (preferred over active)
active boolean no Legacy flag. Auto-synced from status when that is set.
creatorId string no Reassign owner (admin only)

Prefer status over active

Setting status: "unlisted" or status: "archived" is the preferred way to change a product's lifecycle. The active boolean is kept for backward compatibility and is automatically synced.

# Unlist a product (removed from sale, existing licenses still work)
curl -X POST https://us-central1-cg-license-server.cloudfunctions.net/updateProduct \
  -H "Content-Type: application/json" \
  -d '{
    "adminSecret": "YOUR_SECRET",
    "productId": "abc123",
    "status": "unlisted"
  }'

# Archive (discontinued, read-only)
curl -X POST https://us-central1-cg-license-server.cloudfunctions.net/updateProduct \
  -H "Content-Type: application/json" \
  -d '{
    "adminSecret": "YOUR_SECRET",
    "productId": "abc123",
    "status": "archived"
  }'

Create Variant

POST /createVariant

Creates (or overwrites) a variant on a product. Uses set() semantics: submitting the same name for a product overwrites the existing variant. The variant ID is {productId}-{name}.

Request

Field Type Required Description
productId string yes Parent product
name string yes Variant name. Also forms part of the variant ID.
licenseType string no per-machine, floating, or site. Defaults to per-machine.
maxMachines integer no Max simultaneous machine activations (per-machine)
maxConcurrent integer no Max concurrent sessions (floating)
defaultTrialDays integer no Trial duration when no discount code is used
durationDays integer no License duration in days. Omit for perpetual.
price number no Price in the smallest currency unit (e.g. cents)

Response

{
  "success": true,
  "variantId": "abc123-indie"
}
curl -X POST https://us-central1-cg-license-server.cloudfunctions.net/createVariant \
  -H "Content-Type: application/json" \
  -d '{
    "adminSecret": "YOUR_SECRET",
    "productId": "abc123",
    "name": "indie",
    "licenseType": "per-machine",
    "maxMachines": 2,
    "price": 4900
  }'

Update Variant

POST /updateVariant

Updates mutable fields on an existing variant.

Request

Field Type Required Description
variantId string yes Variant to update
maxMachines integer no New machine limit
maxConcurrent integer no New concurrent session limit
defaultTrialDays integer no New default trial duration
durationDays integer no New license duration in days
price number no New price
active boolean no Enable or disable the variant
curl -X POST https://us-central1-cg-license-server.cloudfunctions.net/updateVariant \
  -H "Content-Type: application/json" \
  -d '{
    "adminSecret": "YOUR_SECRET",
    "variantId": "abc123-indie",
    "price": 5900,
    "active": true
  }'

List Variants

GET/POST /listVariants

Returns variants for a product. Returns only active variants by default.

Request

Field Type Required Description
productId string yes Product to list variants for
includeInactive boolean no Set to true to include inactive variants

Response

{
  "success": true,
  "variants": [
    {
      "variantId": "abc123-indie",
      "productId": "abc123",
      "name": "indie",
      "licenseType": "per-machine",
      "maxMachines": 2,
      "maxConcurrent": null,
      "defaultTrialDays": null,
      "durationDays": null,
      "price": 4900,
      "active": true
    }
  ],
  "count": 1
}
curl -X POST https://us-central1-cg-license-server.cloudfunctions.net/listVariants \
  -H "Content-Type: application/json" \
  -d '{
    "apiKey": "YOUR_API_KEY",
    "productId": "abc123",
    "includeInactive": false
  }'

Variant Configuration Examples

Common tier setups. All examples assume productId: "abc123".

curl -X POST .../createVariant \
  -H "Content-Type: application/json" \
  -d '{
    "adminSecret": "YOUR_SECRET",
    "productId": "abc123",
    "name": "indie",
    "licenseType": "per-machine",
    "maxMachines": 2,
    "price": 4900
  }'
curl -X POST .../createVariant \
  -H "Content-Type: application/json" \
  -d '{
    "adminSecret": "YOUR_SECRET",
    "productId": "abc123",
    "name": "studio",
    "licenseType": "per-machine",
    "maxMachines": 5,
    "price": 14900
  }'
curl -X POST .../createVariant \
  -H "Content-Type: application/json" \
  -d '{
    "adminSecret": "YOUR_SECRET",
    "productId": "abc123",
    "name": "site",
    "licenseType": "site",
    "maxMachines": -1,
    "price": 99900
  }'

Set maxMachines: -1 and licenseType: "site" for unlimited activations.

curl -X POST .../createVariant \
  -H "Content-Type: application/json" \
  -d '{
    "adminSecret": "YOUR_SECRET",
    "productId": "abc123",
    "name": "render-farm",
    "licenseType": "floating",
    "maxMachines": 10,
    "maxConcurrent": 5,
    "price": 49900
  }'

Discount Codes

Create Discount Code

POST /createDiscountCode

Creates a trial or discount code. Codes can be scoped to a specific product.

Request

Field Type Required Description
code string yes The redemption code
trialDays integer yes Trial duration granted on redemption
productId string no Restrict code to a specific product
creatorId string no Owner of the code (admin only)
maxUses integer no Maximum number of redemptions
expiresAt string no ISO 8601 expiry timestamp

Response

{
  "success": true,
  "code": "LAUNCH30"
}
curl -X POST .../createDiscountCode \
  -H "Content-Type: application/json" \
  -d '{
    "adminSecret": "YOUR_SECRET",
    "code": "LAUNCH30",
    "trialDays": 30,
    "productId": "abc123",
    "maxUses": 500,
    "expiresAt": "2025-12-31T23:59:59Z"
  }'

List Discount Codes

GET/POST /listDiscountCodes

Request

Field Type Required Description
creatorId string no Filter by creator
productId string no Filter by product

Response

{
  "success": true,
  "codes": [
    {
      "code": "LAUNCH30",
      "productId": "abc123",
      "creatorId": "creator-uid",
      "trialDays": 30,
      "maxUses": 500,
      "usedCount": 12,
      "active": true,
      "createdAt": "2025-01-01T00:00:00Z",
      "expiresAt": "2025-12-31T23:59:59Z"
    }
  ],
  "count": 1
}
curl -X POST .../listDiscountCodes \
  -H "Content-Type: application/json" \
  -d '{
    "apiKey": "YOUR_API_KEY",
    "productId": "abc123"
  }'

Update Discount Code

POST /updateDiscountCode

Request

Field Type Required Description
code string yes Code to update
active boolean no Enable or disable the code
maxUses integer no New maximum use count
expiresAt string no New expiry timestamp

Response

{"success": true}
curl -X POST .../updateDiscountCode \
  -H "Content-Type: application/json" \
  -d '{
    "adminSecret": "YOUR_SECRET",
    "code": "LAUNCH30",
    "active": false
  }'

Delete Discount Code

DELETE/POST /deleteDiscountCode

Permanently deletes a discount code.

Request

Field Type Required Description
code string yes Code to delete

Response

{"success": true}
curl -X POST .../deleteDiscountCode \
  -H "Content-Type: application/json" \
  -d '{
    "adminSecret": "YOUR_SECRET",
    "code": "LAUNCH30"
  }'

Redeem Trial Code

POST /redeemTrialCode

Redeems a trial code and creates a license for the given email.

Request

Field Type Required Description
code string yes Trial code to redeem
productId string yes Product to create the trial license for
email string yes Recipient email address

Response

{
  "success": true,
  "licenseKey": "XXXX-XXXX-XXXX-XXXX",
  "trialDays": 30,
  "expiresAt": "2025-02-15T00:00:00Z"
}

Errors

Code Error Reason
400 invalid Code has been deactivated
400 expired Code has passed its expiry date
400 max_uses Code has reached its redemption limit
400 not_applicable Code is not valid for this product
404 invalid Code does not exist
409 already_redeemed This email has already redeemed this code
curl -X POST .../redeemTrialCode \
  -H "Content-Type: application/json" \
  -d '{
    "code": "LAUNCH30",
    "productId": "abc123",
    "email": "artist@example.com"
  }'