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

# Migrate to Keenable

> Map the request parameters and response fields of Tavily, Exa and the Brave Search API to Keenable's search and fetch endpoints.

Replace another search API with Keenable by changing the request you send and the fields you read. This page maps each provider's parameters and fields to Keenable's.

*Tavily, Exa and Brave parameters checked against each provider's API reference on 2026-11-09. Keenable requests run against the production API the same day.*

## What changes for every provider

| | Keenable |
| - | - |
| Base URL | `https://api.keenable.ai` |
| Auth with a key | `X-API-Key: $KEENABLE_API_KEY` on `/v1/search` and `/v1/fetch` |
| Auth without a key | No key on `/v1/search/public` and `/v1/fetch/public`. Send an `X-Keenable-Title` header with your app's name. |
| Search | `POST /v1/search` with a JSON body. Only `query` is required. |
| Page content | `GET /v1/fetch?url=...`, one URL per request. |
| Page text in search results | `results[].snippet`, in markdown. |

A parameter Keenable doesn't list returns `400` with `Unknown parameter(s)`, so remove the other provider's parameters instead of sending them along. See [Search](https://docs.keenable.ai/api-reference/search.md#request) and [Fetch](https://docs.keenable.ai/api-reference/fetch.md#request) for every parameter, and [Authentication](https://docs.keenable.ai/api-reference.md#authentication) for keys.

## Tavily

### Search request

| Tavily parameter | Keenable parameter |
| - | - |
| `query` | `query` |
| `max_results` | `max_results`, from `1` to `50` |
| `search_depth` | Closest is `mode`: `pro` or `realtime` |
| `chunks_per_source` | Closest is `snippet_max_length`, a cap on the characters in each `snippet` |
| `time_range` | `published_after` with a relative delta: `1d`, `1w`, `1mo` or `1y` |
| `start_date` | `published_after`, same `YYYY-MM-DD` format |
| `end_date` | `published_before`, same `YYYY-MM-DD` format |
| `include_domains` | `site`, one site per request |
| `include_published_date` | Not needed. `published_at` is returned when Keenable knows it. |
| `filter_by_published_date` | Not needed. `published_after` leaves out pages with no known publish date. |
| `include_raw_content` | Call `GET /v1/fetch` for each URL you want in full |
| `include_usage` | Not needed. `usage` is returned on calls made with a key. |
| `include_answer` | No equivalent |
| `topic`, `country`, `language`, `filter_by_language` | No equivalent |
| `exclude_domains`, `include_domains_mode`, `exact_match` | No equivalent |
| `include_images`, `include_image_descriptions`, `include_favicon` | No equivalent |
| `auto_parameters`, `safe_search` | No equivalent |

### Search response

| Tavily field | Keenable field |
| - | - |
| `query` | `query` |
| `results` | `results` |
| `results[].content` | `results[].snippet` |
| `results[].published_date` | `results[].published_at` |
| `usage` | `usage`, on calls made with a key |
| `request_id` | The `X-Request-Id` response header |
| `answer`, `images`, `response_time`, `auto_parameters` | No equivalent |

### Extract request and response

Tavily Extract maps to `GET /v1/fetch`. Fetch takes one URL per request and returns one page object, not a `results` list.

| Tavily parameter or field | Keenable parameter or field |
| - | - |
| `urls` | `url`, one per request |
| `query` | Closest is `prompt`: `content` then holds only what the prompt asks for |
| `format` | Not needed. `content` is always markdown. |
| `include_usage` | Not needed. `usage` is returned on calls made with a key. |
| `chunks_per_source`, `extract_depth`, `timeout` | No equivalent. `max_chars` caps the length of `content`. |
| `include_images`, `include_favicon` | No equivalent |
| `results` | The response body is the page: `url`, `content`, `title` |
| `failed_results` | The HTTP status of each request. See [Fetch errors](https://docs.keenable.ai/api-reference/fetch.md#errors). |
| `request_id` | The `X-Request-Id` response header |
| `response_time` | No equivalent |

### Before and after

Before, a Tavily search request body, sent with `Authorization: Bearer tvly-YOUR_API_KEY`:

```json theme={"system"}
{
  "query": "typescript best practices",
  "max_results": 3,
  "time_range": "year"
}
```

After, the same search on Keenable, with no key:

```bash theme={"system"}
curl -X POST "https://api.keenable.ai/v1/search/public" \
  -H "X-Keenable-Title: My App" \
  -H "Content-Type: application/json" \
  -d '{ "query": "typescript best practices", "max_results": 3, "published_after": "1y" }'
```

The response has `query`, `mode` and up to three `results`. Each result has a `title` and a `url`, and usually a `snippet`, `published_at` and `acquired_at`.

Keenable returns sources and has no generated `answer` field. Pass the `snippet` text to your own model if you need an answer. Tavily's date filters use the publish date or the last-updated date. Keenable's `published_after` and `published_before` use the publish date only.

## Exa

### Search request

| Exa parameter | Keenable parameter |
| - | - |
| `query` | `query` |
| `numResults` | `max_results`, from `1` to `50` |
| `startPublishedDate` | `published_after` |
| `endPublishedDate` | `published_before` |
| `includeDomains` | `site`, one site per request. A path narrows further. |
| `type` | Closest is `mode`: `pro` or `realtime` |
| `contents` | Not needed for page text. Every result carries a `snippet`. Use `snippet_max_length` to cap it. |
| `excludeDomains`, `category`, `userLocation`, `moderation` | No equivalent |
| `additionalQueries`, `objective`, `outputSchema`, `systemPrompt`, `stream` | No equivalent |
| `userIdentifier`, `compliance` | No equivalent |

### Search response

| Exa field | Keenable field |
| - | - |
| `results` | `results` |
| `results[].text`, `results[].highlights` | `results[].snippet` |
| `costDollars` | `usage.credits`, in credits, on calls made with a key |
| `requestId` | The `X-Request-Id` response header |
| `output`, `searchTime`, `warnings`, `notices` | No equivalent |

### Contents request and response

Exa Contents maps to `GET /v1/fetch`. Fetch takes one URL per request and returns one page object, not a `results` list.

| Exa parameter or field | Keenable parameter or field |
| - | - |
| `urls`, `ids` | `url`, one per request |
| `text` | Not needed. `content` always holds the page text as markdown. `max_chars` caps it. |
| `summary`, `highlights` | Closest is `prompt`: `content` then holds only what the prompt asks for |
| `maxAgeHours` | Closest is `live=true`, which fetches the page from the source now |
| `snapshotAsOf`, `subpages`, `subpageTarget`, `livecrawlTimeout`, `extras` | No equivalent |
| `userIdentifier`, `compliance` | No equivalent |
| `results` | The response body is the page: `url`, `content`, `title` |
| `statuses` | The HTTP status of each request. See [Fetch errors](https://docs.keenable.ai/api-reference/fetch.md#errors). |
| `costDollars` | `usage.credits`, in credits, on calls made with a key |
| `requestId` | The `X-Request-Id` response header |
| `searchTime` | No equivalent |

### Before and after

Before, an Exa search request body, sent with `x-api-key: <your-api-key>`:

```json theme={"system"}
{
  "query": "blog post about post-training quantization",
  "numResults": 3,
  "includeDomains": ["arxiv.org"],
  "startPublishedDate": "2026-01-01T00:00:00.000Z"
}
```

After, the same search on Keenable, with no key:

```bash theme={"system"}
curl -X POST "https://api.keenable.ai/v1/search/public" \
  -H "X-Keenable-Title: My App" \
  -H "Content-Type: application/json" \
  -d '{ "query": "blog post about post-training quantization", "max_results": 3, "site": "arxiv.org", "published_after": "2026-01-01T00:00:00.000Z" }'
```

The response has `query`, `mode` and up to three `results` from `arxiv.org`. Each result has a `title` and a `url`, and usually a `snippet`, `published_at` and `acquired_at`.

Keenable returns sources and has no synthesized `output`. Page text comes back in `snippet` on every search, so you don't ask for it with a `contents` object. Field names are snake\_case.

## Brave Search API

### Web search request

Brave web search is a `GET` with query parameters and an `X-Subscription-Token` header. Keenable search is a `POST` with a JSON body.

| Brave parameter | Keenable parameter |
| - | - |
| `q` | `query` |
| `count` | `max_results`, from `1` to `50` |
| `freshness` | `published_after` with a relative delta: `1d` for `pd`, `1w` for `pw`, `1mo` for `pm`, `1y` for `py`. For a custom range, add `published_before`. |
| `extra_snippets` | Closest is `snippet_max_length`, which sets how much page text each `snippet` carries |
| `offset` | No equivalent. Ask for up to 50 results in one request. |
| `country`, `search_lang`, `ui_lang`, `safesearch` | No equivalent |
| `enable_rich_callback` | No equivalent |

### Web search response

| Brave field | Keenable field |
| - | - |
| `web.results` | `results` |
| `web.results[].title` | `results[].title` |
| `web.results[].url` | `results[].url` |
| `web.results[].description`, `web.results[].extra_snippets` | `results[].snippet` |
| `query.original` | `query` |
| `query.more_results_available` | No equivalent |
| `locations.results`, `rich` | No equivalent |

### Before and after

Before, a Brave web search for pages from the past week:

```bash theme={"system"}
curl "https://api.search.brave.com/res/v1/web/search?q=machine+learning+tutorials&freshness=pw" \
  -H "X-Subscription-Token: <YOUR_API_KEY>"
```

After, the same search on Keenable, with no key:

```bash theme={"system"}
curl -X POST "https://api.keenable.ai/v1/search/public" \
  -H "X-Keenable-Title: My App" \
  -H "Content-Type: application/json" \
  -d '{ "query": "machine learning tutorials", "published_after": "1w" }'
```

The response has `query`, `mode` and up to 10 `results`. Each result has a `title` and a `url`, and usually a `snippet`, `published_at` and `acquired_at`.

Results are a flat `results` array at the top level, not nested under `web`. Read page text from `snippet`. Keenable results also have a `description` field, but it is deprecated and always empty.

## Verify the migration

Run one search without a key:

```bash theme={"system"}
curl -X POST "https://api.keenable.ai/v1/search/public" \
  -H "X-Keenable-Title: My App" \
  -H "Content-Type: application/json" \
  -d '{ "query": "typescript best practices", "max_results": 2 }'
```

Check three things in the response:

1. The status is `200`.
2. `results` has at most two entries, because `max_results` is an upper bound. Each entry has a `title` and a `url`.
3. Most entries have a `snippet`. This is the text your code now reads. It is optional in the response, so handle an entry without one.

Then add your key: change the path to `/v1/search` and send `X-API-Key`. The response gains a `usage` object. See [API keys](https://docs.keenable.ai/api-keys.md).


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