Web search (Keenable)
Search the web and read pages straight from your code — with the same key you use for models. Two ways to call it: as a regular model through /v1/chat/completions, or as dedicated endpoints returning plain JSON.
Pricing
$0.005 per request — that is $5 per 1,000 requests.
The price is per call: one call is one request, no matter how many results come back. Finding 50 sources costs the same as finding one.
Failed requests are not billed — neither a search failure nor bad parameters.
| what | counts as |
|---|---|
| a search returning 1 result | 1 request |
| a search returning 50 results | 1 request |
| reading a page | 1 request |
| any error | 0 |
Way 1 — as a model
Works in Cursor, Claude Code, OpenCode and any OpenAI client with no changes at all: just pick the model.
Models:
| model | what it does |
|---|---|
keenable/search | searches the web, returns a list of sources with snippets |
keenable/read | reads a page and returns its text as markdown |
Search
curl https://nordrouter.com/v1/chat/completions \
-H "Authorization: Bearer $NORDROUTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "keenable/search",
"messages": [{"role": "user", "content": "bitcoin price today"}]
}'You get a normal assistant message listing the sources: title, publication date, full URL and a snippet for each.
Reading a page
Put the URL first in your message. After it you may add what exactly you are looking for on that page.
curl https://nordrouter.com/v1/chat/completions \
-H "Authorization: Bearer $NORDROUTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "keenable/read",
"messages": [{"role": "user", "content": "https://en.wikipedia.org/wiki/Tokyo population figures"}]
}'Without the extra instruction you get the whole page as markdown. With it you get only what you asked for — which is both faster and noticeably cheaper in tokens on your side when the result feeds into a model.
Why bother with the instruction
A Wikipedia article is tens of thousands of characters. If you need one number, the instruction saves both time and the tokens of your next step.
Python
from openai import OpenAI
client = OpenAI(
api_key="your-key",
base_url="https://nordrouter.com/v1",
)
found = client.chat.completions.create(
model="keenable/search",
messages=[{"role": "user", "content": "AI news this week"}],
)
print(found.choices[0].message.content)Way 2 — dedicated endpoints
When you need results with fields, dates and filters rather than prose, take the JSON.
POST /v1/search
curl https://nordrouter.com/v1/search \
-H "Authorization: Bearer $NORDROUTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "neural network news",
"max_results": 5,
"published_after": "7d"
}'Parameters:
| parameter | type | default | what it does |
|---|---|---|---|
query | string | required | what to search for |
site | string | — | search only this domain, e.g. github.com |
max_results | number | 10 | how many results, 1 to 50 |
snippet_max_chars | number | — | snippet length, 180 to 10,000 characters |
published_after | string | — | material published after this moment |
published_before | string | — | published before it |
acquired_after | string | — | added to the index after |
acquired_before | string | — | added to the index before |
query_time | string | — | search the index as it stood at this moment |
Dates accept three forms: 2026-08-01, a full ISO 8601 timestamp, or a relative delta — 30min, 12h, 7d, 3mo, 1y.
Published and indexed are not the same
published_* filters by the date of the material itself, acquired_* by the date the page entered the index. The difference matters: a page can be rewritten without touching its publication date. If you want "what appeared online in the last day", use acquired_after.
Response:
{
"query": "neural network news",
"count": 5,
"results": [
{
"title": "Article headline",
"url": "https://example.com/article",
"description": "Short description",
"snippet": "An excerpt from the page text…",
"published_at": "2026-08-25T10:30:00Z",
"acquired_at": "2026-08-25T11:02:00Z"
}
]
}GET /v1/fetch
curl -G https://nordrouter.com/v1/fetch \
-H "Authorization: Bearer $NORDROUTER_KEY" \
--data-urlencode "url=https://example.com/article" \
--data-urlencode "prompt=extract the pricing table"Parameters:
| parameter | type | default | what it does |
|---|---|---|---|
url | string | required | the page address |
max_chars | number | 50,000 | how many characters to return |
prompt | string | — | what exactly to extract, up to 2,000 characters |
live | true / false | false | fetch from the source instead of the index |
Response:
{
"url": "https://example.com/article",
"title": "Headline",
"description": "Description",
"author": "Author",
"content": "# Page text as markdown…",
"published_at": 1787777084
}When you need live
The index answers faster, but it does not hold everything. If the page is fresh or closed to crawlers, set live=true and it will be read straight from the site. Same price.
Limits
| rate | 2 requests per second per key |
| snippet length | 180 to 10,000 characters |
| results per call | up to 50 |
| extraction instruction | up to 2,000 characters |
Going over the rate returns 429 with a Retry-After header — retry in a second.
Errors
| code | what it means | what to do |
|---|---|---|
400 | query or url missing, or the address has no http:// | fix the request |
401 | key not accepted | check Authorization |
402 | balance exhausted | top up in the dashboard |
429 | too fast | retry in a second |
502 | search did not answer | retry; nothing was billed |
503 | search temporarily unavailable | we are on it; nothing was billed |
FAQ
Am I billed for an empty result set? Yes, if the search ran and honestly answered "nothing found" — the work was done. Only errors are free.
Can I stream it? No, and there is no need: results arrive in one piece within a fraction of a second.
Are tokens counted? The price is per call, tokens do not affect it. They are still reported in the model response so client libraries do not treat it as empty.
How is this different from the search/… models? Those search first and then answer with a language model — you pay for two jobs. Here you get the raw results and decide what to do with them yourself.