Search
curl --request POST \
--url https://api.keenable.ai/v1/search \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"query": "<string>",
"site": "<string>",
"acquired_after": "<string>",
"acquired_before": "<string>",
"published_after": "<string>",
"published_before": "<string>",
"query_time": "<string>",
"snippet_max_length": 123,
"max_results": 123
}
'import requests
url = "https://api.keenable.ai/v1/search"
payload = {
"query": "<string>",
"site": "<string>",
"acquired_after": "<string>",
"acquired_before": "<string>",
"published_after": "<string>",
"published_before": "<string>",
"query_time": "<string>",
"snippet_max_length": 123,
"max_results": 123
}
headers = {
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
query: '<string>',
site: '<string>',
acquired_after: '<string>',
acquired_before: '<string>',
published_after: '<string>',
published_before: '<string>',
query_time: '<string>',
snippet_max_length: 123,
max_results: 123
})
};
fetch('https://api.keenable.ai/v1/search', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.keenable.ai/v1/search",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'query' => '<string>',
'site' => '<string>',
'acquired_after' => '<string>',
'acquired_before' => '<string>',
'published_after' => '<string>',
'published_before' => '<string>',
'query_time' => '<string>',
'snippet_max_length' => 123,
'max_results' => 123
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.keenable.ai/v1/search"
payload := strings.NewReader("{\n \"query\": \"<string>\",\n \"site\": \"<string>\",\n \"acquired_after\": \"<string>\",\n \"acquired_before\": \"<string>\",\n \"published_after\": \"<string>\",\n \"published_before\": \"<string>\",\n \"query_time\": \"<string>\",\n \"snippet_max_length\": 123,\n \"max_results\": 123\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-API-Key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.keenable.ai/v1/search")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"query\": \"<string>\",\n \"site\": \"<string>\",\n \"acquired_after\": \"<string>\",\n \"acquired_before\": \"<string>\",\n \"published_after\": \"<string>\",\n \"published_before\": \"<string>\",\n \"query_time\": \"<string>\",\n \"snippet_max_length\": 123,\n \"max_results\": 123\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.keenable.ai/v1/search")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"query\": \"<string>\",\n \"site\": \"<string>\",\n \"acquired_after\": \"<string>\",\n \"acquired_before\": \"<string>\",\n \"published_after\": \"<string>\",\n \"published_before\": \"<string>\",\n \"query_time\": \"<string>\",\n \"snippet_max_length\": 123,\n \"max_results\": 123\n}"
response = http.request(request)
puts response.read_body{
"query": "<string>",
"results": [
{
"title": "<string>",
"url": "<string>",
"description": "<string>",
"snippet": "<string>",
"published_at": "<string>",
"acquired_at": "<string>"
}
]
}API reference
Search
Search the web and return ranked results
POST
/
v1
/
search
Search
curl --request POST \
--url https://api.keenable.ai/v1/search \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"query": "<string>",
"site": "<string>",
"acquired_after": "<string>",
"acquired_before": "<string>",
"published_after": "<string>",
"published_before": "<string>",
"query_time": "<string>",
"snippet_max_length": 123,
"max_results": 123
}
'import requests
url = "https://api.keenable.ai/v1/search"
payload = {
"query": "<string>",
"site": "<string>",
"acquired_after": "<string>",
"acquired_before": "<string>",
"published_after": "<string>",
"published_before": "<string>",
"query_time": "<string>",
"snippet_max_length": 123,
"max_results": 123
}
headers = {
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
query: '<string>',
site: '<string>',
acquired_after: '<string>',
acquired_before: '<string>',
published_after: '<string>',
published_before: '<string>',
query_time: '<string>',
snippet_max_length: 123,
max_results: 123
})
};
fetch('https://api.keenable.ai/v1/search', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.keenable.ai/v1/search",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'query' => '<string>',
'site' => '<string>',
'acquired_after' => '<string>',
'acquired_before' => '<string>',
'published_after' => '<string>',
'published_before' => '<string>',
'query_time' => '<string>',
'snippet_max_length' => 123,
'max_results' => 123
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.keenable.ai/v1/search"
payload := strings.NewReader("{\n \"query\": \"<string>\",\n \"site\": \"<string>\",\n \"acquired_after\": \"<string>\",\n \"acquired_before\": \"<string>\",\n \"published_after\": \"<string>\",\n \"published_before\": \"<string>\",\n \"query_time\": \"<string>\",\n \"snippet_max_length\": 123,\n \"max_results\": 123\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-API-Key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.keenable.ai/v1/search")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"query\": \"<string>\",\n \"site\": \"<string>\",\n \"acquired_after\": \"<string>\",\n \"acquired_before\": \"<string>\",\n \"published_after\": \"<string>\",\n \"published_before\": \"<string>\",\n \"query_time\": \"<string>\",\n \"snippet_max_length\": 123,\n \"max_results\": 123\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.keenable.ai/v1/search")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"query\": \"<string>\",\n \"site\": \"<string>\",\n \"acquired_after\": \"<string>\",\n \"acquired_before\": \"<string>\",\n \"published_after\": \"<string>\",\n \"published_before\": \"<string>\",\n \"query_time\": \"<string>\",\n \"snippet_max_length\": 123,\n \"max_results\": 123\n}"
response = http.request(request)
puts response.read_body{
"query": "<string>",
"results": [
{
"title": "<string>",
"url": "<string>",
"description": "<string>",
"snippet": "<string>",
"published_at": "<string>",
"acquired_at": "<string>"
}
]
}Search the web and return ranked results with URLs, titles, and descriptions.
Part of the API reference, which covers the base URL, authentication, and the limits both endpoints share.
Relative deltas may be combined with absolute values across the two bounds of a window:
For example, at request time
curl -X POST "https://api.keenable.ai/v1/search" \
-H "X-API-Key: keen_<your_key>" \
-H "Content-Type: application/json" \
-d '{ "query": "typescript best practices" }'
import requests
r = requests.post(
"https://api.keenable.ai/v1/search",
headers={"X-API-Key": "keen_<your_key>"},
json={"query": "typescript best practices"},
)
for hit in r.json()["results"]:
print(hit["title"], hit["url"])
const r = await fetch("https://api.keenable.ai/v1/search", {
method: "POST",
headers: {
"X-API-Key": "keen_<your_key>",
"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:
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 for the details. The playground on this page calls the keyed endpoint.
Request
string
required
The search query.
string
Restrict results to a specific site (e.g.
"techcrunch.com").string
Filter to pages acquired/indexed at or after this point in time. See Date and time filters for accepted formats.
string
Filter to pages acquired/indexed at or before this point in time. See Date and time filters for accepted formats.
string
Filter to pages published at or after this point in time. See Date and time filters for accepted formats.
string
Filter to pages published at or before this point in time. See Date and time filters for accepted formats.
string
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.integer
Maximum length, in characters, of the
snippet returned per result. Must be between 180 and 10000. When omitted, a default snippet length is used.integer
Maximum number of results to return. Must be between 1 and 50. When omitted, up to 10 results are returned.
Response
string
The query that was searched.
array
List of search results.
Show result
Show result
string
Page title.
string
Page URL.
string
Short summary of the page.
string
Longer text excerpt from the page content (if available).
string
When the page was published, as an ISO 8601 timestamp in UTC (if available).
string
When the page was acquired/indexed, as an ISO 8601 timestamp in UTC (if available).
Example
{
"query": "typescript best practices",
"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-dateform (YYYY-MM-DD) — covers that whole day in UTC. On an_afterbound it resolves to00:00:00on that date; on a_beforebound it resolves to23:59:59.999on 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 (
<number><unit>, e.g.7d,30min) — resolves to the request time minus the delta, truncated to minute precision, or toquery_timeminus the delta when that is set. Supported units:min(minutes),h(hours),d(days),mo(months),y(years).
| 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 |
{
"query": "...",
"published_after": "1y",
"published_before": "6mo"
}
{
"query": "...",
"acquired_after": "2024-01-01",
"acquired_before": "30d"
}
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.