Snaga Search API
Web search for AI agents: links and what each page says about itself, from our own index of the web.
Everything on this page, as one machine-readable document: openapi.json. Point your client generator at it, or fetch it and read it.
https://search.snaga.aiAuthorization: Bearer ws_…Getting a key
A key is a short string that begins with ws_. It is shown once, when
it is created, and never again, so keep it where you keep secrets: a request
with your key is billed to you.
Write to [email protected] and we will make you a key: for the free plan — 5,000 requests a month, 60 a minute — or for prepaid credit at $1 per 1,000 requests, 600 a minute. The price falls as the month goes on: $0.80 per 1,000 requests after the first million, $0.60 after ten million.
Authentication
Send your key with every request:
Authorization: Bearer ws_k1a2b3c4d_…
X-API-Key: ws_… works as well. Errors are never billed.
Search
GET /v1/search?q=<query>&limit=<1–50>&lang=<code>&site=<domain>
| Parameter | Meaning |
|---|---|
q | Required. The query, up to 500 characters. |
limit | Optional, ten by default. Results, 1–50. |
lang | Optional. Only pages in this language: ISO 639-1, e.g. en, uk, de. |
site | Optional. Only pages of this registered domain, e.g. python.org. |
The query understands:
"exact phrase": the words in this order in the title or description;-word: no page with this word in its title or description;site:python.organdlang:uk, the same as the parameters.
Write the words a page would use. Many words are fine: pages with all of them come first, then pages missing one, then pages with at least half.
A query of operators alone — site:python.org on its own —
has no words to look for, and finds nothing. The answer says so: it
carries no_words, so an agent can tell an empty answer from
a question with no words in it. Put a word beside the operator.
curl -s "https://search.snaga.ai/v1/search?q=asyncio+task+groups" \ -H "Authorization: Bearer $KEY"
An answer holds the query that was asked for, the
results, and the took_ms the search took, in
milliseconds — with the fraction, when the search was that quick.
An answer:
{
"query": "asyncio task groups",
"took_ms": 11.8,
"results": [
{
"url": "https://docs.python.org/3/library/asyncio-task.html",
"title": "Coroutines and Tasks — Python 3.14 documentation",
"description": "This section outlines high-level asyncio APIs to work with coroutines and Tasks.",
"site": "python.org",
"site_rank": 101,
"lang": "en",
"crawled": "2026-09-04",
"score": 41.7,
"matched": ["asyncio", "task", "groups"]
}
]
}
| Field | Meaning |
|---|---|
url | The page; its canonical URL when the page names one on the same host. |
title, description | What the page says about itself: its title and meta description. |
site | The site the page belongs to, e.g. python.org, or user.github.io on a shared platform. |
site_rank | The site's position in our ranking of how much the web links to it; lower is more linked-to. Missing for unranked sites. |
lang | The page's language, when known. |
crawled | When the page was crawled, YYYY-MM-DD. |
score | Relevance, comparable only within one answer. |
matched | Query words the page has — in its title, description or text, as written or as another form of the word. |
missing | Distinctive query words the page lacks; shown only for partial matches. |
excerpt | The sentences of the page's text that best match the query. |
At most two results come from one site, and the same title on the same site counts once.
From Python
import json, urllib.parse, urllib.request
key = "ws_…"
url = "https://search.snaga.ai/v1/search?" + urllib.parse.urlencode(
{"q": "asyncio task groups", "limit": 5})
request = urllib.request.Request(url, headers={"Authorization": "Bearer " + key})
with urllib.request.urlopen(request) as response:
for hit in json.load(response)["results"]:
print(hit["title"], hit["url"])
From JavaScript
const response = await fetch(
"https://search.snaga.ai/v1/search?q=asyncio+task+groups&limit=5",
{ headers: { Authorization: `Bearer ${key}` } });
const { results } = await response.json();
for (const { title, url, description } of results) {
console.log(title, url);
}
The whole API, in one document a code generator can read: openapi.json.
Try it
Here is the engine, without a key, so you can see an answer before you write a line of code. This is the endpoint the public page uses: ten results at a time. The keyed one returns up to fifty and adds each result's score and the query words it matched. Both of them say which words a result lacks.
The box below draws the answer the way the public page draws it: the same text under the title, the same words of the query marked in it, and the same note of the word a result does not have. The same search says the same thing on every page.
The keyless endpoint is GET /site/search. It is free, and it
is rate limited to thirty searches a minute from one address. Its answer
carries the same rate headers, so a client can pace itself; the answer
below shows how many are left.
GET /site/search?q=<query>&lang=<code>&news=1&page=<1–5>
| Parameter | Meaning |
|---|---|
q | Required. The query, up to 500 characters, with the same syntax as above. |
lang | Optional. Only pages in this language. |
news | Optional. Only the pages we crawled in the last fourteen days, when 1. |
page | Optional. Which ten results to return, from one to five. The order does not change from page to page, so a client can walk the pages without missing one. |
The count it prints says how many results the engine found. When it
says at least fifty, the engine stopped at fifty and there are
more; the answer carries the same fact in its total and
capped fields.
The answer carries the query, the page you are
on and the pages there are, the total number of
results, whether they were capped — more than the fifty the
engine returns — the server_time_ms the search took, in whole
milliseconds, and the
results: each one has the url, the
title, the description, the site it
belongs to, the excerpt — the sentences of the page's text
that match the query — the crawled day, and the
missing — the query words the page does not have, when the
result is a partial match. A query of
operators alone comes back with no_words and finds nothing.
Search to see an answer.
The raw answer
Usage and balance
GET /v1/usage
This is free and doesn't count against your rate. The answer holds the
key, the plan it is on, the
rate_per_min it is limited to, the month it
counts in, the requests_this_month and the
spent_usd — and, on the free plan, the
free_requests_left, or, on prepaid credit, the
credit_left_usd.
{
"key": "k1a2b3c4d",
"plan": "payg",
"rate_per_min": 600,
"month": "2026-10",
"requests_this_month": 1520,
"credit_left_usd": 8.48,
"spent_usd": 1.52
}
A free key shows free_requests_left instead of
credit_left_usd.
A search answer carries these headers:
| Header | Meaning |
|---|---|
X-RateLimit-Limit, X-RateLimit-Remaining | Requests per minute, and how many are left now. |
X-Quota-Limit, X-Quota-Remaining | Free plan: the month's requests and what's left after this one. |
X-Credit-Remaining-Usd | Pay as you go: the credit left after this request. |
A search without a key carries only the rate headers. A key on the free plan carries the quota headers and not the credit one; a key on prepaid credit carries the credit header and not the quota ones.
Errors
Errors are JSON, {"error": "<code>", "message": "<what to do>"},
and are never billed.
| Status | Error | Meaning |
|---|---|---|
| 400 | bad_request | The query is missing or too long: q in the keyed search, query in Ollama's. |
| 401 | unauthorized | No key, or an unknown or revoked one. |
| 402 | quota_exceeded | The free plan's requests for this month are used up. The message says when they renew. |
| 402 | credit_exhausted | Not enough prepaid credit for another request. |
| 429 | rate_limited | Over your requests per minute. Wait Retry-After seconds. |
| 500 | search_failed | Our fault. Retrying is safe. |
MCP
The API is also a Model Context Protocol server at POST /mcp,
using the streamable HTTP transport (protocol versions 2025-06-18, 2025-03-26
and 2024-11-05). It has the tool web_search, with these
arguments:
query(required);max_results(1–50, default 8;limitis taken as well);lang;site.
When the server fetches, it has a second tool, web_fetch: one
argument, url, and an answer with the page's title, its content
as Markdown and the links it holds — the same fetch as
POST /api/web_fetch below. A fetch is a request to that page's
own server, and it follows the rules our crawler follows:
/robots.txt is read first, and a page is not fetched again for
fifteen minutes.
The tool returns the results as text for the model, and as
structuredContent.results: the fields of the answer above —
url, title, description,
site, site_rank, lang,
crawled, matched and missing — and
in place of score the content of the page that
matches the query. A query of filters alone has no words to look for:
the text says so, and structuredContent.no_words is there
for a client that reads the fields. A tool call is
billed like a search. initialize and tools/list aren't
billed, but they count toward the rate limit.
{
"mcpServers": {
"web-search": {
"type": "http",
"url": "https://search.snaga.ai/mcp",
"headers": { "Authorization": "Bearer ws_…" }
}
}
}
Ollama's endpoints
Two endpoints have the shape Ollama gives them, so a client written for Ollama can be pointed here instead: the arguments are the same, the answers are the same, and only the address changes. What comes back is from our own index of the web rather than from a search engine that sells its ranking to anyone who asks.
| Endpoint | In | Out |
|---|---|---|
POST /api/web_search | {"query", "max_results"} | {"results": [{"title", "url", "content"}]}, content being what the page says that matches the query |
POST /api/web_fetch | {"url"} | {"title", "content", "links"}, the page's main content as Markdown |
The search takes query — it is required — and
max_results, from one to fifty and five by default. A query
of filters alone has no words to look for, and the answer says so in
no_words, beside the results.
The index
GET /v1/status
It is free. The answer holds pages — how many pages the
index holds — source — where they came from — and
languages: the code of each language and the number of pages
in it, most first.
Your results and your queries
- Results are not filtered by topic. You get what the web has, adult content included, except what law or site owners keep out. What your agent searches for, and does with the results, is your responsibility.
- Results: you may store and cache the results we return, the links and their metadata, and use them in your products for as long as you like. You may not resell results in bulk or use them to rebuild an index.
- Queries: we never record their text. Usage records hold the time, your key, the endpoint, the number of results, the duration and the price.
People can search without a key at search.snaga.ai: the same engine, 30 searches a minute per address, ten results at a time.
And SnagaBot is our crawler: what it fetches, the rules it follows, and how to limit or block it. To have pages removed from the index, write to [email protected].