Backend Quickstart
This page is for building a backend integration against the GeckoGuard API — Discord bots, admin scripts, internal CRMs, anything that creates / lists / revokes licenses on behalf of an organization.
If you only need to validate licenses at runtime in your application, see the Quick Start and use one of the official SDKs instead — that's a different (consumer-side) path.
The credential model
Backend integrations authenticate with an API key that has management scopes. The key is bound to one product, carries a checklist of permissions (license:create, license:revoke, blacklist:write, etc.), and is sent in the X-Api-Key header on every request. No login flow, no refresh tokens, no 2FA.
This replaces the "service-account user with 2FA off" workaround that earlier docs recommended. If you have an existing bot that logs in as a user, migrate to this path — it's simpler, scoped, and won't break when the user rotates their password.
Common pitfalls — read this first. Most "my bot stopped working" reports trace to one of these:
401 UNAUTHORIZED→ noX-Api-Keyheader, the key was revoked, or you're hitting the wrong subdomain (e.g. staging key against prod).403 PERMISSION_DENIED: "API key does not have permission: license:create"→ the key was created without the scope you need. Edit the key in the dashboard and re-check the box, or re-issue.403 FORBIDDEN: "API key is not bound to this product"→ the:productIdin the URL doesn't match the product the key was issued for. One key per product.404 NOT_FOUNDon a license you know exists → the license belongs to a different product than your key. We return 404 (not 403) so a key can't probe for licenses outside its product.- Duplicate licenses after a timeout → you didn't send
Idempotency-Keyon the create. Always send one on writes.
Prerequisites
- An org with at least one product
- An API key issued in the dashboard with the management scopes you need
- The product UUID (visible in the dashboard URL, or returned by
GET /v1/whoami)
Step 1 — Verify the key
export GG=https://api.geckoguard.net
export KEY=gg_live_… # from dashboard → API Keys → Create
curl -s -H "X-Api-Key: $KEY" "$GG/v1/whoami" | jq
{
"ok": true,
"data": {
"apiKeyId": "abc…",
"productId": "f59961e4-63b5-4bd5-a0d3-2944d7843769"
}
}
200 OK confirms the key is valid. Stash the productId — you'll need it for every subsequent call.
export PRODUCT_ID=f59961e4-63b5-4bd5-a0d3-2944d7843769
Step 2 — List existing licenses
curl -s -H "X-Api-Key: $KEY" \
"$GG/v1/products/$PRODUCT_ID/licenses?page=1&pageSize=20" | jq
{
"ok": true,
"data": {
"licenses": [ /* … */ ],
"pagination": { "page": 1, "pageSize": 20, "total": 342, "totalPages": 18 }
}
}
Filter with:
| Query param | Purpose |
|---|---|
?status=ACTIVE|REVOKED|EXPIRED|AVAILABLE | Filter by status |
?key=GG-WDT6-… | Exact-match by printable license key. Returns at most one row. |
?licenseId=<uuid> | Exact-match by license UUID. Returns at most one row. |
?endUserId=<uuid> | Exact-match by owning end user. Returns every license that user owns in this product. |
?search=… | Substring (case-insensitive) match on license key OR hwid. Use this for prefix lookups; use ?key= when you have the full key. |
?page=N&pageSize=N | Pagination. Max pageSize is 1000. |
Exact-match filters take precedence over ?search= if both are sent.
Want the license owner alongside each row? Add the
end_user:readscope to the key. With that scope, every license in the list — andGET …/licenses/:licenseId— gains three flat fields:endUserId,endUserUsername,endUserEmail(eachnullwhen unassigned). Withoutend_user:read, the fields are omitted entirely.
Step 3 — Create a license
curl -s -X POST "$GG/v1/products/$PRODUCT_ID/licenses" \
-H "X-Api-Key: $KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: create-$(date +%s)-$RANDOM" \
-d '{
"expirationMode": "afterActivation",
"expiresAfterDays": 30
}' | jq
The Idempotency-Key header is strongly recommended on every write — without it, a network blip will create duplicate licenses on retry. With it, replays return the original response with Idempotent-Replayed: true. Key format: [A-Za-z0-9_-]{8,128}.
Returns the new license including its key (the string the customer types into your app). The response shape matches the dashboard POST /v1/dashboard/orgs/:orgId/licenses exactly.
Step 4 — Revoke a license
curl -s -X POST "$GG/v1/products/$PRODUCT_ID/licenses/$LICENSE_ID/revoke" \
-H "X-Api-Key: $KEY" \
-H "Idempotency-Key: revoke-$LICENSE_ID-1" | jq
Idempotent — calling twice has no extra effect (and replays return the cached response). The license's status flips to REVOKED and a license.revoked webhook fires.
POST .../unrevoke reverses it. POST .../reset-hwid and POST .../reset-ip clear bindings if the license was sticky-bound.
Step 5 — Look up an end user (license owner)
Two endpoints, both gated by the end_user:read scope.
Search across every product in your org — pick the user, see their full footprint:
curl -s -H "X-Api-Key: $KEY" \
"$GG/v1/end-users?username=alice" | jq
{
"ok": true,
"data": {
"endUsers": [
{
"id": "eu-1",
"username": "alice",
"email": "alice@example.com",
"status": "ACTIVE",
"createdAt": "2026-01-01T00:00:00.000Z",
"licenseCount": 2,
"licenses": [
{ "licenseId": "…", "key": "GG-AAA-…", "status": "ACTIVE", "expiresAt": null,
"product": { "id": "…", "name": "Product A" } },
{ "licenseId": "…", "key": "GG-BBB-…", "status": "REVOKED", "expiresAt": null,
"product": { "id": "…", "name": "Product B" } }
]
}
],
"pagination": { "page": 1, "pageSize": 25, "total": 1, "totalPages": 1 }
}
}
The org is derived from your API key's product — you don't pass an orgId. Filters: ?username= (exact, case-insensitive), ?email= (exact, case-insensitive), ?search= (partial match on either, case-insensitive). ?username=Alice, ?username=ALICE, and ?username=alice all return the same row. Max pageSize is 100.
Or scope the search to just this product — licenseCount and licenses[] exclude every other product the user owns:
curl -s -H "X-Api-Key: $KEY" \
"$GG/v1/products/$PRODUCT_ID/end-users?username=alice" | jq
This variant 404s if the user has no licenses in this product, which makes it the right choice for "is this Discord user a customer of this product?" lookups.
Find every license a known user owns in this product:
curl -s -H "X-Api-Key: $KEY" \
"$GG/v1/products/$PRODUCT_ID/licenses?endUserId=eu-1" | jq
Step 6 — Add a HWID / IP to the blacklist
curl -s -X POST "$GG/v1/products/$PRODUCT_ID/blacklists" \
-H "X-Api-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "HWID",
"value": "abc123…",
"reason": "Caught sharing license"
}' | jq
type is HWID or IP. The value is hashed server-side (so you can list it back, but the raw HWID is not stored). Future authorize calls from that HWID/IP will be denied with reasonCode: HWID_BLACKLISTED / IP_BLACKLISTED.
Where to next
- Authentication — full scope reference and
/v1/whoamiintrospection - API Reference — every endpoint with required scopes
- License Management — bulk operations + the alt JWT path used by the dashboard UI
- Webhooks — react to license events in real time
- API Errors — every error code with retry guidance