STAGING — non-production environment. Billing is disabled; data may be wiped at any time.

API Errors

Every error code the API can return, the HTTP status it pairs with, what triggers it, and what to do about it. If you hit a code that isn't on this page, treat it as a bug and file an issue — we'd rather fix the docs than have you guessing.

Response shape

Standard errors:

{
  "ok": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable description",
    "details": [ /* optional: per-field validation issues */ ]
  }
}

License-validation errors (only on POST /v1/licenses/authorize) use a different envelope so a denial isn't confused with a transport failure:

{
  "ok": false,
  "allow": false,
  "reasonCode": "HWID_MISMATCH",
  "message": "Hardware ID does not match bound device"
}

The status code on a denial is 403, but the body's allow: false is the field you should branch on. See the reason codes below.

How to read this page

  • Code — the literal string in error.code. Match on this, not on the message.
  • Status — the HTTP status that comes with it.
  • Triggered by — what your client did wrong (or what the server is telling you about its own state).
  • Fix — the smallest action that gets you past it.
  • Retry?no means retrying the same request will get the same error; fix the request first. yes means transient; back off and try again. clock means retry after a clock fix or fresh nonce.

Authentication & authorization

CodeStatusTriggered byFixRetry?
UNAUTHORIZED401Missing credential, malformed Authorization header, signature mismatch, or a token that doesn't correspond to a real user. On /v1/dashboard/* this is almost always "I sent an API key on a JWT-only endpoint" or "token never existed / was forged."Check the auth matrix. Use Authorization: Bearer <accessToken> from /v1/auth/login for dashboard routes; use X-Api-Key for /v1/licenses/authorize and /v1/whoami.no — re-login
EXPIRED_TOKEN401The JWT was valid but past its exp claim. Distinct from UNAUTHORIZED so you can branch precisely.Call POST /v1/auth/refresh with your refresh token to get a fresh access token, then retry the original request once. If refresh itself returns 401 UNAUTHORIZED, the refresh token is gone — re-login.yes — refresh once, then retry the original call
FORBIDDEN403Valid credential but you don't have access to the target resource (wrong product, wrong org, insufficient role).Verify the resource's orgId/productId matches your token. For org operations, check your role with GET /v1/dashboard/me.no
API_KEY_NO_PERMISSIONS403The API key was created without any permissions (legacy state).Re-issue the key from the dashboard with at least one permission (e.g. license:authorize).no
PERMISSION_DENIED403The API key lacks the specific scope required for this endpoint. The error message names the missing scope (e.g. "API key does not have permission: license:create").Edit the key in the dashboard (API Keys → [key] → Edit Permissions) and check the missing scope, then retry. No need to re-issue unless you've also rotated the secret.no
INSUFFICIENT_SCOPE403Same idea as PERMISSION_DENIED but for JWT scopes.Use a token issued for a user with the required role, or re-auth.no
APIKEY_NOT_ALLOWED403You used an API key on an endpoint that's user-only (e.g. POST /v1/products outside bootstrap).Use a JWT access token from /v1/auth/login.no
CSRF_BLOCKED403Browser-style request without a valid Authorization header, missing Sec-Fetch-Site, or Origin/Referer not on the allow-list.Server-to-server callers: send Authorization: Bearer <jwt> and the check is bypassed.no

Request signing (license validation)

CodeStatusTriggered byFixRetry?
SIGNATURE_REQUIRED401One or more of X-GG-Timestamp / X-GG-Nonce / X-GG-Signature is missing on a /v1/licenses/authorize call.Sign the request — see the signing recipe. Or use an official SDK.no
INVALID_SIGNATURE401Bad timestamp format, bad nonce length (must be 16–64 chars), or HMAC didn't match what the server computed.Recompute the canonical string exactly: ${METHOD}\n${PATH}\n${TS}\n${NONCE}\n${BODY_SHA256_HEX}. Make sure path has no query string.no
SIGNATURE_EXPIRED401X-GG-Timestamp is more than ±300s from server time.Sync your client clock (NTP). Don't cache signatures.clock — re-sign with current timestamp + fresh nonce
NONCE_REUSED401The nonce was already seen within the last 10 minutes.Generate a fresh random nonce per request — never reuse.clock — re-sign with a fresh nonce
SIGNING_NOT_CONFIGURED500Server-side: no signing secret on the deployment.Contact support.no
SIGNING_UNAVAILABLE503Server-side: nonce store (Redis) is unreachable. The server fails closed when signing is required.Retry with backoff. If persistent, contact support.yes

