License Management

Two paths. Most backend integrations should use the API-key path under /v1/products/:productId/... — see the Backend Quickstart and API Reference → Management Endpoints. It's simpler (no login, no refresh) and uses scoped API keys.

This page documents the JWT-based dashboard path (/v1/dashboard/orgs/:orgId/...) which is what the GeckoGuard web UI uses. The two paths share the same business logic — pick whichever fits your client. JWT is the right choice if your backend impersonates a real human user (e.g. an internal admin tool tied to SSO); API keys are the right choice for everything else.

Manage licenses programmatically through the dashboard API. All endpoints below live under /v1/dashboard/* and require a JWT access token in Authorization: Bearer … — API keys are not accepted here. Get an access token from POST /v1/auth/login. See the Authentication guide for details.

30-second curl quickstart

The full integrator path, end-to-end. Set these once:

export GG=https://api.geckoguard.net
export EMAIL=you@example.com
export PASSWORD='your-password'

1. Log in and grab an access token.

ACCESS_TOKEN=$(curl -sX POST "$GG/v1/auth/login" \
  -H 'Content-Type: application/json' \
  -d "{\"email\":\"$EMAIL\",\"password\":\"$PASSWORD\"}" \
  | jq -r '.data.tokens.accessToken')

2. Find your org UUID.

ORG_ID=$(curl -s "$GG/v1/dashboard/orgs" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  | jq -r '.data.orgs[0].id')
echo "$ORG_ID"

3. Find a product UUID.

PRODUCT_ID=$(curl -s "$GG/v1/dashboard/orgs/$ORG_ID/products" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  | jq -r '.data.products[0].id')
echo "$PRODUCT_ID"

4. List existing licenses.

curl -s "$GG/v1/dashboard/orgs/$ORG_ID/licenses?page=1&pageSize=20" \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq

5. Create a license.

curl -sX POST "$GG/v1/dashboard/orgs/$ORG_ID/licenses" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -H "Idempotency-Key: create-$(date +%s)-$RANDOM" \
  -d "{\"productId\":\"$PRODUCT_ID\",\"expirationMode\":\"afterActivation\",\"expiresAfterDays\":30}" \
  | jq

The Idempotency-Key header is optional but strongly recommended on POST — it lets you safely retry a flaky network without creating duplicates.

6. Revoke a license.

LICENSE_ID="<id from step 5>"
curl -sX POST "$GG/v1/dashboard/licenses/$LICENSE_ID/revoke" \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq

7. Add a HWID or IP to the product blacklist.

curl -sX POST "$GG/v1/dashboard/blacklists/products/$PRODUCT_ID" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"type":"HWID","value":"abc123…"}' | jq

Sanity check. If any of the above returns 401 UNAUTHORIZED, your access token is missing, expired, or you're hitting the wrong base URL. Refresh it with POST /v1/auth/refresh. If it returns 403 FORBIDDEN, your user doesn't have access to that org/product — check GET /v1/dashboard/me.


Idempotency on writes

Every POST / PATCH / DELETE endpoint on this page accepts an Idempotency-Key header. Without it, a network timeout that retries can create duplicate licenses. With it, the server caches the first successful response for 24 hours and replays it on retry. Use this on every mutation:

Idempotency-Key: <unique key, [A-Za-z0-9_-]{8,128}>

Replays return the cached response with Idempotent-Replayed: true. Reusing the same key with a different body returns 409 IDEMPOTENCY_KEY_REUSE. See Authentication → Idempotency.

Endpoints where this matters most: license create (single + bulk), bulk freeze/unfreeze/revoke/extend/delete, and blacklist add/bulk-add.

Create Licenses

POST /v1/dashboard/orgs/:orgId/licenses

Create one or more licenses for a product. Supports bulk creation up to 500 at a time. Send an Idempotency-Key header — without it, retrying on a timeout will create duplicates.

Request Body

FieldTypeRequiredDescription
productIdstring (UUID)YesProduct to create licenses for
countnumberNoNumber of licenses to create (1–500, default 1)
keystringNoCustom license key (only when count is 1)
expirationModestringNo"never", "fixed", "afterActivation", or "both"
expiresAtstring (ISO 8601)NoFixed expiration date (when mode is fixed or both)
expiresAfterDaysnumberNoDays after first activation (when mode is afterActivation or both)
policyOverrideobjectNoOverride default product policy for this license
metadataobjectNoCustom key-value metadata

Example: Single License

const res = await fetch(`https://api.geckoguard.net/v1/dashboard/orgs/${orgId}/licenses`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${sessionToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    productId: 'abc-123',
    expirationMode: 'afterActivation',
    expiresAfterDays: 30,
  }),
});

Response (201)

{
  "ok": true,
  "data": {
    "licenses": [
      {
        "id": "uuid-here",
        "key": "GK-A1B2-C3D4-E5F6",
        "productId": "abc-123",
        "status": "ACTIVE",
        "expiresAt": null,
        "expiresAfterDays": 30,
        "activatedAt": null,
        "createdAt": "2025-01-15T10:30:00.000Z"
      }
    ]
  }
}

Example: Bulk Create

body: JSON.stringify({
  productId: 'abc-123',
  count: 100,
  expirationMode: 'fixed',
  expiresAt: '2025-12-31T23:59:59.000Z',
})

List Licenses

GET /v1/dashboard/orgs/:orgId/licenses

Query Parameters

ParamTypeDescription
pagenumberPage number (default 1)
pageSizenumberItems per page (default 50, max 200)
productIdstringFilter by product
statusstringFilter by status: ACTIVE, EXPIRED, REVOKED, FROZEN
searchstringSearch by license key

Response (200)

{
  "ok": true,
  "data": {
    "licenses": [ /* array of license objects */ ],
    "pagination": {
      "page": 1,
      "pageSize": 50,
      "total": 342,
      "totalPages": 7
    }
  }
}

