{"openapi":"3.0.3","info":{"title":"Keenable API","version":"0.1.24","description":"Web search and page fetching API."},"servers":[{"url":"https://api.keenable.ai","description":"Production"}],"security":[{"apiKey":[]}],"paths":{"/v1/search":{"post":{"operationId":"search_post","tags":["Search"],"summary":"Search the web","description":"Perform a web search using a JSON request body. Supports date filters, site restriction, search mode, and a query_time reference timestamp for point-in-time search.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchRequest"}}}},"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"200":{"description":"Search results","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/SearchForbidden"},"404":{"$ref":"#/components/responses/UrlQueryNotFound"},"409":{"description":"The signed-in caller has no active organization, so there is nothing to bill. Only reachable with bearer authentication. Create or join an organization, then retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"422":{"description":"Search failed with an unretryable upstream error — see `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"$ref":"#/components/responses/UrlQueryBadGateway"},"504":{"$ref":"#/components/responses/UrlQueryTimeout"},"529":{"description":"Search is temporarily overloaded. Retry after the delay in `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/x402/search":{"post":{"operationId":"x402_search_post","tags":["Search"],"summary":"Search the web, paid per request (x402)","description":"Same search as POST /v1/search, paid per request with x402 instead of an API key. The price is stated in the 402 offer ($0.01 per request by default). Also available as GET with the parameters in the query string.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchRequest"}}}},"security":[{"x402":[]}],"responses":{"200":{"description":"Search results","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResponse"}}},"headers":{"PAYMENT-RESPONSE":{"description":"Base64-encoded x402 settlement receipt: transaction hash, network, payer.","schema":{"type":"string"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"$ref":"#/components/responses/X402PaymentRequired"},"403":{"$ref":"#/components/responses/SearchForbidden"},"404":{"$ref":"#/components/responses/UrlQueryNotFound"},"422":{"description":"Search failed with an unretryable upstream error — see `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"$ref":"#/components/responses/UrlQueryBadGateway"},"503":{"$ref":"#/components/responses/X402Unavailable"},"504":{"$ref":"#/components/responses/UrlQueryTimeout"},"529":{"description":"Search is temporarily overloaded. Retry after the delay in `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/fetch":{"get":{"operationId":"fetch","tags":["Fetch"],"summary":"Fetch page content","description":"Extract clean content (markdown) from a web page URL.","parameters":[{"name":"url","in":"query","required":true,"schema":{"type":"string","format":"uri"},"description":"URL of the page to fetch"},{"name":"max_chars","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":50000},"description":"Maximum number of characters of content to return. Content longer than this is truncated. Defaults to 50000 when omitted."},{"name":"live","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Fetch live content. Defaults to false."},{"name":"prompt","in":"query","required":false,"schema":{"type":"string","maxLength":2000},"description":"Optional extraction instruction. When set, an LLM reads the fetched page and the returned content is only the output for this instruction instead of the full page. Example: \"List all pricing tiers with their monthly prices\"."}],"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"200":{"description":"Extracted page content","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FetchResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"description":"Tell the cases apart by `error`. \"Upstream forbidden\": the target server refused the fetch (responded 401, 403, or a vendor block code such as LinkedIn's 999), a bot-wall or access restriction on the target rather than a problem with your credentials. Deterministic: retrying will not help. \"Forbidden\": your API key was revoked; see `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The target page does not exist — the target server responded 404.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The signed-in caller has no active organization, so there is nothing to bill. Only reachable with bearer authentication. Create or join an organization, then retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"422":{"description":"The page was reached but contains no extractable content, or the target server answered with an HTTP error that no other status here covers, such as 410 or 451 — see `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limited. Either your own quota, or the target server throttled the fetch — see `message`. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Internal server error, including a failure inside our own fetcher infrastructure. Not caused by the requested URL; retrying the same URL later is reasonable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"502":{"description":"The fetcher could not get the target page: a DNS/TLS/connection failure, an unreachable host, or the target answered with a 5xx error. Unlike 422, no page content came back; unlike 403, the target did not refuse the fetch. Often transient; a retry is reasonable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"504":{"description":"The upstream fetch to the target page timed out.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/x402/fetch":{"get":{"operationId":"x402_fetch","tags":["Fetch"],"summary":"Fetch page content, paid per request (x402)","description":"Same fetch as GET /v1/fetch, paid per request with x402 instead of an API key. The price is stated in the 402 offer ($0.01 per request by default).","parameters":[{"name":"url","in":"query","required":true,"schema":{"type":"string","format":"uri"},"description":"URL of the page to fetch"},{"name":"max_chars","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":50000},"description":"Maximum number of characters of content to return. Content longer than this is truncated. Defaults to 50000 when omitted."},{"name":"live","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Fetch live content. Defaults to false."},{"name":"prompt","in":"query","required":false,"schema":{"type":"string","maxLength":2000},"description":"Optional extraction instruction. When set, an LLM reads the fetched page and the returned content is only the output for this instruction instead of the full page. Example: \"List all pricing tiers with their monthly prices\"."}],"security":[{"x402":[]}],"responses":{"200":{"description":"Extracted page content","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FetchResponse"}}},"headers":{"PAYMENT-RESPONSE":{"description":"Base64-encoded x402 settlement receipt: transaction hash, network, payer.","schema":{"type":"string"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"$ref":"#/components/responses/X402PaymentRequired"},"403":{"description":"The target server refused the fetch (responded 401, 403, or a vendor block code such as LinkedIn's 999) — a bot-wall or access restriction on the target, not an authentication problem with your API key. Deterministic: retrying will not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"The target page does not exist — the target server responded 404.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"422":{"description":"The page was reached but contains no extractable content, or the target server answered with an HTTP error that no other status here covers, such as 410 or 451 — see `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limited. Either your own quota, or the target server throttled the fetch — see `message`. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Internal server error, including a failure inside our own fetcher infrastructure. Not caused by the requested URL; retrying the same URL later is reasonable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"502":{"description":"The fetcher could not get the target page: a DNS/TLS/connection failure, an unreachable host, or the target answered with a 5xx error. Unlike 422, no page content came back; unlike 403, the target did not refuse the fetch. Often transient; a retry is reasonable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"503":{"$ref":"#/components/responses/X402Unavailable"},"504":{"description":"The upstream fetch to the target page timed out.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/auth/user":{"get":{"operationId":"auth_user","tags":["Auth"],"summary":"Get API key identity","description":"Returns identity information (user, organization) for the authenticated API key.","responses":{"200":{"description":"API key identity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyIdentity"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalError"}}}}},"components":{"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"X-API-Key","description":"API key for authentication. Obtain one from the Keenable dashboard."},"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Access token of a signed-in Keenable user, as issued by our OAuth sign-in. Accepted on /v1/search and /v1/fetch so a hosted MCP client can call them on the caller's behalf. Usage bills the organization the user is currently in; an org_id query parameter is not honored on these endpoints."},"x402":{"type":"apiKey","in":"header","name":"PAYMENT-SIGNATURE","description":"x402 v2 pay-per-request, no account needed. An unpaid request is answered 402 with the offer (USDC, exact scheme) in the base64 PAYMENT-REQUIRED header; retry with the signed payment in PAYMENT-SIGNATURE. The result is returned once the payment settles, with the receipt in PAYMENT-RESPONSE. Failed requests are not charged. See https://www.x402.org/."}},"schemas":{"ApiError":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Error category (e.g. \"Bad request\", \"Not found\")"},"message":{"type":"string","description":"Human-readable detail about what went wrong"},"details":{"description":"Structured details (validation errors, etc.)"}}},"SearchMode":{"type":"string","enum":["pro"],"default":"pro","description":"Retrieval mode; \"pro\" is the default"},"SearchRequest":{"type":"object","required":["query"],"properties":{"query":{"type":"string","description":"Search query"},"mode":{"$ref":"#/components/schemas/SearchMode"},"acquired_after":{"type":"string","description":"Filter: pages acquired after this date (YYYY-MM-DD, ISO 8601, or relative like \"7d\")"},"acquired_before":{"type":"string","description":"Filter: pages acquired before this date"},"published_after":{"type":"string","description":"Filter: pages published after this date, a lower bound on publish time. Use this, not query_time, to get recent results."},"published_before":{"type":"string","description":"Filter: pages published before this date"},"site":{"type":"string","description":"Restrict results to a specific domain"},"query_time":{"type":"string","description":"Leave empty for a normal search. An upper time bound for a point-in-time search: the web as it stood at this moment, with every page newer than it excluded. It sets no lower bound, so this is not a recency filter; for recent results use published_after. Accepts an ISO 8601 datetime (e.g. \"2026-09-20T00:00:00Z\"), a YYYY-MM-DD date, a Unix epoch, or an offset back from now (\"7d\" means as of 7 days ago, not the last 7 days). Units: min, h, d, w, mo, y."},"snippet_max_length":{"type":"integer","minimum":180,"maximum":10000,"description":"Maximum length, in characters, of the snippet returned per result. When omitted, a default snippet length is used."},"max_results":{"type":"integer","minimum":1,"maximum":50,"description":"Maximum number of results to return. When omitted, a default count (10) is used."},"enable_wizards":{"type":"boolean","default":false,"description":"Experimental: use wizards (structured data outputs). Only valid in \"pro\" mode. Not available to an organization under a zero data retention policy: the request is refused with 400 zdr_wizards_unsupported."}}},"SearchResponse":{"type":"object","required":["query","mode","results"],"properties":{"query":{"type":"string","description":"Echo of the search query"},"mode":{"type":"string","description":"Search mode used"},"results":{"type":"array","items":{"$ref":"#/components/schemas/SearchResult"}},"warning":{"type":"string","description":"Present when the request was not answered as a plain search. Either the query was a URL, so the page was fetched directly, or the query carried only a site: operator with no search terms, so the site name was used as the search term. It also says when wizards that the organization's settings turn on were left out because its zero data retention policy does not allow them."},"wizards":{"type":"array","description":"Experimental: structured data from wizards. Present only when enable_wizards is true and a wizard matched the query. The fields of each item depend on its wizard type.","items":{"type":"object","required":["wizard"],"properties":{"wizard":{"type":"string","description":"Wizard type, e.g. finance, sports, product_prices, prediction_markets, places, navigation"}},"additionalProperties":true}},"usage":{"$ref":"#/components/schemas/Usage"}}},"SearchResult":{"type":"object","required":["title","url","description"],"properties":{"title":{"type":"string","description":"Page title"},"url":{"type":"string","description":"Page URL"},"description":{"type":"string","description":"Always empty. Deprecated — the page description now feeds the snippet fallback.","deprecated":true},"snippet":{"type":"string","description":"Search snippet with query-relevant highlights; falls back to the page description"},"published_at":{"type":"string","description":"When the page was published (ISO 8601)"},"acquired_at":{"type":"string","description":"When the page was indexed (ISO 8601)"}}},"FetchResponse":{"type":"object","required":["url","content"],"properties":{"url":{"type":"string","description":"Fetched URL (may differ from input after redirects)"},"title":{"type":"string","description":"Page title"},"content":{"type":"string","description":"Extracted content in markdown"},"description":{"type":"string","description":"Always empty. Deprecated.","deprecated":true},"author":{"type":"string","description":"Author name"},"published_at":{"type":"string","description":"Publish date (ISO 8601)"},"usage":{"$ref":"#/components/schemas/Usage"}}},"Usage":{"type":"object","description":"What this request was billed. Present when the request is billed in credits, that is made with an API key or a signed-in session; absent on keyless, x402 and ZeroClick requests. A request that fails (any non-2xx status) is never billed, and its error body carries no usage.","required":["sku","amount","credits","paid"],"properties":{"sku":{"type":"string","description":"What was billed: search.<mode> for a search, named after the mode it ran in (search.pro by default, search.time_machine for a Time Machine search), and fetch, fetch.live or fetch.time_machine for a fetch. A search whose query is a URL is billed as the fetch it was answered with."},"amount":{"type":"integer","description":"Billed units, 1 for a search or a fetch."},"credits":{"type":"number","description":"Credits charged to the organization for this request."},"paid":{"type":"boolean","description":"true when the credits came from the purchased balance, false when they came from the plan's free credits."}}},"ApiKeyIdentity":{"type":"object","required":["user_id","email","display_name","org_id","org_name"],"properties":{"user_id":{"type":"string"},"email":{"type":"string","nullable":true},"display_name":{"type":"string","nullable":true},"org_id":{"type":"string"},"org_name":{"type":"string","nullable":true}}}},"responses":{"BadRequest":{"description":"Invalid request parameters, or a request the organization's zero data retention policy does not allow (error zdr_wizards_unsupported, zdr_unsupported_profile or zdr_prompt_unsupported)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"Unauthorized":{"description":"Missing or invalid API key – update your API key or contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"InsufficientCredits":{"description":"No credits left — the free monthly allowance and purchased credits are used up; top up in the console","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"RateLimitExceeded":{"description":"Rate limit exceeded — slow down or upgrade your plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"InternalError":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"X402PaymentRequired":{"description":"Payment required: decode the PAYMENT-REQUIRED header (mirrored in the body), sign one of accepts, and retry with PAYMENT-SIGNATURE. Also returned when a payment is rejected, and when a verified payment could not be settled; the result is then withheld.","headers":{"PAYMENT-REQUIRED":{"description":"Base64-encoded x402 v2 PaymentRequired object","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"X402Unavailable":{"description":"Pay-per-request access is unavailable right now. Retry shortly or use an API key on /v1/search and /v1/fetch.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"SearchForbidden":{"description":"Tell the cases apart by `error`. \"Upstream forbidden\" happens only when the query is a URL, which is fetched instead of searched: the target server refused the fetch (responded 401, 403, or a vendor block code such as LinkedIn's 999), a bot-wall or access restriction on the target rather than a problem with your credentials. Deterministic: retrying will not help. \"Forbidden\" means the request itself is not allowed, for example a revoked API key or a search mode your access does not include; see `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"UrlQueryNotFound":{"description":"Only when the query is a URL, which is fetched instead of searched: the target page does not exist — the target server responded 404.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"UrlQueryBadGateway":{"description":"Only when the query is a URL, which is fetched instead of searched: the fetcher could not get the target page: a DNS/TLS/connection failure, an unreachable host, or the target answered with a 5xx error. Unlike 422, no page content came back; unlike 403, the target did not refuse the fetch. Often transient; a retry is reasonable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"UrlQueryTimeout":{"description":"Only when the query is a URL, which is fetched instead of searched: the upstream fetch to the target page timed out.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}}