> ## Documentation Index
> Fetch the complete documentation index at: https://docs.keyport.sbs/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> How KeyPort communicates errors and business-state failures through HTTP codes and status strings.

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.

<Tip>
  Want to see these responses live? Try the [Validate endpoint playground](/api-reference/validate) with a real or intentionally invalid request.
</Tip>

## 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 |

<Warning>
  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.
</Warning>

## HTTP errors

### 401 — Invalid API key

Returned when the `Authorization` header is missing or the API key is not recognized.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "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.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "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](/api-reference/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:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
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.');
}
```

<Tip>
  Treat `status` as the canonical signal in your application logic. It is always present in the response body, regardless of HTTP code.
</Tip>