Get License

GET /v1/dashboard/licenses/:id

Returns full license details including bindings, sessions, and metadata.


Update License

PATCH /v1/dashboard/licenses/:id

Request Body

FieldTypeDescription
statusstringChange status (ACTIVE, FROZEN)
expiresAtstringUpdate fixed expiration
expiresAfterDaysnumberUpdate relative expiration
policyOverrideobjectUpdate per-license policy
metadataobjectUpdate custom metadata

Revoke / Restore

POST /v1/dashboard/licenses/:id/revoke
POST /v1/dashboard/licenses/:id/unrevoke

Revoking a license immediately prevents all future authorizations. Restoring sets it back to ACTIVE.


Reset Bindings

POST /v1/dashboard/licenses/:id/reset-hwid
POST /v1/dashboard/licenses/:id/reset-ip

Clears all HWID or IP bindings, allowing the license to bind to new devices/IPs.


Bulk Operations

All bulk endpoints accept an array of license IDs in the request body. Send an Idempotency-Key on every bulk call — bulk freeze/revoke at scale is exactly the operation you don't want to half-apply on a network blip.

EndpointDescription
POST /v1/dashboard/orgs/:orgId/licenses/freezeFreeze multiple licenses
POST /v1/dashboard/orgs/:orgId/licenses/unfreezeUnfreeze multiple licenses
POST /v1/dashboard/orgs/:orgId/licenses/revokeRevoke multiple licenses
POST /v1/dashboard/orgs/:orgId/licenses/unrevokeRestore multiple licenses
POST /v1/dashboard/orgs/:orgId/licenses/extendExtend expiry for multiple licenses
DELETE /v1/dashboard/orgs/:orgId/licensesDelete multiple licenses

Example: Bulk Extend

await fetch(`https://api.geckoguard.net/v1/dashboard/orgs/${orgId}/licenses/extend`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${sessionToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    licenseIds: ['id-1', 'id-2', 'id-3'],
    days: 30,
  }),
});

Export / Import

Export

GET /v1/dashboard/orgs/:orgId/licenses/export?productId=abc&format=csv

Formats: csv, json

Import

POST /v1/dashboard/orgs/:orgId/licenses/import

Accepts a JSON array of license objects with custom keys.


Analytics

GET /v1/dashboard/orgs/:orgId/licenses/analytics

Returns license counts grouped by status and product.

{
  "ok": true,
  "data": {
    "total": 1500,
    "byStatus": {
      "ACTIVE": 1200,
      "EXPIRED": 250,
      "REVOKED": 30,
      "FROZEN": 20
    },
    "byProduct": [
      { "productId": "abc", "productName": "My App", "count": 800 },
      { "productId": "def", "productName": "My Tool", "count": 700 }
    ]
  }
}