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

# Errors

> Every error the Keenable API returns, with its cause, whether to retry, and the exact fix.

Look up any error the Keenable API returns to find its cause and the exact fix. Each section has a stable anchor, so SDK error messages and agents can link straight to it.

| Status | Error | Retry? |
| - | - | - |
| [`400`](#400-invalid-request) | Invalid request parameters | Fix, then retry |
| [`400`](#400-missing-app-identifier) | Missing app identifier | Fix, then retry |
| [`401`](#401-malformed-key) | Missing or malformed key | Fix, then retry |
| [`401`](#401-invalid-key) | Invalid key | Fix, then retry |
| [`402`](#402) | Out of credits | Do not retry |
| [`403`](#403) | Key rejected or revoked | Do not retry; check the key, then ask a human |
| [`404`](#404) | Page not found | Fix the URL, then retry |
| [`422`](#422) | Content could not be extracted | Fix the URL or skip; do not retry the same URL |
| [`429`](#429) | Rate limit exceeded | Wait, then retry |
| [`5xx`](#5xx) | Server error | Retry up to 3 times |

Over HTTP, an error body is JSON with an `error` and a `message` field. The SDKs raise an error whose message starts with the prefix shown under each entry, followed by the API's `message`.

## 400 Invalid request parameters

**Retry: fix, then retry.**

**What you see**

If a parameter is missing or invalid, search and fetch return `400` with an `error` such as `Invalid parameter`. The body also has a `message`, and can have a `details` field that lists the validation errors.

**Cause**

A request parameter does not match what the endpoint accepts: a missing `query` or `url`, a value outside its range, a bad date filter, or a type the schema rejects. Both the keyed paths, such as `/v1/search` and `/v1/fetch`, and the `/public` paths return it.

**Fix**

1. Read the `message` and any `details` in the response body. They name the field that failed.
2. Check the request against the [Search](https://docs.keenable.ai/api-reference/search.md) or [Fetch](https://docs.keenable.ai/api-reference/fetch.md) parameter list and send the call again with a corrected body.
3. If the body says `Missing app identifier` instead, see [400 Missing app identifier](#400-missing-app-identifier).

## 400 Missing app identifier

**Retry: fix, then retry.**

**What you see**

```json theme={"system"}
{"error": "Missing app identifier", "message": "X-Keenable-Title header is required for token-less requests"}
```

The SDKs send this header for you, so you get this error only over raw HTTP.

**Cause**

A keyless request to `/v1/search/public` or `/v1/fetch/public` arrived without the `X-Keenable-Title` header. Keyed requests to `/v1/search` or `/v1/fetch` never need this header. A `400` on those paths means an [invalid parameter](#400-invalid-request).

**Fix**

Add the header with your application's name. This request includes it:

```bash theme={"system"}
curl -X POST https://api.keenable.ai/v1/search/public \
  -H "Content-Type: application/json" \
  -H "X-Keenable-Title: My agent" \
  -d '{"query": "fastest inference providers 2026", "max_results": 3}'
```

You get search results instead of the `400`. If you have an [API key](https://docs.keenable.ai/api-keys.md), call `/v1/search` instead, which doesn't need the header.

## 401 Malformed key

**Retry: fix, then retry.**

**What you see**

If the key has the wrong format, the API returns:

```json theme={"system"}
{"error": "Authentication failed", "message": "Malformed API key"}
```

If a request to a keyed path such as `/v1/search` or `/v1/fetch` carries no key at all, the API returns:

```json theme={"system"}
{"error": "Missing authentication", "message": "Either X-API-Key header or Authorization Bearer token is required"}
```

The SDKs never send a request with no key. If `KEENABLE_API_KEY` isn't set, they call the keyless `/public` paths, so you get the second body only over raw HTTP. If the key is malformed, the SDKs raise `KeenableAuthError` (Python, TypeScript) or `*keenable.AuthError` (Go) with the message prefix `Keenable authentication failed (401)`.

**Cause**

`Malformed API key` means the value does not look like a Keenable key at all. Common causes are a truncated copy, a stray quote or space, or a placeholder such as `keen_<your_key>` left in place. `Missing authentication` means the request reached a keyed path without an `X-API-Key` or `Authorization: Bearer` header.

**Fix**

1. Check the value of `KEENABLE_API_KEY`. It starts with `keen_` and has no quotes or whitespace around it.
2. Over raw HTTP, send the key in the `X-API-Key` header. To call without a key, use `/v1/search/public` or `/v1/fetch/public` with an `X-Keenable-Title` header instead.
3. To keep working while you sort it out, unset `KEENABLE_API_KEY`. The SDKs fall back to the keyless tier.

## 401 Invalid key

**Retry: fix, then retry.**

**What you see**

The API returns:

```json theme={"system"}
{"error": "Authentication failed", "message": "Invalid API key"}
```

The SDKs raise `KeenableAuthError` (Python, TypeScript) or `*keenable.AuthError` (Go) with the message prefix `Keenable authentication failed (401)`.

**Cause**

The key has the right format, but Keenable has no key with that value. A typo that keeps the `keen_` prefix causes this.

**Fix**

1. Compare `KEENABLE_API_KEY` with the key in the [console ↗](https://app.keenable.ai/console).
2. If you cannot find it there, create a new key and replace the old value. See [API keys](https://docs.keenable.ai/api-keys.md).
3. To keep working while you sort it out, unset `KEENABLE_API_KEY`. The SDKs fall back to the keyless tier.

## 402 Out of credits

**Retry: do not retry.**

**What you see**

The SDKs raise `KeenableAPIError` (Python, TypeScript) or `*keenable.APIError` (Go) with status `402` and the message prefix `Keenable: insufficient credits (402)`.

**Cause**

Your organization has spent its free monthly allowance of 100,000 requests and has no purchased credits left. Every retry returns the same `402` until someone adds credits or the allowance resets. See [credits](https://docs.keenable.ai/credits.md#when-credits-run-out).

**Fix**

Stop and ask a human to buy credits or turn on [auto top-up](https://docs.keenable.ai/auto-top-up.md) in the [console ↗](https://app.keenable.ai/console).

SDK 0.1.x has no dedicated error type for `402`, so catch the generic API error and check its status. This code stops on `402` and passes every other error through:

```python Python theme={"system"}
from keenable import Keenable, KeenableAPIError

client = Keenable()
try:
    results = client.search("fastest inference providers 2026", max_results=3)
except KeenableAPIError as err:
    if err.status_code == 402:
        # Out of credits: retrying will not help. Hand off to a human.
        raise SystemExit("Keenable is out of credits. Buy credits or turn on auto top-up in the console.")
    raise
```

```typescript TypeScript theme={"system"}
import { Keenable, KeenableAPIError } from "keenable";

const client = new Keenable();
try {
  const results = await client.search("fastest inference providers 2026", { maxResults: 3 });
} catch (err) {
  if (err instanceof KeenableAPIError && err.statusCode === 402) {
    // Out of credits: retrying will not help. Hand off to a human.
    throw new Error("Keenable is out of credits. Buy credits or turn on auto top-up in the console.");
  }
  throw err;
}
```

```go Go theme={"system"}
_, err := client.Search(ctx, "fastest inference providers 2026",
	&keenable.SearchOptions{MaxResults: 3})
var apiErr *keenable.APIError
if errors.As(err, &apiErr) && apiErr.StatusCode == 402 {
	// Out of credits: retrying will not help. Hand off to a human.
	log.Fatal("Keenable is out of credits. Buy credits or turn on auto top-up in the console.")
}
if err != nil {
	log.Fatal(err)
}
```

Your program exits with the message instead of retrying. SDK 0.2.0 adds a typed error for `402`: `KeenableCreditsError` in Python and TypeScript, `*keenable.CreditsError` in Go. Once you upgrade, catch that type instead of checking the status.

## 403 Key rejected or revoked

**Retry: do not retry; check the key, then ask a human.**

**What you see**

A revoked key returns:

```json theme={"system"}
{"error": "Forbidden", "message": "Key revoked"}
```

The SDKs raise `KeenableAuthError` (Python, TypeScript) or `*keenable.AuthError` (Go) with the message prefix `Keenable authentication failed (403)`.

**Cause**

`403` means the server won't accept this key. Two cases return the same status:

* On `/v1/search`, `/v1/fetch` and `/mcp`, the key usually exists but was [revoked](https://docs.keenable.ai/api-keys.md#key-lifetime-and-rotation) in your organization.
* On `/mcp-public`, a key the server doesn't accept also returns `403`, not `401`, so a mistyped or unknown key looks like a revoked one. See [MCP server](https://docs.keenable.ai/mcp-server.md#keyless-endpoint-for-listings-and-presets).

Fetch also returns `403` for reasons unrelated to your key. If the body says `"error": "Upstream forbidden"`, the target site blocked the fetch, for example with a bot check. If it says `Fetching from private/internal hosts is not allowed`, the URL points at a private address. In both cases, check the `error` and `message` before you touch the key, and try a different URL instead.

**Fix**

1. Compare the key value with the one in the [console ↗](https://app.keenable.ai/console). Fix typos, stray quotes and leftover placeholders before treating the key as revoked.
2. If the value matches and still gets `403`, ask a human with console access to issue a new key, then update `KEENABLE_API_KEY`.
3. On `/mcp-public`, you can also drop the key and stay on the public tier, or switch the URL to `/mcp` once the key is confirmed.

## 404 Page not found

**Retry: fix the URL, then retry.**

**What you see**

If the target page doesn't exist, fetch returns `404`:

```json theme={"system"}
{"error": "Not found", "message": "The page does not exist at this URL. Check that the URL is correct."}
```

Search does not use this status.

**Cause**

Keenable couldn't find the URL you passed to [Fetch](https://docs.keenable.ai/api-reference/fetch.md): a typo, a moved page, or a link that never resolved.

**Fix**

Check the URL, then call fetch again with a corrected one. Retrying the same URL returns the same `404`. If search gave you the link, try the next result instead.

## 422 Content could not be extracted

**Retry: fix the URL or skip; do not retry the same URL.**

**What you see**

If the page exists but Keenable can't extract readable content from it, fetch returns `422` with `"error": "Unprocessable entity"` and a `message` such as `The page is behind a login or paywall`. Search returns `422` with `Search failed` and a status code in the `message` when the search fails in a way a retry won't fix.

**Cause**

The URL is reachable, but extraction failed: an unsupported domain, a login wall or paywall, a page that is mostly binary or empty of text, an error status from the target site, or a failure inside the extractor.

**Fix**

Don't retry the same URL. It fails the same way every time. Try a different URL from search, or fetch a cleaner source for the same topic. If many URLs from one site fail, the domain is probably unsupported, so skip that site.

## 429 Rate limit exceeded

**Retry: wait, then retry.**

**What you see**

The SDKs raise `KeenableRateLimitError` (Python, TypeScript) or `*keenable.RateLimitError` (Go) with the message prefix `Keenable rate limit exceeded (429)`.

Responses carry these headers, and a `429` adds `Retry-After`:

| Header | Meaning |
| - | - |
| `X-RateLimit-Limit` | Requests allowed in the current window |
| `X-RateLimit-Remaining` | Requests left in the current window |
| `X-RateLimit-Reset` | When the window resets, as an ISO 8601 timestamp |
| `Retry-After` | How long to wait before the next request |

**Cause**

You sent requests faster than your limit allows:

* Keyless: 1,000 requests per hour and 10 per second per IP, on a pool shared with everyone else on your egress address.
* With an API key: 10 requests per second per organization, with no hourly cap.

See [rate limits](https://docs.keenable.ai/rate-limits.md) for every tier.

A `429` can also come from further down the line. If the `message` says the target server rate-limited the fetch, or that search or fetch is temporarily rate-limited, your own limit isn't the cause. Those responses carry `Retry-After: 1`.

**Fix**

Wait for the time in `Retry-After`, then retry. If you hit the keyless hourly cap, add an [API key](https://docs.keenable.ai/api-keys.md): it removes the hourly cap and gives you your own limit. For a higher per-second limit, [talk to us](https://keenable.ai/contact).

## 5xx Server error

**Retry: retry up to 3 times.**

**What you see**

The SDKs raise `KeenableAPIError` (Python, TypeScript) or `*keenable.APIError` (Go) with the message prefix `Keenable API error (5xx)`, where `5xx` is the actual status, such as `500` or `503`.

**Cause**

Something failed on Keenable's side or on the way to the target page. The request itself is fine. The `error` field tells the cases apart:

* `502` `Bad gateway`: fetch couldn't reach the target page, for example after a DNS, TLS or connection failure.
* `503` `Authentication temporarily unavailable`: Keenable couldn't verify your key. The response carries `Retry-After: 5`.
* `504` `Gateway timeout`: the target page took too long to respond.
* `529` `Overloaded`: search is temporarily overloaded. The response carries `Retry-After: 1`.

**Fix**

Retry up to 3 times, waiting longer before each attempt (for example 1, 2 and 4 seconds). If the response has a `Retry-After` header, wait at least that long. If the error persists after 3 attempts, stop and [report it](#report-a-problem).

## Report a problem

Responses from search and fetch carry an `X-Request-Id` header. Include its value when you [get help](https://docs.keenable.ai/help.md), so we can find your request in our logs. The Go SDK adds it to the error message as `request_id`. If Keenable rejects a request before it reaches search or fetch, such as one that returns `400` or `401`, the response has no request ID. For those, include the time of the request and the full response body.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.