Validation

All validation errors are no for retry — the server's verdict won't change unless your request changes.

CodeStatusTriggered byFixRetry?
VALIDATION_ERROR400Request body or query failed schema validation. error.details lists each {field, message} pair.Fix the offending fields.no
BAD_REQUEST400Generic semantic error (e.g. count out of range, ill-formed file path).Read error.message.no
NULL_CONSTRAINT400Required DB column missing.Same as VALIDATION_ERROR — fill the field.no
BAD_IDEMPOTENCY_KEY400Idempotency-Key doesn't match [A-Za-z0-9_-]{8,128}.Use a longer / cleaner key.no
NO_FILE400File-upload endpoint called without a multipart/form-data file.Send the file.no
NO_ACTIVE_ORG400An endpoint that requires an active org context was called without one.Set the active org via POST /v1/dashboard/active-org.no
MISSING_HOSTNAME400Storefront resolver called without a hostname query param.Add ?hostname=<your-storefront-domain>.no
ENDPOINT_DISABLED400Webhook replay called against a disabled endpoint.Re-enable the endpoint, then retry.no

Resource state

CodeStatusTriggered byFixRetry?
NOT_FOUND404The requested resource doesn't exist (or you can't see it).Verify the ID. For :orgId/:productId/:licenseId, all are UUIDs — slugs aren't accepted.no
USER_NOT_FOUND404User lookup returned nothing (only on user-mutation endpoints).Verify the user ID; the user may have been deleted.no
PRODUCT_NOT_FOUND404Product not found, or not in your org.Verify the product UUID and that it's assigned to your active org.no
LICENSE_NOT_FOUND404License lookup returned nothing.Verify the license key or ID.no
LISTING_NOT_FOUND404Storefront listing not found.no
STOREFRONT_NOT_FOUND404Storefront not configured for this hostname.no
ORG_NOT_FOUND404Org doesn't exist for the given hostname / ID.no
CONFLICT409Generic conflict — duplicate, illegal state transition, would-be orphan.Read the message. Common cases: assigning a product already attached to another org, demoting/removing the last OWNER, accepting an already-accepted invite.no
IDEMPOTENCY_KEY_REUSE409Same Idempotency-Key was used with a different request body.Use a fresh key for a new request, or send the original body for a replay.no — change the key first
CLOSED409Tried to act on a closed support ticket.no
LICENSE_NOT_OWNED409End-user tried to act on a license that isn't theirs.no
GONE410Invite expired.Ask the inviter to send a fresh invite.no
STOREFRONT_NOT_SETUP400Tried to add listings before completing storefront setup.Finish storefront onboarding first.no
STRIPE_NOT_READY400Storefront isn't connected to a payments processor yet.Complete Stripe Connect onboarding.no
TARGET_LICENSE_REQUIRED400Listing requires a target license ID.Provide it in the body.no

Rate limiting

CodeStatusTriggered byFixRetry?
RATE_LIMITED429Per-IP, per-key, per-license, login, signup, password-reset, or 2FA limit hit.Honor the Retry-After header (always set on 429s — see below). Implement exponential backoff on top. Check the X-RateLimit-Remaining-* headers to learn which budget you're exhausting.yes — after Retry-After
PRODUCT_RATE_LIMITED429Per-product ceiling hit (rare; means cumulative traffic against one product is too high).Honor Retry-After. Check whether another integration is hammering the same product. Contact support if you need a higher product ceiling.yes — after Retry-After
RATE_LIMIT_UNAVAILABLE503Rate-limit store down. Server fails closed when SDK signing is required.Retry with backoff.yes

End-user portal

CodeStatusTriggered byFixRetry?
USER_BANNED403The end-user account has been banned by the merchant.Contact the merchant.no
LICENSE_INACTIVE403Tried to download a file or use a feature against a non-active license.Re-activate or check expiration.no
LIMIT_REACHED403HWID-reset budget exhausted on a self-service reset.Wait for the budget to reset, or ask the merchant to reset manually.no

DNS & domains

CodeStatusTriggered byFixRetry?
DNS_NOT_FOUND400Domain verification couldn't find the expected DNS record.Add the record exactly as shown in the dashboard, then retry verification. DNS can take up to 24h to propagate.yes — after DNS propagates
TOKEN_MISMATCH400DNS record exists but contains the wrong verification token.Replace the record with the current token from the dashboard.yes — after fixing the record

