# searx.engines.exaapi - SearXNG Documentation (2026.7.28+c01178d03)

Skip to content
Back to top

# Source code for searx.engines.exaapi

    # SPDX-License-Identifier: AGPL-3.0-or-later
    """Engine to search using the official `Exa Search API`_. Exa is a search engine for AI agents.

    .. _Exa Search API: https://exa.ai/docs/reference/search

    Configuration
    =============

    The engine has the following mandatory setting:

    - :py:obj:`api_key`

    You can obtain an API key from the `API Key section <https://dashboard.exa.ai/api-keys>`_ in the Exa dashboard.

    Optional settings are:

    - :py:obj:`results_per_page`
    - :py:obj:`search_type`
    - :py:obj:`content_mode`
    - :py:obj:`content_max_characters`

    .. code:: yaml

      - name: exaapi
        engine: exaapi
        shortcut: exa
        api_key: "..."
        results_per_page: 10
        search_type: auto
        content_mode: highlights
        inactive: false

    The API supports SafeSearch and region-aware results.
    """

    import typing as t

    from dateutil import parser

    from searx.exceptions import SearxEngineAPIException
    from searx.result_types import EngineResults
    from searx.utils import html_to_text

    if t.TYPE_CHECKING:
        from searx.extended_types import SXNG_Response
        from searx.search.processors import OnlineParams

    SearchType = t.Literal["fast", "auto", "instant", "deep", "deep-lite", "deep-reasoning"]
    ContentMode = t.Literal["highlights", "text"]

    about = {
        "website": "https://exa.ai",
        "wikidata_id": None,
        "official_api_documentation": "https://exa.ai/docs/reference/search",
        "use_official_api": True,
        "require_api_key": True,
        "results": "JSON",
    }

    api_key: str = ""
    """API key for Exa Search API (required)."""

    categories = ["general", "web"]
    safesearch = True

    base_url = "https://api.exa.ai/search"
    results_per_page: int = 10
    """Maximum number of results per request. Value must be between 1 and 100, default is 10."""

    search_type: SearchType = "auto"
    """Search type. Default is auto, see documentation for more information."""

    content_mode: ContentMode = "highlights"
    """Content to request from the API: ``highlights`` (excerpts) or ``text`` (page text)."""

    content_max_characters: int = 500
    """Maximum characters for the requested content."""

    def init(_):
        if not api_key:
            raise SearxEngineAPIException("No API key provided")
        if not 1 <= results_per_page <= 100:
            raise ValueError("results_per_page must be between 1 and 100")
        if search_type not in t.get_args(SearchType):
            raise ValueError(f"Unsupported search type: {search_type}")
        if content_mode not in t.get_args(ContentMode):
            raise ValueError(f"Unsupported content mode: {content_mode}")
        if content_max_characters < 1:
            raise ValueError("content_max_characters must be at least 1")

    def _contents_payload() -> dict[str, t.Any]:
        if content_mode == "text":
            return {"text": {"maxCharacters": content_max_characters, "stripLinks": True}}
        return {"highlights": {"maxCharacters": content_max_characters}}

    def _extract_content(result: dict[str, t.Any]) -> str:
        if content_mode == "text":
            return html_to_text(result.get("text") or "")
        return html_to_text(" ".join(result.get("highlights") or []))

    def request(query: str, params: "OnlineParams") -> None:
        """Create the API request."""
        body: dict[str, t.Any] = {
            "query": query,
            "type": search_type,
            "numResults": results_per_page,
            "contents": _contents_payload(),
        }

        # Apply SafeSearch if enabled
        if params["safesearch"]:
            body["moderation"] = True

        # Apply region-aware results if specified
        locale_parts = params["searxng_locale"].split("-")
        region = locale_parts[-1]
        if len(locale_parts) > 1:
            body["userLocation"] = region.upper()

        params["url"] = base_url
        params["method"] = "POST"
        params["headers"]["x-api-key"] = api_key
        params["json"] = body

    def _extract_published_date(value: str | None):
        """Extract and parse the published date from the API response.

        Args:
            value: Raw date string from the API

        Returns:
            Parsed datetime object or None if parsing fails
        """
        if not value:
            return None
        try:
            return parser.parse(value)
        except (parser.ParserError, TypeError, OverflowError):
            return None

    def response(resp: "SXNG_Response") -> EngineResults:
        """Process the API response and return results."""
        res = EngineResults()

        for result in resp.json().get("results", []):
            url = result.get("url")
            if not url:
                continue

            res.add(
                res.types.MainResult(
                    url=url,
                    title=html_to_text(result.get("title") or url),
                    content=_extract_content(result),
                    thumbnail=result.get("image") or "",
                    publishedDate=_extract_published_date(result.get("publishedDate")),
                    author=result.get("author") or "",
                )
            )

        return res