Skip to main content
KeyPort uses two distinct mechanisms to communicate problems: HTTP error codes for infrastructure-level failures, and the status field for business-logic outcomes. Understanding the difference is important for building reliable error handling.
Want to see these responses live? Try the Validate endpoint playground with a real or intentionally invalid request.

Two kinds of failure

TypeHTTP codevalidWhen it happens
HTTP error401 or 429Authentication failure or rate limit hit
Business-state failure200falseLicense or product state prevents access
Do not rely on HTTP status codes alone to determine whether a license is valid. Most failures return HTTP 200. Always read the valid and status fields.

HTTP errors

401 — Invalid API key

Returned when the Authorization header is missing or the API key is not recognized.
{
  "valid": false,
  "status": "invalid_api_key",
  "message": "Missing Authorization header. Use: Authorization: Bearer YOUR_API_KEY"
}

429 — Rate limit exceeded

Returned when the daily validation limit for the product has been exhausted. The response includes a retry_after field (seconds until midnight UTC) and a Retry-After header.
{
  "valid": false,
  "status": "rate_limit_exceeded",
  "message": "This license provider or organization owner has reached the daily API request limit for their current plan. Please ask them to upgrade to a paid plan for higher limits, or try again after midnight UTC.",
  "retry_after": 3600
}
See Rate Limits for plan-level quotas and reset behavior.

Business-state failures

These all return HTTP 200 with valid: false. Use the status value to determine what happened.
StatusMeaning
license_not_foundNo license exists for the key under this product
revokedThe license was manually revoked
expiredThe license’s expiry date has passed
ip_blockedThe calling IP is explicitly blocked on this license
ip_limit_reachedThe license has already registered its maximum allowed IPs
ip_not_registeredThe IP system is in allow-list mode and this IP has not been added
product_not_foundThe product linked to your API key does not exist
product_archivedThe product has been archived
product_disabledThe product has been disabled
org_suspendedThe organization account is suspended
billing_suspendedThe organization’s billing is suspended

Handling errors in code

The pattern below covers both HTTP-level and business-state failures:
async function validateLicense(licenseKey: string): Promise<void> {
  const response = await fetch('https://api.keyport.sbs/api/v1/validate', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer kp_live_xxxxxxxxxxxxxxxxxxxxxxxxx',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ license_key: licenseKey }),
  });

  // Handle HTTP-level errors first
  if (response.status === 401) {
    throw new Error('Invalid API key. Check your Authorization header.');
  }

  if (response.status === 429) {
    const data = await response.json();
    const retryAfter = data.retry_after ?? 3600;
    throw new Error(`Rate limit exceeded. Retry after ${retryAfter} seconds.`);
  }

  const data = await response.json();

  // Now handle business-state failures
  if (!data.valid) {
    switch (data.status) {
      case 'expired':
        throw new Error('License has expired.');
      case 'revoked':
        throw new Error('License has been revoked.');
      case 'ip_limit_reached':
        throw new Error('IP limit reached for this license.');
      case 'license_not_found':
        throw new Error('License key not recognized.');
      default:
        throw new Error(`License invalid: ${data.status}`);
    }
  }

  console.log('License is valid.');
}
Treat status as the canonical signal in your application logic. It is always present in the response body, regardless of HTTP code.