Server / infrastructure

CodeStatusTriggered byFixRetry?
INTERNAL500Unhandled exception.Retry with backoff. If persistent, file an issue with the x-request-id header.yes
CONFIG_ERROR500Server is misconfigured (e.g. missing env var).Contact support.no
DATABASE_ERROR500Database state needs migration or is otherwise inconsistent.Contact support.no
WEBHOOK_ERROR500Outbound webhook delivery failed.Check your endpoint's logs; we retry per the webhook delivery policy.n/a — server-side retry
STAGING_BILLING_DISABLED503Billing endpoints are off in staging.Use the production environment, or your local dev stack.no

License authorize reason codes

These appear as reasonCode on POST /v1/licenses/authorize when allow: false. The response status is 403. Branch on reasonCode, not status, since the same status can mean a network or signing failure.

Reason codeMeaning
LICENSE_NOT_FOUNDLicense key doesn't exist for this product
PRODUCT_MISMATCHLicense exists but belongs to a different product
LICENSE_REVOKEDLicense is REVOKED or FROZEN
LICENSE_EXPIREDPast the fixed expiry date
LICENSE_EXPIRED_RELATIVEPast the days-after-activation window
HWID_MISMATCHSticky-mode: hardware ID doesn't match the bound device
HWID_LIMIT_EXCEEDEDLimit-mode: too many distinct hardware IDs
IP_MISMATCHSticky-mode: IP doesn't match the bound address
IP_LIMIT_EXCEEDEDLimit-mode: too many distinct IPs in the time window
IP_REGION_MISMATCHRegion-locked: client IP is outside the allowed region(s)
CONCURRENCY_LIMIT_EXCEEDEDMore active sessions than the policy allows
HWID_BLACKLISTEDThe hardware ID is on the product's blacklist
IP_BLACKLISTEDThe IP address is on the product's blacklist
const result = await response.json();

if (result.allow) {
  return { valid: true, expiresAt: result.effectiveExpiresAt };
}

switch (result.reasonCode) {
  case 'LICENSE_NOT_FOUND':
  case 'PRODUCT_MISMATCH':
    return { valid: false, message: 'Invalid license key' };
  case 'LICENSE_EXPIRED':
  case 'LICENSE_EXPIRED_RELATIVE':
    return { valid: false, message: 'License has expired' };
  case 'LICENSE_REVOKED':
    return { valid: false, message: 'License has been revoked' };
  case 'HWID_MISMATCH':
  case 'HWID_LIMIT_EXCEEDED':
    return { valid: false, message: 'License is bound to a different device' };
  case 'HWID_BLACKLISTED':
  case 'IP_BLACKLISTED':
    return { valid: false, message: 'Access denied' };
  case 'CONCURRENCY_LIMIT_EXCEEDED':
    return { valid: false, message: 'Too many active sessions. Close other instances first.' };
  default:
    return { valid: false, message: result.message || 'Validation failed' };
}

Retry guidance

Retry with exponential backoff (start at 1s, double each attempt, jitter, cap at 60s, max 5 attempts):

  • 429 RATE_LIMITED / 429 PRODUCT_RATE_LIMITED — honor Retry-After
  • 500 INTERNAL — transient
  • 503 SIGNING_UNAVAILABLE / 503 RATE_LIMIT_UNAVAILABLE — Redis backing-store hiccup
  • 502 / 504 — gateway transient

Do not retry these — fix the request instead:

  • 400 (any) — your payload is wrong
  • 401 (any) — your credential is wrong
  • 403 FORBIDDEN, 403 PERMISSION_DENIED — you don't have access; retrying won't help
  • 404 — the resource doesn't exist
  • 409 — resolve the conflict (or use a fresh idempotency key)
  • 410 GONE — invite expired; ask for a new one

Diagnostic header

Every response includes x-request-id. Capture and log it — when you contact support, that one string lets us pull the exact request out of our logs.

Retry-After is guaranteed on 429

Every 429 response from GeckoGuard sets a Retry-After header (in seconds). You do not need a fallback for the missing-header case — it's always present, on every limiter (per-IP, per-key, per-license, login, signup, forgot-password, TOTP). Default value is 60; longer-window limiters (signup, password-reset) emit the full window length. Honor it as your floor and add exponential backoff on top.