diff --git a/docs/docs.json b/docs/docs.json
index 22a2c963fe..ef874720e9 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -259,6 +259,7 @@
"pages": [
"edge/en/tools/search-research/overview",
"edge/en/tools/search-research/serperdevtool",
+ "edge/en/tools/search-research/searchapitool",
"edge/en/tools/search-research/bravesearchtool",
"edge/en/tools/search-research/exasearchtool",
"edge/en/tools/search-research/linkupsearchtool",
@@ -13912,6 +13913,7 @@
"pages": [
"edge/pt-BR/tools/search-research/overview",
"edge/pt-BR/tools/search-research/serperdevtool",
+ "edge/pt-BR/tools/search-research/searchapitool",
"edge/pt-BR/tools/search-research/bravesearchtool",
"edge/pt-BR/tools/search-research/exasearchtool",
"edge/pt-BR/tools/search-research/linkupsearchtool",
@@ -26630,6 +26632,7 @@
"pages": [
"edge/ko/tools/search-research/overview",
"edge/ko/tools/search-research/serperdevtool",
+ "edge/ko/tools/search-research/searchapitool",
"edge/ko/tools/search-research/bravesearchtool",
"edge/ko/tools/search-research/exasearchtool",
"edge/ko/tools/search-research/linkupsearchtool",
@@ -39777,6 +39780,7 @@
"pages": [
"edge/ar/tools/search-research/overview",
"edge/ar/tools/search-research/serperdevtool",
+ "edge/ar/tools/search-research/searchapitool",
"edge/ar/tools/search-research/bravesearchtool",
"edge/ar/tools/search-research/exasearchtool",
"edge/ar/tools/search-research/linkupsearchtool",
diff --git a/docs/edge/ar/tools/search-research/overview.mdx b/docs/edge/ar/tools/search-research/overview.mdx
index 6b73d12b5d..4d57d32b12 100644
--- a/docs/edge/ar/tools/search-research/overview.mdx
+++ b/docs/edge/ar/tools/search-research/overview.mdx
@@ -14,6 +14,10 @@ mode: "wide"
تكامل مع Google search API لقدرات بحث شاملة على الويب.
+
+ عدة محركات بحث خلف نقطة نهاية واحدة: بحث Google على الويب والأخبار والأبحاث الأكاديمية والوظائف، إضافة إلى Bing و YouTube وغيرها.
+
+
بحث يركز على الخصوصية مع فهرس بحث Brave المستقل.
diff --git a/docs/edge/ar/tools/search-research/searchapitool.mdx b/docs/edge/ar/tools/search-research/searchapitool.mdx
new file mode 100644
index 0000000000..6f10ea92b0
--- /dev/null
+++ b/docs/edge/ar/tools/search-research/searchapitool.mdx
@@ -0,0 +1,74 @@
+---
+title: بحث SearchApi
+description: أداة `SearchApiTool` تبحث في الإنترنت عبر SearchApi، الذي يجمع عدة محركات بحث خلف نقطة نهاية واحدة.
+icon: magnifying-glass
+mode: "wide"
+---
+
+# `SearchApiTool`
+
+## الوصف
+
+أداة `SearchApiTool` تبحث في الإنترنت عبر [SearchApi](https://www.searchapi.io). نقطة نهاية واحدة تجمع عدة محركات، تُختار بالمعامل `engine`، لذا تغطي أداة واحدة بحث Google على الويب والأخبار والأبحاث الأكاديمية والوظائف، إضافة إلى Bing و YouTube و Baidu وبقية [المحركات المدعومة](https://www.searchapi.io/docs). تُرجع الأداة JSON الخاص بالمحرك نفسه، لذا تتطابق النتائج مع توثيق ذلك المحرك.
+
+يحدث أمران للاستجابة قبل أن يراها الـ Agent:
+
+- تُحذف روابط `data:` المضمّنة. يُرجع SearchApi الأيقونات والصور المصغّرة كسلاسل base64، وقد تبلغ الواحدة منها عشرات الكيلوبايتات من السياق الذي لا يعني شيئًا للـ Agent.
+- تُقتطع السلاسل الطويلة عند `max_string_length`، وتُحدَّد كل قائمة `*_results` بعدد `n_results`.
+
+## التثبيت
+
+1. **تثبيت الحزمة**: تأكد من تثبيت حزمة `crewai[tools]` في بيئة Python لديك.
+2. **الحصول على مفتاح API**: احصل على مفتاح SearchApi من https://www.searchapi.io (تتوفر خطة مجانية).
+3. **إعداد البيئة**: احفظ المفتاح في متغير بيئة باسم `SEARCHAPI_API_KEY`.
+
+```shell
+pip install 'crewai[tools]'
+```
+
+يُرسَل المفتاح في ترويسة `Authorization` بدلًا من سلسلة الاستعلام، لذا يبقى خارج سجلات الطلبات وخارج `request_url` الذي يعيده SearchApi في `search_metadata`.
+
+## مثال
+
+```python Code
+from crewai_tools import SearchApiTool
+
+# Initialize the tool for internet searching capabilities
+tool = SearchApiTool()
+```
+
+## المعاملات
+
+- **engine**: محرك SearchApi المطلوب، مثل `google` أو `google_news` أو `google_scholar` أو `google_jobs` أو `bing` أو `youtube` أو `baidu`. القيمة الافتراضية `google`. ويمكن تمريره مع كل استدعاء.
+- **n_results**: الحد الأقصى لطول كل قائمة `*_results` في الاستجابة. القيمة الافتراضية `10`.
+- **country**: اختياري. بلد البحث، يُرسَل باسم `gl` (مثل `uk`).
+- **locale**: اختياري. لغة الواجهة، تُرسَل باسم `hl` (مثل `en`).
+- **location**: اختياري. الموقع المعياري للبحث، مثل `London,England`.
+- **max_string_length**: أطول سلسلة تبقى كما هي في الاستجابة. القيمة الافتراضية `1000`.
+- **timeout**: مهلة الطلب بالثواني. القيمة الافتراضية `30`.
+- **api_key**: اختياري. مفتاح SearchApi الخاص بك. وإن لم يُحدَّد، يُقرأ من متغير البيئة `SEARCHAPI_API_KEY`.
+- **search_url**: نقطة النهاية التي تُستدعى. القيمة الافتراضية `https://www.searchapi.io/api/v1/search`.
+
+## مثال مع المعاملات
+
+```python Code
+from crewai_tools import SearchApiTool
+
+# Recent news, localized to the UK
+news_tool = SearchApiTool(
+ engine="google_news",
+ country="uk",
+ locale="en",
+ location="London,England",
+ n_results=5,
+)
+
+# Academic search through the same tool
+scholar_tool = SearchApiTool(engine="google_scholar")
+```
+
+## الأخطاء
+
+يؤدي فشل الطلب إلى إطلاق `RuntimeError` يحمل رسالة SearchApi نفسها، مثل `SearchApi request failed (HTTP 401): Invalid API key.`.
+
+أما البحث الناجح الذي لم يجد شيئًا فليس خطأً: يُرجع SearchApi استجابة HTTP 200 مع رسالة `error` مثل `"Google didn't return any results."`، وتمرّر الأداة هذه الرسالة كما هي ليقرأ الـ Agent سبب خلو الصفحة.
diff --git a/docs/edge/en/tools/search-research/overview.mdx b/docs/edge/en/tools/search-research/overview.mdx
index 6fdd8410c4..9d73c9599d 100644
--- a/docs/edge/en/tools/search-research/overview.mdx
+++ b/docs/edge/en/tools/search-research/overview.mdx
@@ -14,6 +14,10 @@ These tools enable your agents to search the web, research topics, and find info
Google search API integration for comprehensive web search capabilities.
+
+ Many search engines behind one endpoint: Google web, news, scholar and jobs, plus Bing, YouTube and more.
+
+
Privacy-focused search with Brave's independent search index.
diff --git a/docs/edge/en/tools/search-research/searchapitool.mdx b/docs/edge/en/tools/search-research/searchapitool.mdx
new file mode 100644
index 0000000000..77ce8bcfe1
--- /dev/null
+++ b/docs/edge/en/tools/search-research/searchapitool.mdx
@@ -0,0 +1,74 @@
+---
+title: SearchApi Search
+description: The `SearchApiTool` searches the internet through SearchApi, which fronts many search engines behind one endpoint.
+icon: magnifying-glass
+mode: "wide"
+---
+
+# `SearchApiTool`
+
+## Description
+
+The `SearchApiTool` searches the internet through [SearchApi](https://www.searchapi.io). One endpoint fronts many engines, chosen with the `engine` parameter, so a single tool covers Google web search, news, scholar and jobs, plus Bing, YouTube, Baidu and the rest of the [supported engines](https://www.searchapi.io/docs). The tool returns the engine's own JSON, so results line up with the engine's documentation.
+
+Two things happen to the response before an agent sees it:
+
+- Inline `data:` URIs are dropped. SearchApi returns favicons and thumbnails as base64 strings, and a single one can run to tens of kilobytes of context that means nothing to an agent.
+- Long strings are truncated at `max_string_length`, and every `*_results` list is capped at `n_results`.
+
+## Installation
+
+1. **Package Installation**: Confirm that the `crewai[tools]` package is installed in your Python environment.
+2. **API Key Acquisition**: Get a SearchApi key at https://www.searchapi.io (free tier available).
+3. **Environment Configuration**: Store the key in an environment variable named `SEARCHAPI_API_KEY`.
+
+```shell
+pip install 'crewai[tools]'
+```
+
+The key is sent in the `Authorization` header rather than the query string, so it stays out of request logs and out of the `request_url` SearchApi echoes back in `search_metadata`.
+
+## Example
+
+```python Code
+from crewai_tools import SearchApiTool
+
+# Initialize the tool for internet searching capabilities
+tool = SearchApiTool()
+```
+
+## Parameters
+
+- **engine**: The SearchApi engine to query, such as `google`, `google_news`, `google_scholar`, `google_jobs`, `bing`, `youtube` or `baidu`. Default is `google`. Can also be passed per call.
+- **n_results**: Cap on the length of each `*_results` list in the response. Default is `10`.
+- **country**: Optional. Country of the search, sent as `gl` (for example `uk`).
+- **locale**: Optional. Interface language, sent as `hl` (for example `en`).
+- **location**: Optional. Canonical location of the search, for example `London,England`.
+- **max_string_length**: Longest string kept intact in the response. Default is `1000`.
+- **timeout**: Request timeout in seconds. Default is `30`.
+- **api_key**: Optional. Your SearchApi key. Falls back to the `SEARCHAPI_API_KEY` environment variable.
+- **search_url**: The endpoint to call. Default is `https://www.searchapi.io/api/v1/search`.
+
+## Example with Parameters
+
+```python Code
+from crewai_tools import SearchApiTool
+
+# Recent news, localized to the UK
+news_tool = SearchApiTool(
+ engine="google_news",
+ country="uk",
+ locale="en",
+ location="London,England",
+ n_results=5,
+)
+
+# Academic search through the same tool
+scholar_tool = SearchApiTool(engine="google_scholar")
+```
+
+## Errors
+
+A failed request raises a `RuntimeError` carrying SearchApi's own message, for example `SearchApi request failed (HTTP 401): Invalid API key.`.
+
+A successful search that found nothing is not an error: SearchApi returns HTTP 200 with an `error` message such as `"Google didn't return any results."`, and the tool passes that through so the agent can read why the page was empty.
diff --git a/docs/edge/ko/tools/search-research/overview.mdx b/docs/edge/ko/tools/search-research/overview.mdx
index ff59c38971..d565e32c10 100644
--- a/docs/edge/ko/tools/search-research/overview.mdx
+++ b/docs/edge/ko/tools/search-research/overview.mdx
@@ -14,6 +14,10 @@ mode: "wide"
종합적인 웹 검색 기능을 위한 Google 검색 API 통합.
+
+ 하나의 엔드포인트로 여러 검색 엔진을 제공합니다: Google 웹, 뉴스, 학술, 채용 정보에 더해 Bing, YouTube 등.
+
+
Brave의 독립적인 검색 인덱스를 활용한 프라이버시 중심의 검색.
diff --git a/docs/edge/ko/tools/search-research/searchapitool.mdx b/docs/edge/ko/tools/search-research/searchapitool.mdx
new file mode 100644
index 0000000000..bc0a5471aa
--- /dev/null
+++ b/docs/edge/ko/tools/search-research/searchapitool.mdx
@@ -0,0 +1,74 @@
+---
+title: SearchApi 검색
+description: SearchApiTool은(는) 여러 검색 엔진을 하나의 엔드포인트로 제공하는 SearchApi를 통해 인터넷을 검색합니다.
+icon: magnifying-glass
+mode: "wide"
+---
+
+# `SearchApiTool`
+
+## 설명
+
+`SearchApiTool`은 [SearchApi](https://www.searchapi.io)를 통해 인터넷을 검색합니다. 하나의 엔드포인트가 여러 엔진을 제공하며 `engine` 파라미터로 선택하므로, 단일 도구로 Google 웹 검색, 뉴스, 학술, 채용 정보는 물론 Bing, YouTube, Baidu 및 나머지 [지원 엔진](https://www.searchapi.io/docs)까지 다룰 수 있습니다. 이 도구는 엔진의 JSON을 그대로 반환하므로 결과는 해당 엔진의 문서와 일치합니다.
+
+에이전트가 응답을 보기 전에 두 가지 처리가 이루어집니다:
+
+- 인라인 `data:` URI가 제거됩니다. SearchApi는 파비콘과 썸네일을 base64 문자열로 반환하는데, 하나만으로도 수십 킬로바이트에 이를 수 있으며 에이전트에게는 아무런 의미가 없습니다.
+- 긴 문자열은 `max_string_length`에서 잘리고, 모든 `*_results` 목록은 `n_results` 개수로 제한됩니다.
+
+## 설치
+
+1. **패키지 설치**: Python 환경에 `crewai[tools]` 패키지가 설치되어 있는지 확인하세요.
+2. **API 키 발급**: https://www.searchapi.io 에서 SearchApi 키를 발급받으세요(무료 티어 제공).
+3. **환경 설정**: 발급받은 키를 `SEARCHAPI_API_KEY`라는 환경 변수에 저장하세요.
+
+```shell
+pip install 'crewai[tools]'
+```
+
+키는 쿼리 문자열이 아니라 `Authorization` 헤더로 전송되므로, 요청 로그와 SearchApi가 `search_metadata`에 되돌려주는 `request_url`에 노출되지 않습니다.
+
+## 예제
+
+```python Code
+from crewai_tools import SearchApiTool
+
+# Initialize the tool for internet searching capabilities
+tool = SearchApiTool()
+```
+
+## 파라미터
+
+- **engine**: 호출할 SearchApi 엔진입니다. 예: `google`, `google_news`, `google_scholar`, `google_jobs`, `bing`, `youtube`, `baidu`. 기본값은 `google`입니다. 호출할 때마다 전달할 수도 있습니다.
+- **n_results**: 응답에서 각 `*_results` 목록의 최대 길이입니다. 기본값은 `10`입니다.
+- **country**: 선택 사항. 검색 대상 국가이며 `gl`로 전송됩니다(예: `uk`).
+- **locale**: 선택 사항. 인터페이스 언어이며 `hl`로 전송됩니다(예: `en`).
+- **location**: 선택 사항. 검색의 표준 위치입니다(예: `London,England`).
+- **max_string_length**: 응답에서 그대로 유지되는 문자열의 최대 길이입니다. 기본값은 `1000`입니다.
+- **timeout**: 요청 타임아웃(초)입니다. 기본값은 `30`입니다.
+- **api_key**: 선택 사항. SearchApi 키입니다. 지정하지 않으면 `SEARCHAPI_API_KEY` 환경 변수를 사용합니다.
+- **search_url**: 호출할 엔드포인트입니다. 기본값은 `https://www.searchapi.io/api/v1/search`입니다.
+
+## 파라미터를 사용한 예제
+
+```python Code
+from crewai_tools import SearchApiTool
+
+# Recent news, localized to the UK
+news_tool = SearchApiTool(
+ engine="google_news",
+ country="uk",
+ locale="en",
+ location="London,England",
+ n_results=5,
+)
+
+# Academic search through the same tool
+scholar_tool = SearchApiTool(engine="google_scholar")
+```
+
+## 오류
+
+요청이 실패하면 SearchApi의 메시지를 담은 `RuntimeError`가 발생합니다. 예: `SearchApi request failed (HTTP 401): Invalid API key.`
+
+검색은 성공했지만 결과가 없는 경우는 오류가 아닙니다. SearchApi는 HTTP 200과 함께 `"Google didn't return any results."` 같은 `error` 메시지를 반환하며, 이 도구는 에이전트가 그 이유를 읽을 수 있도록 해당 메시지를 그대로 전달합니다.
diff --git a/docs/edge/pt-BR/tools/search-research/overview.mdx b/docs/edge/pt-BR/tools/search-research/overview.mdx
index bece4353eb..ab97a08485 100644
--- a/docs/edge/pt-BR/tools/search-research/overview.mdx
+++ b/docs/edge/pt-BR/tools/search-research/overview.mdx
@@ -14,6 +14,10 @@ Essas ferramentas permitem que seus agentes pesquisem na web, explorem tópicos
Integração com a API de busca do Google para capacidades abrangentes de pesquisa na web.
+
+ Vários motores de busca em um único endpoint: web, notícias, artigos acadêmicos e vagas do Google, além de Bing, YouTube e outros.
+
+
Pesquisa voltada para privacidade com o índice independente de busca do Brave.
diff --git a/docs/edge/pt-BR/tools/search-research/searchapitool.mdx b/docs/edge/pt-BR/tools/search-research/searchapitool.mdx
new file mode 100644
index 0000000000..3dad234ebe
--- /dev/null
+++ b/docs/edge/pt-BR/tools/search-research/searchapitool.mdx
@@ -0,0 +1,74 @@
+---
+title: Pesquisa SearchApi
+description: O `SearchApiTool` pesquisa na internet através do SearchApi, que reúne vários motores de busca em um único endpoint.
+icon: magnifying-glass
+mode: "wide"
+---
+
+# `SearchApiTool`
+
+## Descrição
+
+O `SearchApiTool` pesquisa na internet através do [SearchApi](https://www.searchapi.io). Um único endpoint reúne vários motores, escolhidos com o parâmetro `engine`, de modo que uma só ferramenta cobre a busca web do Google, notícias, artigos acadêmicos e vagas, além de Bing, YouTube, Baidu e os demais [motores suportados](https://www.searchapi.io/docs). A ferramenta retorna o JSON do próprio motor, então os resultados correspondem à documentação dele.
+
+Duas coisas acontecem com a resposta antes que um agente a veja:
+
+- URIs `data:` embutidas são descartadas. O SearchApi retorna favicons e miniaturas como strings base64, e uma única delas pode chegar a dezenas de kilobytes de contexto que não significam nada para um agente.
+- Strings longas são truncadas em `max_string_length`, e cada lista `*_results` é limitada a `n_results`.
+
+## Instalação
+
+1. **Instalação do pacote**: confirme que o pacote `crewai[tools]` está instalado no seu ambiente Python.
+2. **Obtenção da chave de API**: obtenha uma chave do SearchApi em https://www.searchapi.io (há um plano gratuito).
+3. **Configuração do ambiente**: guarde a chave em uma variável de ambiente chamada `SEARCHAPI_API_KEY`.
+
+```shell
+pip install 'crewai[tools]'
+```
+
+A chave é enviada no cabeçalho `Authorization` em vez da query string, portanto não aparece nos logs de requisição nem no `request_url` que o SearchApi devolve em `search_metadata`.
+
+## Exemplo
+
+```python Code
+from crewai_tools import SearchApiTool
+
+# Initialize the tool for internet searching capabilities
+tool = SearchApiTool()
+```
+
+## Parâmetros
+
+- **engine**: o motor do SearchApi a consultar, como `google`, `google_news`, `google_scholar`, `google_jobs`, `bing`, `youtube` ou `baidu`. O padrão é `google`. Também pode ser passado a cada chamada.
+- **n_results**: limite do tamanho de cada lista `*_results` na resposta. O padrão é `10`.
+- **country**: opcional. País da pesquisa, enviado como `gl` (por exemplo, `uk`).
+- **locale**: opcional. Idioma da interface, enviado como `hl` (por exemplo, `en`).
+- **location**: opcional. Localização canônica da pesquisa, por exemplo `London,England`.
+- **max_string_length**: maior string mantida intacta na resposta. O padrão é `1000`.
+- **timeout**: tempo limite da requisição em segundos. O padrão é `30`.
+- **api_key**: opcional. Sua chave do SearchApi. Na ausência dela, usa a variável de ambiente `SEARCHAPI_API_KEY`.
+- **search_url**: o endpoint chamado. O padrão é `https://www.searchapi.io/api/v1/search`.
+
+## Exemplo com parâmetros
+
+```python Code
+from crewai_tools import SearchApiTool
+
+# Recent news, localized to the UK
+news_tool = SearchApiTool(
+ engine="google_news",
+ country="uk",
+ locale="en",
+ location="London,England",
+ n_results=5,
+)
+
+# Academic search through the same tool
+scholar_tool = SearchApiTool(engine="google_scholar")
+```
+
+## Erros
+
+Uma requisição malsucedida levanta um `RuntimeError` com a mensagem do próprio SearchApi, por exemplo `SearchApi request failed (HTTP 401): Invalid API key.`.
+
+Uma pesquisa bem-sucedida que não encontrou nada não é um erro: o SearchApi retorna HTTP 200 com uma mensagem `error` como `"Google didn't return any results."`, e a ferramenta repassa isso para que o agente possa ler por que a página veio vazia.
diff --git a/lib/crewai-tools/src/crewai_tools/__init__.py b/lib/crewai-tools/src/crewai_tools/__init__.py
index 2db6be89bc..5eba03badc 100644
--- a/lib/crewai-tools/src/crewai_tools/__init__.py
+++ b/lib/crewai-tools/src/crewai_tools/__init__.py
@@ -161,6 +161,7 @@
from crewai_tools.tools.scrapfly_scrape_website_tool.scrapfly_scrape_website_tool import (
ScrapflyScrapeWebsiteTool,
)
+from crewai_tools.tools.searchapi_tool.searchapi_tool import SearchApiTool
from crewai_tools.tools.selenium_scraping_tool.selenium_scraping_tool import (
SeleniumScrapingTool,
)
@@ -308,6 +309,7 @@
"ScrapegraphScrapeTool",
"ScrapegraphScrapeToolSchema",
"ScrapflyScrapeWebsiteTool",
+ "SearchApiTool",
"SeleniumScrapingTool",
"SerpApiGoogleSearchTool",
"SerpApiGoogleShoppingTool",
diff --git a/lib/crewai-tools/src/crewai_tools/tools/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/__init__.py
index 2653490f76..8eb0ec6da9 100644
--- a/lib/crewai-tools/src/crewai_tools/tools/__init__.py
+++ b/lib/crewai-tools/src/crewai_tools/tools/__init__.py
@@ -149,6 +149,7 @@
from crewai_tools.tools.scrapfly_scrape_website_tool.scrapfly_scrape_website_tool import (
ScrapflyScrapeWebsiteTool,
)
+from crewai_tools.tools.searchapi_tool.searchapi_tool import SearchApiTool
from crewai_tools.tools.selenium_scraping_tool.selenium_scraping_tool import (
SeleniumScrapingTool,
)
@@ -290,6 +291,7 @@
"ScrapegraphScrapeTool",
"ScrapegraphScrapeToolSchema",
"ScrapflyScrapeWebsiteTool",
+ "SearchApiTool",
"SeleniumScrapingTool",
"SerpApiGoogleSearchTool",
"SerpApiGoogleShoppingTool",
diff --git a/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/README.md b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/README.md
new file mode 100644
index 0000000000..50658a9fa6
--- /dev/null
+++ b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/README.md
@@ -0,0 +1,81 @@
+# SearchApi Tool
+
+## Description
+
+The `SearchApiTool` searches the internet through [SearchApi](https://www.searchapi.io). SearchApi puts many engines behind one endpoint, so a single tool covers Google web search, news, scholar, jobs, Bing, YouTube, Baidu and the rest of the [supported engines](https://www.searchapi.io/docs) by changing the `engine` argument. The tool returns the engine's own JSON, so results line up with the engine's documentation.
+
+Two things happen to the response before an agent sees it:
+
+- Inline `data:` URIs are dropped. SearchApi returns favicons and thumbnails as base64 strings, and a single one can run to tens of kilobytes of context that means nothing to an agent.
+- Long strings are truncated at `max_string_length`, and every `*_results` list is capped at `n_results`.
+
+## Installation
+
+No extra package is needed:
+
+```shell
+uv add 'crewai[tools]'
+```
+
+## Environment Variables
+
+Set your SearchApi key:
+
+```bash
+export SEARCHAPI_API_KEY='your_searchapi_key'
+```
+
+The key is sent in the `Authorization` header rather than the query string, so it stays out of request logs and out of the `request_url` SearchApi echoes back in `search_metadata`.
+
+## Example
+
+```python
+from crewai import Agent, Crew, Task
+from crewai_tools import SearchApiTool
+
+search_tool = SearchApiTool()
+
+researcher = Agent(
+ role="Market Researcher",
+ goal="Find what people are saying about a company right now",
+ backstory="An analyst who checks the record before forming a view.",
+ tools=[search_tool],
+ verbose=True,
+)
+
+research_task = Task(
+ description="Search for recent coverage of CrewAI.",
+ expected_output="A short report on what the search returned.",
+ agent=researcher,
+)
+
+crew = Crew(agents=[researcher], tasks=[research_task], verbose=True)
+result = crew.kickoff()
+print(result)
+```
+
+Point the same tool at a different engine:
+
+```python
+news_tool = SearchApiTool(engine="google_news")
+scholar_tool = SearchApiTool(engine="google_scholar", n_results=5)
+uk_tool = SearchApiTool(country="uk", locale="en", location="London,England")
+```
+
+## Arguments
+
+- `engine` (str, optional): The SearchApi engine to query, such as `"google"`, `"google_news"`, `"google_scholar"`, `"google_jobs"`, `"bing"`, `"youtube"` or `"baidu"`. Defaults to `"google"`. Can also be passed per call.
+- `n_results` (int, optional): Cap on the length of each `*_results` list in the response. Defaults to `10`.
+- `country` (str, optional): Country of the search, sent as `gl` (for example `"uk"`). Defaults to `None`.
+- `locale` (str, optional): Interface language, sent as `hl` (for example `"en"`). Defaults to `None`.
+- `location` (str, optional): Canonical location of the search, for example `"London,England"`. Defaults to `None`.
+- `max_string_length` (int, optional): Longest string kept intact in the response. Defaults to `1000`.
+- `timeout` (int, optional): Request timeout in seconds. Defaults to `30`.
+- `api_key` (str, optional): Your SearchApi key. Falls back to the `SEARCHAPI_API_KEY` environment variable.
+- `search_url` (str, optional): The endpoint to call. Defaults to `https://www.searchapi.io/api/v1/search`.
+
+## Errors
+
+A failed request raises a `RuntimeError` carrying SearchApi's own message, for example `SearchApi request failed (HTTP 401): Invalid API key.`.
+
+A successful search that found nothing is not an error: SearchApi returns HTTP 200 with an `error` message such as `"Google didn't return any results."`, and the tool passes that through so the agent can read why the page was empty.
diff --git a/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/searchapi_tool.py b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/searchapi_tool.py
new file mode 100644
index 0000000000..01acdb78dd
--- /dev/null
+++ b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/searchapi_tool.py
@@ -0,0 +1,188 @@
+"""Search the web through SearchApi (https://www.searchapi.io)."""
+
+import json
+import os
+from typing import Any
+
+from crewai.tools import BaseTool, EnvVar
+from pydantic import BaseModel, Field
+import requests
+
+
+DEFAULT_SEARCH_URL = "https://www.searchapi.io/api/v1/search"
+
+_RESULT_LIST_SUFFIX = "_results"
+
+
+def _is_data_uri(value: Any) -> bool:
+ """Return True for an inline ``data:`` URI, which is never useful to an agent."""
+ return isinstance(value, str) and value.startswith("data:")
+
+
+def _sanitize(value: Any, max_string_length: int) -> Any:
+ """Drop inline data URIs and truncate long strings, recursively.
+
+ SearchApi ships favicons and thumbnails as ``data:image/png;base64,...``
+ strings on most result items. They mean nothing to an agent and each one
+ can run to tens of kilobytes, so they are dropped rather than truncated.
+
+ Args:
+ value: Any part of a decoded JSON response.
+ max_string_length: Longest string kept intact; longer ones are cut.
+
+ Returns:
+ The same structure with data URIs removed and long strings truncated.
+ """
+ if isinstance(value, dict):
+ return {
+ key: _sanitize(item, max_string_length)
+ for key, item in value.items()
+ if not _is_data_uri(item)
+ }
+ if isinstance(value, list):
+ return [
+ _sanitize(item, max_string_length)
+ for item in value
+ if not _is_data_uri(item)
+ ]
+ if isinstance(value, str) and len(value) > max_string_length:
+ return value[:max_string_length] + "..."
+ return value
+
+
+def _error_detail(response: requests.Response) -> str:
+ """Pull SearchApi's error message out of a failed response.
+
+ Args:
+ response: A response whose status code is not a success.
+
+ Returns:
+ The API's own error message, or the start of the raw body when the
+ response is not the documented ``{"error": ...}`` shape.
+ """
+ try:
+ body = response.json()
+ except ValueError:
+ return response.text[:500]
+ error = body.get("error") if isinstance(body, dict) else None
+ if isinstance(error, str):
+ return error
+ return json.dumps(body)[:500]
+
+
+class SearchApiToolSchema(BaseModel):
+ """Input for SearchApiTool."""
+
+ search_query: str = Field(
+ ..., description="Mandatory search query you want to use to search the internet"
+ )
+
+
+class SearchApiTool(BaseTool):
+ """Search the internet through SearchApi's unified search endpoint.
+
+ One endpoint fronts many engines, selected with ``engine``, so the same
+ tool covers web search, news, scholar, jobs and the rest. The response is
+ the engine's own JSON, so its shape follows the engine's documentation at
+ https://www.searchapi.io/docs.
+ """
+
+ name: str = "Search the internet with SearchApi"
+ description: str = (
+ "A tool that can be used to search the internet with a search_query using "
+ "SearchApi. One endpoint fronts many engines, set with 'engine': 'google' "
+ "(default), 'google_news', 'google_scholar', 'google_jobs', 'bing', "
+ "'youtube', 'baidu' and others. Returns the engine's structured JSON results."
+ )
+ args_schema: type[BaseModel] = SearchApiToolSchema
+ search_url: str = DEFAULT_SEARCH_URL
+ engine: str = "google"
+ n_results: int = 10
+ country: str | None = None
+ locale: str | None = None
+ location: str | None = None
+ max_string_length: int = 1000
+ timeout: int = 30
+ api_key: str | None = None
+ env_vars: list[EnvVar] = Field(
+ default_factory=lambda: [
+ EnvVar(
+ name="SEARCHAPI_API_KEY",
+ description="API key for SearchApi",
+ required=True,
+ ),
+ ]
+ )
+
+ def _run(self, **kwargs: Any) -> dict[str, Any]:
+ """Execute the search operation.
+
+ Args:
+ **kwargs: ``search_query`` (or ``query``) to search for, and an
+ optional ``engine`` overriding the configured one for this call.
+
+ Returns:
+ The engine's JSON response, with data URIs dropped, long strings
+ truncated and result lists capped at ``n_results``. A response that
+ carries an ``error`` message instead of results, which is how
+ SearchApi reports a page with nothing on it, is passed through so
+ the agent can read why.
+
+ Raises:
+ ValueError: No search query was given, or no API key is configured.
+ RuntimeError: SearchApi answered with an error status.
+ """
+ search_query: str | None = kwargs.get("search_query") or kwargs.get("query")
+ if not search_query:
+ raise ValueError("search_query is required")
+
+ api_key = self.api_key or os.getenv("SEARCHAPI_API_KEY")
+ if not api_key:
+ raise ValueError(
+ "SEARCHAPI_API_KEY environment variable is required for SearchApiTool"
+ )
+
+ params: dict[str, Any] = {
+ "engine": kwargs.get("engine", self.engine),
+ "q": search_query,
+ }
+ if self.country:
+ params["gl"] = self.country
+ if self.locale:
+ params["hl"] = self.locale
+ if self.location:
+ params["location"] = self.location
+
+ # The key travels in the Authorization header rather than the query
+ # string, so it stays out of request logs and out of the request_url
+ # SearchApi echoes back in search_metadata.
+ response = requests.get(
+ self.search_url,
+ headers={"Authorization": f"Bearer {api_key}"},
+ params=params,
+ timeout=self.timeout,
+ )
+ if not response.ok:
+ raise RuntimeError(
+ f"SearchApi request failed (HTTP {response.status_code}): "
+ f"{_error_detail(response)}"
+ )
+
+ return self._format_results(response.json())
+
+ def _format_results(self, results: dict[str, Any]) -> dict[str, Any]:
+ """Cap result lists and strip payload that only burns context.
+
+ Args:
+ results: The decoded JSON body of a successful search.
+
+ Returns:
+ The same body with every ``*_results`` list capped at
+ ``n_results`` and every value sanitized.
+ """
+ formatted: dict[str, Any] = {}
+ for key, value in results.items():
+ if key.endswith(_RESULT_LIST_SUFFIX) and isinstance(value, list):
+ value = value[: self.n_results]
+ formatted[key] = _sanitize(value, self.max_string_length)
+ return formatted
diff --git a/lib/crewai-tools/tests/tools/searchapi_tool_test.py b/lib/crewai-tools/tests/tools/searchapi_tool_test.py
new file mode 100644
index 0000000000..78a68312a0
--- /dev/null
+++ b/lib/crewai-tools/tests/tools/searchapi_tool_test.py
@@ -0,0 +1,193 @@
+import os
+from unittest.mock import MagicMock, patch
+
+import pytest
+import requests as requests_lib
+
+from crewai_tools.tools.searchapi_tool.searchapi_tool import SearchApiTool
+
+
+def _mock_response(
+ status_code: int = 200,
+ json_data: dict | None = None,
+ text: str = "",
+) -> MagicMock:
+ """Build a ``requests.Response``-like mock with the attributes ``_run`` uses."""
+ resp = MagicMock(spec=requests_lib.Response)
+ resp.status_code = status_code
+ resp.ok = 200 <= status_code < 400
+ resp.text = text or (str(json_data) if json_data else "")
+ resp.json.return_value = json_data if json_data is not None else {}
+ return resp
+
+
+@pytest.fixture(autouse=True)
+def _searchapi_env():
+ with patch.dict(os.environ, {"SEARCHAPI_API_KEY": "test-api-key"}):
+ yield
+
+
+@pytest.fixture
+def tool():
+ return SearchApiTool()
+
+
+def test_default_attributes(tool):
+ assert tool.search_url == "https://www.searchapi.io/api/v1/search"
+ assert tool.engine == "google"
+ assert tool.n_results == 10
+ assert tool.env_vars[0].name == "SEARCHAPI_API_KEY"
+
+
+def test_missing_query_raises(tool):
+ with pytest.raises(ValueError, match="search_query is required"):
+ tool._run()
+
+
+def test_missing_api_key_raises():
+ with patch.dict(os.environ, {}, clear=True):
+ with pytest.raises(ValueError, match="SEARCHAPI_API_KEY"):
+ SearchApiTool()._run(search_query="test")
+
+
+@patch("crewai_tools.tools.searchapi_tool.searchapi_tool.requests.get")
+def test_key_is_sent_as_bearer_header_not_query_param(mock_get, tool):
+ """The key must stay out of the query string, which SearchApi echoes back."""
+ mock_get.return_value = _mock_response(json_data={"organic_results": []})
+
+ tool._run(search_query="crewai")
+
+ kwargs = mock_get.call_args.kwargs
+ assert kwargs["headers"]["Authorization"] == "Bearer test-api-key"
+ assert "api_key" not in kwargs["params"]
+
+
+@patch("crewai_tools.tools.searchapi_tool.searchapi_tool.requests.get")
+def test_explicit_api_key_takes_precedence_over_env(mock_get):
+ mock_get.return_value = _mock_response(json_data={})
+
+ SearchApiTool(api_key="explicit-key")._run(search_query="crewai")
+
+ assert mock_get.call_args.kwargs["headers"]["Authorization"] == "Bearer explicit-key"
+
+
+@patch("crewai_tools.tools.searchapi_tool.searchapi_tool.requests.get")
+def test_localization_params_are_only_sent_when_set(mock_get):
+ mock_get.return_value = _mock_response(json_data={})
+
+ SearchApiTool(country="uk", locale="en", location="London,England")._run(
+ search_query="fish"
+ )
+
+ params = mock_get.call_args.kwargs["params"]
+ assert params == {
+ "engine": "google",
+ "q": "fish",
+ "gl": "uk",
+ "hl": "en",
+ "location": "London,England",
+ }
+
+
+@patch("crewai_tools.tools.searchapi_tool.searchapi_tool.requests.get")
+def test_engine_can_be_overridden_per_call(mock_get, tool):
+ mock_get.return_value = _mock_response(json_data={})
+
+ tool._run(search_query="crewai", engine="google_news")
+
+ assert mock_get.call_args.kwargs["params"]["engine"] == "google_news"
+
+
+@patch("crewai_tools.tools.searchapi_tool.searchapi_tool.requests.get")
+def test_query_alias_is_accepted(mock_get, tool):
+ mock_get.return_value = _mock_response(json_data={})
+
+ tool._run(query="crewai")
+
+ assert mock_get.call_args.kwargs["params"]["q"] == "crewai"
+
+
+@patch("crewai_tools.tools.searchapi_tool.searchapi_tool.requests.get")
+def test_result_lists_are_capped_at_n_results(mock_get):
+ mock_get.return_value = _mock_response(
+ json_data={
+ "organic_results": [{"position": i} for i in range(10)],
+ "related_questions": [{"question": f"q{i}"} for i in range(10)],
+ }
+ )
+
+ result = SearchApiTool(n_results=3)._run(search_query="crewai")
+
+ assert len(result["organic_results"]) == 3
+ # Only lists named *_results are capped; everything else passes through.
+ assert len(result["related_questions"]) == 10
+
+
+@patch("crewai_tools.tools.searchapi_tool.searchapi_tool.requests.get")
+def test_data_uris_are_dropped_from_results(mock_get, tool):
+ mock_get.return_value = _mock_response(
+ json_data={
+ "organic_results": [
+ {
+ "title": "CrewAI",
+ "link": "https://crewai.com",
+ "favicon": "data:image/png;base64," + "A" * 20000,
+ }
+ ],
+ "inline_images": ["data:image/png;base64,AAAA", "https://img.co/a.png"],
+ }
+ )
+
+ result = tool._run(search_query="crewai")
+
+ assert result["organic_results"][0] == {
+ "title": "CrewAI",
+ "link": "https://crewai.com",
+ }
+ assert result["inline_images"] == ["https://img.co/a.png"]
+
+
+@patch("crewai_tools.tools.searchapi_tool.searchapi_tool.requests.get")
+def test_long_strings_are_truncated(mock_get):
+ mock_get.return_value = _mock_response(
+ json_data={"organic_results": [{"snippet": "x" * 500}]}
+ )
+
+ result = SearchApiTool(max_string_length=100)._run(search_query="crewai")
+
+ assert result["organic_results"][0]["snippet"] == "x" * 100 + "..."
+
+
+@patch("crewai_tools.tools.searchapi_tool.searchapi_tool.requests.get")
+def test_empty_result_page_is_returned_not_raised(mock_get, tool):
+ """A 200 carrying `error` is SearchApi reporting no results, not a failure."""
+ mock_get.return_value = _mock_response(
+ json_data={
+ "search_metadata": {"id": "search_1", "status": "Success"},
+ "error": "Google didn't return any results.",
+ }
+ )
+
+ result = tool._run(search_query="asdkjhaskdjh")
+
+ assert result["error"] == "Google didn't return any results."
+
+
+@patch("crewai_tools.tools.searchapi_tool.searchapi_tool.requests.get")
+def test_error_status_raises_with_api_message(mock_get, tool):
+ mock_get.return_value = _mock_response(
+ status_code=401, json_data={"error": "Invalid API key."}
+ )
+
+ with pytest.raises(RuntimeError, match="HTTP 401.*Invalid API key."):
+ tool._run(search_query="crewai")
+
+
+@patch("crewai_tools.tools.searchapi_tool.searchapi_tool.requests.get")
+def test_non_json_error_body_falls_back_to_text(mock_get, tool):
+ resp = _mock_response(status_code=502, text="Bad gateway")
+ resp.json.side_effect = ValueError("not json")
+ mock_get.return_value = resp
+
+ with pytest.raises(RuntimeError, match="HTTP 502.*Bad gateway"):
+ tool._run(search_query="crewai")
diff --git a/lib/crewai-tools/tool.specs.json b/lib/crewai-tools/tool.specs.json
index 2e540f8c13..4fea757151 100644
--- a/lib/crewai-tools/tool.specs.json
+++ b/lib/crewai-tools/tool.specs.json
@@ -21130,6 +21130,163 @@
"type": "object"
}
},
+ {
+ "description": "A tool that can be used to search the internet with a search_query using SearchApi. One endpoint fronts many engines, set with 'engine': 'google' (default), 'google_news', 'google_scholar', 'google_jobs', 'bing', 'youtube', 'baidu' and others. Returns the engine's structured JSON results.",
+ "env_vars": [
+ {
+ "default": null,
+ "description": "API key for SearchApi",
+ "name": "SEARCHAPI_API_KEY",
+ "required": true
+ }
+ ],
+ "humanized_name": "Search the internet with SearchApi",
+ "init_params_schema": {
+ "$defs": {
+ "EnvVar": {
+ "properties": {
+ "default": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "title": "Default"
+ },
+ "description": {
+ "title": "Description",
+ "type": "string"
+ },
+ "name": {
+ "title": "Name",
+ "type": "string"
+ },
+ "required": {
+ "default": true,
+ "title": "Required",
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "name",
+ "description"
+ ],
+ "title": "EnvVar",
+ "type": "object"
+ },
+ "ToolFailurePolicy": {
+ "description": "How an agent reacts when one of its tools reports a failure.",
+ "enum": [
+ "ignore",
+ "warn",
+ "raise"
+ ],
+ "title": "ToolFailurePolicy",
+ "type": "string"
+ }
+ },
+ "description": "Search the internet through SearchApi's unified search endpoint.\n\nOne endpoint fronts many engines, selected with ``engine``, so the same\ntool covers web search, news, scholar, jobs and the rest. The response is\nthe engine's own JSON, so its shape follows the engine's documentation at\nhttps://www.searchapi.io/docs.",
+ "properties": {
+ "api_key": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "title": "Api Key"
+ },
+ "country": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "title": "Country"
+ },
+ "engine": {
+ "default": "google",
+ "title": "Engine",
+ "type": "string"
+ },
+ "locale": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "title": "Locale"
+ },
+ "location": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "title": "Location"
+ },
+ "max_string_length": {
+ "default": 1000,
+ "title": "Max String Length",
+ "type": "integer"
+ },
+ "n_results": {
+ "default": 10,
+ "title": "N Results",
+ "type": "integer"
+ },
+ "search_url": {
+ "default": "https://www.searchapi.io/api/v1/search",
+ "title": "Search Url",
+ "type": "string"
+ },
+ "timeout": {
+ "default": 30,
+ "title": "Timeout",
+ "type": "integer"
+ }
+ },
+ "required": [],
+ "title": "SearchApiTool",
+ "type": "object"
+ },
+ "name": "SearchApiTool",
+ "package_dependencies": [],
+ "run_params_schema": {
+ "description": "Input for SearchApiTool.",
+ "properties": {
+ "search_query": {
+ "description": "Mandatory search query you want to use to search the internet",
+ "title": "Search Query",
+ "type": "string"
+ }
+ },
+ "required": [
+ "search_query"
+ ],
+ "title": "SearchApiToolSchema",
+ "type": "object"
+ }
+ },
{
"description": "A tool that can be used to read a website content.",
"env_vars": [],