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: archived → false, 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¶
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 |
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"
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¶
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 |
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
}
Variant Configuration Examples¶
Common tier setups. All examples assume productId: "abc123".
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¶
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
}
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¶
Delete Discount Code¶
DELETE/POST /deleteDiscountCode
Permanently deletes a discount code.
Request¶
| Field | Type | Required | Description |
|---|---|---|---|
code |
string | yes | Code to delete |
Response¶
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 |