
Brave Search
- 268 installs
- 49 repo stars
- Updated February 11, 2026
- ratacat/claude-skills
Query Brave Search from agent workflows for privacy-oriented web retrieval when researching topics, APIs, vendors, or recent announcements.
About
Claude skill for integrating Brave Search into agent and automation workflows, enabling privacy-focused web retrieval for research, vendor evaluation, API discovery, and current-event fact gathering during early exploration.
- Brave Search API integration patterns
- Privacy-oriented web retrieval
- Agent-friendly query workflows
- Vendor and API discovery support
- Timely announcement and doc lookup
Brave Search by the numbers
- 268 all-time installs (skills.sh)
- Ranked #2,414 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ratacat/claude-skills --skill brave-searchAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 268 |
|---|---|
| repo stars | ★ 49 |
| Last updated | February 11, 2026 |
| Repository | ratacat/claude-skills ↗ |
What it does
Query Brave Search from agent workflows for privacy-oriented web retrieval when researching topics, APIs, vendors, or recent announcements.
Files
Brave Search API
Overview
Search the web, images, and news using Brave's privacy-focused Search API. Also supports AI Grounding for cited answers via OpenAI SDK compatibility.
Prerequisites
API Key required. Get one at https://api-dashboard.search.brave.com
export BRAVE_API_KEY="your-api-key"Free tier: 2,000 queries/month. Pro plans unlock Local POI search and AI Grounding.
When to Use
- Searching the web for current information
- Finding images on a topic
- Getting recent news articles
- AI-grounded answers with citations
- Autocomplete suggestions
- Location/POI searches (Pro plan)
High-Throughput Usage
Go wide, go fast. This API supports high concurrency—up to 50 requests/second. Don't hold back:
- Fire searches in parallel. Need to research 10 different topics? Launch 10 searches simultaneously. Use multiple Bash tool calls in a single message.
- Use subagents for heavy results. When expecting lots of context (many results, extra snippets, research mode), dispatch a subagent to run the search and synthesize findings. This preserves your main context window.
- Batch related queries. Searching for competitors, alternatives, or multiple aspects of a topic? Run them all at once.
digraph parallel_search {
rankdir=LR;
node [shape=box];
task [label="Research task"];
q1 [label="Query 1"];
q2 [label="Query 2"];
q3 [label="Query 3"];
subagent [label="Subagent\n(preserves context)"];
synthesize [label="Synthesized\nfindings"];
task -> q1;
task -> q2;
task -> q3;
q1 -> subagent [style=dashed];
q2 -> subagent [style=dashed];
q3 -> subagent [style=dashed];
subagent -> synthesize;
}When to use subagents:
- Searching for many sources on a topic (launch search subagent, return summary)
- Deep research with AI Grounding (high token usage, let subagent handle)
- Comparing multiple options (run parallel searches, subagent synthesizes)
When to search directly:
- Quick single queries where you need the raw URLs/snippets
- Low result counts where context isn't a concern
Quick Reference
| Task | Command |
|---|---|
| Web search | brave-search web "query" |
| Image search | brave-search images "query" |
| News search | brave-search news "query" |
| AI answer | brave-search ai "question" |
| Suggestions | brave-search suggest "partial query" |
| Check key | brave-search check-key |
API Endpoints
| API | Endpoint | Plan |
|---|---|---|
| Web Search | /res/v1/web/search | Free |
| Image Search | /res/v1/images/search | Free |
| News Search | /res/v1/news/search | Free |
| Suggest | /res/v1/suggest/search | Free |
| AI Grounding | /res/v1/chat/completions | AI Grounding |
| Local POI | /res/v1/local/pois | Pro |
| Summarizer | /res/v1/summarizer/search | Pro |
Common Parameters
Web Search
# Basic search
brave-search web "python async tutorial" --count 10
# Filter by freshness (pd=24h, pw=7d, pm=31d, py=365d)
brave-search web "latest news" --freshness pd
# Filter by country and language
brave-search web "local restaurants" --country US --lang en
# Safe search (off, moderate, strict)
brave-search web "query" --safesearch strict
# Get extra snippets
brave-search web "query" --extra-snippets
# Filter result types (web, news, videos, images, discussions)
brave-search web "query" --filter web,newsImage Search
# Basic image search
brave-search images "mountain sunset"
# With safe search
brave-search images "landscape" --safesearch strict --count 20News Search
# Recent news
brave-search news "AI developments" --count 10
# News with freshness filter
brave-search news "election results" --freshness pdAI Grounding (Cited Answers)
# Get an AI answer with citations
brave-search ai "What is the tallest building in the world?"
# Enable deep research (multiple searches, slower)
brave-search ai "Compare React and Vue in 2024" --researchWorkflow
digraph brave_search {
rankdir=TB;
node [shape=box];
need [label="What do you need?" shape=diamond];
web [label="Web Search\nbrave.py web"];
images [label="Image Search\nbrave.py images"];
news [label="News Search\nbrave.py news"];
ai [label="AI Answer\nbrave.py ai"];
results [label="Parse JSON results"];
cite [label="Include citations\nfor AI answers"];
need -> web [label="web pages"];
need -> images [label="images"];
need -> news [label="recent news"];
need -> ai [label="cited answer"];
web -> results;
images -> results;
news -> results;
ai -> cite;
}Response Structure
Web Search Results
{
"web": {
"results": [
{
"title": "Page Title",
"url": "https://example.com",
"description": "Snippet from the page...",
"extra_snippets": ["Additional context..."]
}
]
},
"query": {
"original": "search query",
"altered": "modified query if spellchecked"
}
}AI Grounding Response
Returns OpenAI-compatible format with inline citations:
The tallest building is the Burj Khalifa[1] at 828 meters...
[1] https://source-url.comCommon Options
| Option | Values | Description |
|---|---|---|
--count | 1-20 (web), 1-200 (images) | Number of results |
--country | US, GB, DE, FR, etc. | Search region |
--lang | en, de, fr, es, etc. | Search language |
--safesearch | off, moderate, strict | Adult content filter |
--freshness | pd, pw, pm, py | Time filter |
--json | flag | Output raw JSON |
Error Handling
| Error | Cause | Fix |
|---|---|---|
| 401 Unauthorized | Invalid/missing API key | Check BRAVE_API_KEY |
| 429 Rate Limited | Too many requests | Wait or upgrade plan |
| 422 Validation | Invalid parameters | Check parameter values |
Rate Limits
- Free: 1 req/sec, 2,000/month
- Pro: Higher limits, check dashboard
- Response headers show remaining quota:
X-RateLimit-Remaining
Common Mistakes
| Mistake | Fix |
|---|---|
| API key not set | export BRAVE_API_KEY="..." |
| Wrong endpoint for plan | Check subscription at dashboard |
| Too many results | Web max is 20, use offset for pagination |
| No AI grounding | Requires AI Grounding subscription |
# Build artifacts
*.egg-info/
__pycache__/
*.pyc
dist/
build/
Brave Search AGENTS.md Blurb
Copy this into your AGENTS.md or CLAUDE.md file to ensure Claude uses Brave Search for web lookups:
---
Web Search
Use `brave-search` for ALL web searches. Do not use WebSearch or WebFetch for general web lookups.
brave-search web "query" # Web search
brave-search news "query" # News
brave-search images "query" # Images
brave-search ai "question" # AI-grounded answer with citationsWhen user asks to "search", "look up", "find out about", or asks about current/latest information—use brave-search, not built-in tools.
#!/usr/bin/env python3
"""
Brave Search API client.
Supports web search, image search, news search, AI grounding, and suggestions.
Requires BRAVE_API_KEY environment variable.
Get an API key at: https://api-dashboard.search.brave.com
"""
import argparse
import json
import os
import sys
import urllib.request
import urllib.parse
import urllib.error
BASE_URL = "https://api.search.brave.com/res/v1"
def get_api_key():
"""Get API key from environment, with helpful error if missing."""
key = os.environ.get('BRAVE_API_KEY')
if not key:
print("""
╔══════════════════════════════════════════════════════════════════╗
║ BRAVE_API_KEY not set ║
╠══════════════════════════════════════════════════════════════════╣
║ Get a free API key at: ║
║ https://api-dashboard.search.brave.com ║
║ ║
║ Then set the environment variable: ║
║ ║
║ export BRAVE_API_KEY="your-key-here" ║
║ ║
║ Add to ~/.bashrc or ~/.zshrc to persist across sessions. ║
╚══════════════════════════════════════════════════════════════════╝
""", file=sys.stderr)
return None
return key
def make_request(endpoint, params=None, method="GET", json_body=None):
"""Make an API request to Brave Search."""
key = get_api_key()
if not key:
return None
url = f"{BASE_URL}/{endpoint}"
if params:
url = f"{url}?{urllib.parse.urlencode(params, doseq=True)}"
headers = {
"Accept": "application/json",
"Accept-Encoding": "gzip",
"X-Subscription-Token": key,
}
data = None
if json_body:
headers["Content-Type"] = "application/json"
data = json.dumps(json_body).encode('utf-8')
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as response:
# Handle gzip
if response.info().get('Content-Encoding') == 'gzip':
import gzip
return json.loads(gzip.decompress(response.read()).decode('utf-8'))
return json.loads(response.read().decode('utf-8'))
except urllib.error.HTTPError as e:
error_body = ""
if e.fp:
raw = e.read()
# Handle gzip-encoded error responses
if raw[:2] == b'\x1f\x8b': # gzip magic bytes
import gzip
try:
error_body = gzip.decompress(raw).decode('utf-8')
except Exception:
error_body = f"[gzip error body, {len(raw)} bytes]"
else:
try:
error_body = raw.decode('utf-8')
except UnicodeDecodeError:
error_body = f"[binary error body, {len(raw)} bytes]"
print(f"HTTP Error {e.code}: {e.reason}", file=sys.stderr)
if error_body:
try:
error_json = json.loads(error_body)
print(f"Details: {json.dumps(error_json, indent=2)}", file=sys.stderr)
except json.JSONDecodeError:
print(f"Details: {error_body[:500]}", file=sys.stderr)
return None
except urllib.error.URLError as e:
print(f"URL Error: {e.reason}", file=sys.stderr)
return None
def web_search(query, count=10, country="US", lang="en", safesearch="moderate",
freshness=None, extra_snippets=False, result_filter=None, offset=0):
"""
Perform a web search.
Args:
query: Search query string
count: Number of results (max 20)
country: Country code (US, GB, DE, etc.)
lang: Language code (en, de, fr, etc.)
safesearch: off, moderate, or strict
freshness: pd (24h), pw (7d), pm (31d), py (365d), or date range
extra_snippets: Include additional snippets
result_filter: Comma-separated list (web, news, videos, discussions, faq)
offset: Pagination offset (0-9)
"""
params = {
"q": query,
"count": min(count, 20),
"country": country,
"search_lang": lang,
"safesearch": safesearch,
"offset": offset,
}
if freshness:
params["freshness"] = freshness
if extra_snippets:
params["extra_snippets"] = "true"
if result_filter:
params["result_filter"] = result_filter
return make_request("web/search", params)
def image_search(query, count=20, country="US", lang="en", safesearch="moderate"):
"""
Search for images.
Args:
query: Search query
count: Number of results (max 200)
country: Country code
lang: Language code
safesearch: off, moderate, or strict
"""
params = {
"q": query,
"count": min(count, 200),
"country": country,
"search_lang": lang,
"safesearch": safesearch,
}
return make_request("images/search", params)
def news_search(query, count=10, country="US", lang="en", safesearch="moderate",
freshness=None):
"""
Search for news articles.
Args:
query: Search query
count: Number of results
country: Country code
lang: Language code
safesearch: off, moderate, or strict
freshness: pd (24h), pw (7d), pm (31d), py (365d)
"""
params = {
"q": query,
"count": count,
"country": country,
"search_lang": lang,
"safesearch": safesearch,
}
if freshness:
params["freshness"] = freshness
return make_request("news/search", params)
def suggest(query, country="US", count=5):
"""
Get search suggestions/autocomplete.
Args:
query: Partial query string
country: Country code
count: Number of suggestions
"""
params = {
"q": query,
"country": country,
"count": count,
}
return make_request("suggest/search", params)
def ai_grounding(question, country="us", language="en", enable_research=False,
enable_citations=True):
"""
Get AI-grounded answer with citations.
Requires AI Grounding subscription.
Args:
question: The question to answer
country: Country code (lowercase)
language: Language code
enable_research: Allow multiple searches (slower, more thorough)
enable_citations: Include source citations
"""
body = {
"messages": [{"role": "user", "content": question}],
"model": "brave",
"stream": False,
"country": country,
"language": language,
"enable_citations": enable_citations,
"enable_research": enable_research,
}
return make_request("chat/completions", method="POST", json_body=body)
def format_web_results(results, show_json=False):
"""Format web search results for display."""
if show_json:
print(json.dumps(results, indent=2))
return
if not results:
print("No results")
return
# Show query info
query = results.get("query", {})
if query.get("altered"):
print(f"Showing results for: {query['altered']}")
print(f"(Original query: {query['original']})\n")
# Web results
web = results.get("web", {})
web_results = web.get("results", [])
if not web_results:
print("No web results found")
return
for i, r in enumerate(web_results, 1):
print(f"{i}. {r.get('title', 'No title')}")
print(f" {r.get('url', '')}")
print(f" {r.get('description', '')[:200]}")
if r.get('extra_snippets'):
for snippet in r['extra_snippets'][:2]:
print(f" → {snippet[:150]}...")
print()
def format_image_results(results, show_json=False):
"""Format image search results for display."""
if show_json:
print(json.dumps(results, indent=2))
return
if not results:
print("No results")
return
images = results.get("results", [])
if not images:
print("No images found")
return
for i, img in enumerate(images, 1):
props = img.get("properties", {})
thumb = img.get("thumbnail", {})
print(f"{i}. {img.get('title', 'No title')}")
print(f" Source: {img.get('url', '')}")
print(f" Image: {props.get('url', '')}")
if props.get('width') and props.get('height'):
print(f" Size: {props['width']}x{props['height']}")
print()
def format_news_results(results, show_json=False):
"""Format news search results for display."""
if show_json:
print(json.dumps(results, indent=2))
return
if not results:
print("No results")
return
news = results.get("results", [])
if not news:
print("No news found")
return
for i, article in enumerate(news, 1):
print(f"{i}. {article.get('title', 'No title')}")
print(f" {article.get('url', '')}")
print(f" Source: {article.get('meta_url', {}).get('hostname', 'Unknown')}")
if article.get('age'):
print(f" Age: {article['age']}")
print(f" {article.get('description', '')[:200]}")
print()
def format_suggest_results(results, show_json=False):
"""Format suggest results for display."""
if show_json:
print(json.dumps(results, indent=2))
return
if not results:
print("No suggestions")
return
suggestions = results.get("results", [])
if not suggestions:
print("No suggestions found")
return
print("Suggestions:")
for i, s in enumerate(suggestions, 1):
query = s.get("query", "")
print(f" {i}. {query}")
def format_ai_results(results, show_json=False):
"""Format AI grounding results for display."""
if show_json:
print(json.dumps(results, indent=2))
return
if not results:
print("No answer available")
return
choices = results.get("choices", [])
if not choices:
print("No answer in response")
return
message = choices[0].get("message", {})
content = message.get("content", "")
print("Answer:")
print("-" * 60)
print(content)
print("-" * 60)
usage = results.get("usage", {})
if usage:
print(f"\nTokens: {usage.get('total_tokens', 'N/A')}")
def main():
parser = argparse.ArgumentParser(
description="Brave Search API client",
epilog="Requires BRAVE_API_KEY environment variable."
)
subparsers = parser.add_subparsers(dest='command', help='Commands')
# Web search
web_parser = subparsers.add_parser('web', help='Web search')
web_parser.add_argument('query', help='Search query')
web_parser.add_argument('--count', '-c', type=int, default=10, help='Number of results (max 20)')
web_parser.add_argument('--country', default='US', help='Country code (US, GB, DE, etc.)')
web_parser.add_argument('--lang', default='en', help='Language code')
web_parser.add_argument('--safesearch', choices=['off', 'moderate', 'strict'], default='moderate')
web_parser.add_argument('--freshness', help='Time filter: pd, pw, pm, py, or YYYY-MM-DDtoYYYY-MM-DD')
web_parser.add_argument('--extra-snippets', action='store_true', help='Include extra snippets')
web_parser.add_argument('--filter', help='Result types: web,news,videos,discussions,faq')
web_parser.add_argument('--offset', type=int, default=0, help='Pagination offset (0-9)')
web_parser.add_argument('--json', '-j', action='store_true', help='Output raw JSON')
# Image search
img_parser = subparsers.add_parser('images', help='Image search')
img_parser.add_argument('query', help='Search query')
img_parser.add_argument('--count', '-c', type=int, default=20, help='Number of results (max 200)')
img_parser.add_argument('--country', default='US', help='Country code')
img_parser.add_argument('--lang', default='en', help='Language code')
img_parser.add_argument('--safesearch', choices=['off', 'moderate', 'strict'], default='moderate')
img_parser.add_argument('--json', '-j', action='store_true', help='Output raw JSON')
# News search
news_parser = subparsers.add_parser('news', help='News search')
news_parser.add_argument('query', help='Search query')
news_parser.add_argument('--count', '-c', type=int, default=10, help='Number of results')
news_parser.add_argument('--country', default='US', help='Country code')
news_parser.add_argument('--lang', default='en', help='Language code')
news_parser.add_argument('--safesearch', choices=['off', 'moderate', 'strict'], default='moderate')
news_parser.add_argument('--freshness', help='Time filter: pd, pw, pm, py')
news_parser.add_argument('--json', '-j', action='store_true', help='Output raw JSON')
# Suggest
suggest_parser = subparsers.add_parser('suggest', help='Get search suggestions')
suggest_parser.add_argument('query', help='Partial query')
suggest_parser.add_argument('--count', '-c', type=int, default=5, help='Number of suggestions')
suggest_parser.add_argument('--country', default='US', help='Country code')
suggest_parser.add_argument('--json', '-j', action='store_true', help='Output raw JSON')
# AI Grounding
ai_parser = subparsers.add_parser('ai', help='AI-grounded answer (requires AI Grounding plan)')
ai_parser.add_argument('question', help='Question to answer')
ai_parser.add_argument('--country', default='us', help='Country code (lowercase)')
ai_parser.add_argument('--lang', default='en', help='Language code')
ai_parser.add_argument('--research', action='store_true', help='Enable deep research (multiple searches)')
ai_parser.add_argument('--no-citations', action='store_true', help='Disable citations')
ai_parser.add_argument('--json', '-j', action='store_true', help='Output raw JSON')
# Check key
subparsers.add_parser('check-key', help='Check if API key is set')
args = parser.parse_args()
if not args.command:
parser.print_help()
return
if args.command == 'check-key':
key = get_api_key()
if key:
print(f"API key is set: {key[:8]}...{key[-4:]}")
return
if args.command == 'web':
results = web_search(
args.query,
count=args.count,
country=args.country,
lang=args.lang,
safesearch=args.safesearch,
freshness=args.freshness,
extra_snippets=args.extra_snippets,
result_filter=args.filter,
offset=args.offset,
)
format_web_results(results, args.json)
elif args.command == 'images':
results = image_search(
args.query,
count=args.count,
country=args.country,
lang=args.lang,
safesearch=args.safesearch,
)
format_image_results(results, args.json)
elif args.command == 'news':
results = news_search(
args.query,
count=args.count,
country=args.country,
lang=args.lang,
safesearch=args.safesearch,
freshness=args.freshness,
)
format_news_results(results, args.json)
elif args.command == 'suggest':
results = suggest(
args.query,
country=args.country,
count=args.count,
)
format_suggest_results(results, args.json)
elif args.command == 'ai':
results = ai_grounding(
args.question,
country=args.country,
language=args.lang,
enable_research=args.research,
enable_citations=not args.no_citations,
)
format_ai_results(results, args.json)
if __name__ == '__main__':
main()
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "brave-search-cli"
version = "1.0.0"
description = "Brave Search API CLI client for web, image, news search and AI grounding"
readme = "SKILL.md"
requires-python = ">=3.8"
license = {text = "MIT"}
keywords = ["brave", "search", "api", "cli", "web-search"]
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
[project.scripts]
brave-search = "brave_search.cli:main"
[tool.setuptools.packages.find]
where = ["src"]
# ABOUTME: Brave Search API client package.
# ABOUTME: Provides web, image, news search and AI grounding via Brave Search API.
__version__ = "1.0.0"
#!/usr/bin/env python3
# ABOUTME: Brave Search API CLI client.
# ABOUTME: Provides web, image, news search and AI grounding via Brave Search API.
"""
Brave Search API client.
Supports web search, image search, news search, AI grounding, and suggestions.
Requires BRAVE_API_KEY environment variable.
Get an API key at: https://api-dashboard.search.brave.com
"""
import argparse
import json
import os
import sys
import urllib.request
import urllib.parse
import urllib.error
BASE_URL = "https://api.search.brave.com/res/v1"
def get_api_key():
"""Get API key from environment, with helpful error if missing."""
key = os.environ.get('BRAVE_API_KEY')
if not key:
print("""
╔══════════════════════════════════════════════════════════════════╗
║ BRAVE_API_KEY not set ║
╠══════════════════════════════════════════════════════════════════╣
║ Get a free API key at: ║
║ https://api-dashboard.search.brave.com ║
║ ║
║ Then set the environment variable: ║
║ ║
║ export BRAVE_API_KEY="your-key-here" ║
║ ║
║ Add to ~/.bashrc or ~/.zshrc to persist across sessions. ║
╚══════════════════════════════════════════════════════════════════╝
""", file=sys.stderr)
return None
return key
def make_request(endpoint, params=None, method="GET", json_body=None):
"""Make an API request to Brave Search."""
key = get_api_key()
if not key:
return None
url = f"{BASE_URL}/{endpoint}"
if params:
url = f"{url}?{urllib.parse.urlencode(params, doseq=True)}"
headers = {
"Accept": "application/json",
"Accept-Encoding": "gzip",
"X-Subscription-Token": key,
}
data = None
if json_body:
headers["Content-Type"] = "application/json"
data = json.dumps(json_body).encode('utf-8')
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as response:
# Handle gzip
if response.info().get('Content-Encoding') == 'gzip':
import gzip
return json.loads(gzip.decompress(response.read()).decode('utf-8'))
return json.loads(response.read().decode('utf-8'))
except urllib.error.HTTPError as e:
error_body = ""
if e.fp:
raw = e.read()
# Handle gzip-encoded error responses
if raw[:2] == b'\x1f\x8b': # gzip magic bytes
import gzip
try:
error_body = gzip.decompress(raw).decode('utf-8')
except Exception:
error_body = f"[gzip error body, {len(raw)} bytes]"
else:
try:
error_body = raw.decode('utf-8')
except UnicodeDecodeError:
error_body = f"[binary error body, {len(raw)} bytes]"
print(f"HTTP Error {e.code}: {e.reason}", file=sys.stderr)
if error_body:
try:
error_json = json.loads(error_body)
print(f"Details: {json.dumps(error_json, indent=2)}", file=sys.stderr)
except json.JSONDecodeError:
print(f"Details: {error_body[:500]}", file=sys.stderr)
return None
except urllib.error.URLError as e:
print(f"URL Error: {e.reason}", file=sys.stderr)
return None
def web_search(query, count=10, country="US", lang="en", safesearch="moderate",
freshness=None, extra_snippets=False, result_filter=None, offset=0):
"""
Perform a web search.
Args:
query: Search query string
count: Number of results (max 20)
country: Country code (US, GB, DE, etc.)
lang: Language code (en, de, fr, etc.)
safesearch: off, moderate, or strict
freshness: pd (24h), pw (7d), pm (31d), py (365d), or date range
extra_snippets: Include additional snippets
result_filter: Comma-separated list (web, news, videos, discussions, faq)
offset: Pagination offset (0-9)
"""
params = {
"q": query,
"count": min(count, 20),
"country": country,
"search_lang": lang,
"safesearch": safesearch,
"offset": offset,
}
if freshness:
params["freshness"] = freshness
if extra_snippets:
params["extra_snippets"] = "true"
if result_filter:
params["result_filter"] = result_filter
return make_request("web/search", params)
def image_search(query, count=20, country="US", lang="en", safesearch="moderate"):
"""
Search for images.
Args:
query: Search query
count: Number of results (max 200)
country: Country code
lang: Language code
safesearch: off, moderate, or strict
"""
params = {
"q": query,
"count": min(count, 200),
"country": country,
"search_lang": lang,
"safesearch": safesearch,
}
return make_request("images/search", params)
def news_search(query, count=10, country="US", lang="en", safesearch="moderate",
freshness=None):
"""
Search for news articles.
Args:
query: Search query
count: Number of results
country: Country code
lang: Language code
safesearch: off, moderate, or strict
freshness: pd (24h), pw (7d), pm (31d), py (365d)
"""
params = {
"q": query,
"count": count,
"country": country,
"search_lang": lang,
"safesearch": safesearch,
}
if freshness:
params["freshness"] = freshness
return make_request("news/search", params)
def suggest(query, country="US", count=5):
"""
Get search suggestions/autocomplete.
Args:
query: Partial query string
country: Country code
count: Number of suggestions
"""
params = {
"q": query,
"country": country,
"count": count,
}
return make_request("suggest/search", params)
def ai_grounding(question, country="us", language="en", enable_research=False,
enable_citations=True):
"""
Get AI-grounded answer with citations.
Requires AI Grounding subscription.
Args:
question: The question to answer
country: Country code (lowercase)
language: Language code
enable_research: Allow multiple searches (slower, more thorough)
enable_citations: Include source citations
"""
body = {
"messages": [{"role": "user", "content": question}],
"model": "brave",
"stream": False,
"country": country,
"language": language,
"enable_citations": enable_citations,
"enable_research": enable_research,
}
return make_request("chat/completions", method="POST", json_body=body)
def format_web_results(results, show_json=False):
"""Format web search results for display."""
if show_json:
print(json.dumps(results, indent=2))
return
if not results:
print("No results")
return
# Show query info
query = results.get("query", {})
if query.get("altered"):
print(f"Showing results for: {query['altered']}")
print(f"(Original query: {query['original']})\n")
# Web results
web = results.get("web", {})
web_results = web.get("results", [])
if not web_results:
print("No web results found")
return
for i, r in enumerate(web_results, 1):
print(f"{i}. {r.get('title', 'No title')}")
print(f" {r.get('url', '')}")
print(f" {r.get('description', '')[:200]}")
if r.get('extra_snippets'):
for snippet in r['extra_snippets'][:2]:
print(f" → {snippet[:150]}...")
print()
def format_image_results(results, show_json=False):
"""Format image search results for display."""
if show_json:
print(json.dumps(results, indent=2))
return
if not results:
print("No results")
return
images = results.get("results", [])
if not images:
print("No images found")
return
for i, img in enumerate(images, 1):
props = img.get("properties", {})
thumb = img.get("thumbnail", {})
print(f"{i}. {img.get('title', 'No title')}")
print(f" Source: {img.get('url', '')}")
print(f" Image: {props.get('url', '')}")
if props.get('width') and props.get('height'):
print(f" Size: {props['width']}x{props['height']}")
print()
def format_news_results(results, show_json=False):
"""Format news search results for display."""
if show_json:
print(json.dumps(results, indent=2))
return
if not results:
print("No results")
return
news = results.get("results", [])
if not news:
print("No news found")
return
for i, article in enumerate(news, 1):
print(f"{i}. {article.get('title', 'No title')}")
print(f" {article.get('url', '')}")
print(f" Source: {article.get('meta_url', {}).get('hostname', 'Unknown')}")
if article.get('age'):
print(f" Age: {article['age']}")
print(f" {article.get('description', '')[:200]}")
print()
def format_suggest_results(results, show_json=False):
"""Format suggest results for display."""
if show_json:
print(json.dumps(results, indent=2))
return
if not results:
print("No suggestions")
return
suggestions = results.get("results", [])
if not suggestions:
print("No suggestions found")
return
print("Suggestions:")
for i, s in enumerate(suggestions, 1):
query = s.get("query", "")
print(f" {i}. {query}")
def format_ai_results(results, show_json=False):
"""Format AI grounding results for display."""
if show_json:
print(json.dumps(results, indent=2))
return
if not results:
print("No answer available")
return
choices = results.get("choices", [])
if not choices:
print("No answer in response")
return
message = choices[0].get("message", {})
content = message.get("content", "")
print("Answer:")
print("-" * 60)
print(content)
print("-" * 60)
usage = results.get("usage", {})
if usage:
print(f"\nTokens: {usage.get('total_tokens', 'N/A')}")
def main():
parser = argparse.ArgumentParser(
description="Brave Search API client",
epilog="Requires BRAVE_API_KEY environment variable."
)
subparsers = parser.add_subparsers(dest='command', help='Commands')
# Web search
web_parser = subparsers.add_parser('web', help='Web search')
web_parser.add_argument('query', help='Search query')
web_parser.add_argument('--count', '-c', type=int, default=10, help='Number of results (max 20)')
web_parser.add_argument('--country', default='US', help='Country code (US, GB, DE, etc.)')
web_parser.add_argument('--lang', default='en', help='Language code')
web_parser.add_argument('--safesearch', choices=['off', 'moderate', 'strict'], default='moderate')
web_parser.add_argument('--freshness', help='Time filter: pd, pw, pm, py, or YYYY-MM-DDtoYYYY-MM-DD')
web_parser.add_argument('--extra-snippets', action='store_true', help='Include extra snippets')
web_parser.add_argument('--filter', help='Result types: web,news,videos,discussions,faq')
web_parser.add_argument('--offset', type=int, default=0, help='Pagination offset (0-9)')
web_parser.add_argument('--json', '-j', action='store_true', help='Output raw JSON')
# Image search
img_parser = subparsers.add_parser('images', help='Image search')
img_parser.add_argument('query', help='Search query')
img_parser.add_argument('--count', '-c', type=int, default=20, help='Number of results (max 200)')
img_parser.add_argument('--country', default='US', help='Country code')
img_parser.add_argument('--lang', default='en', help='Language code')
img_parser.add_argument('--safesearch', choices=['off', 'moderate', 'strict'], default='moderate')
img_parser.add_argument('--json', '-j', action='store_true', help='Output raw JSON')
# News search
news_parser = subparsers.add_parser('news', help='News search')
news_parser.add_argument('query', help='Search query')
news_parser.add_argument('--count', '-c', type=int, default=10, help='Number of results')
news_parser.add_argument('--country', default='US', help='Country code')
news_parser.add_argument('--lang', default='en', help='Language code')
news_parser.add_argument('--safesearch', choices=['off', 'moderate', 'strict'], default='moderate')
news_parser.add_argument('--freshness', help='Time filter: pd, pw, pm, py')
news_parser.add_argument('--json', '-j', action='store_true', help='Output raw JSON')
# Suggest
suggest_parser = subparsers.add_parser('suggest', help='Get search suggestions')
suggest_parser.add_argument('query', help='Partial query')
suggest_parser.add_argument('--count', '-c', type=int, default=5, help='Number of suggestions')
suggest_parser.add_argument('--country', default='US', help='Country code')
suggest_parser.add_argument('--json', '-j', action='store_true', help='Output raw JSON')
# AI Grounding
ai_parser = subparsers.add_parser('ai', help='AI-grounded answer (requires AI Grounding plan)')
ai_parser.add_argument('question', help='Question to answer')
ai_parser.add_argument('--country', default='us', help='Country code (lowercase)')
ai_parser.add_argument('--lang', default='en', help='Language code')
ai_parser.add_argument('--research', action='store_true', help='Enable deep research (multiple searches)')
ai_parser.add_argument('--no-citations', action='store_true', help='Disable citations')
ai_parser.add_argument('--json', '-j', action='store_true', help='Output raw JSON')
# Check key
subparsers.add_parser('check-key', help='Check if API key is set')
args = parser.parse_args()
if not args.command:
parser.print_help()
return
if args.command == 'check-key':
key = get_api_key()
if key:
print(f"API key is set: {key[:8]}...{key[-4:]}")
return
if args.command == 'web':
results = web_search(
args.query,
count=args.count,
country=args.country,
lang=args.lang,
safesearch=args.safesearch,
freshness=args.freshness,
extra_snippets=args.extra_snippets,
result_filter=args.filter,
offset=args.offset,
)
format_web_results(results, args.json)
elif args.command == 'images':
results = image_search(
args.query,
count=args.count,
country=args.country,
lang=args.lang,
safesearch=args.safesearch,
)
format_image_results(results, args.json)
elif args.command == 'news':
results = news_search(
args.query,
count=args.count,
country=args.country,
lang=args.lang,
safesearch=args.safesearch,
freshness=args.freshness,
)
format_news_results(results, args.json)
elif args.command == 'suggest':
results = suggest(
args.query,
country=args.country,
count=args.count,
)
format_suggest_results(results, args.json)
elif args.command == 'ai':
results = ai_grounding(
args.question,
country=args.country,
language=args.lang,
enable_research=args.research,
enable_citations=not args.no_citations,
)
format_ai_results(results, args.json)
if __name__ == '__main__':
main()