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

> ## Agent Instructions
> Keenable search and fetch work without an API key. To add them to code, use the official SDK for the project's language: https://docs.keenable.ai/sdks.md
> Read an API key from the KEENABLE_API_KEY environment variable. Never ask a user to paste a key into chat; send them to https://docs.keenable.ai/api-keys.md
> On 402 with an API key (code insufficient_credits), stop and ask a human to add credits; don't retry. On the pay-per-request paths a 402 is the payment offer: pay it and retry, see https://docs.keenable.ai/pay-per-request.md. On 429, wait the number of seconds in Retry-After, then retry.
> Every error, with its cause and fix: https://docs.keenable.ai/errors.md

# Best practices

> Get relevant Keenable search results at a low context cost: describe the page you want, run narrow searches in parallel, filter by site and date, cap result size, and fetch only the pages you need.

These practices get your agent relevant pages in as few characters of context as possible. Each section stands alone. The samples run with no API key.

## Describe the page you want

Write the query as a description of the page that would answer the question. Search is semantic, so describe the page in plain language instead of stacking keywords.

| Instead of | Write |
| - | - |
| `React vs Vue` | `blog post comparing React and Vue performance` |
| `postgres replication slot full` | `postmortem of an outage caused by a full Postgres replication slot` |
| `node release` | `release notes for a new Node.js version` |

Name the kind of page you want: documentation, postmortem, release notes, comparison.

## Split a broad question into parallel searches

