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.
Two kinds of failure
| Type | HTTP code | valid | When it happens |
|---|
| HTTP error | 401 or 429 | — | Authentication failure or rate limit hit |
| Business-state failure | 200 | false | License 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.
| Status | Meaning |
|---|
license_not_found | No license exists for the key under this product |
revoked | The license was manually revoked |
expired | The license’s expiry date has passed |
ip_blocked | The calling IP is explicitly blocked on this license |
ip_limit_reached | The license has already registered its maximum allowed IPs |
ip_not_registered | The IP system is in allow-list mode and this IP has not been added |
product_not_found | The product linked to your API key does not exist |
product_archived | The product has been archived |
product_disabled | The product has been disabled |
org_suspended | The organization account is suspended |
billing_suspended | The 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.