API reference
Every ContextFetch endpoint on one page: parameters, example requests and responses, headers and errors. The search, contents and findSimilar endpoints follow Exa's request and response shapes, and /tavily/search follows Tavily's, so the official SDKs work after a base-URL change to https://api.contextfetch.dev.
If you’re an LLM, read llms.txt
- Base URL
- https://api.contextfetch.dev
- Auth
- Authorization: Bearer <key>, x-api-key, api_key (body), or none for the free tier
Costs
- 1 credit per call to POST /v1/search, /v1/findSimilar, /tavily/search and GET /r/{url} (and their aliases).
- POST /v1/contents: 1 credit per URL that loads (max 10 URLs per call).
- A paid request that ends in an error is refunded. Keyless requests count even when they fail.
- Cache hits are charged like live calls.
- costDollars.total = credits used x 0.001.
Endpoints
Request bodies are JSON. Exa-shaped endpoints accept camelCase and snake_case field names.
POST/v1/search
Try itCredits: 1Also at: /search, /exa/search
Exa-compatible search. Pass contents (text, highlights, pruned_context) to get page Markdown, highlights and a query-pruned context per result, included in the 1 credit; without contents, results carry no page text. Accepts camelCase or snake_case.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| query | body | string | yes | Search query, up to 2,000 characters. |
| numResults / num_results | body | int | no | Default 10, capped at 25. |
| type | body | string | no | Any Exa search type is accepted; all run the same pipeline. |
| includeDomains / include_domains | body | string[] | no | Only return results from these domains. |
| excludeDomains / exclude_domains | body | string[] | no | Never return results from these domains. |
| startPublishedDate / start_published_date | body | ISO date | no | Only results published on or after this date. |
| contents | body | object | bool | no | text, highlights and pruned_context (or prunedContext), each true/false or an options object as Exa SDKs send it. Omit it and results have no text, highlights or prunedContext. |
Example request
curl -s https://api.contextfetch.dev/v1/search \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query":"vector database benchmarks 2026","numResults":3,"contents":{"text":true,"highlights":true,"pruned_context":true}}'# pip install exa-pyfrom exa_py import Exa exa = Exa(api_key="YOUR_API_KEY", base_url="https://api.contextfetch.dev") res = exa.search( "vector database benchmarks 2026", num_results=5, contents={"text": True, "highlights": True},)for r in res.results: print(r.url, r.highlights)// npm i exa-jsimport Exa from "exa-js"; const exa = new Exa("YOUR_API_KEY", "https://api.contextfetch.dev"); const res = await exa.search("vector database benchmarks 2026", { numResults: 5, contents: { text: true, highlights: true },});for (const r of res.results) console.log(r.url, r.highlights);Example response
{
"requestId": "4f9d2c0e8b5a4b0f9a1e6c2d7b3f8a10",
"results": [
{
"id": "0b6f3c1e-5d2a-5f4b-9c8e-7a1d2e3f4b5c",
"url": "https://example.com/blog/vector-database-benchmarks",
"title": "Vector database benchmarks",
"score": 0.93,
"publishedDate": "2026-02-11",
"text": "# Vector database benchmarks\n\nWe measured recall and p99 latency for...",
"highlights": ["We measured recall and p99 latency for..."],
"highlightScores": [0.97],
"prunedContext": "We measured recall and p99 latency for..."
}
],
"costDollars": {"total": 0.001},
"searchTimeMs": 412,
"contextfetch": {"cache": "miss", "timingsMs": {...}, "tokens": {...}, "sources": {...}}
}Response headers
| Header | Meaning |
|---|---|
| X-Credits-Remaining | On every billable response: free requests left today for keyless calls, credits left for a paid key. |
Errors
| Status | Body | When |
|---|---|---|
| 401 | {"error": "invalid_api_key"} | The key is unknown. |
| 402 | {"error": "insufficient_credits" | "keyless_limit", "message": "...", "checkout_url": "...", "pricing": {...}, "packs": [...], "action": {"type": "payment_link", "url": "https://api.contextfetch.dev/buy/instant", "prompt_to_user": "..."}} | Out of credits, or the keyless allowance is used up. Also sends X-Payment-URL. |
| 422 | - | Request validation failed, e.g. a query over 2,000 characters or a threshold outside 100 to 100,000. |
| 503 | {"error": "recall_unavailable", "message": "..."} | Web search backends are down; retry shortly. |
POST/v1/contents
Try itCredits: 1 per URL that loads (max 10)Also at: /contents
Fetch pages as Markdown, with optional query-aware highlights. Failed URLs come back in statuses and are not billed.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| urls / ids | body | string[] | yes | 1 to 10 URLs in total. |
| text | body | bool | no | Return the page as Markdown. Default true. |
| highlights | body | bool | {query, numSentences, highlightsPerUrl} | no | 400 to 700 character Markdown chunks, not sentences: numSentences is accepted and ignored. |
| pruned_context | body | bool | no | Return prunedContext for the highlight query. |
Example request
curl -s https://api.contextfetch.dev/v1/contents \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"urls":["https://docs.python.org/3/library/asyncio-task.html"],"text":true,"highlights":{"query":"task groups"}}'Example response
{
"requestId": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
"results": [
{
"id": "c7e1a2b3-4d5e-5f60-8a9b-0c1d2e3f4a5b",
"url": "https://docs.python.org/3/library/asyncio-task.html",
"title": "Coroutines and Tasks",
"text": "# Coroutines and Tasks\n\n...",
"highlights": ["...the chunk most relevant to task groups..."],
"highlightScores": [0.95],
"prunedContext": "## Task Groups\n\n..."
}
],
"costDollars": {"total": 0.001}
}Response headers
| Header | Meaning |
|---|---|
| X-Credits-Remaining | On every billable response: free requests left today for keyless calls, credits left for a paid key. |
Errors
| Status | Body | When |
|---|---|---|
| 400 | {"error": "url_not_allowed"} | The URL is not public http(s): private, loopback, link-local and similar addresses are rejected, also after redirects. |
| 401 | {"error": "invalid_api_key"} | The key is unknown. |
| 402 | {"error": "insufficient_credits" | "keyless_limit", "message": "...", "checkout_url": "...", "pricing": {...}, "packs": [...], "action": {"type": "payment_link", "url": "https://api.contextfetch.dev/buy/instant", "prompt_to_user": "..."}} | Out of credits, or the keyless allowance is used up. Also sends X-Payment-URL. |
| 422 | - | Request validation failed, e.g. a query over 2,000 characters or a threshold outside 100 to 100,000. |
POST/v1/findSimilar
Credits: 1Also at: /findSimilar
Scrapes the URL, extracts its topic terms and searches outside its domain. Same response shape as /search.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| url | body | string | yes | Public http(s) URL. |
| numResults / num_results | body | int | no | Default 10, capped at 25. |
| includeDomains / include_domains | body | string[] | no | Only return results from these domains. |
| excludeDomains / exclude_domains | body | string[] | no | Never return results from these domains. |
| startPublishedDate / start_published_date | body | ISO date | no | Only results published on or after this date. |
| contents | body | object | bool | no | text, highlights and pruned_context (or prunedContext), each true/false or an options object as Exa SDKs send it. Omit it and results have no text, highlights or prunedContext. |
Example request
curl -s https://api.contextfetch.dev/v1/findSimilar \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://example.com/blog/vector-database-benchmarks","numResults":5,"contents":{"text":true,"highlights":true}}'Example response
{
"requestId": "4f9d2c0e8b5a4b0f9a1e6c2d7b3f8a10",
"results": [
{
"id": "0b6f3c1e-5d2a-5f4b-9c8e-7a1d2e3f4b5c",
"url": "https://example.com/blog/vector-database-benchmarks",
"title": "Vector database benchmarks",
"score": 0.93,
"publishedDate": "2026-02-11",
"text": "# Vector database benchmarks\n\nWe measured recall and p99 latency for...",
"highlights": ["We measured recall and p99 latency for..."],
"highlightScores": [0.97],
"prunedContext": "We measured recall and p99 latency for..."
}
],
"costDollars": {"total": 0.001},
"searchTimeMs": 412,
"contextfetch": {"cache": "miss", "timingsMs": {...}, "tokens": {...}, "sources": {...}}
}Response headers
| Header | Meaning |
|---|---|
| X-Credits-Remaining | On every billable response: free requests left today for keyless calls, credits left for a paid key. |
Errors
| Status | Body | When |
|---|---|---|
| 400 | {"error": "url_not_allowed"} | The URL is not public http(s): private, loopback, link-local and similar addresses are rejected, also after redirects. |
| 401 | {"error": "invalid_api_key"} | The key is unknown. |
| 402 | {"error": "insufficient_credits" | "keyless_limit", "message": "...", "checkout_url": "...", "pricing": {...}, "packs": [...], "action": {"type": "payment_link", "url": "https://api.contextfetch.dev/buy/instant", "prompt_to_user": "..."}} | Out of credits, or the keyless allowance is used up. Also sends X-Payment-URL. |
| 422 | - | Request validation failed, e.g. a query over 2,000 characters or a threshold outside 100 to 100,000. |
| 502 | {"error": "fetch_failed", "message": "..."} | The target page could not be fetched. |
POST/tavily/search
Credits: 1 (basic or advanced)
Tavily-compatible search. tavily-python and @tavily/core use the base URL https://api.contextfetch.dev/tavily.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| query | body | string | yes | Search query. |
| api_key | body | string | no | Optional when a header carries the key. |
| search_depth | body | "basic" | "advanced" | no | Both cost 1 credit. |
| max_results | body | int | no | Default 5, capped at 25. |
| include_raw_content | body | bool | no | Adds raw_content as Markdown. |
| include_answer | body | bool | no | Adds answer quoted from the most relevant passages, or null. No generative model runs. |
| include_domains / exclude_domains | body | string[] | no | Domain filters. |
Example request
curl -s https://api.contextfetch.dev/tavily/search \ -H "Content-Type: application/json" \ -d '{"api_key":"YOUR_API_KEY","query":"vector database benchmarks 2026","max_results":3,"include_raw_content":true}'# pip install tavily-pythonfrom tavily import TavilyClient tavily = TavilyClient( api_key="YOUR_API_KEY", # or "keyless" for the free tier api_base_url="https://api.contextfetch.dev/tavily",) res = tavily.search("vector database benchmarks 2026", max_results=5, include_raw_content=True)for r in res["results"]: print(r["url"], r["score"])// npm i @tavily/coreimport { tavily } from "@tavily/core"; const tvly = tavily({ apiKey: "YOUR_API_KEY", apiBaseURL: "https://api.contextfetch.dev/tavily",}); const res = await tvly.search("vector database benchmarks 2026", { searchDepth: "basic", maxResults: 5 });for (const r of res.results) console.log(r.url, r.score);Example response
{
"query": "vector database benchmarks 2026",
"results": [
{
"title": "Vector database benchmarks",
"url": "https://example.com/blog/vector-database-benchmarks",
"content": "We measured recall and p99 latency for...",
"raw_content": "# Vector database benchmarks\n\n...",
"score": 0.93
}
],
"response_time": 0.84
}Response headers
| Header | Meaning |
|---|---|
| X-Credits-Remaining | On every billable response: free requests left today for keyless calls, credits left for a paid key. |
Errors
| Status | Body | When |
|---|---|---|
| 401 | {"detail": {"error": "invalid_api_key"}} | The key is unknown. |
| 422 | - | Request validation failed, e.g. a query over 2,000 characters or a threshold outside 100 to 100,000. |
| 432 | {"detail": {"error": "..."}} | /tavily/* only: out of credits (Tavily's own status), with the same X-Payment-URL header. Other /tavily/* errors also use this body shape. |
GET/r/{url}
Try itCredits: 1
Any public URL as Markdown (text/markdown). Add ?q= to prune the page to the chunks relevant to a question.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| url | path | string | yes | Full http(s) URL. GitHub blob URLs are read as raw files, arXiv PDFs as their HTML versions. |
| q | query | string | no | Question or topic to prune to. |
Example request
curl -si "https://api.contextfetch.dev/r/https://docs.python.org/3/library/asyncio-task.html?q=task+groups" \ -H "Authorization: Bearer YOUR_API_KEY"Example response
HTTP/2 200
content-type: text/markdown; charset=utf-8
x-token-savings: 91.0
x-raw-tokens: 18240
x-output-tokens: 1650
## Task Groups
...Response headers
| Header | Meaning |
|---|---|
| X-Token-Savings | Percent saved: Markdown vs raw HTML, or pruned vs full Markdown when q is given. |
| X-Raw-Tokens | Tokens before, ceil(chars / 4). |
| X-Output-Tokens | Tokens returned. |
| X-Credits-Remaining | On every billable response: free requests left today for keyless calls, credits left for a paid key. |
Errors
| Status | Body | When |
|---|---|---|
| 400 | {"error": "url_not_allowed"} | The URL is not public http(s): private, loopback, link-local and similar addresses are rejected, also after redirects. |
| 401 | {"error": "invalid_api_key"} | The key is unknown. |
| 402 | {"error": "insufficient_credits" | "keyless_limit", "message": "...", "checkout_url": "...", "pricing": {...}, "packs": [...], "action": {"type": "payment_link", "url": "https://api.contextfetch.dev/buy/instant", "prompt_to_user": "..."}} | Out of credits, or the keyless allowance is used up. Also sends X-Payment-URL. |
| 502 | {"error": "fetch_failed", "message": "..."} | The target page could not be fetched. |
GET/v1/balance
Credits: 0
Credits left on a key, its auto-reload settings and the average credits used per day over the last 7 days. The key goes in a header only; ?key= is not accepted, so keys never reach access logs. MCP tool: contextfetch_balance.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| Authorization / x-api-key | header | string | yes | The key to check. |
Example request
curl -s https://api.contextfetch.dev/v1/balance -H "Authorization: Bearer YOUR_API_KEY"Example response
{
"credits": 14873,
"auto_reload": {
"enabled": true,
"threshold": 1000,
"pack": "builder",
"monthly_max_usd": 500,
"spent_this_month_usd": 50,
"last_error": null
},
"burn_rate_per_day": 412
}Errors
| Status | Body | When |
|---|---|---|
| 401 | {"error": "invalid_api_key"} | The key is unknown. |
POST/v1/billing/auto-reload
Credits: 0
Turn auto-reload on or off, or change it. After a deduction leaves the balance strictly below threshold, the card saved at checkout buys pack once, as long as this UTC month's auto-reload spend plus the pack price stays within monthly_max_usd. Credits arrive when the payment succeeds. A declined payment turns auto-reload off and sets last_error. Through the API a key can turn it off, lower values or change the pack, but monthly_max_usd can never go above $500; a higher cap needs the browser success page. MCP tool: contextfetch_auto_reload.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| Authorization / x-api-key | header | string | yes | The key to change. |
| enabled | body | bool | yes | true to turn auto-reload on, false to turn it off. |
| threshold | body | int | no | Reload when credits drop below this. 100 to 100,000, default 1,000. |
| pack | body | "instant" | "builder" | "scale" | no | Pack to buy on each reload, default builder. |
| monthly_max_usd | body | number | no | Monthly spending cap in USD, at most 500 through the API. Default 500. |
Example request
curl -s https://api.contextfetch.dev/v1/billing/auto-reload \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"enabled":true,"threshold":1000,"pack":"builder","monthly_max_usd":500}'Example response
{
"credits": 14873,
"auto_reload": {
"enabled": true,
"threshold": 1000,
"pack": "builder",
"monthly_max_usd": 500,
"spent_this_month_usd": 50,
"last_error": null
},
"burn_rate_per_day": 412
}Errors
| Status | Body | When |
|---|---|---|
| 401 | {"error": "invalid_api_key"} | The key is unknown. |
| 409 | {"error": "no_payment_method", "message": "..."} | Auto-reload requested for a key without a saved card. The message carries a buy link; buy with auto-reload on to save one. |
| 422 | - | Request validation failed, e.g. a query over 2,000 characters or a threshold outside 100 to 100,000. |
GET/buy/{pack}
Credits: 0
A link for a human, not an API call: answers 303 straight to Stripe Checkout (card, Apple Pay, Google Pay) for instant ($15, 15,000 credits), builder ($50, 60,000 credits), scale ($150, 200,000 credits). After payment the browser lands on https://contextfetch.dev/success, which shows the key and copies it to the clipboard. Optional query parameters opt in to auto-reload and save the card for it.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| pack | path | "instant" | "builder" | "scale" | yes | Pack to buy. |
| auto_reload | query | 1 | no | Turn auto-reload on and save the card. Off when omitted. |
| threshold | query | int | no | Default 1,000. |
| reload_pack | query | string | no | Default builder. |
| monthly_max_usd | query | number | no | Default 500. |
Example request
# Open in a browser, or show it to your human:https://api.contextfetch.dev/buy/builderhttps://api.contextfetch.dev/buy/builder?auto_reload=1&threshold=1000&reload_pack=builder&monthly_max_usd=500Example response
HTTP/2 303
location: https://checkout.stripe.com/c/pay/cs_live_...Errors
| Status | Body | When |
|---|---|---|
| 400 | {"error": "url_not_allowed"} | The URL is not public http(s): private, loopback, link-local and similar addresses are rejected, also after redirects. |
| 401 | {"error": "invalid_api_key"} | The key is unknown. |
| 402 | {"error": "insufficient_credits" | "keyless_limit", "message": "...", "checkout_url": "...", "pricing": {...}, "packs": [...], "action": {"type": "payment_link", "url": "https://api.contextfetch.dev/buy/instant", "prompt_to_user": "..."}} | Out of credits, or the keyless allowance is used up. Also sends X-Payment-URL. |
| 409 | {"error": "no_payment_method", "message": "..."} | Auto-reload requested for a key without a saved card. The message carries a buy link; buy with auto-reload on to save one. |
| 422 | - | Request validation failed, e.g. a query over 2,000 characters or a threshold outside 100 to 100,000. |
| 502 | {"error": "fetch_failed", "message": "..."} | The target page could not be fetched. |
| 503 | {"error": "recall_unavailable", "message": "..."} | Web search backends are down; retry shortly. |
| 432 | {"detail": {"error": "..."}} | /tavily/* only: out of credits (Tavily's own status), with the same X-Payment-URL header. Other /tavily/* errors also use this body shape. |
Rate limits
| Limit | Value |
|---|---|
| Keyless | 25 requests per IP per UTC day (IPv6: per /64). Failed keyless requests still count. |
| Paid keys | No per-second or per-minute rate limit; each call is limited only by the key's credits. |
| query | 1 to 2,000 characters. |
| numResults / max_results | Up to 25. |
| /v1/contents URLs | Up to 10 per call. |
| Domain filters | Up to 100 entries per list. |
| Request body | Up to 1 MB. |
| Page fetches | http/https only, at most 5 redirects, 5 MB body, 15 s timeout. |