> ## Documentation Index
> Fetch the complete documentation index at: https://exa.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> The Exa API is served at https://api.exa.ai. Authenticate with `Authorization: Bearer $EXA_API_KEY` (or `x-api-key: $EXA_API_KEY`); create keys at https://dashboard.exa.ai/api-keys?utm_source=docs&utm_medium=docs&utm_campaign=markdown_instructions. Every way an agent can get access to Exa is described at https://exa.ai/auth.md.
> Prefer the official SDKs, `exa-py` (`pip install exa-py`) and `exa-js` (`npm install exa-js`); both read `EXA_API_KEY` from the environment.
> Tool-using agents can call Exa without writing code through the hosted MCP server at https://mcp.exa.ai/mcp, or install the Exa agent skill with `npx skills add exa-labs/agent-skills` (skill file: https://exa.ai/docs/skill.md).
> The OpenAPI specs at https://exa.ai/docs/exa-spec.yaml and https://exa.ai/docs/team-management-spec.yaml are the source of truth for request and response schemas.

# OpenAI Tool Calling

> Use OpenAI tool calling to add Exa web search and page contents to your application.

> [!NOTE]
>
> OpenAI recommends the Responses API for all new projects. See the [Responses API](#responses-api) section below.

OpenAI's [tool calling](https://platform.openai.com/docs/guides/function-calling?lang=python) allows models to call functions that you define in your code. The Exa SDKs ship ready-made web search and page reading tools for OpenAI, so you don't have to hand-write the tool schema, parse tool calls, or format Exa results yourself.

## Get started

**1. Install the SDKs**

```bash Python theme={null}
pip install openai exa_py

# Or, if using uv:
uv add openai exa_py
```

```bash JavaScript theme={null}
npm install openai exa-js
```

**2. Set up your API keys**

Set the `EXA_API_KEY` and `OPENAI_API_KEY` environment variables. Visit the [OpenAI dashboard](https://platform.openai.com/api-keys) and the [Exa dashboard](https://dashboard.exa.ai/api-keys?utm_source=docs\&utm_medium=docs\&utm_campaign=integrations_tool_calling_openai) to generate your API keys.

> [card] [Get your Exa API key](https://dashboard.exa.ai/api-keys?utm_source=docs\&utm_medium=docs\&utm_campaign=get_key_card)
>
> Create a key in the dashboard. New accounts start with free credits.

**3. Add the Exa tools to your tool loop**

Pass the tools in the request's `tools` list, then hand the assistant message to `handle_tool_calls`. It executes every Exa tool call in the message and returns the matching `role: "tool"` messages, ready to append to the conversation.

`web_search` searches the web for pages the model hasn't seen; `get_contents` reads pages it already has URLs for, whether from an earlier search or from the user. Register either or both.

```python Python theme={null}
from exa_py import Exa
from openai import OpenAI

exa = Exa()  # reads EXA_API_KEY from the environment
openai_client = OpenAI()

messages = [{"role": "user", "content": "What's the latest on AI chips?"}]

completion = openai_client.chat.completions.create(
    model="gpt-5.6",
    reasoning_effort="none",
    messages=messages,
    tools=[exa.openai.web_search(), exa.openai.get_contents()],
)

message = completion.choices[0].message
messages.append(message)
messages += exa.openai.handle_tool_calls(message)

completion = openai_client.chat.completions.create(
    model="gpt-5.6",
    reasoning_effort="none",
    messages=messages,
)
print(completion.choices[0].message.content)
```

```javascript JavaScript theme={null}
import Exa from "exa-js";
import { OpenAI } from "openai";

const exa = new Exa(); // reads EXA_API_KEY from the environment
const openai = new OpenAI();

const messages = [
  { role: "user", content: "What's the latest on AI chips?" },
];

let completion = await openai.chat.completions.create({
  model: "gpt-5.6",
  reasoning_effort: "none",
  messages,
  tools: [exa.openai.webSearch(), exa.openai.getContents()],
});

const message = completion.choices[0].message;
messages.push(message, ...(await exa.openai.handleToolCalls(message)));

completion = await openai.chat.completions.create({
  model: "gpt-5.6",
  reasoning_effort: "none",
  messages,
});
console.log(completion.choices[0].message.content);
```

This is one round for brevity. A real agent keeps `tools` on every request and repeats the handler step until the model replies without tool calls — that's how a search result turns into a follow-up page read.

Calling the factories with no arguments gives Exa's recommended defaults. The search tool runs a `type="auto"` search with `contents={"highlights": True}`, which returns query-relevant excerpts instead of page text capped at 10,000 characters. The contents tool returns page text. The Python SDK caps that text at 10,000 characters only when you pass no `text`, `summary`, or `extras` option.

## Responses API

For the OpenAI Responses API, use the `responses` factory with the same `handle_tool_calls` helper. The handler returns `function_call_output` items for a follow-up request.

```python Python theme={null}
response = openai_client.responses.create(
    model="gpt-5.6",
    input=messages,
    tools=[exa.openai.responses.web_search(), exa.openai.responses.get_contents()],
)

messages += response.output
messages += exa.openai.responses.handle_tool_calls(response)
```

```javascript JavaScript theme={null}
const response = await openai.responses.create({
  model: "gpt-5.6",
  input: messages,
  tools: [exa.openai.responses.webSearch(), exa.openai.responses.getContents()],
});

messages.push(...response.output);
messages.push(...(await exa.openai.responses.handleToolCalls(response)));
```

> [!NOTE]
>
> Chat Completions and the Responses API use different tool shapes and reject each other's, so use the factory that matches the endpoint you're calling.

## Configuring the tools

Call the factories with no arguments to get Exa's recommended defaults:

> [!NOTE]
>
> * `web_search` runs `exa.search(query, type="auto", num_results=10, contents={"highlights": True})` — query-relevant excerpts from each result.
> * `get_contents` runs `exa.get_contents(urls)` — page text, capped at 10,000 characters by the Python SDK by default.
>
> Any keyword argument you pass to a factory overrides these — search options go to `exa.search()`, contents options to `exa.get_contents()` — but we recommend the defaults.

```python Python theme={null}
tools = [exa.openai.web_search(), exa.openai.get_contents()]
```

```javascript JavaScript theme={null}
const tools = [exa.openai.webSearch(), exa.openai.getContents()];
```

The model picks the search `query` and `objective` and the `urls` to read; everything else is fixed when you create the tool, so it can't change what gets crawled or extracted. Read more about the [`objective` parameter](https://exa.ai/docs/search/best-practices#pass-the-objective-from-agents).

`name` (defaulting to `"web_search"` and `"get_contents"`) and `description` override the tool definition the model sees. Use a custom `name` to run differently-configured Exa tools side by side, or to avoid clashes with other tools that reserve those names.

## Mixing in your own tools

The handlers answer every tool call in the message: a call naming a tool they can't resolve gets an `Error: unknown tool "<name>"` output instead of being dropped, so the follow-up request never omits a required tool response. If you run your own tools alongside Exa's, replace those error outputs with your own results before the next request.

## Writing the loop by hand

If you'd rather own the tool schema and execution yourself, define the tool and process the calls manually. `exa.tools.web_search()` and `exa.tools.get_contents()` give you the same provider-neutral tool specs (with a `run` method) for hand-rolled loops, or you can write everything from scratch:

```python Python theme={null}
import json

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "exa_search",
            "description": "Perform a search query on the web, and retrieve the most relevant URLs/web data.",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "The search query to perform.",
                    },
                    "objective": {
                        "type": "string",
                        "description": "Goal for this search turn; say which documents should rank first, which should be excluded, and what specific facts or figures to pull from them.",
                    },
                },
                "required": ["query", "objective"],
            },
        },
    }
]

def exa_search(query: str, objective: str | None = None):
    return exa.search(
        query=query, objective=objective, type="auto", contents={"highlights": True}
    )

def process_tool_calls(tool_calls, messages):
    for tool_call in tool_calls:
        if tool_call.function.name == "exa_search":
            args = json.loads(tool_call.function.arguments)
            messages.append(
                {
                    "role": "tool",
                    "content": str(exa_search(**args)),
                    "tool_call_id": tool_call.id,
                }
            )
    return messages
```

See the [SDK quickstart](https://exa.ai/docs/sdks/quickstart.md) for search and contents options in Python and TypeScript.
