# API reference Source: https://docs.keenable.ai/api-reference The HTTP API behind the Keenable CLI, MCP server, and integrations. Two endpoints sit under everything else Keenable ships — the [CLI](/cli), the [MCP server](/mcp-server), and every [integration](/integrations) call one or both of them. Reach for the HTTP API directly when you are building something none of those cover. ## Base URL ``` https://api.keenable.ai ``` ## Authentication There are three ways in, and the one you pick decides the path you call. **With an API key**, in the `X-API-Key` header: ```bash theme={"system"} curl -X POST "https://api.keenable.ai/v1/search" \ -H "X-API-Key: keen_" \ -H "Content-Type: application/json" \ -d '{ "query": "typescript best practices" }' ``` `Authorization: Bearer keen_` works too, and `X-API-Key` wins if both are present. See [Authentication](/authentication) for obtaining and rotating a key, and for the error responses both endpoints share. **Without one**, on the `/public` twin of each endpoint: ```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" }' ``` `X-Keenable-Title` names the application making the call. It is **required** on the public endpoints — a request without it is rejected with `400 Missing app identifier` — and it is how keyless traffic is attributed. Values longer than 256 characters are truncated, and `?keenable_title=` carries the same value when a header is not an option. **Paying per request**, on the `/v1/x402/*` paths, with no key and no account: each call pays \$0.01 in USDC on Base over the x402 protocol. See [Pay per request](/pay-per-request) for the payment flow and a client example. Parameters and response shapes are identical across all three, so moving between them means changing the path and swapping one header. The keyless tier is what the [CLI](/cli), the [MCP server](/mcp-server), and every [integration](/integrations) fall back to when no key is configured. ## Endpoints | Endpoint | Method | Path | Keyless path | Pay-per-request path | Returns | | - | - | - | - | - | - | | [Search](/api-reference/search) | `POST` | `/v1/search` | `/v1/search/public` | `/v1/x402/search` | Ranked results with URLs, titles, descriptions, and snippets | | [Fetch](/api-reference/fetch) | `GET` | `/v1/fetch` | `/v1/fetch/public` | `/v1/x402/fetch` | Page content as markdown, with title, description, and author | Each endpoint page carries the full request and response shape, examples in curl, Python, and TypeScript, and a playground you can call with your own key. ## Limits and billing Authenticated requests are rate limited per organization rather than per IP — see [Rate limits](/rate-limits) for the numbers — and draw on your [credit](/credits) balance. Keyless requests are limited per IP instead, on a public pool capped at 1,000 requests per hour and 10 per second, and they consume no credits. Responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`, and a `429` adds `Retry-After`. That pool is shared with everyone else on your egress address, so its headroom is outside your control: use a key for anything past a first look. Pay-per-request calls consume no credits either: each one is paid for on its own, and has its own [limits](/pay-per-request#limits). # Fetch Source: https://docs.keenable.ai/api-reference/fetch GET https://api.keenable.ai/v1/fetch Fetch webpage contents Retrieve content from a URL in markdown format. Part of the [API reference](/api-reference), which covers the base URL, authentication, and the limits both endpoints share. ```bash curl theme={"system"} curl "https://api.keenable.ai/v1/fetch?url=https://example.com/ts-best-practices" \ -H "X-API-Key: keen_" ``` ```python Python theme={"system"} import requests r = requests.get( "https://api.keenable.ai/v1/fetch", headers={"X-API-Key": "keen_"}, params={"url": "https://example.com/ts-best-practices"}, ) print(r.json()["content"]) ``` ```typescript TypeScript theme={"system"} const url = "https://example.com/ts-best-practices"; const r = await fetch( `https://api.keenable.ai/v1/fetch?url=${encodeURIComponent(url)}`, { headers: { "X-API-Key": "keen_" } }, ); const { content } = await r.json(); ``` By default, only URLs that Keenable indexes are supported, and attempting to fetch a document that is not indexed results in an error. Pass `live=true` to fetch directly from the source, including URLs that are not indexed. ## Without a key `GET /v1/fetch/public` takes the same query parameters and returns the same shape, with no API key: ```bash theme={"system"} curl "https://api.keenable.ai/v1/fetch/public?url=https://example.com/ts-best-practices" \ -H "X-Keenable-Title: My App" ``` `X-Keenable-Title` names your application and is required on the public endpoint; without it the request is rejected with `400 Missing app identifier`. Keyless calls are rate limited per IP and consume no credits — see [Authentication](/api-reference#authentication) for the details. The playground on this page calls the keyed endpoint. ## Request URL to fetch. Maximum number of characters of content to return. Longer content is truncated. Fetch the page live from the source instead of returning Keenable's indexed copy. Enables fetching URLs that are not indexed. Optional extraction instruction, at most 2000 characters. When set, an LLM reads the fetched page and `content` contains only the output for this instruction instead of the full page. Example: `List all pricing tiers with their monthly prices`. ## Response The fetched URL. Page title (if available). Short summary of the page (if available). Page author (if available). Extracted page content in markdown. Publication time as a Unix timestamp in seconds (if available). ### Example ```json theme={"system"} { "url": "https://example.com/ts-best-practices", "title": "TypeScript Best Practices 2026", "description": "A comprehensive guide to modern TypeScript patterns and best practices.", "author": "Jane Doe", "published_at": 1768435200, "content": "# TypeScript Best Practices 2026\n\nUse strict mode, prefer interfaces over type aliases for object shapes..." } ``` # Search Source: https://docs.keenable.ai/api-reference/search POST https://api.keenable.ai/v1/search Search the web and return ranked results Search the web and return ranked results with URLs, titles, and descriptions. Part of the [API reference](/api-reference), which covers the base URL, authentication, and the limits both endpoints share. ```bash curl theme={"system"} curl -X POST "https://api.keenable.ai/v1/search" \ -H "X-API-Key: keen_" \ -H "Content-Type: application/json" \ -d '{ "query": "typescript best practices" }' ``` ```python Python theme={"system"} import requests r = requests.post( "https://api.keenable.ai/v1/search", headers={"X-API-Key": "keen_"}, json={"query": "typescript best practices"}, ) for hit in r.json()["results"]: print(hit["title"], hit["url"]) ``` ```typescript TypeScript theme={"system"} const r = await fetch("https://api.keenable.ai/v1/search", { method: "POST", headers: { "X-API-Key": "keen_", "Content-Type": "application/json", }, body: JSON.stringify({ query: "typescript best practices" }), }); const { results } = await r.json(); ``` ## Without a key `POST /v1/search/public` takes the same body and returns the same shape, with no API 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" }' ``` `X-Keenable-Title` names your application and is required on the public endpoint; without it the request is rejected with `400 Missing app identifier`. Keyless calls are rate limited per IP and consume no credits — see [Authentication](/api-reference#authentication) for the details. The playground on this page calls the keyed endpoint. ## Request The search query. Search mode: `pro` for deeper retrieval, `realtime` for the fastest results. Each mode is billed as its own SKU, `search.pro` or `search.realtime`; see [Credits](/credits#what-a-call-costs). Restrict results to a specific site (e.g. `"techcrunch.com"`). Filter to pages acquired/indexed at or after this point in time. See [Date and time filters](#date-and-time-filters) for accepted formats. Filter to pages acquired/indexed at or before this point in time. See [Date and time filters](#date-and-time-filters) for accepted formats. Filter to pages published at or after this point in time. See [Date and time filters](#date-and-time-filters) for accepted formats. Filter to pages published at or before this point in time. See [Date and time filters](#date-and-time-filters) for accepted formats. Search the index as it stood at this point in time: pages acquired after it are excluded. Accepts a timestamp or a date (a date resolves to `00:00:00` UTC, not to the end of the day). Also re-bases relative deltas — see [Point-in-time search](/mcp-server#point-in-time-search). Maximum length, in characters, of the `snippet` returned per result. Must be between 180 and 10000. When omitted, a default snippet length is used. Maximum number of results to return. Must be between 1 and 50. When omitted, up to 10 results are returned. ## Response The query that was searched. The search mode that served the request, `pro` or `realtime`. List of search results. Page title. Page URL. Short summary of the page. Longer text excerpt from the page content (if available). When the page was published, as an ISO 8601 timestamp in UTC (if available). When the page was acquired/indexed, as an ISO 8601 timestamp in UTC (if available). ### Example ```json theme={"system"} { "query": "typescript best practices", "mode": "pro", "results": [ { "title": "TypeScript Best Practices 2026", "url": "https://example.com/ts-best-practices", "description": "A comprehensive guide to modern TypeScript patterns and best practices.", "snippet": "TypeScript Best Practices 2026 Use strict mode, prefer interfaces over type aliases for object shapes...", "published_at": "2026-01-15T10:30:00Z", "acquired_at": "2026-01-16T08:12:34Z" } ] } ``` ### Date and time filters `acquired_after`, `acquired_before`, `published_after`, and `published_before` each accept **one** of the following formats: * **Date** in RFC 3339 `full-date` form (`YYYY-MM-DD`) — covers that whole day in UTC. On an `_after` bound it resolves to `00:00:00` on that date; on a `_before` bound it resolves to `23:59:59.999` on that date, so pages from the named day are kept at either end. Pass a timestamp instead to cut at an exact instant. * **Timestamp** in ISO 8601 form (`YYYY-MM-DDTHH:MM:SS[.sss][±HH:MM]`). When a timezone offset is not provided, the timezone is interpreted as UTC. * **Relative delta** (``, e.g. `7d`, `30min`) — resolves to the request time minus the delta, truncated to minute precision, or to `query_time` minus the delta when that is set. Supported units: `min` (minutes), `h` (hours), `d` (days), `mo` (months), `y` (years). Example values: | Value | Resolves to | | - | - | | `2026-01-15` on `acquired_after` / `published_after` | `2026-01-15T00:00:00Z` | | `2026-01-15` on `acquired_before` / `published_before` | `2026-01-15T23:59:59.999Z` | | `2026-01-15T10:30:00` | `2026-01-15T10:30:00Z` (no offset → UTC) | | `2026-01-15T10:30:00Z` | `2026-01-15T10:30:00Z` | | `2026-01-15T10:30:00.500-05:00` | `2026-01-15T15:30:00.500Z` | | `7d` | 7 days before request time, truncated to the minute | | `30min` | 30 minutes before request time, truncated to the minute | Relative deltas may be combined with absolute values across the two bounds of a window: ```json theme={"system"} { "query": "...", "published_after": "1y", "published_before": "6mo" } ``` ```json theme={"system"} { "query": "...", "acquired_after": "2024-01-01", "acquired_before": "30d" } ``` For example, at request time `2026-05-18T14:23:45Z`, `acquired_after: "2h"` resolves to `2026-05-18T12:23:00Z` — a document acquired at `12:22:59Z` is dropped, one acquired at `12:23:00Z` is kept. Mind the difference between a date and a timestamp on a `_before` bound: `acquired_before: "2026-05-01"` keeps a page acquired at `2026-05-01T14:31:13Z`, while `acquired_before: "2026-05-01T00:00:00Z"` drops it. Use the date form to mean "up to and including that day", and the timestamp form to cut at midnight. # Authentication Source: https://docs.keenable.ai/authentication Get an API key, pass it to the API, CLI and MCP server, and read the error responses. Keenable authenticates with an API key. [Log in](https://app.keenable.ai/login) to obtain one. Authenticated requests remove the hourly request limit, are rate limited per organization rather than per IP, and report per-call usage back to you. See [rate limits](/rate-limits) for the numbers. ## CLI Run `keenable login` and follow the browser flow to authenticate. The CLI uses device authorization, so an agent can provide the link for a human to complete. ```bash theme={"system"} keenable login ``` ## MCP The easiest way is via the CLI: ```bash theme={"system"} # if not authenticated yet keenable login keenable configure-mcp --all ``` This detects supported clients and configures them with your credentials. Alternatively, create an API key in the [console](https://app.keenable.ai/console) and add it manually — see [API key header](#api-key-header) below. ## API key header For manual MCP setup, pass your API key as the `X-API-Key` header. ```bash Claude Code theme={"system"} claude mcp add keenable \ --transport http https://api.keenable.ai/mcp \ --scope user \ --header "X-API-Key: keen_" ``` ```toml Codex theme={"system"} [mcp_servers.keenable] url = "https://api.keenable.ai/mcp" http_headers = { "X-API-Key" = "keen_" } ``` ```json JSON theme={"system"} { "mcpServers": { "keenable": { "url": "https://api.keenable.ai/mcp", "headers": { "X-API-Key": "keen_" } } } } ``` ```json Claude Desktop theme={"system"} { "mcpServers": { "keenable": { "command": "npx", "args": ["-y", "@keenable/mcp-server"], "env": { "KEENABLE_API_KEY": "keen_" } } } } ``` Claude Desktop needs that last shape: its config file accepts only local `command` servers, so a remote `"url"` entry is ignored without an error. If you don't need your key applied, skip the file and add Keenable as a [connector](/mcp-server#claude-remote-connector) instead. Keys are scoped to your workspace, never expire on their own, and can be rotated at any time. Alternatively, you can pass your API key in the `Authorization` header using the `Bearer` scheme. This can be useful when integrating with MCP clients or frameworks that only support bearer tokens. ```http theme={"system"} Authorization: Bearer keen_ ``` If both headers are present, `X-API-Key` takes precedence. ## Evaluating without a key The MCP server, the CLI and the HTTP API all run unauthenticated, so you can try them before creating an account. For the HTTP API this means the `/public` endpoints described in the [API reference](/api-reference#authentication), which take no key but require an `X-Keenable-Title` header naming your application. Unauthenticated requests draw on a shared public pool capped at 1,000 requests per hour and 10 requests per second per IP, and they carry no usage metadata — the `_meta["keenable/usage"]` block described in the [MCP reference](/mcp-server#metadata-for-integrators) is only returned for authenticated calls. Use an API key for anything beyond a first look: the per-IP pool is shared with everyone else on your network or egress address, so its available headroom is outside your control. ## Paying per request without a key A third option sits between the two: an agent with a wallet can [pay per request](/pay-per-request) over the x402 protocol — \$0.01 per search or fetch in USDC on Base, on the `/v1/x402/*` paths, with no account and no key. It has its own limits and error responses, described on that page. For regular use an API key stays cheaper. ## Error responses | Status | Meaning | | - | - | | `400` | Malformed API key (invalid format) | | `401` | Missing or invalid API key | | `402` | No credits available — the monthly allowance is spent and no purchased credits remain (see [credits](/credits)) | | `403` | API key disabled or revoked | | `429` | Rate limit exceeded | # Auto top-up Source: https://docs.keenable.ai/auto-top-up Refill purchased credits automatically: the threshold, amount and monthly limit you set, when your card is charged, and what happens when a charge is declined. Auto top-up keeps your purchased balance from running out. You choose a threshold and an amount, and whenever the balance falls below the threshold, Keenable charges your saved card that amount and adds the credits it buys. A monthly limit caps how much can be charged in one month. It is off until you turn it on, under **Billing** in the [console](https://app.keenable.ai/console/billing). ## Settings | Setting | Default | Allowed | | - | - | - | | When balance falls below | \$5 | \$1 to \$10,000 | | Add | \$20 | \$5 to \$10,000 | | Monthly limit | \$1,000 | \$5 to \$100,000, and no less than one top-up | A top-up buys credits at the same price as a purchase in the console, and the console shows how many requests each amount buys. Change the settings at any time while auto top-up is on: **Edit**, then **Save**. ## Turning it on The first time, the console sends you to Stripe to add a card. If your balance is already below the threshold, Stripe takes the first top-up in the same step; otherwise it only saves the card. When you come back, auto top-up is on. Once a card is saved, turning it on or saving new settings takes effect at once. Turning it on, or saving new settings, charges straight away when your balance is already below the threshold. The console says so above the button before you confirm. ## When your card is charged **The threshold watches your purchased balance**, the dollar balance shown on the Billing page. The free [monthly allowance](/credits#monthly-allowance) does not count toward it. Free requests are used first, so while some are left your purchased balance does not go down and nothing is charged. **Every charge is an invoice.** An automatic top-up appears in the console's invoice list, like any purchase. **The monthly limit counts whole top-ups.** A top-up is charged only if all of it fits under what is left of the limit. Once the next one would go over, nothing more is charged until the limit resets; the console shows how much has been charged this month and when the limit resets, on the same day as the free allowance. If the balance and the allowance both run out before then, requests return `402`, as described in [Credits](/credits#when-credits-run-out). ## When a charge fails If the card is declined, or the bank asks to confirm the payment (3-D Secure, which cannot happen while you are away), auto top-up turns off. Nothing is retried. The person who turned it on gets an email, and the Billing page shows **Off · payment failed** with the reason. To start again, check the card, or replace it with **Manage payment methods** at the top of the Billing page, then turn auto top-up back on. If your balance is below the threshold at that point, the first top-up is charged straight away. If a charge fails for a reason that is not the card's, such as an outage on our side or Stripe's, auto top-up stays on and tries again the next time the balance is low. ## Changing the card or turning it off While auto top-up is on, **Change card** opens Stripe to set a different card. **Turn off** stops all further charges at once; the credits already added stay on your balance and never expire. Automatic charges are covered by the Automatic Top-Ups section of the [Terms of Service](https://keenable.ai/terms). Turning auto top-up on authorizes charges for exactly the amounts you set. # Changelog Source: https://docs.keenable.ai/changelog Product updates and announcements from Keenable. **Pay per request with x402** — search and fetch can now be bought one call at a time, with no account and no API key. `/v1/x402/search` and `/v1/x402/fetch` answer `402` with an [x402](https://www.x402.org) offer; the client pays \$0.01 in USDC on Base and retries, and gets the result with an on-chain receipt. Any standard x402 client works. See [Pay per request](/pay-per-request). **Auto top-up** — purchased credits can now refill themselves. Set a threshold, an amount and a monthly limit under Billing in the console, and Keenable charges your saved card whenever your balance falls below the threshold. A declined card turns it off and emails you; nothing is retried. See [Auto top-up](/auto-top-up). **Built into Hermes Agent.** Keenable ships in [Hermes Agent](/integrations/hermes-agent) as a bundled web backend for `web_search` and `web_extract`, with no plugin and no key. `hermes config set web.backend keenable` selects it; the standalone `hermes-keenable-web` plugin is no longer needed. **Keenable on Baseten** — web search and page fetch run as [server-side tools](/integrations/baseten) on Baseten Model APIs. Name the two tools in a chat request and the model searches and reads pages inside Baseten's inference loop; no MCP client and no Keenable key, because Baseten meters the calls onto your Baseten invoice. **Keenable SELECT in both directories** — the [SELECT](/select) server is now a listed connector in Claude (Settings → Connectors → **Keenable SELECT**) and a published plugin in OpenAI's Plugins Directory, shared by ChatGPT and Codex. Both sign you in with your Keenable account; nothing to paste. The custom-connector and developer-mode routes keep working for accounts the listings have not reached yet. **Point-in-time search** — [`query_time`](/mcp-server#point-in-time-search) is now documented on both the MCP tool and the [Search API](/api-reference/search): search the index as it stood at an instant, and have relative date filters resolve against that instant rather than now. **Two more search parameters** — `max_results` caps how many results come back (1–50), and `snippet_max_length` caps the snippet per result. Both are documented on the [MCP server](/mcp-server) and [Search API](/api-reference/search) pages. **Integrations directory** — a [catalog](/integrations) of the frameworks, agents and editors Keenable plugs into, with one-click installs where the client supports them. Added this month: Claude, Convex, OpenCode, Pipecat, the Vercel AI SDK tool package, Lightpanda, LibreChat, and a listing in the OpenAI Plugins Directory. **Keyless endpoints, written down** — `/v1/search/public` and `/v1/fetch/public` are documented in the [API reference](/api-reference). They exist to evaluate the API without an account; keyed access is what production use wants. **Credits, on their own page** — the monthly credit allowance and the per-SKU pricing model moved out of Rate limits into [Credits](/credits). **Prompt-driven extraction** — the [Fetch endpoint](/api-reference/fetch) accepts a `prompt`, and returns what an LLM read out of the page for that instruction instead of the whole page. **Usage and overrides over MCP** — every authenticated tool call now reports its cost under [`_meta["keenable/usage"]`](/credits#reading-usage-per-call), and an integrator can pin the search mode with [`_meta["keenable/overrides"]`](/mcp-server#operator-overrides-request-meta). **Date-only cutoffs cover the whole day** — a bare `YYYY-MM-DD` on `acquired_before` / `published_before` now resolves to `23:59:59.999` on that date, so pages from the named day are kept at either end of a window. Pass a timestamp to cut at an exact instant. **Live fetching on the Fetch API** — the [Fetch endpoint](/api-reference/fetch) now accepts a `live` parameter to retrieve a page in real time instead of serving the cached copy. Use it when freshness matters more than latency. **Published rate limits** — request limits are now documented: 10 requests/second per organization for authenticated API keys, and a 1,000 requests/hour quota for unauthenticated use. See [Rate limits](/rate-limits). **One-click MCP install** — added one-click install buttons for Cursor and VS Code (including VS Code Insiders) so you can add Keenable to your editor without editing config by hand. See [MCP server](/mcp-server). **Unauthenticated access** — the [CLI](/cli) and [MCP server](/mcp-server) now answer requests without an API key, on a shared public tier, so you can evaluate them before creating an account. See [Authentication](/authentication). **Authentication & rate-limit guides** — new [Authentication](/authentication) and [Rate limits](/rate-limits) pages cover the device-code login flow, API keys, and the unauthenticated vs. authenticated tiers. **Keenable API launch** — the initial public release of the Keenable platform for AI agents: the [Search API](/api-reference/search) (`/v1/search`), the [Fetch API](/api-reference/fetch) (`/v1/fetch`), the [CLI](/cli), and the [MCP server](/mcp-server) — one API key across all of them. # CLI Source: https://docs.keenable.ai/cli Authenticate, configure MCP, and search from the terminal [`keenable-cli`](https://github.com/keenableai/keenable-cli) is a single-binary CLI for Keenable — configure MCP for your AI clients, search the web, and manage API keys from the terminal. Run `keenable login` once to authenticate; the CLI stores your credentials and reuses them for every command. See [authentication](/authentication) for the details, and [rate limits](/rate-limits) for the caps that apply with and without a key. ## Installation ```bash Homebrew theme={"system"} brew install keenableai/tap/keenable-cli ``` ```bash macOS / Linux theme={"system"} curl --proto '=https' --tlsv1.2 -LsSf \ https://github.com/keenableai/keenable-cli/releases/latest/download/keenable-cli-installer.sh \ | sh ``` ```powershell PowerShell theme={"system"} irm https://github.com/keenableai/keenable-cli/releases/latest/download/keenable-cli-installer.ps1 | iex ``` ```bash Source theme={"system"} cargo install --git https://github.com/keenableai/keenable-cli ``` ## Quick start ```bash theme={"system"} # Login with your Keenable account keenable login # Configure MCP for your AI clients (Claude Code, Cursor, etc.) keenable configure-mcp --all # Search the web keenable search "rust async patterns" -p ``` ## Authentication ```bash theme={"system"} # Device-code login (opens browser) keenable login # Save API key directly (CI, servers) keenable login --api-key # Clear stored credentials keenable logout ``` ## MCP setup ```bash theme={"system"} # Show client status keenable configure-mcp # Configure all detected clients keenable configure-mcp --all # Configure a specific client keenable configure-mcp --cursor # Remove Keenable from all clients keenable reset --all ``` Supported clients: Claude Code, Cursor, Windsurf, Codex, OpenCode. ## Search ```bash theme={"system"} # YAML output (for agents) keenable search "AI news" # Pretty output (for humans) keenable search "AI news" -p # Restrict to site keenable search "AI news" --site techcrunch.com # Date filter keenable search "AI news" --published-after 2026-01-01 # Date filter keenable search "AI news" --acquired-before 2026-05-01 # Longer snippets, in characters (180-10000) keenable search "rust async" --snippet-max-length 2000 # More results (1-50, default 10) keenable search "rust async" --max-results 25 # Point-in-time search: the index as it stood then keenable search "AI news" --query-time 2026-01-01T00:00:00Z # Use a specific API key keenable search "AI news" --api-key KEY ``` ### Date and time filters `--acquired-after`, `--acquired-before`, `--published-after`, and `--published-before` each accept **one** of the following formats: * **Date** in RFC 3339 `full-date` form (`YYYY-MM-DD`) — covers that whole day in UTC. On an `--*-after` flag it resolves to `00:00:00` on that date; on a `--*-before` flag it resolves to `23:59:59.999` on that date, so pages from the named day are kept at either end. Pass a timestamp instead to cut at an exact instant. * **Timestamp** in ISO 8601 form (`YYYY-MM-DDTHH:MM:SS[.sss][±HH:MM]`). When a timezone offset is not provided, the timezone is interpreted as UTC. * **Relative delta** (``, e.g. `7d`, `30min`) — resolves to the request time minus the delta, truncated to minute precision, or to [`--query-time`](#point-in-time-search) minus the delta when that is set. Supported units: `min` (minutes), `h` (hours), `d` (days), `mo` (months), `y` (years). ```bash theme={"system"} # Resolves to 2026-01-15T00:00:00Z keenable search "AI news" --published-after 2026-01-15 # Resolves to 2026-01-15T23:59:59.999Z — the 15th is kept keenable search "AI news" --published-before 2026-01-15 # No offset → UTC keenable search "AI news" --published-after 2026-01-15T10:30:00 keenable search "AI news" --published-after 2026-01-15T10:30:00.500-05:00 # 7 days before request time keenable search "AI news" --acquired-after 7d # 30 minutes before request time keenable search "AI news" --acquired-after 30min ``` Relative deltas may be combined with absolute values across the two bounds of a window: ```bash theme={"system"} keenable search "AI news" --published-after 1y --published-before 6mo keenable search "AI news" --acquired-after 2024-01-01 --acquired-before 30d ``` For example, at request time `2026-05-18T14:23:45Z`, `--acquired-after 2h` resolves to `2026-05-18T12:23:00Z` — a document acquired at `12:22:59Z` is dropped, one acquired at `12:23:00Z` is kept. Mind the difference between a date and a timestamp on a `--*-before` flag: `--acquired-before 2026-05-01` keeps a page acquired at `2026-05-01T14:31:13Z`, while `--acquired-before 2026-05-01T00:00:00Z` drops it. Use the date form to mean "up to and including that day", and the timestamp form to cut at midnight. ### Point-in-time search `--query-time` moves the whole search back to an instant: a page acquired after it is not a candidate at all, so the answer is the one the index would have given then rather than today's answer filtered down. It takes the same formats as the filters above. ```bash theme={"system"} keenable search "champions league final" --query-time 2025-05-30T00:00:00Z ``` It also **re-bases every relative delta in the same command**. A delta resolves against the request time only when `--query-time` is absent; with it, `--published-after 30d` means thirty days before `--query-time`: ```bash theme={"system"} keenable search "cloud computing" --query-time 2026-06-13T00:00:00Z --published-after 30d ``` returns pages published between `2026-05-14` and `2026-06-13` — not the last thirty days. Absolute bounds are unaffected. A date-only `--query-time` resolves to `00:00:00` UTC, which is the opposite end of the day from a date-only `--*-before` flag: `--query-time 2026-06-13` cuts at the start of 13 June, while `--acquired-before 2026-06-13` keeps all of it. ## Fetch ```bash theme={"system"} # Fetch page content (YAML) keenable fetch https://example.com # Pretty output keenable fetch https://example.com -p # LLM extraction: return only the instruction's output instead of the full page keenable fetch https://example.com --prompt "List all pricing tiers with their monthly prices" ``` ## Updating The CLI checks for updates automatically once per hour. To update manually: ```bash theme={"system"} # Homebrew brew update && brew upgrade keenable-cli ``` Or re-run the installer script. # Credits Source: https://docs.keenable.ai/credits The free allowance of 100,000 requests a month, how authenticated usage is metered against it, and what happens when it runs out. [Rate limits](/rate-limits) cap how *fast* you can call the API. Credits cap how *much*: authenticated usage is metered in credits, and every organization gets an allowance of them at no cost. ## Monthly allowance **100,000 requests a month, free.** The allowance resets each month, so it is a recurring budget rather than a one-off trial. To keep going once it is spent, buy a credit package in the [console](https://app.keenable.ai/console) — that is also where your current allowance and remaining balance are shown. Quoting it in requests works because a credit *is* a request for the two operations that matter here: search and fetch each cost one. Anything priced differently draws more than one credit per call, which is why the rule below still holds — read what a call cost from the response rather than counting calls. Unauthenticated requests are not metered at all. They are capped by the [request limits](/rate-limits) instead, so evaluating without a key does not draw on the allowance. [Pay-per-request](/pay-per-request) calls do not touch it either: each one is paid for in USDC as it is made. ## What a call costs Each billed operation has its own credit price, keyed by SKU: | SKU | Operation | | - | - | | `search.realtime` | A search served in `realtime` mode | | `search.pro` | A search served in `pro` mode | | `fetch` | A fetch of Keenable's indexed copy of a page | | `fetch.live` | A fetch made live from the source ([`live=true`](/api-reference/fetch)) | Two things to keep in mind when you budget against these: * **Prices are per-organization.** Read what a call actually cost from the response rather than assuming a flat rate. * **Search mode picks the SKU.** Over HTTP, pass [`mode`](/api-reference/search#request) on `/v1/search`; over MCP an integrator can pin it with [`_meta["keenable/overrides"]`](/mcp-server#operator-overrides-request-meta). When neither is set the search runs in `pro` mode, unless your organization has a different default. The response echoes the mode that served the call. ## Reading usage per call Over MCP, every authenticated tool call reports its cost under `_meta["keenable/usage"]`, alongside the result rather than inside the model-visible `content`: ```json theme={"system"} { "content": [{ "type": "text", "text": "..." }], "_meta": { "keenable/usage": { "sku": "search.realtime", "amount": 1, "credits": 1, "paid": true } } } ``` `credits` is what the call cost, and `paid` tells you which bucket it came from — `false` for the free monthly allowance, `true` for purchased credits. This is what to attribute cost against per call; see [Metadata for integrators](/mcp-server#metadata-for-integrators) for the full field list. Unauthenticated calls are unbilled and omit the block entirely. ## When credits run out With the allowance spent and no purchased credits remaining, `/v1/search` and `/v1/fetch` return [`402`](/authentication#error-responses). Buy a package in the [console](https://app.keenable.ai/console) to resume, or wait for the allowance to reset. To keep it from getting that far, turn on [auto top-up](/auto-top-up): it buys more credits whenever your purchased balance runs low. # Quickstart Source: https://docs.keenable.ai/index Install the Keenable CLI, log in, wire the MCP server into your agents, and run your first search. Keenable provides web search and content access for AI agents. There are three ways in — for an agent, **MCP is the recommended one**; the CLI wires it up and searches from the terminal, and the REST API is there when you want to call the endpoints directly. Add tools to your agent — the recommended path Search from the terminal, and configure MCP in one command Call the endpoints directly over HTTP Install the CLI with your preferred method: ```bash Homebrew theme={"system"} brew install keenableai/tap/keenable-cli ``` ```bash macOS / Linux theme={"system"} curl --proto '=https' --tlsv1.2 -LsSf \ https://github.com/keenableai/keenable-cli/releases/latest/download/keenable-cli-installer.sh \ | sh ``` ```powershell PowerShell theme={"system"} irm https://github.com/keenableai/keenable-cli/releases/latest/download/keenable-cli-installer.ps1 | iex ``` Then authenticate — this stores your credentials so every command and every MCP client the CLI configures below picks them up: ```bash theme={"system"} keenable login ``` To give your agents web search, set up MCP. The CLI can configure it for all supported agents in one command: ```bash theme={"system"} keenable configure-mcp --all ``` This sets up Claude Code, Cursor, Windsurf, and other detected clients. See [MCP](/mcp-server) for manual setup. Once installed, run a search straight from the terminal: ```bash theme={"system"} keenable search "AI news" -p ``` ## Prefer raw HTTP? The REST API is a first-class way in — no CLI, no MCP. `POST` your query with your key in the `X-API-Key` header: ```bash theme={"system"} curl -X POST "https://api.keenable.ai/v1/search" \ -H "X-API-Key: keen_" \ -H "Content-Type: application/json" \ -d '{ "query": "typescript best practices" }' ``` See the [API reference](/api-reference) for every endpoint, the keyless `/public` twins, and playgrounds you can call with your own key. For an agent, though, MCP is the smoother path — it hands the tools to the model directly rather than leaving it to compose HTTP calls. That's it. See [authentication](/authentication) for API keys and [rate limits](/rate-limits) for the request caps that apply. # Integrations Source: https://docs.keenable.ai/integrations Connect Keenable web search to your agent stack — LangChain, LlamaIndex, Haystack, MCP, and more. Bring Keenable's web search and content retrieval into the agents and AI tools you already use. Every integration takes the same `KEENABLE_API_KEY` credential, so one key covers your whole stack — see [Authentication](/authentication). Tell your agent to install this itself: `Read https://app.keenable.ai/SKILL.md and install this skill.` ## MCP server Point any MCP-compatible client at `https://api.keenable.ai/mcp` for web search and page-fetch tools. See the [MCP server reference](/mcp-server) for one-click install links (Cursor, VS Code) and per-client setup (Claude Code, Codex, Claude Desktop, Claude connectors). Also listed on [Smithery](https://smithery.ai/servers/keenable/web-search), [Glama](https://glama.ai/mcp/connectors/ai.keenable.api/keenable-web-search), the [official MCP Registry](https://registry.modelcontextprotocol.io/v0/servers?search=ai.keenable), and [PulseMCP](https://www.pulsemcp.com/servers/keenable-web-search). ## Available now ### Inference providers | Integration | Description | | - | - | | [Baseten](/integrations/baseten) | Server-side tools on Baseten Model APIs — no client, no key. | ### Frameworks & SDKs | Integration | Description | | - | - | | [Haystack](/integrations/haystack) | Listed in the Haystack integrations catalog. | | [LangChain](/integrations/langchain) | `langchain-keenable`, listed in the LangChain docs. | | [LlamaIndex](/integrations/llamaindex) | `KeenableToolSpec` for search and read. | | [Mastra](/integrations/mastra) | `@keenable/mastra` search and fetch tools. | | [NVIDIA NeMo Agent Toolkit](/integrations/nemo-agent-toolkit) | `keenable__search` / `keenable__fetch`. | | [Vercel AI SDK](/integrations/vercel-ai-sdk) | `@keenable/ai-sdk` tools for `generateText` and `streamText`. | ### Coding agents | Integration | Description | | - | - | | [Codex](/integrations/codex) | Both routes — `config.toml` or the plugin listing — plus the one line that stops the built-in search winning. | | [DeepSeek Harness](/integrations/deepseek-harness) | Plugin for the built-in `web_search` and `web_fetch`, with any model and no key. | | [OpenCode](/integrations/opencode) | Native tools, no CLI or login step. | | [Pi](/integrations/pi) | Web search and page fetch for the Pi coding agent. | ### Chat apps | Integration | Description | | - | - | | [ChatGPT](/integrations/chatgpt) | Published in OpenAI's Plugins Directory — install from the catalog. | | [Claude](/integrations/claude) | Listed in Claude's connector directory — nothing to paste. | | [LibreChat](/integrations/librechat) | Built-in search provider and scraper — a fully keyless web-search stack. | ### Low-code builders | Integration | Description | | - | - | | [Convex](/integrations/convex) | `@keenable/convex` — `search` and `fetch` component actions. | | [Dify](/integrations/dify) | Search and page fetch in the Dify marketplace. | | [Langflow](/integrations/langflow) | Drag-and-drop search component. | | [n8n](/integrations/n8n) | Community node with Search and Fetch operations. | | [RAGFlow](/integrations/ragflow) | Built into the visual agent builder. | ### Search & RAG | Integration | Description | | - | - | | [de-anthropocentric research engine](/integrations/de-anthropocentric-research-engine) | Pre-wired in the engine's bundled MCP config. | | [Hermes Agent](/integrations/hermes-agent) | Built-in web backend for `web_search` and `web_extract`, keyless. | | [ketch](/integrations/ketch) | Built into the Go CLI, zero-config. | | [Lightpanda](/integrations/lightpanda) | The keyless engine behind the headless browser's `search` tool. | | [Nanobot](/integrations/nanobot) | Built in, with a DuckDuckGo fallback. | | [Omnigent](/integrations/omnigent) | Built-in backend for the `web_search` tool. | | [OpenClaw](/integrations/openclaw) | Installable from ClawHub or npm. | | [SearXNG](/integrations/searxng) | Core engine, shortcut `!keen`. | | [Vellum Assistant](/integrations/vellum-assistant) | Built-in provider, selected in `config.json`. | | [web-search-plus (Hermes)](/integrations/web-search-plus) | Community plugin for the Hermes agent. | | [WeKnora (Tencent)](/integrations/weknora) | Built into Tencent WeKnora. | | [ZeroClaw](/integrations/zeroclaw) | Built-in provider for `web_search_tool`, selected in `config.toml`. | ### Memory | Integration | Description | | - | - | | [cognee](/integrations/cognee) | Web-fetch backend for URL ingestion into graph memory, built into cognee 1.5+. | ### Voice agents | Integration | Description | | - | - | | [Pipecat](/integrations/pipecat) | `KeenableWebSearch`, tuned for voice latency. | ## Coming soon * **AnythingLLM** — a built-in Keenable web-search provider. * **AstrBot** — Keenable web search and page-fetch tools for AstrBot. * **CrewAI** — `KeenableSearchTool`, a web-search tool for CrewAI agents. * **Flowise** — a Keenable Search tool node for the Flowise canvas. * **GPT-Researcher** — a Keenable web-search retriever for the GPT-Researcher deep-research agent. * **Lobe Chat** — a built-in Keenable web-search provider. * **Onyx** — a built-in Keenable web-search provider. * **smolagents (Hugging Face)** — `KeenableSearchTool`, a web-search tool for smolagents (and txtai agents through it). Missing yours? [Get in touch](https://keenable.ai/contact) — integrations ship weekly. # Baseten Source: https://docs.keenable.ai/integrations/baseten Keenable web search and page fetch as server-side tools on Baseten Model APIs — the model calls them inside the inference loop. *Server-side tools* Baseten Model APIs run Keenable's tools on their own side of the request. You send one chat request that names the tools; the model searches, reads the pages it picked, and answers from what it read. Nothing runs in your process — no MCP client, no tool loop to write, and no Keenable API key, because Baseten meters the calls and bills them with your inference. → [Baseten Model APIs](https://docs.baseten.co/inference/model-apis/overview) ## Install ### Enable server tools Server tools are in early access. Ask Baseten to enable them for your workspace, then send the `x-baseten-server-tools: true` header with each request. ### Name the tools in a request ```bash theme={"system"} curl -sS -X POST https://inference.baseten.co/v1/messages \ -H "Authorization: Bearer ${BASETEN_API_KEY}" \ -H "Content-Type: application/json" \ -H "x-baseten-server-tools: true" \ -d '{ "model": "deepseek-ai/DeepSeek-V4.1-Flash", "max_tokens": 4096, "messages": [ {"role": "user", "content": "What shipped in the latest Bun release? Cite the pages you used."} ], "tools": [ {"type": "baseten__keenable__search_web_pages"}, {"type": "baseten__keenable__fetch_page_content"} ] }' ``` A tool entry is a type and nothing else. The schemas live on Baseten's side, so there is no Keenable JSON schema to paste and no arguments to declare. ### Or use an SDK The endpoint is the Anthropic Messages API, so an Anthropic SDK client reaches it by base URL. The SDK sends the key as `x-api-key`, which Baseten does not read, so set both headers yourself: ```python theme={"system"} import os from anthropic import Anthropic api_key = os.environ["BASETEN_API_KEY"] client = Anthropic( api_key=api_key, base_url="https://inference.baseten.co", default_headers={ "Authorization": f"Bearer {api_key}", "x-baseten-server-tools": "true", }, ) answer = client.messages.create( model="deepseek-ai/DeepSeek-V4.1-Flash", max_tokens=4096, messages=[{"role": "user", "content": "What shipped in the latest Bun release?"}], tools=[ {"type": "baseten__keenable__search_web_pages"}, {"type": "baseten__keenable__fetch_page_content"}, ], ) ``` The tools also work on `/v1/chat/completions` and on `/v1/responses`. Messages is the one to prefer: mirroring the assistant, tool-call and tool-result blocks back byte for byte is straightforward there, and that is what keeps the KV cache warm across turns. ## The two tools | Tool | What it does | | - | - | | `baseten__keenable__search_web_pages` | Searches the web and returns ranked results with URLs, titles and snippets. | | `baseten__keenable__fetch_page_content` | Reads one URL and returns the page as clean markdown. | They are the [MCP server](/mcp-server)'s two tools, with the same names and the same arguments — the `baseten__keenable__` prefix is Baseten's namespace for hosted tools. The model fills in every argument it sees. ## What comes back The whole loop is in one response. Each round the model ran is a `tool_use` block — its `name` is the full tool type, its `input` is the arguments the model wrote — followed by the `tool_result` block Keenable answered with, and the final answer is the last `text` block. Tool calls the model emits together are executed together, so independent lookups overlap instead of running one after another. Two things end a turn early and are worth handling: * **The iteration cap.** Baseten limits how many tool rounds one request may run. A turn that hits the cap comes back with `stop_reason: "pause_turn"`, and it can stop on a tool result with no answer written. Send the message history back in a new request to continue with a fresh budget. * **Failed tool calls.** A call that fails is marked as an error for the model, which usually retries or works around it. It is not billed. * **`max_tokens`.** The budget covers the reasoning and commentary between the tool rounds, not the answer alone, so a grounded answer needs more of it than the same question asked without tools. Too low a value returns `stop_reason: "max_tokens"` and a sentence that stops mid-word. ## Reading your usage Alongside the content, the response carries a `baseten` object with one entry per tool call: the `provider` and tool `name`, its `status`, whether it was `billable`, and the Keenable [SKU](/credits#what-a-call-costs) and quantity it was metered as. Failed calls show `billable: false` and no SKU. Use it to attribute cost per call, and treat the shape as informational — it is Baseten's extension field, not a stable protocol surface. ## Models Server tools are enabled per model, on a set that keeps growing — Baseten's model list is what to check. The flagship open models carry them — DeepSeek V4.1 Flash, DeepSeek V4 Pro, GLM 5.3 and 5.3 Fast, and Kimi K3 among them. ## Billing No Keenable account is involved and nothing is metered on your Keenable [credits](/credits). Baseten reads the SKU and amount Keenable reports for each successful call — one of the [four SKUs](/credits#what-a-call-costs) — and puts the usage on your Baseten invoice. Failed calls are not billed. Per-model tool usage is at the bottom of the model's metrics page in the Baseten console. ## When to call Keenable directly The hosted tools trade control for having nothing to run. Bring your own key and use the [MCP server](/mcp-server) or the [REST API](/api-reference) instead when you want to pick the search mode, filter by site or date, search the index as it stood at [a past instant](/mcp-server#point-in-time-search), or extract a page with a [prompt](/api-reference/fetch). # ChatGPT Source: https://docs.keenable.ai/integrations/chatgpt Keenable is published in OpenAI's Plugins Directory — install it from the catalog, with no config file to edit. *Plugin directory listing* Keenable is a published plugin in OpenAI's Plugins Directory — no config file, no developer mode, and no MCP URL to paste. It bundles web search and page fetch with two skills: focused lookups, and multi-source research with citations. → [Keenable in the OpenAI Plugins Directory](https://chatgpt.com/plugins/plugin_asdk_app_6984a0b396708191a7856354b86ce047) ## Install ### Add the plugin Open the listing, or find **Keenable** in the directory, and install it. ### Sign in The plugin authenticates with your Keenable account through OAuth — click through the sign-in prompt on first use. There is no API key to paste: usage is attributed to the account you connect and metered against that account's organization, so it draws on the same [rate limits](/rate-limits) and [credits](/credits) as every API key the organization has issued. ### Use it Two tools become available: `search_web_pages` for ranked results, and `fetch_page_content` to read any URL as clean markdown. Ask something that needs the live web, or point it at a URL. **Keenable SELECT** is a second Keenable plugin in the same directory. It is a separate server that answers a question with one SQL query over live web results and returns a table; see [SELECT](/select). **Using Codex?** It installs from this same directory, but it also ships its own web search, enabled by default, which will keep winning unless you turn it off — and the `config.toml` route is the one that takes your own API key. See [Codex](/integrations/codex). # Claude Source: https://docs.keenable.ai/integrations/claude Keenable is a listed MCP connector in Claude — connect it from Settings → Connectors. *MCP connector* Claude (claude.ai, Desktop, and mobile) connects to Keenable through its built-in connector directory — no config file and no OAuth client to register. ## Install ### Add the connector Keenable is a listed connector in Claude, so open **Settings → Connectors**, find **Keenable**, and there is nothing to paste. Not listed yet in your account? Add it by URL instead — **Settings → Connectors → Add custom connector** — leaving OAuth Client ID / Secret empty: ``` https://api.keenable.ai/mcp ``` ### Connect **A listed connector is not yet an active one.** Finding Keenable in the directory only adds it. Open **Manage connectors**, find **Keenable**, and click **Connect** — the tools appear immediately. If Claude answers without ever searching, this is the step that was missed. ### Enable it in a chat In the composer, click **+ → Connectors** and toggle **Keenable** on for the conversation. Claude's connector UI has no field for a request header, so the connector runs on the shared public tier at [lower rate limits](/rate-limits). To apply your own API key on Claude Desktop, use the [local bridge](/mcp-server#local-bridge-with-an-api-key) instead of the connector. **Keenable SELECT** is listed in the same directory as a second connector. It is a separate server that answers a question with one SQL query over live web results and returns a table; see [SELECT](/select). → [Full MCP server reference](/mcp-server) # Codex Source: https://docs.keenable.ai/integrations/codex Wire Keenable into Codex over MCP — and stop its built-in web search from winning every lookup. *MCP server* Codex reaches Keenable two ways: the plugin listing it shares with ChatGPT, or an MCP entry in `config.toml`. The MCP route takes your own API key, so it is the one to use on a paid tier — but it needs a second step that is easy to miss. **Adding the server is not enough.** Codex ships its own web search, enabled by default, and it will keep reaching for that instead of Keenable — so the tools look connected in `/mcp` yet never get called unless you name them in the prompt. [Turn the built-in search off](#turn-off-the-built-in-search) and the problem goes away. ## Install ### Add the server Add the following to `~/.codex/config.toml`: ```toml theme={"system"} [mcp_servers.keenable] url = "https://api.keenable.ai/mcp" http_headers = { "X-API-Key" = "keen_" } ``` The key travels in the `X-API-Key` header, not an environment variable — Codex reads nothing from `KEENABLE_API_KEY` for a remote server. Create one in the [console](https://app.keenable.ai/console); see [Authentication](/authentication). Omit the `http_headers` line and the server still answers, on the shared public tier at [lower rate limits](/rate-limits). ### Turn off the built-in search Codex enables web search by default for local chats, in `cached` mode — results come from an index OpenAI maintains rather than a live fetch. Setting `web_search` to `disabled` removes that tool entirely, and with nothing to fall back on Codex uses `search_web_pages` for every lookup. It is a **top-level** key, so it has to sit above the `[mcp_servers.keenable]` line. A bare key written after a table header belongs to that table, which would make it `mcp_servers.keenable.web_search` — not a setting Codex looks at, and it fails quietly. The finished file: ```toml theme={"system"} web_search = "disabled" [mcp_servers.keenable] url = "https://api.keenable.ai/mcp" http_headers = { "X-API-Key" = "keen_" } ``` The mode key takes four values: `cached` (the default), `indexed`, `live`, and `disabled`. A full-access sandbox — `--yolo` or `danger-full-access` — silently upgrades `cached` to `live`, so a permissive session searches the live web whether or not you asked for it. Don't confuse the mode with the `[tools.web_search]` table, which carries settings like `allowed_domains` and `context_size` and does **not** switch the tool off. Older releases used `[tools] web_search = true` and then `[features] web_search_request = true`; both are deprecated in favour of the top-level key. Want to keep the built-in tool for other work? Scope the change to a profile instead, and invoke it with `codex --profile research`. Here the key belongs inside the table, which is the one place nesting it is correct: ```toml theme={"system"} [profiles.research] web_search = "disabled" ``` ### Tell the agent which tool to use Codex reads `AGENTS.md` for project instructions — `~/.codex/AGENTS.md` applies everywhere. Naming the tools there is worth doing even with the built-in search disabled, because Codex loads MCP tool schemas on demand: it sees a tool's name and description upfront and fetches the rest only when it decides the tool is relevant, so that decision is made on the description alone. ```markdown theme={"system"} ## Web search - Use `search_web_pages` for anything that needs the web. - Follow a search with `fetch_page_content` to read the sources you cite. ``` ### Verify Run `/mcp` in the TUI to confirm the server connected and both tools are listed, then ask something that needs the live web. Codex should call `search_web_pages` without being told to. Still going around Keenable on a recent Codex? Check `codex --version`. Releases before v0.121.0 registered on-demand tool names in a format their own lookup table did not match, which produced exactly this symptom for every MCP server, not just this one. ## No config file Keenable is also published in OpenAI's Plugins Directory, the catalog Codex shares with ChatGPT. Install it from the [listing](https://chatgpt.com/plugins/plugin_asdk_app_6984a0b396708191a7856354b86ce047) and sign in through OAuth — there is no key to paste. Usage is attributed to the account you connect and metered against that account's organization, so it draws on the same [rate limits](/rate-limits) and [credits](/credits) as every API key the organization has issued. The same listing covers [ChatGPT](/integrations/chatgpt). The built-in search competes with the plugin exactly as it does with the MCP server, so the `web_search` change above is worth making on either route. **Keenable SELECT** is a second Keenable plugin in the same directory — a separate server that answers a question with one SQL query over live web results and returns a table. See [SELECT](/select) for both routes into it. → [Full MCP server reference](/mcp-server) # cognee Source: https://docs.keenable.ai/integrations/cognee Keenable as the web-fetch backend behind cognee's URL ingestion and scheduled scraper: pages come back as clean Markdown before they enter graph memory. *Fetch backend* cognee turns what an agent reads into persistent, graph-backed memory. From cognee 1.5.0 it can fetch URLs through Keenable: pass a URL to `remember()` or `add()`, and cognee asks Keenable for the page as Markdown instead of crawling the HTML itself. The rest of the pipeline (chunking, embeddings, entity extraction, recall) does not change. → [View the change on GitHub](https://github.com/topoteretes/cognee/pull/4421) · [cognee's URL-ingestion guide](https://docs.cognee.ai/guides/web-url-ingestion) ## Install ### No separate install The backend lives in cognee itself (`cognee/tasks/web_scraper/`) and uses the `httpx` client cognee already ships, so there is no Keenable package to add: ```bash theme={"system"} pip install "cognee>=1.5.0" ``` ### Set the key ```bash theme={"system"} export KEENABLE_API_KEY="keen_..." ``` This is one of the few integrations where the key is not optional. cognee picks Keenable **because** `KEENABLE_API_KEY` is set, and it calls the keyed `/v1/fetch` endpoint, so a missing key means a different backend is chosen, not a keyless call. Create a key at [keenable.ai/console](https://keenable.ai/console). ### Check the precedence cognee chooses a fetch backend from the environment, in this order: 1. Tavily, if `TAVILY_API_KEY` is set 2. Keenable, if `KEENABLE_API_KEY` is set 3. The built-in crawler Tavily wins when both keys are present. If a Tavily key is already in your environment, unset it to route URL ingestion through Keenable, or select the backend explicitly (below). ## Use ### Give cognee a URL ```python theme={"system"} import cognee await cognee.remember("https://docs.example.com/pricing") await cognee.recall("what does the enterprise tier cost?") ``` `remember()` fetches the page through Keenable, ingests the Markdown, and builds the graph; `recall()` answers from what was learned, without touching the original HTML again. ### Fetch pages that are not indexed By default Keenable returns its indexed copy of a page and rejects URLs it has not indexed. For arbitrary URLs, turn on live fetching: ```bash theme={"system"} export KEENABLE_LIVE_FETCH="true" ``` With live fetching on, every request goes to the source page. Leave it off when you only ingest pages Keenable already indexes and want the faster cached path. ### Select the backend explicitly When calling cognee's fetch layer directly, the backend is a parameter and the environment precedence does not apply: ```python theme={"system"} from cognee.tasks.web_scraper import fetch_page_content pages = await fetch_page_content(urls, preferred_tool="keenable") ``` ### Narrow the page with an extraction prompt `KeenableConfig` carries the per-call options. `prompt` makes Keenable return only the part of the page you ask for instead of the whole document: ```python theme={"system"} from cognee.tasks.web_scraper import fetch_page_content from cognee.tasks.web_scraper.config import KeenableConfig cfg = KeenableConfig(live=True, prompt="Extract the pricing table and plan limits only.") pages = await fetch_page_content(url, preferred_tool="keenable", keenable_config=cfg) ``` ### Track a page over time cognee's scheduled scraper runs through the same fetch path (it needs `apscheduler`): ```python theme={"system"} from cognee.tasks.web_scraper import cron_web_scraper_task from cognee.tasks.web_scraper.config import KeenableConfig await cron_web_scraper_task( url=["https://docs.example.com/changelog"], schedule="0 6 * * *", job_name="daily_changelog", keenable_config=KeenableConfig(live=True), tavily_api_key=None, # defaults to TAVILY_API_KEY, which would win ) ``` Each run writes `WebPage` nodes into the graph, linked `is_part_of` to a `WebSite`, which the `ScrapingJob` links to with `is_scraping`. Every page carries a SHA-256 `content_hash`, so the job can tell whether a page changed between runs. Unlike `remember()`, this path indexes the nodes directly and skips chunking and entity extraction; what the two share is the Keenable fetch. ## How it behaves * **Request.** `GET /v1/fetch?url=...` with `X-API-Key`, plus `live=true` and `prompt=...` when set. cognee keeps the `content` field of the response. * **Concurrency.** Up to 5 URLs in flight at once (`KeenableConfig.concurrency`), 30-second timeout per request (`timeout`, 1 to 120). * **Failures are isolated.** A URL that fails is skipped with a warning and the rest of the batch continues. If every URL fails, the first error is raised instead of returning an empty result, so a bad key surfaces as an authentication error and not as a silent empty ingest. * **Logs stay clean.** Warnings identify a failed URL by its position in the batch and the exception class, never by the URL, so credentials in query strings cannot leak into logs. ## Configuration | Setting | Environment variable | `KeenableConfig` field | Default | | - | - | - | - | | API key | `KEENABLE_API_KEY` | `api_key` | none (required) | | Base URL | `KEENABLE_BASE_URL` | `base_url` | `https://api.keenable.ai` | | Live fetch | `KEENABLE_LIVE_FETCH` | `live` | `false` | | Extraction prompt | — | `prompt` | none (max 2000 chars) | | Concurrency | — | `concurrency` | `5` | | Timeout (s) | — | `timeout` | `30` | The environment variables cover the memory path (`remember()`, `add()`). The `KeenableConfig` fields cover direct calls to `fetch_page_content`, `web_scraper_task` and `cron_web_scraper_task`, and override the environment for that call. # Convex Source: https://docs.keenable.ai/integrations/convex @keenable/convex — a Convex component exposing search and fetch as component actions. *Convex component* `@keenable/convex` installs into a Convex app's `convex.config.ts` and exposes `search` and `fetch` as component actions behind a typed client. Both env vars are optional, so registering the component is the whole setup — no key, no env wiring before the first call. → [@keenable/convex on npm](https://www.npmjs.com/package/@keenable/convex) ## Install ### Install the package ```bash theme={"system"} npm install @keenable/convex ``` ### Register the component ```typescript theme={"system"} // convex/convex.config.ts import { defineApp } from "convex/server"; import keenable from "@keenable/convex/convex.config"; const app = defineApp(); app.use(keenable); export default app; ``` ### Call it from your own action Wrap the component in an app-owned action rather than exposing it directly — that wrapper is where your auth and rate limiting belong. ```typescript theme={"system"} // convex/search.ts import { KeenableClient, toContext } from "@keenable/convex"; import { v } from "convex/values"; import { action } from "./_generated/server"; import { components } from "./_generated/api"; const keenable = new KeenableClient(components.keenable); export const searchWeb = action({ args: { query: v.string() }, handler: async (ctx, args) => { const { results } = await keenable.search(ctx, { query: args.query, maxResults: 5 }); return toContext(results); // numbered, citable sources for a prompt }, }); ``` `toContext` renders results as numbered sources and drops whole sources at the budget rather than cutting one mid-page. `keenable.fetch(ctx, { url })` returns the page as markdown with a `truncated` flag, so a caller can tell a whole page from one the budget cut. ### Add a key to lift the rate limit The component cannot read `process.env` — Convex passes env vars in through the app, so declare the key on the app and bind it: ```typescript theme={"system"} const app = defineApp({ env: { KEENABLE_API_KEY: v.string() } }); app.use(keenable, { env: { KEENABLE_API_KEY: app.env.KEENABLE_API_KEY } }); ``` ```bash theme={"system"} npx convex env set KEENABLE_API_KEY keen_... ``` Binding a key removes the hourly request cap. Without one, calls run on the shared public tier at [lower rate limits](/rate-limits). ## Parameters `search` takes `site`, `publishedAfter` / `publishedBefore`, `acquiredAfter` / `acquiredBefore`, `snippetMaxLength` (default 1000) and `maxResults`. `fetch` takes `contentMaxLength` (default 10000). # de-anthropocentric research engine Source: https://docs.keenable.ai/integrations/de-anthropocentric-research-engine Keenable is one of the MCP servers the de-anthropocentric research engine ships with, pre-wired in its bundled config. *Bundled MCP server* Keenable is one of the seven MCP servers the de-anthropocentric research engine ships with, pre-wired in its bundled config. → [View the change on GitHub](https://github.com/yogsoth-ai/de-anthropocentric-research-engine/pull/29) Copy the bundled example config and Keenable is already in it, and the engine's web-search skill lists it as a first-class provider so research runs can pick it explicitly. ## Install ### Copy the bundled MCP config ```bash theme={"system"} cp mcp.example.json .mcp.json ``` ### What it wires up ```json theme={"system"} { "mcpServers": { "keenable": { "type": "http", "url": "https://api.keenable.ai/mcp" } } } ``` ### Use it Research runs can name Keenable as the web-search provider and call `search_web_pages` / `fetch_page_content`. See the [MCP server reference](/mcp-server) for the full tool list. Set `KEENABLE_API_KEY` to authenticate — this removes the hourly request cap. Without a key, calls run on the shared public tier at [lower rate limits](/rate-limits). # DeepSeek Harness Source: https://docs.keenable.ai/integrations/deepseek-harness Keenable web search and page fetch for DeepSeek Harness's built-in web_search and web_fetch tools, with any model and no API key. *Plugin* A DeepSeek Harness plugin that answers the built-in `web_search` and `web_fetch` tools through Keenable. The search provider Harness ships with needs a DeepSeek account or API key; this one works with whatever model you run, local models included, as soon as it is installed. → [@keenable/dsh-keenable on npm](https://www.npmjs.com/package/@keenable/dsh-keenable) ## Install ### Install the plugin In Harness, open **Plugins → Add plugin**, enter `@keenable/dsh-keenable`, then choose **Install → Enable now**. Or install from the terminal into the profile you run, then restart Harness: ```bash theme={"system"} npx @deepseek-ai/dsh plugin --profile web add @keenable/dsh-keenable npx @deepseek-ai/dsh web ``` Profiles are separate: `web`, `tui`, and `headless` each need their own install. Requires DeepSeek Harness 0.1.7-rc.2 or newer and Node.js 22.12 or newer. ### Use it There is nothing to configure. The plugin points both web tools at Keenable, so ask the model to search the web or read a URL. `web_search` returns a title, URL, excerpt, and publication date (when known) for each result. `web_fetch` returns the page as markdown. Pages are retrieved on Keenable's servers, so a URL the model chose is never requested from the machine running Harness, and private or internal hosts are refused. ### Add a key (optional) Without a key, both tools use Keenable's public endpoints. To lift their per-IP limit, either set `KEENABLE_API_KEY` in the environment Harness starts in, or enter the key as **apiKey** in the plugin's settings on the **Plugins** page. A key in the settings wins over the environment variable. To keep Harness's local fetch provider and use Keenable for search only, see the [plugin README](https://github.com/keenableai/dsh-keenable#keeping-the-local-fetch-provider). Set `KEENABLE_API_KEY` to authenticate — this removes the hourly request cap. Without a key, calls run on the shared public tier at [lower rate limits](/rate-limits). # Dify Source: https://docs.keenable.ai/integrations/dify Keenable web search and page fetch as a plugin in the Dify marketplace. *Marketplace plugin* Keenable web search and page fetch as a plugin in the Dify marketplace. → [keenable on Dify Marketplace](https://marketplace.dify.ai/plugins/keenable/keenable) ## Install ### Install from the Marketplace Open the Dify Marketplace, search for "Keenable", and install the plugin. ### Use it Add Keenable Search / Keenable Fetch to an Agent or Workflow, and paste your `keen_…` key into the tool's API key field. Paste a `keen_…` key to authenticate — this removes the hourly request cap. Leave the field empty and calls run on the shared public tier at [lower rate limits](/rate-limits). # Haystack Source: https://docs.keenable.ai/integrations/haystack Keenable web search and page-fetch components for Haystack pipelines. *Pipeline components* Keenable web search and page-fetch components for Haystack pipelines — listed in the Haystack integrations catalog. → [keenable in the Haystack integrations catalog](https://haystack.deepset.ai/integrations/keenable)