Run one narrow search per aspect of a broad question instead of one wide query. Cap how many run at once so you stay under your [rate limit](https://docs.keenable.ai/rate-limits.md), 10 requests per second by default.

This sample runs three searches, at most two at a time, and prints one citable block per query:

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

const client = new Keenable();
const queries = [
  "official Postgres documentation on logical replication slots",
  "postmortem of an outage caused by a full Postgres replication slot",
  "guide to monitoring Postgres replication lag with pg_stat_replication",
];

async function main() {
  const blocks: string[] = new Array(queries.length);
  let next = 0;
  async function worker() {
    while (next < queries.length) {
      const i = next++;
      const results = await client.search(queries[i], { maxResults: 3, snippetMaxLength: 500 });
      blocks[i] = results.toContext();
    }
  }
  await Promise.all([worker(), worker()]); // at most 2 searches in flight
  blocks.forEach((block, i) => console.log(`## ${queries[i]}\n${block}\n`));
}

main();
```

```python Python theme={"system"}
import asyncio

from keenable import AsyncKeenable

QUERIES = [
    "official Postgres documentation on logical replication slots",
    "postmortem of an outage caused by a full Postgres replication slot",
    "guide to monitoring Postgres replication lag with pg_stat_replication",
]


async def main():
    limit = asyncio.Semaphore(2)  # at most 2 searches in flight

    async with AsyncKeenable() as client:

        async def search(query):
            async with limit:
                results = await client.search(query, max_results=3, snippet_max_length=500)
                return results.to_context()

        blocks = await asyncio.gather(*(search(q) for q in QUERIES))

    for query, block in zip(QUERIES, blocks):
        print(f"## {query}\n{block}\n")


asyncio.run(main())
```

You see three blocks of three sources each, about 5,400 characters in total:

```text Example output · results vary theme={"system"}
## official Postgres documentation on logical replication slots
[1] 47.2. Logical Decoding Concepts (https://postgresql.org/docs/current/logicaldecoding-explanation.html)
In the context of logical replication, a slot represents a stream of changes that can be replayed to a client…

[2] 53.20. pg_replication_slots (https://postgresql.org/docs/current/view-pg-replication-slots.html)
…

## postmortem of an outage caused by a full Postgres replication slot
[1] Our Postgres Survived 14 TB. The Replica Nobody Watched Took Us Down. (https://medium.com/beyond-localhost/6be8c52a3fb1?sk=637721028350a012c7b0cc34dd707f21) - published 2026-05-04T14:01:02Z
…
```

## Narrow with filters, not extra words

Use `site` to restrict results to one site and `published_after` to get recent pages. A filter removes every page that doesn't match it. If you set `published_after`, the results also leave out pages with no known publish date.

This sample keeps pages on `nodejs.org` published in the last 30 days:

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

async function main() {
  const client = new Keenable();
  const results = await client.search("release notes for a new Node.js version", {
    site: "nodejs.org",
    publishedAfter: "30d",
    maxResults: 3,
  });
  for (const r of results) console.log(r.publishedAt?.slice(0, 10), r.title, r.url);
}

main();
```

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

client = Keenable()
results = client.search(
    "release notes for a new Node.js version",
    site="nodejs.org",
    published_after="30d",
    max_results=3,
)
for r in results:
    print((r.published_at or "")[:10], r.title, r.url)
```

You see a publish date, a title and a URL per result, all on `nodejs.org`:

```text Example output · results vary theme={"system"}
2026-11-05 Node.js — Blog https://nodejs.org/en/blog
2026-10-29 Node.js — Run JavaScript Everywhere https://nodejs.org/en/download/archive/v27.0.0-alpha.0.0.0
2026-10-29 Node.js — Node.js Releases https://nodejs.org/en/about/previous-releases
```

For recent results, use `published_after`, not `query_time`. `query_time` sets only an upper bound: `"7d"` searches the index as it stood 7 days ago and excludes everything newer. See [Date and time filters](https://docs.keenable.ai/api-reference/search.md#date-and-time-filters) for every accepted format.

## Choose the mode

Leave `mode` at its default, `pro`, unless response time matters more than depth.

| Mode | Use it for |
| - | - |
| `pro` (default) | Deeper retrieval and higher-quality results. |
| `realtime` | The fastest response, for latency-sensitive tasks. |

Each mode is billed as its own SKU. See [Credits](https://docs.keenable.ai/credits.md#what-a-call-costs).

In SDK 0.1.x, the Python and TypeScript clients accept only `pro`. This request calls the HTTP API with `realtime`:

```bash curl theme={"system"}
curl -X POST https://api.keenable.ai/v1/search/public \
  -H "Content-Type: application/json" \
  -H "X-Keenable-Title: My App" \
  -d '{"query": "current weather warnings for Lisbon", "mode": "realtime", "max_results": 3, "snippet_max_length": 200}'
```

The response's `mode` field names the mode that served the request:

```json Example output · results vary theme={"system"}
{
  "query": "current weather warnings for Lisbon",
  "mode": "realtime",
  "results": [
    {
      "title": "Lisboa, Lisboa, Portugal Weather Advisories - Warnings & Watches | AccuWeather",
      "url": "https://accuweather.com/en/pt/lisbon/1-274087_1_al/weather-warnings/1-274087_1_al",
      "description": "",
      "snippet": "or advisories are issued by official government meteorological authorities. Current conditions in Lisbon: Mostly cloudy, 73°, RealFeel® 80°. See the [full forecast for",
      "acquired_at": "2026-07-23T09:41:20Z"
    }
  ]
}
```

## Control context size

Set the size of a search response in the request, then set the size of the prompt block when you render it. If you set neither, you get 10 results whose snippets often run to several thousand characters each.

| Setting | Where | What it caps |
| - | - | - |
| `max_results` | Search request | Results returned, 1 to 50. Defaults to 10. |
| `snippet_max_length` | Search request | Characters of text per result, 180 to 10000. |
| `max_chars` | `to_context()` | Characters in the whole block. Defaults to 12000. |
| `max_results` | `to_context()` | Results considered, from the top. |
| `include_dates` | `to_context()` | Set to `false` to drop the publish date from each header. |

`to_context()` adds whole results in rank order and stops before the result that would pass `max_chars`. Call `cited()` with the same limits to get the results that the block kept.

This sample asks for 5 short results and renders them into a block of at most 1,200 characters:

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

async function main() {
  const client = new Keenable();
  const results = await client.search("how HTTP/3 connection migration works", {
    maxResults: 5,
    snippetMaxLength: 400,
  });
  const block = results.toContext({ maxChars: 1200, includeDates: false });
  const cited = results.cited({ maxChars: 1200, includeDates: false });
  console.log(`${results.length} results, ${cited.length} in the block, ${block.length} characters`);
}

main();
```

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

client = Keenable()
results = client.search(
    "how HTTP/3 connection migration works",
    max_results=5,
    snippet_max_length=400,
)
block = results.to_context(max_chars=1200, include_dates=False)
cited = results.cited(max_chars=1200, include_dates=False)
print(f"{len(results)} results, {len(cited)} in the block, {len(block)} characters")
```

You see that the block kept 3 of the 5 results:

```text Example output · results vary theme={"system"}
5 results, 3 in the block, 1138 characters
```

See [Context size](https://docs.keenable.ai/sdks.md#context-size) for the option names in each language.

## Read snippets first, then fetch

Answer from the search snippets when you can. Each `snippet` is page text chosen for the query, so it often holds the answer. Fetch a page only if its snippet falls short, you need exact wording, or the user names a URL.

When you fetch, ask for the part you need. `prompt` makes a model read the page and return only its answer in `content`. `max_chars` caps the characters of `content`, which defaults to 50000. SDK 0.1.x sends neither, so this request calls the HTTP API:

```bash curl theme={"system"}
curl -G https://api.keenable.ai/v1/fetch/public \
  -H "X-Keenable-Title: My App" \
  --data-urlencode "url=https://www.postgresql.org/docs/current/logicaldecoding-explanation.html" \
  --data-urlencode "prompt=What happens to WAL when a replication slot's consumer stops? Answer in two sentences." \
  --data-urlencode "max_chars=600"
```

`content` holds two sentences instead of the whole page:

```json Example output · results vary theme={"system"}
{
  "url": "https://www.postgresql.org/docs/current/logicaldecoding-explanation.html",
  "title": "47.2. Logical Decoding Concepts",
  "content": "The replication slot will prevent the removal of required WAL even when there is no connection using it. This consumes storage because the required WAL cannot be removed as long as it is required by the replication slot.",
  "description": ""
}
```

See [Fetch](https://docs.keenable.ai/products/fetch.md#key-parameters) for `live` and what each fetch costs.

## Group an agent run with a session ID

Send the same session ID on every search and fetch in one agent run, so Keenable can group the run's requests. Name your app on every request too, so Keenable attributes the traffic to your product.

| What | HTTP | SDK |
| - | - | - |
| Session ID, up to 128 characters | `X-Keenable-Session-Id` header, or `session_id` in the search body or the fetch query string | Not sent in 0.1.x |
| App name | `X-Keenable-Title` header | `client_source` (Python), `clientSource` (TypeScript) |

If you send both the header and the `session_id` parameter, the header wins. The keyless `/public` endpoints require `X-Keenable-Title`. The SDKs send `Keenable SDK` unless you set the client source.

This request tags one search with an app name and a session ID:

```bash curl theme={"system"}
curl -X POST https://api.keenable.ai/v1/search/public \
  -H "Content-Type: application/json" \
  -H "X-Keenable-Title: Acme research agent" \
  -H "X-Keenable-Session-Id: run-2026-11-09-0042" \
  -d '{"query": "official Postgres documentation on logical replication slots", "max_results": 1, "snippet_max_length": 180}'
```

You see an ordinary search response.

Over MCP, pass `session_id` as a tool argument. See [MCP](https://docs.keenable.ai/mcp-server.md#available-tools).

## Handle 429 and 402

On `429`, wait for the time in the `Retry-After` header, then retry the same request. See [429 Rate limit exceeded](https://docs.keenable.ai/errors.md#429) and [Rate limits](https://docs.keenable.ai/rate-limits.md).

On `402`, stop: your organization is out of credits, and every retry returns `402` until someone adds credits. See [402 Out of credits](https://docs.keenable.ai/errors.md#402).

## Next steps

* [Search](https://docs.keenable.ai/products/search.md) and [Fetch](https://docs.keenable.ai/products/fetch.md): cost and key parameters.
* [Search API reference](https://docs.keenable.ai/api-reference/search.md) and [Fetch API reference](https://docs.keenable.ai/api-reference/fetch.md): every parameter and response field.
* [SDKs](https://docs.keenable.ai/sdks.md): configuration, methods and error classes.


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