# Billing and Rate Limits Source: https://exa.ai/docs/admin/billing Manage Exa credits, invoices, and API rate limits. Exa offers a Free tier, pay-as-you-go billing, and custom Enterprise plans. API usage draws from your team's credit balance, while rate limits control how quickly the team can make requests. Add credits, configure auto recharge, and view invoices. Review usage and set a lower limit for an individual key. Compare current rates across Exa products. ## Plans at a glance | Plan | Billing | Rate limit | Agent concurrency | | ----------------- | -------------------------------------------------------------------- | ------------------------------------------------ | ----------------- | | **Free** | \$20 in introductory credits, then \$10 in credits refreshed monthly | 10 QPS | 50 active runs | | **Pay as you go** | Prepaid credits with no subscription or minimum spend | 10 QPS, [up to 25 QPS](#25-qps-on-pay-as-you-go) | 50 active runs | | **Enterprise** | Custom volume pricing and optional postpaid invoicing | Custom | Custom | We'll advise on the best set up to handle latency, scale, ZDR, and more. ## Billing basics Requests are charged against prepaid credits at the rates in [Pricing](/docs/admin/pricing) or under your Enterprise contract. Team owners can add credits from the [Billing dashboard](https://dashboard.exa.ai/billing); payments are processed through Stripe. If your team exhausts its credits, requests return `402 Payment Required`. An API key that reaches its assigned budget also returns `402`. Add credits or ask a team administrator to adjust the key's budget. See [Error codes](/docs/admin/error-codes). For historical usage by API key, use [Get API key usage](/docs/reference/team-management/get-api-key-usage). ## Rate limits Rate limits are measured in queries per second (QPS) and apply to your team as a whole, across all of its API keys. You can give an individual key a lower limit from the [API Keys](https://dashboard.exa.ai/api-keys) page, but its traffic still counts toward the team limit. | Endpoint | Default limit | | -------------------------------------------------------------- | ------------------------ | | `/search`, `/answer`, `/chat/completions` | 10 QPS | | `/search` with `type` `deep-lite`, `deep`, or `deep-reasoning` | 5 QPS | | `/contents` | 100 QPS | | `/agent/runs`, `/responses` | 5 QPS and 50 active runs | | `/websets/*` | 20 QPS | Some endpoints share rate limit capacity. Limits are subject to change and can vary by plan; Websets searches also have plan-based concurrency limits, which you can check with [Get Team Info](/docs/websets/api/teams/get-team-info). When you exceed a limit, requests return `429 Too Many Requests`. Wait for the `Retry-After` header when present, or retry with exponential backoff. See [Error codes](/docs/admin/error-codes). ### Agent limits Agent limits are two separate controls: how many runs can be in progress at once, and how fast you can start new ones. * **Concurrency**: 50 Agent runs can be in progress at a time. This limit is separate from your QPS and does not change when your QPS is raised. Starting a run past the limit returns `429` with error code `CONCURRENCY_LIMIT_REACHED`; wait for a run to finish or contact us to raise your concurrency limit. * **Starting runs**: `POST /agent/runs` draws from your account QPS, and each run start counts as two requests. You can start runs at half your QPS, so an account with the default 10 QPS can start 5 runs per second, and 25 QPS allows 12 per second. * **Polling**: `GET` requests for run status, events, and run lists do not count against your QPS and never block dispatch, so poll running Agents independently of how fast you start new ones. ### 25 QPS on pay as you go Add \$1,000 in credits within any 30-day window and your team's rate limit rises to **25 QPS for 90 days**, automatically. The threshold counts credits you purchase, not credits you spend, and requalifying resets the 90 days. Track your progress on the [Billing dashboard](https://dashboard.exa.ai/billing). Need more than 25 QPS? [Talk to sales](https://exa.ai/contact/sales). ## Auto recharge Auto recharge purchases credits when your balance reaches a threshold you choose. Configure it from the [Billing dashboard](https://dashboard.exa.ai/billing). | Setting | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------- | | **Recharge amount** | Credits purchased each time auto recharge triggers, from \$5 to \$10,000. | | **Recharge threshold** | The balance at which the recharge triggers. | | **Monthly maximum** | Optional cap on auto-recharge purchases during the billing cycle. Set it to \$0 or leave it blank for no cap. | For example, a \$100 recharge amount, \$10 threshold, and \$500 monthly maximum purchases \$100 whenever the balance reaches \$10, up to \$500 in automatic purchases during the cycle. For an upcoming launch or other high-volume workload, add enough credits in advance and set an auto-recharge amount that avoids many small payment attempts. ## Receipts and invoices Exa emails receipts for credit purchases and auto recharges from [billing@exa.ai](mailto:billing@exa.ai). Add the address to your allow list if needed. Your full invoice history is available in the [Billing dashboard](https://dashboard.exa.ai/billing). Postpaid invoice billing is available with an Enterprise plan. ## Get help Request more than 25 QPS, custom concurrency, volume pricing, or postpaid billing. Get help with payments, credits, invoices, or account billing questions. # Error Codes Source: https://exa.ai/docs/admin/error-codes Reference for common error codes used by the Exa API Exa APIs signal failures with standard HTTP status codes and a JSON error body. ## HTTP status codes | Code | Meaning | What to do | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | | `400` Bad Request | The body, query parameters, headers, or combination of options is invalid. | Correct the request using the returned message. | | `401` Unauthorized | The API key is missing or invalid. | Verify the authentication header and API key. | | `402` Payment Required | Credits are exhausted or a spending budget was exceeded. | [Top up credits](https://dashboard.exa.ai) or contact your team administrator. | | `403` Forbidden | The API key does not have access to the requested feature, or the request was blocked by policy. | Check the returned message and your plan's feature access. | | `404` Not Found | The route or requested resource does not exist. | Verify the endpoint and resource ID. | | `409` Conflict | The request conflicts with existing state — for example, a Webset with the same `externalId` already exists. | Fetch the existing resource or use a different identifier. | | `422` Unprocessable Entity | A Websets preview query could not be decomposed into a valid entity and criteria. | Rephrase the preview query. | | `429` Too Many Requests | Your API key, team, or network exceeded a rate or concurrency limit. | Reduce your request rate; wait for `Retry-After` seconds when present, otherwise use exponential backoff. | | `500` Internal Server Error | An unexpected server error occurred. | Retry after a brief delay. Contact support if it persists. | | `503` Service Unavailable | Exa is temporarily over capacity (`SERVICE_OVERLOADED`) or unavailable. The request was not processed and is not billed. | Retry with exponential backoff. This is independent of your request rate, so reducing it does not help — retrying does. | | `504` Gateway Timeout | The request exceeded its processing deadline. | Retry the request or reduce its scope. | URL-level failures from `/contents` are reported in the `statuses` field of a successful `200` response, not as request-level errors. See [Content fetch status tags](#content-fetch-status-tags). ## Error response structure Error responses return a `requestId`, a human-readable `error` message, and a machine-readable `tag`: ```json theme={null} { "requestId": "67207943fab9832d162b5317f4cca830", "error": "Invalid request body | Validation error: Invalid value for type", "tag": "INVALID_REQUEST_BODY" } ``` Include the `requestId` when contacting support for faster troubleshooting. The set of tags is open-ended and tag names are self-explanatory. Branch on the HTTP status code first and treat unrecognized tags as additional detail rather than parse failures. ## Common error tags ### Account, billing, and access | Tag | HTTP code | Description | | ------------------------- | --------- | ------------------------------------------------------------------------------------------------------------ | | `INVALID_API_KEY` | `401` | The API key is missing, empty, or invalid. | | `NO_MORE_CREDITS` | `402` | The account has no remaining credits — top up at [dashboard.exa.ai](https://dashboard.exa.ai). | | `API_KEY_BUDGET_EXCEEDED` | `402` | The API key exceeded its spending budget — contact your team administrator. | | `TEAM_BUDGET_EXCEEDED` | `402` | The team exceeded its spending budget for the current billing period. | | `FEATURE_DISABLED` | `403` | The requested endpoint, search type, or option is not enabled for your plan. | | `PROHIBITED_CONTENT` | `403` | The request was rejected by content safety moderation. | | `CONTENT_FILTER_ERROR` | `403` | Content was rejected by a safety policy during processing. | | `RATE_LIMIT_EXCEEDED` | `429` | Your API key, team, or network exceeded its own rate limit — reduce your request rate. | | `SERVICE_OVERLOADED` | `503` | Exa is temporarily over capacity and shed the request before processing it — retry with exponential backoff. | ### Request validation | Tag | HTTP code | Description | | ------------------------- | --------- | ------------------------------------------------------------------------------------------- | | `INVALID_REQUEST_BODY` | `400` | The JSON body failed schema validation. | | `INVALID_REQUEST` | `400` | Options conflict with each other, or a beta feature was used without its `Exa-Beta` header. | | `INVALID_NUM_RESULTS` | `400` | `numResults` must be ≤ 100 when highlights are requested. | | `NUM_RESULTS_EXCEEDED` | `400` | The requested result count exceeds your plan's limit. | | `INVALID_JSON_SCHEMA` | `400` | The provided output schema is invalid. | | `SUBPAGES_LIMIT_EXCEEDED` | `400` | `/contents` allows at most 100 subpages per request. | ### Payment protocols Requests paid through x402 or MPP can also return: | Tag | HTTP code | Description | | -------------------------- | --------- | ----------------------------------------------- | | `X402_PAYMENT_REQUIRED` | `402` | Payment is required. | | `X402_INVALID_SIGNATURE` | `400` | The x402 payment signature is invalid. | | `X402_VERIFICATION_FAILED` | `402` | The x402 payment could not be verified. | | `MPP_VERIFICATION_FAILED` | `402` | The MPP payment could not be verified. | | `X402_TOO_MANY_UNPAID` | `429` | Too many x402 requests are awaiting payment. | | `X402_WALLET_RATE_LIMITED` | `429` | The x402 wallet exceeded its rate limit. | | `X402_INTERNAL_ERROR` | `500` | Exa could not create x402 payment requirements. | ## Content fetch status tags When `/contents` receives multiple URLs, one URL can fail while the others succeed. URL-level failures are returned in the `statuses` field and do not fail the request: ```json theme={null} { "results": [], "statuses": [ { "id": "https://example.com", "status": "error", "error": { "tag": "CRAWL_NOT_FOUND", "httpStatusCode": 404 } } ] } ``` `httpStatusCode` describes the target page, not the `/contents` response. | Tag | Description | How to handle | | ------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------ | | `CRAWL_NOT_FOUND` | The target page was not found. | Verify the URL is correct and accessible. | | `CRAWL_HTTP_{status}` | The target returned an HTTP error, such as `CRAWL_HTTP_403`. | Handle the embedded target status. | | `CRAWL_TIMEOUT` | The crawl timed out while fetching the target page. | Retry the request or try again later. | | `CRAWL_LIVECRAWL_TIMEOUT` | Live retrieval exceeded your requested `livecrawlTimeout`. | Increase `livecrawlTimeout` or adjust `maxAgeHours`. | | `SOURCE_NOT_AVAILABLE` | Access to the source is forbidden or the source is unavailable. | Check whether the source requires authentication or is restricted. | | `UNSUPPORTED_URL` | The URL scheme is not supported for content fetching. | Use a standard HTTP or HTTPS URL. | | `CRAWL_UNKNOWN_ERROR` | The crawl failed for another reason. | Retry the request; contact support if persistent. | These status tags are specific to `/contents`; `/search` does not return a `statuses` field. ## Getting help * Check [Exa Status](/docs/admin/status) when `500`, `503`, or `504` errors persist. * Check [Rate Limits](/docs/admin/billing#rate-limits) for current limits. * Review the endpoint's [API reference](/docs/reference/search) for request requirements. * Contact [hello@exa.ai](mailto:hello@exa.ai) with the response status, error body, and `requestId`. # FAQs Source: https://exa.ai/docs/admin/faqs Answers to common questions about Exa products, search index, freshness, grounding, security, and pricing. Exa provides web search and research infrastructure for AI applications. It combines an independent search index with content extraction and agentic research APIs, so applications can find sources, retrieve their contents, and produce grounded outputs. * Use the [Search API](/docs/search/quickstart) to find ranked web results and optionally return highlights, full text, or summaries. * Use the [Contents API](/docs/contents/quickstart) when you already have URLs and need their extracted contents. * Use the [Agent API](/docs/agent/quickstart) for asynchronous, multi-step research, list building, and structured enrichment. * Use [Monitors](/docs/monitors/quickstart) to run recurring searches and receive newly discovered results. [Exa Connect](/docs/agent/connect/overview) gives Exa Agent access to premium data providers alongside web search in the same run. Add providers through `dataSources`, and Exa Agent chooses when to query each source before combining partner data and web research into one grounded, structured output. For self-serve providers, Exa handles provider authentication and usage billing, so you do not need to build a separate integration or establish a separate provider account. Exa Search is built for programmatic retrieval rather than ad-driven browsing. It can search by meaning, accept natural-language queries, and return page contents in the same request. Search modes range from low-latency retrieval to multi-step research with structured output. See the [Search quickstart](/docs/search/quickstart) for available search types and response formats. As of August 2026, Exa's index tracks 1.4 trillion URLs and serves 100 billion pages from across the public web. The index changes continuously as pages are discovered, refreshed, or removed. Exa discovers and refreshes pages continuously, with timing that varies by source and how often a page changes. When you need content newer than the indexed copy, use the Contents API's [`maxAgeHours`](/docs/contents/quickstart#content-freshness) option to control cache age and live retrieval. Yes. Exa operates `ExaSearchBot` to discover and refresh pages on the public web for search and retrieval. It respects the Robots Exclusion Protocol, limits request rates per site, and does not attempt to bypass logins, paywalls, or CAPTCHAs. `robots.txt` controls crawling. To remove an already indexed page, use a `noindex` robots meta tag or an `X-Robots-Tag: noindex` response header; Exa removes the page after its next re-fetch. See [Exa Search Crawler](https://crawler.exa.ai/) for the user agent, cryptographic verification instructions, and crawler controls. Exa returns source URLs and the web content used for retrieval, allowing an application to generate answers with citations and inspect the supporting evidence. Search quality and source grounding can reduce unsupported claims, but the application and its language model remain responsible for how retrieved information is interpreted and presented. Yes. Use `includeDomains` to limit Search to selected domains or `excludeDomains` to remove unwanted sources. Exa also provides data categories for source-specific retrieval such as companies, people, news, and code. See [Search best practices](/docs/search/best-practices) and [Data](/docs/search/data/overview). Exa offers security and compliance controls for production and enterprise use cases, including [Zero Data Retention](/docs/admin/security/zero-data-retention) and [HIPAA compliance](/docs/admin/security/hipaa) for eligible Enterprise customers. See [Security & Compliance](/docs/admin/security/overview) for details. API usage is billed from account credits according to the endpoint and options used. New accounts receive free credits, and paid usage is pay-as-you-go unless your organization has an enterprise contract. See [Pricing](/docs/admin/pricing) and [Billing](/docs/admin/billing). # Enterprise Managed Auth for Claude Source: https://exa.ai/docs/admin/mcp-enterprise-managed-auth Set up Enterprise Managed Auth (EMA) so Claude connects to Exa MCP through your identity provider, including Okta Cross App Access (XAA). By default, every member connects the [Exa connector](/docs/get-started/exa-mcp) in Claude by signing in to Exa once through OAuth. With **Enterprise Managed Auth (EMA)** they get it silently through Okta instead: no Exa login screen, no consent prompt, no API keys handed around. Access follows your directory: deprovision someone in Okta and their Exa access through Claude stops with it. EMA is the MCP [enterprise managed authorization extension](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization). ## Before you start * A Claude Team or Enterprise organization with your identity provider connected, and admin access to it. * An Exa **organization** (not a personal team) with SSO and directory sync, and admin access to it. * Okta as your identity provider, on Okta Identity Engine with [Cross App Access (XAA)](https://help.okta.com/en-us/content/topics/apps/apps-cross-app-access.htm) enabled, and Super Admin access to the tenant. Okta is the only identity provider supported today. ## Exa values you'll need | Field | Value | | --------------------------------------- | ------------------------ | | Issuer URL (Exa's authorization server) | `https://auth.exa.ai` | | Resource / MCP server URL | `https://mcp.exa.ai/mcp` | | Scope | `mcp:tools` | ## Set up EMA Every member who will use the connector must already exist in Exa and belong to a team in your Exa organization, with the same email address Okta asserts, on a domain verified on your organization. EMA never creates accounts. Use directory sync, or [invite them to the team](/docs/admin/team-management). In the Exa dashboard, open [Organization](https://dashboard.exa.ai/organization), find **Enterprise-managed auth (Claude MCP)**, and click **Register identity provider**. Paste your Okta SSO / app embed URL (`https://your-org.okta.com/app/.../sso/saml`). Exa validates the URL when you register it. The registration stays **Pending verification** until the first provisioned member successfully connects Claude through Okta, then flips to **Active** on its own. There is nothing else to click. Issuers that Exa set up for you show as **Managed by Exa**; contact support to change those. If Exa doesn't recognize your URL, contact [support@exa.ai](mailto:support@exa.ai). Follow [Okta's Cross App Access guide for Claude EMA](https://support.okta.com/help/s/article/claude-enterprise-managed-auth-with-okta-cross-app-access-xaa-beta-participation-guide). For Exa: 1. Open the Exa application in the Okta Admin Console, go to **Resource Server**, enable XAA, and set the Resource URL and Issuer URL to `https://auth.exa.ai`. Leave Audience/tenant ID empty. 2. If the Exa app is a custom SAML app, confirm its **Name ID Format** is `EmailAddress`, since Exa matches the asserted email to the member's Exa account. 3. Register the Claude AI Agent under **Directory → AI Agents**, add its public key from Anthropic, add the Claude app as a delegated caller, and add Exa as a **Resource Connection** using the Client ID Anthropic gives you. In Claude, go to **Organization settings → Connectors**, select the Exa connector, and on the **Configuration** tab click **Set up** next to Managed authorization. Confirm the IdP connection, run the test, choose the roles that inherit the connector, and save. See [Anthropic's admin guide](https://support.claude.com/en/articles/15537633-authorize-mcp-connectors-for-your-entire-organization) for the role and scope options. Members get the connector the next time they sign in. You can leave browser sign-in enabled alongside managed authorization; Claude tries managed authorization first and falls back to the normal OAuth login if it fails. Usage through Claude bills to the member's Exa team, under that team's plan and rate limits, the same as anything else they run on the team. ## Revoking access * **One member:** remove them in Okta, or from their team in Exa. Either one ends their access through Claude. * **Everyone:** remove the issuer on the Organization page, or turn managed authorization off in Claude. New connections stop immediately and sessions already open end shortly after. You can register the issuer again at any time. ## Troubleshooting The failing member isn't resolvable in Exa. Check that they exist in Exa with the exact email Okta asserts, on a domain verified on your organization, and that they belong to a team in that organization. A directory sync group mapping is the usual culprit. Check the issuer's status on the Organization page. Still **Pending verification** means no connection has succeeded yet. Usually the Okta configuration isn't finished, the Issuer URL on the Exa app doesn't match `https://auth.exa.ai`, or the member who tried isn't provisioned in Exa. Fix that, then connect again as a provisioned member. An issuer belongs to exactly one Exa organization. If it isn't listed on your Organization page, contact [support@exa.ai](mailto:support@exa.ai). For anything else, contact [support@exa.ai](mailto:support@exa.ai) with your Exa organization name, the affected member's email, and roughly when the attempt happened. # OpenAPI Specification Source: https://exa.ai/docs/admin/openapi-spec Download the OpenAPI specs for the Exa public API and Team Management API in YAML or JSON. *** You can view up-to-date versions of our OpenAPI specs here: * Public API Spec: [YAML](/docs/exa-spec.yaml) · [JSON](/docs/exa-spec.json) * [Team Management API Spec](/docs/team-management-spec.yaml) The raw OpenAPI specs are the source of truth for request and response schemas. Generated reference pages may omit recipes, migration notes, and recommended defaults; use the guides and examples for current integration guidance such as `type: "auto"`, highlights, and freshness settings. These links always point at the specs served by this docs deployment. # Pricing Source: https://exa.ai/docs/admin/pricing Pay-as-you-go rates for Exa Search, Contents, Answer, Monitors, and the Agent API *** Exa is pay-as-you-go. There is no subscription and no minimum spend: you load credits and are charged per request, at the rates below. **Start for free.** New accounts get \$20 in free credits (around 2,800 searches) and the Free Tier adds \$10 in credits every month. Get an API key and start building. **Scaling up?** For high volume, custom indexes, higher rate limits, SLAs, or Zero Data Retention, [talk to us](https://exa.ai/contact/sales) about an [Enterprise plan](#enterprise) with volume discounts. Create a key in the dashboard. New accounts start with free credits. ## Products **\$7** / 1k requests Real-time search with token-efficient page contents. **\$12–15** / 1k requests Multi-step research with structured outputs and citations. **\$1** / 1k pages Full page text, highlights, and summaries for known URLs. **\$5** / 1k requests An LLM answer to a question, with citations. **\$15** / 1k requests Scheduled searches that surface new events on the web. **\$0.012–\$1.00** / fixed-effort run, or usage-based Async deep research, list building, and enrichment. ## Search, Contents, Answer, and Monitors Each endpoint has a base price per request that includes up to 10 results. Additional results and Exa-generated page summaries are billed on top. | Endpoint | Base price
(up to 10 results) | Each result above 10 | AI page summaries | | ----------- | ---------------------------------- | -------------------- | ----------------- | | `/search` | \$7 / 1k requests | \$1 / 1k results | \$1 / 1k pages | | `/answer` | \$5 / 1k requests | — | — | | `/monitors` | \$15 / 1k requests | \$1 / 1k results | \$1 / 1k pages | | `/contents` | \$1 / 1k pages, per content type | — | \$1 / 1k pages | ## Agent Set a fixed `effort` on [Agent](/docs/agent/quickstart) for a predictable per-request price. `auto` is the default metered mode; beta `max` is also metered and uses the same usage rates: | Effort | Price | | --------- | ----------------- | | `minimal` | \$0.012 / request | | `low` | \$0.025 / request | | `medium` | \$0.10 / request | | `high` | \$0.50 / request | | `xhigh` | \$1.00 / request | Metered runs bill actual usage up to their per-run cap. `auto` defaults to a \$5 cap; beta `max` defaults to a \$20 cap: | Usage component | Price | | ------------------------ | --------------------- | | Agent Compute Units | \$0.10 / ACU | | Search tool calls | \$0.005 / search | | Email contact enrichment | \$0.02 / email | | Phone contact enrichment | \$0.07 / phone number | ### Connect providers Runs that use [Exa Connect](/docs/agent/connect/overview) data sources additionally bill each provider call — for example [Fiber.ai](/docs/agent/connect/fiber#pricing) at \$0.02 per credit and [Baselayer](/docs/agent/connect/baselayer#pricing) at \$0.15–\$4.00 per order depending on the operation. See [Connect pricing](/docs/agent/connect/overview#pricing) for all provider rates. ## Deep Search Set with `type` on [`/search`](/docs/search/deep-search). Additional results and AI page summaries cost the same as standard search. | Type | Base price
(up to 10 results) | Latency | Best for | | ---------------- | ---------------------------------- | ------------- | -------------------------------------------- | | `deep-lite` | \$12 / 1k requests | \~4 seconds | Lightweight synthesis | | `deep` | \$12 / 1k requests | 4–15 seconds | Multi-step reasoning with structured outputs | | `deep-reasoning` | \$15 / 1k requests | 12–40 seconds | Harder research tasks | ## Enterprise For high volume, custom datasets, and stricter security requirements. Up to 1,000 results per search, requests above 25 results, custom rate limits (QPS), tailored moderation, and custom indexes. SLAs and MSAs, 1:1 onboarding and support, and [Zero Data Retention](/docs/admin/security/zero-data-retention). Volume discounts and postpaid invoice billing. Get a quote for enterprise volume and terms ## Cost glossary One API call to an endpoint. Prices are quoted per 1,000 requests, so a \$7 / 1k rate is \$0.007 per call. One search result returned in a response. The base price covers the first 10 results in a request; every result above 10 adds \$1 / 1k results. Requesting `numResults: 20` therefore costs the base price plus 10 additional results. A page is one URL that Exa returns content for. A content type is one view of that page: `text`, `highlights`, or `summary`. `/contents` bills each content type separately, so one page with `text` and `highlights` counts as two. An Exa-generated summary of a page, produced by an extra LLM call on our side. Billed at \$1 / 1k pages on any endpoint that returns one. The unit of model computation an Agent run consumes, reported as `usage.agentComputeUnits`. Longer runs, larger `input.data`, and more reasoning steps consume more ACUs. The Agent parameter that trades cost and latency against thoroughness. `auto` bills by consumption (ACUs plus tool calls) up to a default \$5 cap; beta `max` uses the same usage rates up to a default \$20 cap. Fixed efforts bill a flat price per request. See [Agent effort modes](/docs/agent/quickstart#effort). An Agent lookup that returns an email address or phone number for a person or company. Billed per contact found, on top of the run's other costs. Prepaid dollar balance on your account. Usage draws down credits at the rates above. Add credits, set up auto recharge, and find your invoices # HIPAA Source: https://exa.ai/docs/admin/security/hipaa Use HIPAA compliance mode for eligible cached retrieval requests. HIPAA compliance is available for Enterprise customers after Exa enables it for your team. Contact [sales@exa.ai](mailto:sales@exa.ai) to discuss Enterprise access, BAA requirements, and enablement. HIPAA mode is controlled per request with a top-level `compliance` field: ```json theme={null} { "compliance": "hipaa" } ``` When this field is present on an eligible team, Exa handles the request with HIPAA compliance controls. If your team is not enabled, the API returns `403 FEATURE_DISABLED`. HIPAA mode includes [Zero Data Retention](/docs/admin/security/zero-data-retention) for those requests: Exa does not persist PHI. ## Supported endpoints The `compliance` field is recognized on: * [`/search`](/docs/reference/search) * [`/contents`](/docs/reference/get-contents) Other endpoints reject the field. ## Requirements HIPAA mode supports cached retrieval only. Compatible requests: * On `/search`, set `type` to `instant` or `fast` * Request `text` or `highlights` (not `summary`) * Use cache-only content: omit freshness fields, or set `maxAgeHours: -1` on `/contents` Incompatible requests return `400 INVALID_REQUEST_BODY`, including: * `summary` on `/contents`, or `contents.summary` on `/search` * Freshness settings that require a live fetch, such as `maxAgeHours: 0` or a positive `maxAgeHours` * Search requests that omit `type`, or use a type other than `instant` or `fast` ## Example ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/contents" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "urls": ["https://example.com/article"], "compliance": "hipaa", "highlights": true, "maxAgeHours": -1 }' ``` ## Access To enable HIPAA mode for your team, contact [sales@exa.ai](mailto:sales@exa.ai). See the [Trust Center](https://trust.exa.ai) for Exa security documentation. # Security Overview Source: https://exa.ai/docs/admin/security/overview Exa security, compliance, and regional access information. *** Exa takes data security and privacy seriously. We are proud to be SOC 2 Type II certified, demonstrating our commitment to maintaining rigorous information security practices and controls. Contact us at [sales@exa.ai](mailto:sales@exa.ai) to discuss an Enterprise plan if you are interested in [Zero Data Retention](/docs/admin/security/zero-data-retention), [HIPAA compliance](/docs/admin/security/hipaa), or other customized data security solutions. Visit our [Trust Center](https://trust.exa.ai) to view our SOC 2 reports, Data Processing Agreement, and other security documentation. ## Regional access restrictions To comply with sanctions and trade restrictions, Exa blocks API access from sanctioned or otherwise restricted countries and regions, including Crimea, Cuba, Iran, North Korea, Russia, Syria, Ukraine, and Venezuela. Requests from these locations may be blocked by Cloudflare before they reach Exa. In that case, the response may be a Cloudflare WAF block page with a Ray ID instead of the standard Exa API error JSON. If you believe your traffic is being geolocated incorrectly, contact [hello@exa.ai](mailto:hello@exa.ai) with your source IP address, country or region, request timestamp, and Cloudflare Ray ID. # Zero Data Retention Source: https://exa.ai/docs/admin/security/zero-data-retention Zero Data Retention availability across Exa products. Zero Data Retention (ZDR) is available on Enterprise plans and enabled per team. Contact [sales@exa.ai](mailto:sales@exa.ai) for details. | Product | ZDR availability | | -------- | ----------------------- | | Search | Available | | Contents | Available | | Agent | Available | | Answer | Not currently supported | | Websets | Not currently supported | # Status Page Source: https://exa.ai/docs/admin/status Live availability, active incidents, and uptime history for Exa services. *** Exa publishes service availability at [status.exa.ai](https://status.exa.ai): the current state of the Search API, Websets, and Exa MCP, active incidents with updates as they progress, and 90 days of uptime history. ## Subscribe to updates Subscribe on the status page to be notified when an incident is opened, updated, or resolved. Notifications are delivered by email, Slack, Google Chat, or webhook. ## Get help If requests keep failing while all systems report operational, the cause is likely request-level — check [Error codes](/docs/admin/error-codes) first. Send the response status, error body, and `requestId` for the fastest diagnosis. Uptime and support SLAs are available on Enterprise plans. # Managing Your Team Source: https://exa.ai/docs/admin/team-management Details on Team structure and account management for the Exa platform *** Create teams, invite members, and manage billing. Exa organizes account usage and paid feature access through 'Teams': Upon account creation, you're placed in a 'Personal' Team. You can use the dropdown in the top-left of the Exa dashboard shown below to create a new Team or select between other Teams you have. You can make as many Teams as you like. ## Seeing your teams Team dropdown (top-left) within the Exa dashboard under Team settings Team dropdown (top-left) within the Exa dashboard under Team settings ## Topping up a Team's balance With the desired Team selected, you can top up your credit balance in the Billing page. Billing page credit balance top up ## Inviting people to your team Team admins can add members via the Invite feature in Team settings. Inviting a member in Team settings Once a team member is invited, their status will be 'Pending' on the team management menu. Team member listed with Pending invite status They will receive an email inviting them to join the team. Team invitation email Once accepted, you'll see both members are 'Accepted'. All Team members share the usage limits and features of their respective Team's plan. Team members list showing Accepted status ## Team Management API Create and manage API keys programmatically with the [Team Management API](/docs/reference/team-management/create-api-key). The Team Management API is enabled per team. It authenticates with a service account API key, which is created from the **Service keys** tab on the [API keys page](https://dashboard.exa.ai/api-keys) once the feature is enabled for your team. Contact [support@exa.ai](mailto:support@exa.ai) to request access. # Agent Best Practices Source: https://exa.ai/docs/agent/best-practices Tune query quality, structured output, effort, and cost for production Exa Agent integrations. Use this guide after the [Exa Agent quickstart](/docs/agent/quickstart) to improve query quality, structure outputs, and control runtime and cost. For complete requests, start with [Agent examples](/docs/agent/examples). ## Core principles Treat `query` as a task specification. Name what Agent should find, the scope of the work, the evidence required, and what a complete result looks like. ```python Python theme={null} run = exa.agent.runs.create( query="Find up to 10 current engineering leaders at AI infrastructure companies that raised a Series A or B in the last 6 months. Include only people whose current role and company funding can be verified from public sources.", ) ``` ```javascript JavaScript theme={null} const run = await exa.agent.runs.create({ query: "Find up to 10 current engineering leaders at AI infrastructure companies that raised a Series A or B in the last 6 months. Include only people whose current role and company funding can be verified from public sources." }); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Find up to 10 current engineering leaders at AI infrastructure companies that raised a Series A or B in the last 6 months. Include only people whose current role and company funding can be verified from public sources." }' ``` Without an `outputSchema`, Agent returns prose in `output.text` and citations in `output.grounding`. Add another field only when it has a clear job: | Field | Use it when | | ----------------------- | ------------------------------------------------------------------------------- | | `outputSchema` | Downstream code needs structured fields | | `input.data` | You already have rows to enrich | | `input.exclusion` | Known records should not be returned | | `dataSources` | A field should come from an [Exa Connect](/docs/agent/connect/overview) partner | | `previousRunId` | The request continues a completed run | | `effort` | Cost or research depth needs an explicit setting | | `budget.maxCostDollars` | An `auto` or `max` run needs a hard cost ceiling | Keep rows, exclusions, and response shape in their dedicated fields rather than embedding them in `query`. ## Writing list-building and enrichment queries For list building, define the entity, target count, qualification criteria, exclusions, and evidence bar. For enrichment, put the existing records in `input.data` and describe only the research Agent should add. Ask for a rationale when qualification requires judgment. Give examples only when a criterion has multiple plausible interpretations. ```text Query theme={null} Find up to 20 current engineering leaders at US-based AI infrastructure companies that announced a Series A or B between March 1 and August 31, 2026. Include CTOs, VPs of Engineering, and Heads of Engineering. Exclude founders without an operating engineering role and anyone whose current employment cannot be verified. For each person, return their name, current title, company, company website, funding announcement date, and a short explanation of why they qualify. Verify employment on the company website or another current source, and verify funding from the company announcement or a reputable business publication. ``` See [Find all GTM members](/docs/agent/examples#find-all-code) for a discovery request and [Enrich input rows](/docs/agent/examples#enrich-input-rows-code) for the corresponding row-enrichment pattern. ## Handle asynchronous runs Agent runs can take seconds to minutes while they search, read, and reason. Build around the lifecycle instead of holding an application request open. Create the run and save its returned `id` with your request metadata. The create response is not the final result. Use an SDK polling helper, poll `GET /agent/runs/{id}`, or consume the SSE stream. Continue while the run is `queued` or `running`. Stop waiting at `completed`, `failed`, or `cancelled`, then persist the terminal response and grounding. Persisting the run ID lets your application recover after a restart, reconnect to a stream, and inspect failures. Keep latency down by narrowing the scope, limiting result count, keeping the schema focused, and choosing `minimal` or `low` when speed matters more than completeness. For batches, benchmark representative tasks before estimating concurrency or putting Agent on a synchronous UI path. Runtime varies with item count, schema complexity, source availability, and effort. For Zero Data Retention teams, consume the live stream or poll within the retention window. `previousRunId` and Connect `dataSources` are not available. See [Zero Data Retention](/docs/admin/security/zero-data-retention). ## Write custom JSON schemas for structured output Use `outputSchema` when downstream code needs machine-readable fields, normalized values, table rows, or enrichment records. If a prose answer is enough, omit it and read `output.text`; structured output adds formatting work and can increase latency. Keep research instructions in `query` and response shape in `outputSchema`. Use clear property names and descriptions, choose the narrowest useful types, and bound arrays with `maxItems`. ```json Output schema expandable theme={null} { "type": "object", "properties": { "people": { "type": "array", "maxItems": 10, "description": "Current engineering leaders who satisfy every criterion in the query.", "items": { "type": "object", "properties": { "name": { "type": "string", "description": "The person's full name." }, "job_title": { "type": "string", "description": "Their current title at the qualifying company." }, "company": { "type": "string", "description": "The qualifying company's canonical name." }, "qualification_rationale": { "type": "string", "description": "A concise explanation of how the person satisfies the query criteria." } }, "required": ["name", "job_title", "company", "qualification_rationale"] } } }, "required": ["people"] } ``` Schema adherence validates shape, not facts. Agent may return `null` when evidence does not support a field, even if the submitted schema marks it as required or non-nullable. `stopReason: schema_satisfied` means Agent considers the expected shape complete with those nulls allowed; it does not guarantee strict validation against the submitted schema. Do not duplicate Exa's built-in citations or confidence in your schema. Add a rationale field only when each item should explain why it qualifies, and persist `output.grounding` with the structured result. Verify important claims against their sources and test schema changes on representative inputs before shipping. Browse the [structured Agent examples](/docs/agent/examples) to compare schemas for list building, KYB, job postings, exclusions, and continued runs. ## Agent vs Search | Need | Start with | | ------------------------------------------------------ | --------------------------------------- | | Web results for your LLM | [Search](/docs/search/quickstart) | | Fast research and synthesis | [Deep Search](/docs/search/deep-search) | | Async list building, multi-hop research, or enrichment | [Agent](/docs/agent/quickstart) | Use Agent when the work requires several retrieval steps, per-entity verification, or enrichment over known records. Use Search when you need pages quickly and your application will perform the remaining reasoning. ## Tips for common use cases | If you need | Use | Avoid | | -------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------ | | A researched list of unknown size | `auto` and a bounded `outputSchema` | A fixed cheap effort and an unbounded array | | Enrichment of records you already have | `input.data` plus fields to add | Pasting the table into `query` | | A follow-up over the last result set | `previousRunId` | Re-sending the full previous output | | Records that should not appear again | `input.exclusion` plus downstream deduplication | Treating exclusions as a strict identity guarantee | | Premium provider data | [Exa Connect](/docs/agent/connect/overview) with `dataSources` | Asking Agent to infer provider-only fields from the open web | | Predictable per-request cost | A fixed `effort` | `auto` or `max` without a budget | | Completeness over latency & cost | `xhigh` or `max` | Raising effort before tightening the query | ## Next steps Create a run, stream events, set effort, and read structured output. Copy complete list-building, enrichment, KYB, exclusion, and follow-up requests. Add premium company, people, traffic, compliance, finance, and other provider data. Retrieval quality, latency, and synthesis when Search is enough. # Additional providers Source: https://exa.ai/docs/agent/connect/additional-partners Exa Connect partners beyond the self-serve provider set. Beyond the self-serve providers, Exa Connect supports additional data partners. These are **available upon request**, [connect with our team to enable them for your account](mailto:sales@exa.ai).
Crunchbase
Definitive Healthcare
Faraday AI Faraday AI
Harmonic Harmonic
Intellizence
Kernel Kernel
ZoomInfo ZoomInfo
Tell us which partner your Exa Agent needs and we'll get you connected. Have data to offer? Apply to join Exa Connect as a partner. # Affiliate.com Source: https://exa.ai/docs/agent/connect/affiliatecom Search product catalogs across merchants and affiliate networks. [Affiliate.com](https://affiliate.com) aggregates product catalogs across merchants and affiliate networks into a single searchable index, with live pricing, brands, and direct merchant links. Attach `affiliate` to an [Exa Agent](/docs/agent/quickstart) run through [Exa Connect](/docs/agent/connect/overview), and the agent queries Affiliate.com alongside Exa web search. ## Use it for * Product discovery and price comparison across merchants. * Powering shopping assistants and buying-guide content. * Surfacing affiliate links alongside research. ## Provider ID Use this value in `dataSources`: ```text theme={null} affiliate ``` ## Example Find wireless noise-cancelling headphones under \$300 and compare pricing. ```python Python theme={null} from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query="Find wireless noise-cancelling headphones under $300 with pricing from multiple merchants.", data_sources=[{"provider": "affiliate"}], output_schema={ "type": "object", "required": ["products"], "properties": { "products": { "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["name", "brand", "price", "merchant"], "properties": { "name": {"type": "string"}, "brand": {"type": "string"}, "price": {"type": "string", "description": "price with currency"}, "merchant": {"type": "string"}, }, }, } }, }, ) run = exa.agent.runs.poll_until_finished(run.id) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "Find wireless noise-cancelling headphones under $300 with pricing from multiple merchants.", dataSources: [{ provider: "affiliate" }], outputSchema: { type: "object", required: ["products"], properties: { products: { type: "array", maxItems: 10, items: { type: "object", required: ["name", "brand", "price", "merchant"], properties: { name: { type: "string" }, brand: { type: "string" }, price: { type: "string", description: "price with currency" }, merchant: { type: "string" }, }, }, }, }, }, }); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Find wireless noise-cancelling headphones under $300 with pricing from multiple merchants.", "dataSources": [{ "provider": "affiliate" }], "outputSchema": { "type": "object", "required": ["products"], "properties": { "products": { "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["name", "brand", "price", "merchant"], "properties": { "name": { "type": "string" }, "brand": { "type": "string" }, "price": { "type": "string", "description": "price with currency" }, "merchant": { "type": "string" } } } } } } }' ``` ## Pairs well with * [Similarweb](/docs/agent/connect/similarweb): gauge a merchant's reach before recommending it. * [Fiber.ai](/docs/agent/connect/fiber): research the company behind a merchant or brand. ## Next steps The Exa Connect quickstart covers `dataSources`, pricing, and the full partner catalog. Attach up to five partners to one run and shape the query so each one fires. Create runs, stream progress, design output schemas, and control effort and cost. Create a key in the dashboard and run this page's example as-is. New accounts start with free credits. # Baselayer Source: https://exa.ai/docs/agent/connect/baselayer Verify US businesses and retrieve KYB data: officers, registrations, risk scores. [Baselayer](https://baselayer.com) is a Know Your Business (KYB) platform that verifies US entities against authoritative registration and risk data. It resolves a business by name and address and returns the full profile: officers, state registrations, entity structure, and verification status. Attach `baselayer` to an [Exa Agent](/docs/agent/quickstart) run through [Exa Connect](/docs/agent/connect/overview), and the agent queries Baselayer alongside Exa web search. ## Use it for * KYB onboarding and vendor/customer verification. * Due diligence on officers, registrations, and entity structure. * Screening businesses for risk and watchlist hits. ## Provider ID Use this value in `dataSources`: ```text theme={null} baselayer ``` ## Pricing Baselayer bills per order, and the rate depends on the operation and its parameters: | Operation | Price | | ------------------------------------------------------------------- | ------------------------------------------- | | Business search | `$1.00 / search` | | Business lookup / officers / registrations / officer reverse lookup | Free (reads of a prior search) | | Lien search | `$2.00 / state searched` | | Litigation search | `$1.00 / category (litigation, bankruptcy)` | | Watchlist screen | `$0.10 – $0.25 / list requested` | | Industry classification | `$0.35 / call` | | Website analysis | `$0.35 / call` | | Web presence | `$0.15 – $0.35 / selected analysis` | | International business search | `$4.00 / search` | Parameter choices change the price: a lien search across two states costs \$4.00, a watchlist screen of all six supported lists costs \$1.35, and a web presence call is the sum of the analyses you select (or of Baselayer's default set — NAICS prediction and website analysis — when you select none). ## Example Verify a business and retrieve its officer and registration details. ```python Python theme={null} from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query="Verify the business 'Stripe, Inc.' in San Francisco, CA and return its officers and registration status.", data_sources=[{"provider": "baselayer"}], output_schema={ "type": "object", "required": ["business"], "properties": { "business": { "type": "object", "required": ["name", "verified", "incorporationState", "officers"], "properties": { "name": {"type": "string"}, "verified": {"type": "boolean"}, "incorporationState": {"type": "string"}, "officers": { "type": "array", "items": { "type": "object", "required": ["name", "title"], "properties": { "name": {"type": "string"}, "title": {"type": "string"}, }, }, }, }, } }, }, ) run = exa.agent.runs.poll_until_finished(run.id) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "Verify the business 'Stripe, Inc.' in San Francisco, CA and return its officers and registration status.", dataSources: [{ provider: "baselayer" }], outputSchema: { type: "object", required: ["business"], properties: { business: { type: "object", required: ["name", "verified", "incorporationState", "officers"], properties: { name: { type: "string" }, verified: { type: "boolean" }, incorporationState: { type: "string" }, officers: { type: "array", items: { type: "object", required: ["name", "title"], properties: { name: { type: "string" }, title: { type: "string" }, }, }, }, }, }, }, }, }); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Verify the business Stripe, Inc. in San Francisco, CA and return its officers and registration status.", "dataSources": [{ "provider": "baselayer" }], "outputSchema": { "type": "object", "required": ["business"], "properties": { "business": { "type": "object", "required": ["name", "verified", "incorporationState", "officers"], "properties": { "name": { "type": "string" }, "verified": { "type": "boolean" }, "incorporationState": { "type": "string" }, "officers": { "type": "array", "items": { "type": "object", "required": ["name", "title"], "properties": { "name": { "type": "string" }, "title": { "type": "string" } } } } } } } } }' ``` ## Pairs well with * [Fiber.ai](/docs/agent/connect/fiber): enrich a verified business with firmographics, headcount, and contacts. * [Financial Datasets](/docs/agent/connect/financialdatasets): add recent news coverage for public entities. * [Similarweb](/docs/agent/connect/similarweb): benchmark a verified company's web traffic and competitors. ## Next steps The Exa Connect quickstart covers `dataSources`, pricing, and the full partner catalog. Attach up to five partners to one run and shape the query so each one fires. Create runs, stream progress, design output schemas, and control effort and cost. Create a key in the dashboard and run this page's example as-is. New accounts start with free credits. # Combining providers Source: https://exa.ai/docs/agent/connect/combining-providers Use several data partners together in a single Exa Agent run. Attaching a partner to `dataSources` makes it available to the Exa Agent as a tool — it does **not** force the agent to call it. Whether a partner fires depends on your `query` and `outputSchema`: name the kind of result you want from each partner, and the Exa Agent reaches for the matching tool instead of guessing from a web page. You can attach up to five partners per run; the Exa Agent picks which to call for each step, with Exa web search available alongside them. Need more than five for a single run? [Contact us](mailto:sales@exa.ai) to raise the limit. ## Two partners in one run List several partners together and the Exa Agent draws on each where it's strongest. Two is just an example here — attach up to five partners to `dataSources`, and the same principle applies: ask for each one's data explicitly. This investor-briefing run combines [Financial Datasets](/docs/agent/connect/financialdatasets) for ticker news with [Particle](/docs/agent/connect/particle) for podcast commentary. The query asks for each partner's distinctive data and the schema splits the output into `financialNews` and `podcastChatter`, so the Exa Agent calls **both** partners in the same run. ```python Python theme={null} from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query=( "Give me an investor briefing on NVIDIA (NVDA): (1) the latest financial and " "earnings news, and (2) what podcast hosts and guests have recently been saying " "about NVIDIA, with speaker-attributed quotes and their stance." ), data_sources=[ {"provider": "financial_datasets"}, {"provider": "particle"}, ], output_schema={ "type": "object", "required": ["ticker", "financialNews", "podcastChatter"], "properties": { "ticker": {"type": "string"}, "financialNews": { "type": "array", "maxItems": 6, "items": { "type": "object", "required": ["title", "source", "date", "theme"], "properties": { "title": {"type": "string"}, "source": {"type": "string"}, "date": {"type": "string"}, "theme": {"type": "string", "description": "earnings, guidance, analyst rating, product, or market"}, }, }, }, "podcastChatter": { "type": "array", "maxItems": 6, "items": { "type": "object", "required": ["podcast", "speaker", "quote", "stance"], "properties": { "podcast": {"type": "string"}, "speaker": {"type": "string"}, "quote": {"type": "string"}, "stance": {"type": "string", "description": "bullish, bearish, or neutral"}, }, }, }, }, }, ) run = exa.agent.runs.poll_until_finished(run.id) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "Give me an investor briefing on NVIDIA (NVDA): (1) the latest financial and " + "earnings news, and (2) what podcast hosts and guests have recently been saying " + "about NVIDIA, with speaker-attributed quotes and their stance.", dataSources: [ { provider: "financial_datasets" }, { provider: "particle" }, ], outputSchema: { type: "object", required: ["ticker", "financialNews", "podcastChatter"], properties: { ticker: { type: "string" }, financialNews: { type: "array", maxItems: 6, items: { type: "object", required: ["title", "source", "date", "theme"], properties: { title: { type: "string" }, source: { type: "string" }, date: { type: "string" }, theme: { type: "string", description: "earnings, guidance, analyst rating, product, or market" }, }, }, }, podcastChatter: { type: "array", maxItems: 6, items: { type: "object", required: ["podcast", "speaker", "quote", "stance"], properties: { podcast: { type: "string" }, speaker: { type: "string" }, quote: { type: "string" }, stance: { type: "string", description: "bullish, bearish, or neutral" }, }, }, }, }, }, }); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Give me an investor briefing on NVIDIA (NVDA): (1) the latest financial and earnings news, and (2) what podcast hosts and guests have recently been saying about NVIDIA, with speaker-attributed quotes and their stance.", "dataSources": [ { "provider": "financial_datasets" }, { "provider": "particle" } ], "outputSchema": { "type": "object", "required": ["ticker", "financialNews", "podcastChatter"], "properties": { "ticker": { "type": "string" }, "financialNews": { "type": "array", "maxItems": 6, "items": { "type": "object", "required": ["title", "source", "date", "theme"], "properties": { "title": { "type": "string" }, "source": { "type": "string" }, "date": { "type": "string" }, "theme": { "type": "string", "description": "earnings, guidance, analyst rating, product, or market" } } } }, "podcastChatter": { "type": "array", "maxItems": 6, "items": { "type": "object", "required": ["podcast", "speaker", "quote", "stance"], "properties": { "podcast": { "type": "string" }, "speaker": { "type": "string" }, "quote": { "type": "string" }, "stance": { "type": "string", "description": "bullish, bearish, or neutral" } } } } } } }' ``` Make each partner's data explicit in your *query* — name the kind of result you want from each one (here: ticker financial news vs. speaker-attributed podcast quotes). If the request is generic ("latest news"), the Exa Agent tends to fall back to web search instead of a partner. Mirroring those distinct asks in your `outputSchema` fields reinforces it. # Fiber.ai Source: https://exa.ai/docs/agent/connect/fiber Search Fiber.ai's B2B database for companies, people, and LinkedIn profiles. [Fiber.ai](https://fiber.ai) is a B2B data platform with fresh data on 40M+ companies, 850M+ people, and 30M+ jobs. Search live company, people, and job data, and enrich incomplete records with work emails, personal emails, and phone numbers. Attach `fiber` to an [Exa Agent](/docs/agent/quickstart) run through [Exa Connect](/docs/agent/connect/overview), and the agent queries Fiber.ai alongside Exa web search. ## Use it for * Cleaning up a CRM by reverse-looking-up a work or personal email to a person, or enriching a partial company/person record. * Tracking real-time LinkedIn signals: job changes, promotions, new jobs, headcount changes, and fundraising. * Finding relevant posts across LinkedIn, X, Instagram, TikTok, Reddit, and YouTube, pulling their comments and reactions, then enriching the authors' contact info. * Searching across 40M+ companies and 850M+ people and enriching prospects with work email, personal email, and phone numbers. ## Provider ID Use this value in `dataSources`: ```text theme={null} fiber ``` ## Pricing Fiber.ai bills in credits at `$0.02 / credit`, and each call is charged the credits Fiber reports for it: | Operation | Credits | | ------------------------------------ | -------------------------- | | Search | 2 + 1 per result returned | | Company lookup | \~2 per candidate returned | | Person lookup / email reverse lookup | 2 | | Contact reveal | 2 (work email) – 5 (phone) | Calls that return no match (or whose charge Fiber refunds) are free. Parameter choices change the price: a company lookup's `numResults` sets how many candidates you pay for, and a search's result count drives most of its cost. ## Example Build a B2B prospecting list of Series A fintech companies in New York with 50–200 employees. ```python Python theme={null} from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query="I'm building a B2B sales prospecting list using a B2B company database. Find Series A fintech companies in New York with 50-200 employees, and for each return the company's LinkedIn profile, domain, employee count, and funding stage.", data_sources=[{"provider": "fiber"}], output_schema={ "type": "object", "required": ["companies"], "properties": { "companies": { "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["name", "domain", "employeeCount", "fundingStage"], "properties": { "name": {"type": "string"}, "domain": {"type": "string"}, "employeeCount": {"type": "number"}, "fundingStage": {"type": "string"}, }, }, } }, }, ) run = exa.agent.runs.poll_until_finished(run.id) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "I'm building a B2B sales prospecting list using a B2B company database. Find Series A fintech companies in New York with 50-200 employees, and for each return the company's LinkedIn profile, domain, employee count, and funding stage.", dataSources: [{ provider: "fiber" }], outputSchema: { type: "object", required: ["companies"], properties: { companies: { type: "array", maxItems: 10, items: { type: "object", required: ["name", "domain", "employeeCount", "fundingStage"], properties: { name: { type: "string" }, domain: { type: "string" }, employeeCount: { type: "number" }, fundingStage: { type: "string" }, }, }, }, }, }, }); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "I'\''m building a B2B sales prospecting list using a B2B company database. Find Series A fintech companies in New York with 50-200 employees, and for each return the company'\''s LinkedIn profile, domain, employee count, and funding stage.", "dataSources": [{ "provider": "fiber" }], "outputSchema": { "type": "object", "required": ["companies"], "properties": { "companies": { "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["name", "domain", "employeeCount", "fundingStage"], "properties": { "name": { "type": "string" }, "domain": { "type": "string" }, "employeeCount": { "type": "number" }, "fundingStage": { "type": "string" } } } } } } }' ``` ## Pairs well with * [Similarweb](/docs/agent/connect/similarweb): size up a prospect's web presence and competitors. * [Baselayer](/docs/agent/connect/baselayer): verify officers and registrations for shortlisted US businesses. * [Particle](/docs/agent/connect/particle): find what podcasts are saying about a company or executive. ## Next steps The Exa Connect quickstart covers `dataSources`, pricing, and the full partner catalog. Attach up to five partners to one run and shape the query so each one fires. Create runs, stream progress, design output schemas, and control effort and cost. Create a key in the dashboard and run this page's example as-is. New accounts start with free credits. # Financial Datasets Source: https://exa.ai/docs/agent/connect/financialdatasets Structured financial and market data for 27,000+ U.S. tickers: prices, fundamentals, earnings, SEC filings, ownership, and stock screening. [Financial Datasets](https://financialdatasets.ai) provides machine-ready company and market data for AI agents. Through [Exa Connect](/docs/agent/connect/overview), agents can retrieve real-time and historical prices, company facts, financial statements and valuation metrics, earnings, insider and institutional ownership, SEC filings and filing sections, company news, and screen the U.S. market by fundamental criteria. Attach `financial_datasets` to an [Exa Agent](/docs/agent/quickstart) run, and the agent queries Financial Datasets alongside Exa web search. ## Use it for * Building structured company research snapshots. * Analyzing financial performance, valuation, and historical trends. * Reading SEC filings and extracting sections such as risk factors and MD\&A. * Examining insider transactions and institutional ownership. * Screening the U.S. market by fundamental criteria. * Monitoring company news and relevant developments. ## Data available Each of the following datasets is available under the `financial_datasets` provider; the agent selects whichever fits the task: | Dataset | What it returns | | ----------------------- | -------------------------------------------------------------------------------------------------------- | | Beneficial ownership | 5%+ beneficial owners from Schedules 13D/13G, including activist and passive stakes. | | Company facts | Name, sector, industry, exchange, location, SEC CIK, SIC classification. | | Company news | Recent news articles for a ticker. | | Earnings | Quarterly revenue and EPS with YoY change and beat/miss surprises. | | Financial metrics | Market cap, EV, P/E, P/B, P/S, EV/EBITDA, PEG, margins, ROE/ROA/ROIC, growth, EPS. | | Financial statements | Income statement, balance sheet, and cash flow from SEC filings. | | Historical stock prices | OHLCV bars over a date range at day/week/month/year granularity. | | Index-fund holdings | ETF/index-fund constituents by weight, or the funds that hold a given security. | | Insider ownership | Insider holdings from SEC Forms 3 and 5 (shares owned by officers, directors, 10% owners). | | Insider trades | SEC Form 4 insider transactions (name, role, type, shares, value). | | Institutional ownership | 13F institutional holders, shares, and reported value. | | Interest rates | Current and historical central-bank policy rates (Fed, ECB, BOJ, and more). | | SEC filing items | Extracted text of specific 10-K/10-Q/8-K items (e.g. risk factors, MD\&A). | | SEC filings | Filing metadata and direct EDGAR links, optionally filtered by form type. | | Segmented financials | Revenue, operating income, and other line items broken down by product, business segment, and geography. | | Stock price snapshot | Current real-time price, day change, and quote time. | | Stock screener | Companies matching fundamental filter criteria. | ## Provider ID Use this value in `dataSources`: ```text theme={null} financial_datasets ``` ## Example Build a structured company-research snapshot for NVIDIA. ```python Python theme={null} from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query=( "Analyze NVIDIA using its latest price, valuation metrics, most recent " "quarterly financial statements and earnings, institutional and insider " "activity, and material SEC filing sections. Return a structured " "company-research snapshot with reporting dates." ), data_sources=[{"provider": "financial_datasets"}], output_schema={ "type": "object", "required": ["ticker", "price", "valuation", "financials", "earnings", "ownership", "filings"], "properties": { "ticker": {"type": "string"}, "price": { "type": "object", "required": ["latest", "asOf"], "properties": { "latest": {"type": "number"}, "asOf": {"type": "string"}, }, }, "valuation": { "type": "object", "properties": { "marketCap": {"type": "number"}, "peRatio": {"type": "number"}, "evToEbitda": {"type": "number"}, }, }, "financials": { "type": "object", "required": ["reportPeriod", "summary"], "properties": { "reportPeriod": {"type": "string"}, "summary": {"type": "string"}, }, }, "earnings": { "type": "object", "required": ["reportPeriod", "summary"], "properties": { "reportPeriod": {"type": "string"}, "summary": {"type": "string"}, }, }, "ownership": { "type": "object", "properties": { "institutionalHighlights": {"type": "string"}, "insiderActivity": {"type": "string"}, }, }, "filings": { "type": "array", "maxItems": 5, "items": { "type": "object", "required": ["formType", "filedAt", "keySection"], "properties": { "formType": {"type": "string"}, "filedAt": {"type": "string"}, "keySection": {"type": "string"}, }, }, }, }, }, ) run = exa.agent.runs.poll_until_finished(run.id) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "Analyze NVIDIA using its latest price, valuation metrics, most recent quarterly financial statements and earnings, institutional and insider activity, and material SEC filing sections. Return a structured company-research snapshot with reporting dates.", dataSources: [{ provider: "financial_datasets" }], outputSchema: { type: "object", required: ["ticker", "price", "valuation", "financials", "earnings", "ownership", "filings"], properties: { ticker: { type: "string" }, price: { type: "object", required: ["latest", "asOf"], properties: { latest: { type: "number" }, asOf: { type: "string" }, }, }, valuation: { type: "object", properties: { marketCap: { type: "number" }, peRatio: { type: "number" }, evToEbitda: { type: "number" }, }, }, financials: { type: "object", required: ["reportPeriod", "summary"], properties: { reportPeriod: { type: "string" }, summary: { type: "string" }, }, }, earnings: { type: "object", required: ["reportPeriod", "summary"], properties: { reportPeriod: { type: "string" }, summary: { type: "string" }, }, }, ownership: { type: "object", properties: { institutionalHighlights: { type: "string" }, insiderActivity: { type: "string" }, }, }, filings: { type: "array", maxItems: 5, items: { type: "object", required: ["formType", "filedAt", "keySection"], properties: { formType: { type: "string" }, filedAt: { type: "string" }, keySection: { type: "string" }, }, }, }, }, }, }); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Analyze NVIDIA using its latest price, valuation metrics, most recent quarterly financial statements and earnings, institutional and insider activity, and material SEC filing sections. Return a structured company-research snapshot with reporting dates.", "dataSources": [{ "provider": "financial_datasets" }], "outputSchema": { "type": "object", "required": ["ticker", "price", "valuation", "financials", "earnings", "ownership", "filings"], "properties": { "ticker": { "type": "string" }, "price": { "type": "object", "required": ["latest", "asOf"], "properties": { "latest": { "type": "number" }, "asOf": { "type": "string" } } }, "valuation": { "type": "object", "properties": { "marketCap": { "type": "number" }, "peRatio": { "type": "number" }, "evToEbitda": { "type": "number" } } }, "financials": { "type": "object", "required": ["reportPeriod", "summary"], "properties": { "reportPeriod": { "type": "string" }, "summary": { "type": "string" } } }, "earnings": { "type": "object", "required": ["reportPeriod", "summary"], "properties": { "reportPeriod": { "type": "string" }, "summary": { "type": "string" } } }, "ownership": { "type": "object", "properties": { "institutionalHighlights": { "type": "string" }, "insiderActivity": { "type": "string" } } }, "filings": { "type": "array", "maxItems": 5, "items": { "type": "object", "required": ["formType", "filedAt", "keySection"], "properties": { "formType": { "type": "string" }, "filedAt": { "type": "string" }, "keySection": { "type": "string" } } } } } } }' ``` ## Pairs well with * [Particle](/docs/agent/connect/particle): compare published coverage with podcast commentary. * [Baselayer](/docs/agent/connect/baselayer): verify the underlying entity behind a ticker. * [Fiber.ai](/docs/agent/connect/fiber): enrich a public company with private-market peers and leadership contacts. ## Next steps The Exa Connect quickstart covers `dataSources`, pricing, and the full partner catalog. Attach up to five partners to one run and shape the query so each one fires. Create runs, stream progress, design output schemas, and control effort and cost. Create a key in the dashboard and run this page's example as-is. New accounts start with free credits. # Jinko Source: https://exa.ai/docs/agent/connect/jinko Flight and Hotel search with real-time pricing. [Jinko](https://gojinko.com) is a travel search platform providing flight and hotel search with real-time pricing. Search live flight offers for a route and date, shop hotel rooms and rates for a destination or specific properties, and explore destinations reachable from your departure airports. Attach `jinko` to an [Exa Agent](/docs/agent/quickstart) run through [Exa Connect](/docs/agent/connect/overview), and the agent queries Jinko alongside Exa web search. ## Use it for * Searching live flight offers with fares, baggage, and change policies for a route and date. * Finding hotels with live room rates for a destination, or re-shopping specific hotels. * Discovering destinations and flexible dates across date windows, cabin classes, and budgets. ## Provider ID Use this value in `dataSources`: ```text theme={null} jinko ``` ## Example Find beach destinations reachable from New York for under \$400 round-trip in March. ```python Python theme={null} from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query="Find beach destinations reachable from New York for under $400 round-trip in March.", data_sources=[{"provider": "jinko"}], output_schema={ "type": "object", "required": ["destinations"], "properties": { "destinations": { "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["city", "iataCode", "lowestFare"], "properties": { "city": {"type": "string"}, "iataCode": {"type": "string"}, "lowestFare": {"type": "number", "description": "round-trip fare in USD"}, }, }, } }, }, ) run = exa.agent.runs.poll_until_finished(run.id) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "Find beach destinations reachable from New York for under $400 round-trip in March.", dataSources: [{ provider: "jinko" }], outputSchema: { type: "object", required: ["destinations"], properties: { destinations: { type: "array", maxItems: 10, items: { type: "object", required: ["city", "iataCode", "lowestFare"], properties: { city: { type: "string" }, iataCode: { type: "string" }, lowestFare: { type: "number", description: "round-trip fare in USD" }, }, }, }, }, }, }); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Find beach destinations reachable from New York for under $400 round-trip in March.", "dataSources": [{ "provider": "jinko" }], "outputSchema": { "type": "object", "required": ["destinations"], "properties": { "destinations": { "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["city", "iataCode", "lowestFare"], "properties": { "city": { "type": "string" }, "iataCode": { "type": "string" }, "lowestFare": { "type": "number", "description": "round-trip fare in USD" } } } } } } }' ``` ## Pairs well with * [Similarweb](/docs/agent/connect/similarweb): research the travel sites and booking platforms behind a destination. * [Particle](/docs/agent/connect/particle): pull recent coverage and travel commentary for a place. ## Next steps The Exa Connect quickstart covers `dataSources`, pricing, and the full partner catalog. Attach up to five partners to one run and shape the query so each one fires. Create runs, stream progress, design output schemas, and control effort and cost. Create a key in the dashboard and run this page's example as-is. New accounts start with free credits. # Exa Connect Source: https://exa.ai/docs/agent/connect/overview Give your Exa Agent live access to premium data partners, alongside Exa web search, in a single run. Exa Connect integrates premium data partners into the Exa Agent loop. Attach a provider to a run, and the Exa Agent queries that partner's database alongside web search before combining the results into one grounded, structured answer. New to agent runs? Start with the [Exa Agent guide](/docs/agent/quickstart), then come back to attach data partners. Exa Agent already searches the full [data index](/docs/search/data/overview), the same news, code, company, and people sources as the Search API. Exa Connect adds premium partner databases on top of it. Prefer MCP? Exa Agent and [Exa Connect](/docs/agent/connect/overview) are available in [Exa MCP](/docs/get-started/exa-mcp#exa-agent). Enable `tools=agent_run` to run multi-step research, list-building, enrichment, and structured output from Claude, Cursor, and other MCP clients. ## Why Exa Connect * **Premium data without separate integrations.** Access partner data without signing contracts or wiring up an SDK. You call one Exa API. * **Exa handles the plumbing.** We manage provider authentication, tool selection, retries, and result ranking. * **The Exa Agent chooses the source.** When your `outputSchema` asks for "monthly visits from Similarweb" or "verified officers," the Exa Agent calls the matching partner tool instead of guessing from a web page. * **Index and partner data in one run.** Connect sits on top of the Exa index. The Exa Agent uses each source where it is strongest and cites the results. ## How it works 1. **Attach** one or more providers via the `dataSources` array on [`POST /agent/runs`](/docs/reference/agent-api/create-a-run). 2. The Exa Agent **selects the right tool** for each step based on your query and `outputSchema`: partner data or Exa web search. 3. Partner results are **fused with web research** into your structured output, with sources attached. ## Pricing Exa Connect pricing is additive with standard [Agent run pricing](/docs/agent/quickstart#pricing). You pay the usual Agent compute and search costs, plus the provider call charge for each Exa Connect tool call. | Provider | Price | | ---------------------------------------------------- | --------------------------------------------- | | [Fiber.ai](/docs/agent/connect/fiber#pricing) | `$0.02 / credit` | | [Similarweb](/docs/agent/connect/similarweb#pricing) | `$0.30 / credit` | | [Baselayer](/docs/agent/connect/baselayer#pricing) | `$0.10 – $4.00 / order (varies by operation)` | | [Polymarket](/docs/agent/connect/polymarket#pricing) | `Free` | | Affiliate.com | `$0.015 / call` | | Particle | `$0.015 / call` | | Financial Datasets | `$0.01 / call` | | Jinko | `$0.005 / call` | Fiber.ai bills in credits rather than per call, because its own charge varies by call: a search costs 2 credits plus 1 per result returned, a company or person lookup is billed per candidate returned (so raising a company lookup's `numResults` to disambiguate an ambiguous name costs more), and a contact reveal 2–5 credits depending on whether you ask for work email, personal email, or phone. You are charged the credits Fiber reports for each call; calls that return no match are free. See [Fiber.ai pricing](/docs/agent/connect/fiber#pricing). Similarweb bills in data credits (roughly one per metric × row × month), so a call's price follows its `numResults`/`months` — 1 to 15 credits per call. You are charged the credits Similarweb reports for each call; calls that return no data are free. See [Similarweb pricing](/docs/agent/connect/similarweb#pricing). Baselayer bills per order, and the rate depends on the operation: a KYB business search is \$1.00, a UCC lien search is \$2.00 per state searched, a litigation/bankruptcy docket search is \$1.00 per category, watchlist screening is \$0.10–\$0.25 per list requested, industry classification and website analysis are \$0.35 each, web presence is the sum of the selected analyses (\$0.15–\$0.35 each), and an international business search is \$4.00. Follow-up reads of a prior business search (business lookup, officers, registrations, officer reverse lookup) are free. See [Baselayer pricing](/docs/agent/connect/baselayer#pricing). ```python Python theme={null} from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query="Profile Anthropic: total funding and estimated monthly web traffic.", data_sources=[{"provider": "fiber"}, {"provider": "similarweb"}], output_schema={ "type": "object", "required": ["company"], "properties": { "company": { "type": "object", "required": ["name", "totalFunding", "monthlyVisits"], "properties": { "name": {"type": "string"}, "totalFunding": {"type": "string", "description": "from Fiber.ai"}, "monthlyVisits": {"type": "number", "description": "from Similarweb"}, }, } }, }, ) run = exa.agent.runs.poll_until_finished(run.id) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "Profile Anthropic: total funding and estimated monthly web traffic.", dataSources: [{ provider: "fiber" }, { provider: "similarweb" }], outputSchema: { type: "object", required: ["company"], properties: { company: { type: "object", required: ["name", "totalFunding", "monthlyVisits"], properties: { name: { type: "string" }, totalFunding: { type: "string", description: "from Fiber.ai" }, monthlyVisits: { type: "number", description: "from Similarweb" }, }, }, }, }, }); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Profile Anthropic: total funding and estimated monthly web traffic.", "dataSources": [{ "provider": "fiber" }, { "provider": "similarweb" }], "outputSchema": { "type": "object", "required": ["company"], "properties": { "company": { "type": "object", "required": ["name", "totalFunding", "monthlyVisits"], "properties": { "name": { "type": "string" }, "totalFunding": { "type": "string", "description": "from Fiber.ai" }, "monthlyVisits": { "type": "number", "description": "from Similarweb" } } } } } }' ``` ## Data partners
**GTM & recruiting.** B2B database of companies and people for lead discovery and contact research. **Web analytics.** Traffic estimates, global rankings, and competitor discovery for any domain. **Compliance & KYB.** Verify US businesses: officers, registrations, and risk signals. **Prediction markets.** Prediction market odds, price history, and trader positions from Polymarket. **Commerce.** Product catalog search with pricing, brands, and merchant links. **Media intelligence.** Search podcast transcripts with speaker attribution and timestamps. **Finance.** Prices, fundamentals, earnings, SEC filings, ownership, and stock screening for 27,000+ U.S. tickers. **Travel.** Flight and Hotel search with real-time pricing.
Need a source that isn't listed above? See our [Additional providers](/docs/agent/connect/additional-partners), which are available upon request by contacting our team. ## Usage ### Combining providers Attach as many partners as your task needs. The Exa Agent calls each one where it is strongest and blends the results with web search into a single structured answer: ```json theme={null} { "dataSources": [ { "provider": "similarweb" }, { "provider": "fiber" }, { "provider": "harmonic" } ] } ``` For a full walkthrough, including how to shape your query and `outputSchema` so every partner fires, see [Combining providers](/docs/agent/connect/combining-providers). # Particle Source: https://exa.ai/docs/agent/connect/particle Search podcast transcripts with speaker attribution and timestamps. [Particle](https://particle.news)' Podcast Intelligence indexes 100,000+ shows, fully transcribed, diarized, speaker-identified, labeled, and enriched with metadata within minutes of airing, making spoken conversations searchable. Each result is a speaker-attributed transcript window with timestamps. Attach `particle` to an [Exa Agent](/docs/agent/quickstart) run through [Exa Connect](/docs/agent/connect/overview), and the agent queries Particle alongside Exa web search. ## Use it for * Finding expert commentary and quotable soundbites. * Media and brand monitoring. * Narrative and sentiment research. * Discovering and staying up to date with podcasts. ## Provider ID Use this value in `dataSources`: ```text theme={null} particle ``` ## Example Find what podcast hosts are saying about AI regulation. ```python Python theme={null} from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query="What are prominent podcast hosts and guests saying about AI regulation in 2025?", data_sources=[{"provider": "particle"}], output_schema={ "type": "object", "required": ["mentions"], "properties": { "mentions": { "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["podcast", "episode", "speaker", "quote", "stance"], "properties": { "podcast": {"type": "string"}, "episode": {"type": "string"}, "speaker": {"type": "string"}, "quote": {"type": "string"}, "stance": {"type": "string", "description": "pro-regulation, anti-regulation, or nuanced"}, }, }, } }, }, ) run = exa.agent.runs.poll_until_finished(run.id) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "What are prominent podcast hosts and guests saying about AI regulation in 2025?", dataSources: [{ provider: "particle" }], outputSchema: { type: "object", required: ["mentions"], properties: { mentions: { type: "array", maxItems: 10, items: { type: "object", required: ["podcast", "episode", "speaker", "quote", "stance"], properties: { podcast: { type: "string" }, episode: { type: "string" }, speaker: { type: "string" }, quote: { type: "string" }, stance: { type: "string", description: "pro-regulation, anti-regulation, or nuanced" }, }, }, }, }, }, }); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "What are prominent podcast hosts and guests saying about AI regulation in 2025?", "dataSources": [{ "provider": "particle" }], "outputSchema": { "type": "object", "required": ["mentions"], "properties": { "mentions": { "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["podcast", "episode", "speaker", "quote", "stance"], "properties": { "podcast": { "type": "string" }, "episode": { "type": "string" }, "speaker": { "type": "string" }, "quote": { "type": "string" }, "stance": { "type": "string", "description": "pro-regulation, anti-regulation, or nuanced" } } } } } } }' ``` ## Pairs well with * [Financial Datasets](/docs/agent/connect/financialdatasets): cross-check podcast chatter against published news. * [Fiber.ai](/docs/agent/connect/fiber): attach company and contact context to the people being discussed. ## Next steps The Exa Connect quickstart covers `dataSources`, pricing, and the full partner catalog. Attach up to five partners to one run and shape the query so each one fires. Create runs, stream progress, design output schemas, and control effort and cost. Create a key in the dashboard and run this page's example as-is. New accounts start with free credits. # Polymarket Source: https://exa.ai/docs/agent/connect/polymarket Get prediction-market odds, price history, order books, and trader positions. [Polymarket](https://polymarket.com) is a prediction-market platform where market prices represent the crowd's implied probability of real-world outcomes. [Exa Connect](/docs/agent/connect/overview) provides read-only access to Polymarket's public market data. Attach `polymarket` to an [Exa Agent](/docs/agent/quickstart) run, and the agent queries Polymarket alongside Exa web search. ## Use it for * Finding prediction markets and current market-implied odds for a topic. * Comparing how the implied probability of an outcome changed over time. * Inspecting market liquidity, bid/ask depth, and top position holders. * Reviewing a trader's current positions and recent on-chain activity. ## Provider ID Use this value in `dataSources`: ```text theme={null} polymarket ``` ## Pricing Polymarket's read APIs are unauthenticated and free, so Polymarket tool calls cost nothing: you pay only the standard [Agent run pricing](/docs/agent/quickstart#pricing). ## Data available | Data | Description | | ------------------- | -------------------------------------------------------------------------------------------------- | | Markets and events | Current prediction markets and events, with implied-probability prices, volume, and liquidity. | | Price history | How an outcome's implied probability moved over time. | | Order books | Live bid/ask depth and spread for a market outcome. | | Holders and traders | Top position holders for a market, plus a trader's current positions and recent on-chain activity. | ## Example Get the market-implied odds of a Fed rate cut and how they moved over the past month. ```python Python theme={null} from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query=( "What are the current market-implied odds of a Fed rate cut at the " "next FOMC meeting, and how have they moved over the past month?" ), data_sources=[{"provider": "polymarket"}], output_schema={ "type": "object", "required": ["market", "currentProbability", "trend"], "properties": { "market": {"type": "string", "description": "the market question"}, "currentProbability": {"type": "number", "description": "between 0 and 1"}, "trend": {"type": "string", "description": "how the implied probability moved over the past month"}, }, }, ) run = exa.agent.runs.poll_until_finished(run.id) ``` ```typescript TypeScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "What are the current market-implied odds of a Fed rate cut at the next FOMC meeting, and how have they moved over the past month?", dataSources: [{ provider: "polymarket" }], outputSchema: { type: "object", required: ["market", "currentProbability", "trend"], properties: { market: { type: "string", description: "the market question" }, currentProbability: { type: "number", description: "between 0 and 1" }, trend: { type: "string", description: "how the implied probability moved over the past month" }, }, }, }); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "What are the current market-implied odds of a Fed rate cut at the next FOMC meeting, and how have they moved over the past month?", "dataSources": [{ "provider": "polymarket" }], "outputSchema": { "type": "object", "required": ["market", "currentProbability", "trend"], "properties": { "market": { "type": "string", "description": "the market question" }, "currentProbability": { "type": "number", "description": "between 0 and 1" }, "trend": { "type": "string", "description": "how the implied probability moved over the past month" } } } }' ``` ## Pairs well with * [Exa web search](/docs/search/quickstart): add reporting and background context to market odds. * [Particle](/docs/agent/connect/particle): pull the news coverage behind a move in the odds. * [Financial Datasets](/docs/agent/connect/financialdatasets): connect market-implied odds to prices, fundamentals, and macro data. ## Next steps The Exa Connect quickstart covers `dataSources`, pricing, and the full partner catalog. Attach up to five partners to one run and shape the query so each one fires. Create runs, stream progress, design output schemas, and control effort and cost. Create a key in the dashboard and run this page's example as-is. New accounts start with free credits. # Similarweb Source: https://exa.ai/docs/agent/connect/similarweb Get website traffic estimates, global rankings, and competitor discovery. [Similarweb](https://www.similarweb.com) is a leading source of digital market intelligence. It models the traffic and engagement of millions of websites and apps, covering estimated visits, traffic sources, audience demographics, and the competitive set around any domain. Attach `similarweb` to an [Exa Agent](/docs/agent/quickstart) run through [Exa Connect](/docs/agent/connect/overview), and the agent queries Similarweb alongside Exa web search. ## Use it for * Benchmarking a company's web traffic and engagement against its peers. * Mapping a domain's competitors and audience-overlapping sites. * Sizing markets and screening companies by digital footprint. * Enriching company and category research with real behavioral data. ## Provider ID Use this value in `dataSources`: ```text theme={null} similarweb ``` ## Pricing Similarweb bills in data credits at `$0.30 / credit`, and each call is charged the credits Similarweb reports for it. Credits scale with the data returned — roughly one credit per data point (metric × row × month) — so the price of a call is set by its parameters: | Tool | Credits | | --------------------- | -------------------------------------------------------------- | | Traffic and rank | up to 7 per month requested (1–2 months) | | Similar sites | 3 per site returned (1–5 sites) | | Traffic sources | 10 | | Top referrers | 3 per referrer returned (1–5) | | Top countries | 3 per country returned (1–5) | | Top pages | 2 per page returned (1–7) | | Top keywords | 1–10 (about 1 per 100 keyword data points; 50 keywords is \~7) | | Keyword overview | 1–2 | | Audience demographics | 8 | | Audience overlap | 2 per domain combination (2–3 domains: 6–14) | | Technologies | 10 | | Top sites by category | 1 per site returned (1–10) | Calls that return no data (an unknown or low-traffic domain, a keyword with no search volume) are free. `numResults` and `months` set how many data points you pay for, so keep them as small as the task needs. ## Example Find 10 fast-growing B2B SaaS companies and their estimated web traffic. ```python Python theme={null} from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query="Find 10 fast-growing B2B SaaS companies and their estimated web traffic.", data_sources=[{"provider": "similarweb"}], output_schema={ "type": "object", "required": ["companies"], "properties": { "companies": { "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["name", "domain", "monthlyVisits"], "properties": { "name": {"type": "string"}, "domain": {"type": "string"}, "monthlyVisits": {"type": "number", "description": "from Similarweb"}, }, }, } }, }, ) run = exa.agent.runs.poll_until_finished(run.id) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "Find 10 fast-growing B2B SaaS companies and their estimated web traffic.", dataSources: [{ provider: "similarweb" }], outputSchema: { type: "object", required: ["companies"], properties: { companies: { type: "array", maxItems: 10, items: { type: "object", required: ["name", "domain", "monthlyVisits"], properties: { name: { type: "string" }, domain: { type: "string" }, monthlyVisits: { type: "number", description: "from Similarweb" }, }, }, }, }, }, }); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Find 10 fast-growing B2B SaaS companies and their estimated web traffic.", "dataSources": [{ "provider": "similarweb" }], "outputSchema": { "type": "object", "required": ["companies"], "properties": { "companies": { "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["name", "domain", "monthlyVisits"], "properties": { "name": { "type": "string" }, "domain": { "type": "string" }, "monthlyVisits": { "type": "number", "description": "from Similarweb" } } } } } } }' ``` ## Pairs well with * [Fiber.ai](/docs/agent/connect/fiber): turn discovered competitors into enriched company records. * [Affiliate.com](/docs/agent/connect/affiliatecom): gauge a merchant's reach before recommending products from it. ## Next steps The Exa Connect quickstart covers `dataSources`, pricing, and the full partner catalog. Attach up to five partners to one run and shape the query so each one fires. Create runs, stream progress, design output schemas, and control effort and cost. Create a key in the dashboard and run this page's example as-is. New accounts start with free credits. # Examples Source: https://exa.ai/docs/agent/examples Production Exa Agent examples for list building, KYB intelligence, job postings, and structured outputs. These examples show production workflow patterns for Exa Agent, including list building, KYB intelligence, job postings, and structured outputs.
Example Agent response
output.structured.gtm\_members
A B C D E F
1 full\_name company title gtm\_function company\_verified\_as\_exa\_ai evidence\_urls
2 Marcus Holm Exa Chief Revenue Officer (CRO) Revenue / GTM leadership true 2 links
3 Karan Singh Exa VP, Revenue Operations Revenue Operations true 2 links
4 Jatin Gupta Exa GTM Engineer GTM Engineering / Growth true 3 links
5 Gavin Smith Exa Founding Strategic Account Executive Sales true 1 link
6 Chris Perpignani Exa Enterprise Growth at Exa Enterprise Growth / Sales true 2 links
7 ... plus 7 more
Example Agent response
output.structured.company\_intel
A B C D E F
1 company category signal detail source disposition
2 Ramp Business Corporation Identity Ramp Business Corporation Finance operations platform for corporate cards, expense management, AP, procurement, and reporting. ramp.com/about-us verified
3 Ramp Business Corporation Partnership Microsoft Microsoft 365, Copilot, and Teams integration highlighted in Ramp press materials. ramp.com/press ecosystem
4 Ramp Business Corporation Partnership Visa Deepened payments-network partnership for autonomous AI corporate payments. PRNewswire payments
5 Ramp Business Corporation Investment Series F \$750M financing round at a \$44B valuation. PRNewswire 2026-06-04
6 Ramp Business Corporation Risk Bank / compliance dependency Financial institution partners and business-only KYB workflows are material diligence areas. legal terms review
Example Agent response
output.structured.job\_postings
A B C D E F
1 company title function location company\_verified\_as\_exa\_ai source\_platform
2 Exa Account Executive @ Exa Sales San Francisco, CA true Ashby
3 Exa Sales Development Representative Sales Development Not specified true Ashby
4 Exa Founding Account Executive - NYC Sales New York City true Ashby
5 Exa Executive Assistant to Founders Operations San Francisco, CA true Ashby
6 Exa Software Engineer, Backend Engineering San Francisco, CA true Ashby
Example Agent response
output.structured.reports
A B C D E F
1 company domain overview buyingSignals sourceUrls verified\_domain
2 Ramp ramp.com Finance automation for cards, expenses, bill pay, and procurement. enterprise procurement 3 links true
3 Mercury mercury.com Business banking, treasury, and financial workflows for startups. startup focus banking 2 links true
Anonymized sample response
output.structured.contacts
A B C D E F
1 company contact\_label role relevanceReason evidence\_scope contact\_cost\_bound
2 LangChain Contact A Partnerships lead Owns ecosystem integrations and partner motion. role evidence max 2
3 Modal Contact B GTM lead Relevant for compute, developer, and platform partnerships. role evidence max 2
Synthetic sample response
output.structured.companies
A B C D E F
1 company domain reason sourceUrl excluded\_match status
2 Sample output; excluded input rows: Cursor and CodeRabbit
3 PatchPilot patchpilot.ai Builds agentic code repair infrastructure. funding none new
4 StackRelay stackrelay.dev Routes coding-agent tasks across tools. homepage none new
Synthetic sample response
output.structured.companies
A B C D E F
1 company website hiringPageUrl hiringEvidence location followup\_source
2 Sample output continued from a previous run: narrowed prior company results to San Francisco hiring matches
3 Northstar IDE northstar.dev /careers/platform Open platform engineer role. San Francisco previous run
4 TraceForge traceforge.dev /jobs Hiring infra engineer for agent runtime team. San Francisco previous run
## Find all GTM members at Exa.ai Use a discovery prompt when Agent should find the rows. Be explicit about company disambiguation and require current-employment evidence so similarly named companies do not leak into the result. ```python Python expandable theme={null} from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query="Find all GTM members currently working at Exa.ai. Verify each person is at the correct company: Exa, the AI search company at exa.ai, not any other company named Exa. Include the company name in every row. Include only people with public evidence that they currently work at Exa and whose role is go-to-market, such as sales, business development, partnerships, customer success, marketing, growth, or revenue. Return one row per person.", effort="high", output_schema={ "type": "object", "required": ["gtm_members"], "properties": { "gtm_members": { "type": "array", "maxItems": 100, "items": { "type": "object", "required": ["full_name", "company", "title", "gtm_function", "company_verified_as_exa_ai", "evidence_urls"], "properties": { "full_name": {"type": "string"}, "company": {"type": "string"}, "title": {"type": "string"}, "gtm_function": {"type": "string"}, "company_verified_as_exa_ai": {"type": "boolean"}, "evidence_summary": {"type": "string"}, "profile_url": {"type": "string", "format": "uri"}, "evidence_urls": { "type": "array", "items": {"type": "string", "format": "uri"}, "minItems": 1, }, }, }, } }, }, ) print(run) ``` ```javascript JavaScript expandable theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "Find all GTM members currently working at Exa.ai. Verify each person is at the correct company: Exa, the AI search company at exa.ai, not any other company named Exa. Include the company name in every row. Include only people with public evidence that they currently work at Exa and whose role is go-to-market, such as sales, business development, partnerships, customer success, marketing, growth, or revenue. Return one row per person.", effort: "high", outputSchema: { type: "object", required: ["gtm_members"], properties: { gtm_members: { type: "array", maxItems: 100, items: { type: "object", required: ["full_name", "company", "title", "gtm_function", "company_verified_as_exa_ai", "evidence_urls"], properties: { full_name: { type: "string" }, company: { type: "string" }, title: { type: "string" }, gtm_function: { type: "string" }, company_verified_as_exa_ai: { type: "boolean" }, evidence_summary: { type: "string" }, profile_url: { type: "string", format: "uri" }, evidence_urls: { type: "array", items: { type: "string", format: "uri" }, minItems: 1 } } } } } } }); console.log(run); ``` ```bash cURL expandable theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "x-api-key: $EXA_API_KEY" \ -d ' { "query": "Find all GTM members currently working at Exa.ai. Verify each person is at the correct company: Exa, the AI search company at exa.ai, not any other company named Exa. Include the company name in every row. Include only people with public evidence that they currently work at Exa and whose role is go-to-market, such as sales, business development, partnerships, customer success, marketing, growth, or revenue. Return one row per person.", "effort": "high", "outputSchema": { "type": "object", "required": ["gtm_members"], "properties": { "gtm_members": { "type": "array", "maxItems": 100, "items": { "type": "object", "required": ["full_name", "company", "title", "gtm_function", "company_verified_as_exa_ai", "evidence_urls"], "properties": { "full_name": { "type": "string" }, "company": { "type": "string" }, "title": { "type": "string" }, "gtm_function": { "type": "string" }, "company_verified_as_exa_ai": { "type": "boolean" }, "evidence_summary": { "type": "string" }, "profile_url": { "type": "string", "format": "uri" }, "evidence_urls": { "type": "array", "items": { "type": "string", "format": "uri" }, "minItems": 1 } } } } } } } ' ``` ## KYC / KYB intelligence Use Agent when a partner or vendor review needs public web intelligence, not just a single profile page. Ask for identity verification, business context, public signals, investment activity, and explicit risk notes in one structured object. ```python Python expandable theme={null} from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query="Research Ramp (ramp.com) as a potential partner for a fintech infrastructure company. Verify the company identity. Return concise KYB intelligence covering company identity, brand or ecosystem partnerships, public mentions or notable news, recent investment activity, and risk notes. Use public sources and include source URLs.", effort="medium", output_schema={ "type": "object", "required": ["company_intel"], "properties": { "company_intel": { "type": "object", "required": [ "company", "domain", "identity_verified", "business_summary", "brand_partnerships", "public_mentions", "investment_activity", "risk_notes", "source_urls", ], "properties": { "company": {"type": "string"}, "domain": {"type": "string"}, "identity_verified": {"type": "boolean"}, "business_summary": {"type": "string"}, "brand_partnerships": { "type": "array", "maxItems": 3, "items": { "type": "object", "required": ["partner", "relationship", "evidence"], "properties": { "partner": {"type": "string"}, "relationship": {"type": "string"}, "evidence": {"type": "string"}, }, }, }, "public_mentions": { "type": "array", "maxItems": 3, "items": { "type": "object", "required": ["mention", "source"], "properties": { "mention": {"type": "string"}, "date": {"type": "string"}, "source": {"type": "string", "format": "uri"}, }, }, }, "investment_activity": { "type": "array", "maxItems": 3, "items": { "type": "object", "required": ["event", "source"], "properties": { "event": {"type": "string"}, "amount_or_valuation": {"type": "string"}, "date": {"type": "string"}, "source": {"type": "string", "format": "uri"}, }, }, }, "risk_notes": { "type": "array", "maxItems": 3, "items": {"type": "string"}, }, "source_urls": { "type": "array", "maxItems": 8, "items": {"type": "string", "format": "uri"}, }, }, } }, }, ) print(run) ``` ```javascript JavaScript expandable theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "Research Ramp (ramp.com) as a potential partner for a fintech infrastructure company. Verify the company identity. Return concise KYB intelligence covering company identity, brand or ecosystem partnerships, public mentions or notable news, recent investment activity, and risk notes. Use public sources and include source URLs.", effort: "medium", outputSchema: { type: "object", required: ["company_intel"], properties: { company_intel: { type: "object", required: [ "company", "domain", "identity_verified", "business_summary", "brand_partnerships", "public_mentions", "investment_activity", "risk_notes", "source_urls" ], properties: { company: { type: "string" }, domain: { type: "string" }, identity_verified: { type: "boolean" }, business_summary: { type: "string" }, brand_partnerships: { type: "array", maxItems: 3, items: { type: "object", required: ["partner", "relationship", "evidence"], properties: { partner: { type: "string" }, relationship: { type: "string" }, evidence: { type: "string" } } } }, public_mentions: { type: "array", maxItems: 3, items: { type: "object", required: ["mention", "source"], properties: { mention: { type: "string" }, date: { type: "string" }, source: { type: "string", format: "uri" } } } }, investment_activity: { type: "array", maxItems: 3, items: { type: "object", required: ["event", "source"], properties: { event: { type: "string" }, amount_or_valuation: { type: "string" }, date: { type: "string" }, source: { type: "string", format: "uri" } } } }, risk_notes: { type: "array", maxItems: 3, items: { type: "string" } }, source_urls: { type: "array", maxItems: 8, items: { type: "string", format: "uri" } } } } } } }); console.log(run); ``` ```bash cURL expandable theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "x-api-key: $EXA_API_KEY" \ -d ' { "query": "Research Ramp (ramp.com) as a potential partner for a fintech infrastructure company. Verify the company identity. Return concise KYB intelligence covering company identity, brand or ecosystem partnerships, public mentions or notable news, recent investment activity, and risk notes. Use public sources and include source URLs.", "effort": "medium", "outputSchema": { "type": "object", "required": ["company_intel"], "properties": { "company_intel": { "type": "object", "required": [ "company", "domain", "identity_verified", "business_summary", "brand_partnerships", "public_mentions", "investment_activity", "risk_notes", "source_urls" ], "properties": { "company": { "type": "string" }, "domain": { "type": "string" }, "identity_verified": { "type": "boolean" }, "business_summary": { "type": "string" }, "brand_partnerships": { "type": "array", "maxItems": 3, "items": { "type": "object", "required": ["partner", "relationship", "evidence"], "properties": { "partner": { "type": "string" }, "relationship": { "type": "string" }, "evidence": { "type": "string" } } } }, "public_mentions": { "type": "array", "maxItems": 3, "items": { "type": "object", "required": ["mention", "source"], "properties": { "mention": { "type": "string" }, "date": { "type": "string" }, "source": { "type": "string", "format": "uri" } } } }, "investment_activity": { "type": "array", "maxItems": 3, "items": { "type": "object", "required": ["event", "source"], "properties": { "event": { "type": "string" }, "amount_or_valuation": { "type": "string" }, "date": { "type": "string" }, "source": { "type": "string", "format": "uri" } } } }, "risk_notes": { "type": "array", "maxItems": 3, "items": { "type": "string" } }, "source_urls": { "type": "array", "maxItems": 8, "items": { "type": "string", "format": "uri" } } } } } } } ' ``` ## Job postings Use Agent when open roles are spread across company pages, aggregators, and startup directories. This example constrains the source to Exa's Ashby-hosted job board and still requires company disambiguation. ```python Python expandable theme={null} from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query="Find current open job postings for Exa on Ashby. Use Ashby-hosted postings, preferably jobs.ashbyhq.com/exa, and verify each job is for Exa, the AI search company at exa.ai. Return up to 5 postings with company name, title, function, location, Ashby posting URL, and why it is relevant.", effort="medium", output_schema={ "type": "object", "required": ["job_postings"], "properties": { "job_postings": { "type": "array", "maxItems": 5, "items": { "type": "object", "required": [ "title", "company", "function", "location", "company_verified_as_exa_ai", "posting_url", "source_platform", "why_relevant", ], "properties": { "title": {"type": "string"}, "company": {"type": "string"}, "function": {"type": "string"}, "location": {"type": "string"}, "company_verified_as_exa_ai": {"type": "boolean"}, "posting_url": {"type": "string", "format": "uri"}, "source_platform": {"type": "string"}, "why_relevant": {"type": "string"}, }, }, } }, }, ) print(run) ``` ```javascript JavaScript expandable theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "Find current open job postings for Exa on Ashby. Use Ashby-hosted postings, preferably jobs.ashbyhq.com/exa, and verify each job is for Exa, the AI search company at exa.ai. Return up to 5 postings with company name, title, function, location, Ashby posting URL, and why it is relevant.", effort: "medium", outputSchema: { type: "object", required: ["job_postings"], properties: { job_postings: { type: "array", maxItems: 5, items: { type: "object", required: [ "title", "company", "function", "location", "company_verified_as_exa_ai", "posting_url", "source_platform", "why_relevant" ], properties: { title: { type: "string" }, company: { type: "string" }, function: { type: "string" }, location: { type: "string" }, company_verified_as_exa_ai: { type: "boolean" }, posting_url: { type: "string", format: "uri" }, source_platform: { type: "string" }, why_relevant: { type: "string" } } } } } } }); console.log(run); ``` ```bash cURL expandable theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "x-api-key: $EXA_API_KEY" \ -d ' { "query": "Find current open job postings for Exa on Ashby. Use Ashby-hosted postings, preferably jobs.ashbyhq.com/exa, and verify each job is for Exa, the AI search company at exa.ai. Return up to 5 postings with company name, title, function, location, Ashby posting URL, and why it is relevant.", "effort": "medium", "outputSchema": { "type": "object", "required": ["job_postings"], "properties": { "job_postings": { "type": "array", "maxItems": 5, "items": { "type": "object", "required": [ "title", "company", "function", "location", "company_verified_as_exa_ai", "posting_url", "source_platform", "why_relevant" ], "properties": { "title": { "type": "string" }, "company": { "type": "string" }, "function": { "type": "string" }, "location": { "type": "string" }, "company_verified_as_exa_ai": { "type": "boolean" }, "posting_url": { "type": "string", "format": "uri" }, "source_platform": { "type": "string" }, "why_relevant": { "type": "string" } } } } } } } ' ``` ## Enrich input rows Use `input.data` when your system already has the records and Agent should research or enrich each one. This pattern keeps row identity in structured input instead of burying it in the prompt. ```python Python expandable theme={null} from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query="For each input company, produce a concise research brief. Use recent, reputable sources. Return one report per input row.", effort="medium", input={"data": [{"company": "Ramp", "domain": "ramp.com"}, {"company": "Mercury", "domain": "mercury.com"}]}, output_schema={ "type": "object", "required": ["reports"], "properties": { "reports": { "type": "array", "maxItems": 2, "items": { "type": "object", "required": ["company", "domain", "overview", "buyingSignals", "sourceUrls", "verified_domain"], "properties": { "company": {"type": "string"}, "domain": {"type": "string"}, "overview": {"type": "string"}, "buyingSignals": { "type": "array", "maxItems": 3, "items": {"type": "string"}, }, "sourceUrls": { "type": "array", "minItems": 1, "items": {"type": "string", "format": "uri"}, }, "verified_domain": {"type": "boolean"}, }, }, } }, }, ) print(run) ``` ```javascript JavaScript expandable theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "For each input company, produce a concise research brief. Use recent, reputable sources. Return one report per input row.", effort: "medium", input: { data: [{ company: "Ramp", domain: "ramp.com" }, { company: "Mercury", domain: "mercury.com" }] }, outputSchema: { type: "object", required: ["reports"], properties: { reports: { type: "array", maxItems: 2, items: { type: "object", required: ["company", "domain", "overview", "buyingSignals", "sourceUrls", "verified_domain"], properties: { company: { type: "string" }, domain: { type: "string" }, overview: { type: "string" }, buyingSignals: { type: "array", maxItems: 3, items: { type: "string" } }, sourceUrls: { type: "array", minItems: 1, items: { type: "string", format: "uri" } }, verified_domain: { type: "boolean" } } } } } } }); console.log(run); ``` ```bash cURL expandable theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "x-api-key: $EXA_API_KEY" \ -d ' { "query": "For each input company, produce a concise research brief. Use recent, reputable sources. Return one report per input row.", "effort": "medium", "input": { "data": [ { "company": "Ramp", "domain": "ramp.com" }, { "company": "Mercury", "domain": "mercury.com" } ] }, "outputSchema": { "type": "object", "required": ["reports"], "properties": { "reports": { "type": "array", "maxItems": 2, "items": { "type": "object", "required": ["company", "domain", "overview", "buyingSignals", "sourceUrls", "verified_domain"], "properties": { "company": { "type": "string" }, "domain": { "type": "string" }, "overview": { "type": "string" }, "buyingSignals": { "type": "array", "maxItems": 3, "items": { "type": "string" } }, "sourceUrls": { "type": "array", "minItems": 1, "items": { "type": "string", "format": "uri" } }, "verified_domain": { "type": "boolean" } } } } } } } ' ``` ## Enrich rows with bounded contact fields Contact-oriented workflows can trigger separate contact enrichment charges. This anonymized docs version demonstrates the shape without publishing personal names, emails, phone numbers, or profile URLs. ```python Python expandable theme={null} from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query="For each company, find the best sales or partnerships contact. Prefer leadership or go-to-market roles. Return anonymized contact labels in examples and include evidence for why each role is relevant.", effort="high", input={"data": [{"company": "LangChain", "domain": "langchain.com"}, {"company": "Modal", "domain": "modal.com"}]}, output_schema={ "type": "object", "required": ["contacts"], "properties": { "contacts": { "type": "array", "maxItems": 2, "items": { "type": "object", "required": ["company", "contact_label", "role", "relevanceReason"], "properties": { "company": {"type": "string"}, "contact_label": {"type": "string"}, "role": {"type": "string"}, "relevanceReason": {"type": "string"}, "contact_cost_bound": {"type": "string"}, }, }, } }, }, ) print(run) ``` ```javascript JavaScript expandable theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "For each company, find the best sales or partnerships contact. Prefer leadership or go-to-market roles. Return anonymized contact labels in examples and include evidence for why each role is relevant.", effort: "high", input: { data: [{ company: "LangChain", domain: "langchain.com" }, { company: "Modal", domain: "modal.com" }] }, outputSchema: { type: "object", required: ["contacts"], properties: { contacts: { type: "array", maxItems: 2, items: { type: "object", required: ["company", "contact_label", "role", "relevanceReason"], properties: { company: { type: "string" }, contact_label: { type: "string" }, role: { type: "string" }, relevanceReason: { type: "string" }, contact_cost_bound: { type: "string" } } } } } } }); console.log(run); ``` ```bash cURL expandable theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "x-api-key: $EXA_API_KEY" \ -d ' { "query": "For each company, find the best sales or partnerships contact. Prefer leadership or go-to-market roles. Return anonymized contact labels in examples and include evidence for why each role is relevant.", "effort": "high", "input": { "data": [ { "company": "LangChain", "domain": "langchain.com" }, { "company": "Modal", "domain": "modal.com" } ] }, "outputSchema": { "type": "object", "required": ["contacts"], "properties": { "contacts": { "type": "array", "maxItems": 2, "items": { "type": "object", "required": ["company", "contact_label", "role", "relevanceReason"], "properties": { "company": { "type": "string" }, "contact_label": { "type": "string" }, "role": { "type": "string" }, "relevanceReason": { "type": "string" }, "contact_cost_bound": { "type": "string" } } } } } } } ' ``` ## Exclude known records Use `input.exclusion` when Agent should avoid records your product has already reviewed, shown, or rejected. The sheet preview uses synthetic returned company names so the exclusion pattern is visible without implying those rows came from a live production run. ```python Python expandable theme={null} from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query="Find 10 seed-stage companies building infrastructure for AI coding agents. Do not return companies in the exclusion list.", effort="auto", input={"exclusion": [{"company": "Cursor", "domain": "cursor.com"}, {"company": "CodeRabbit", "domain": "coderabbit.ai"}]}, output_schema={ "type": "object", "required": ["companies"], "properties": { "companies": { "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["company", "domain", "reason", "sourceUrl"], "properties": { "company": {"type": "string"}, "domain": {"type": "string"}, "reason": {"type": "string"}, "sourceUrl": {"type": "string", "format": "uri"}, "excluded_match": {"type": "string"}, "status": {"type": "string"}, }, }, } }, }, ) print(run) ``` ```javascript JavaScript expandable theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "Find 10 seed-stage companies building infrastructure for AI coding agents. Do not return companies in the exclusion list.", effort: "auto", input: { exclusion: [{ company: "Cursor", domain: "cursor.com" }, { company: "CodeRabbit", domain: "coderabbit.ai" }] }, outputSchema: { type: "object", required: ["companies"], properties: { companies: { type: "array", maxItems: 10, items: { type: "object", required: ["company", "domain", "reason", "sourceUrl"], properties: { company: { type: "string" }, domain: { type: "string" }, reason: { type: "string" }, sourceUrl: { type: "string", format: "uri" }, excluded_match: { type: "string" }, status: { type: "string" } } } } } } }); console.log(run); ``` ```bash cURL expandable theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "x-api-key: $EXA_API_KEY" \ -d ' { "query": "Find 10 seed-stage companies building infrastructure for AI coding agents. Do not return companies in the exclusion list.", "effort": "auto", "input": { "exclusion": [ { "company": "Cursor", "domain": "cursor.com" }, { "company": "CodeRabbit", "domain": "coderabbit.ai" } ] }, "outputSchema": { "type": "object", "required": ["companies"], "properties": { "companies": { "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["company", "domain", "reason", "sourceUrl"], "properties": { "company": { "type": "string" }, "domain": { "type": "string" }, "reason": { "type": "string" }, "sourceUrl": { "type": "string", "format": "uri" }, "excluded_match": { "type": "string" }, "status": { "type": "string" } } } } } } } ' ``` ## Continue a completed workflow Use `previousRunId` when the next request should build on a completed run's context. The sheet preview uses synthetic returned company names to illustrate the continuation pattern without exposing a real prior run. ```python Python expandable theme={null} from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query="From the companies you found, narrow the list to those hiring platform engineers in San Francisco. Add the hiring page URL for each match.", previous_run_id="agent_run_01j...", output_schema={ "type": "object", "required": ["companies"], "properties": { "companies": { "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["company", "website", "hiringPageUrl"], "properties": { "company": {"type": "string"}, "website": {"type": "string", "format": "uri"}, "hiringPageUrl": {"type": "string", "format": "uri"}, "hiringEvidence": {"type": "string"}, "location": {"type": "string"}, "followup_source": {"type": "string"}, }, }, } }, }, ) print(run) ``` ```javascript JavaScript expandable theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ previousRunId: "agent_run_01j...", query: "From the companies you found, narrow the list to those hiring platform engineers in San Francisco. Add the hiring page URL for each match.", outputSchema: { type: "object", required: ["companies"], properties: { companies: { type: "array", maxItems: 10, items: { type: "object", required: ["company", "website", "hiringPageUrl"], properties: { company: { type: "string" }, website: { type: "string", format: "uri" }, hiringPageUrl: { type: "string", format: "uri" }, hiringEvidence: { type: "string" }, location: { type: "string" }, followup_source: { type: "string" } } } } } } }); console.log(run); ``` ```bash cURL expandable theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "x-api-key: $EXA_API_KEY" \ -d '{ "previousRunId": "agent_run_01j...", "query": "From the companies you found, narrow the list to those hiring platform engineers in San Francisco. Add the hiring page URL for each match.", "outputSchema": { "type": "object", "required": ["companies"], "properties": { "companies": { "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["company", "website", "hiringPageUrl"], "properties": { "company": { "type": "string" }, "website": { "type": "string", "format": "uri" }, "hiringPageUrl": { "type": "string", "format": "uri" }, "hiringEvidence": { "type": "string" }, "location": { "type": "string" }, "followup_source": { "type": "string" } } } } } } }' ``` ## Broad list building with Agent Max Use `effort: "max"` for work where completeness and thoroughness matter more than latency or cost, including large list building, deep multi-source research, and criteria that are hard to verify. Agent Max is in public beta: send `Exa-Beta: agent-max-effort-2026-07-27` with the request. The header accepts a comma-separated list of beta tokens. ```python Python expandable theme={null} from exa_py import Exa exa = Exa() run = exa.beta.agent.runs.create( query="Find all companies building browser automation tools in the United States.", effort="max", budget={"maxCostDollars": 10}, betas=["agent-max-effort-2026-07-27"], ) print(run) ``` ```javascript JavaScript expandable theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.beta.agent.runs.create({ query: "Find all companies building browser automation tools in the United States.", effort: "max", budget: { maxCostDollars: 10 }, betas: ["agent-max-effort-2026-07-27"] }); console.log(run); ``` ```bash cURL expandable theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "x-api-key: $EXA_API_KEY" \ -H "Exa-Beta: agent-max-effort-2026-07-27" \ -d '{ "query": "Find all companies building browser automation tools in the United States.", "effort": "max", "budget": { "maxCostDollars": 10 } }' ``` ## Attach a data partner with Exa Connect Use `dataSources` to let Exa Agent pull from a premium data partner during a run. Here it enriches each company with traffic from Similarweb and funding from Harmonic, blended with web research. See [Exa Connect](/docs/agent/connect/overview) for the full list of partners. ```python Python expandable theme={null} from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query="For each input company, report estimated monthly visits and total funding raised. Use the attached data partners.", effort="medium", data_sources=[ {"provider": "similarweb"}, {"provider": "harmonic"}, ], input={ "data": [ {"company": "Ramp", "domain": "ramp.com"}, {"company": "Mercury", "domain": "mercury.com"}, ] }, output_schema={ "type": "object", "required": ["companies"], "properties": { "companies": { "type": "array", "maxItems": 2, "items": { "type": "object", "required": ["company", "domain", "monthlyVisits", "totalFunding"], "properties": { "company": {"type": "string"}, "domain": {"type": "string"}, "monthlyVisits": {"type": "number", "description": "from Similarweb"}, "totalFunding": {"type": "string", "description": "from Harmonic"}, }, }, } }, }, ) print(run) ``` ```javascript JavaScript expandable theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "For each input company, report estimated monthly visits and total funding raised. Use the attached data partners.", effort: "medium", dataSources: [{ provider: "similarweb" }, { provider: "harmonic" }], input: { data: [ { company: "Ramp", domain: "ramp.com" }, { company: "Mercury", domain: "mercury.com" } ] }, outputSchema: { type: "object", required: ["companies"], properties: { companies: { type: "array", maxItems: 2, items: { type: "object", required: ["company", "domain", "monthlyVisits", "totalFunding"], properties: { company: { type: "string" }, domain: { type: "string" }, monthlyVisits: { type: "number", description: "from Similarweb" }, totalFunding: { type: "string", description: "from Harmonic" } } } } } } }); console.log(run); ``` ```bash cURL expandable theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "x-api-key: $EXA_API_KEY" \ -d '{ "query": "For each input company, report estimated monthly visits and total funding raised. Use the attached data partners.", "effort": "medium", "dataSources": [ { "provider": "similarweb" }, { "provider": "harmonic" } ], "input": { "data": [ { "company": "Ramp", "domain": "ramp.com" }, { "company": "Mercury", "domain": "mercury.com" } ] }, "outputSchema": { "type": "object", "required": ["companies"], "properties": { "companies": { "type": "array", "maxItems": 2, "items": { "type": "object", "required": ["company", "domain", "monthlyVisits", "totalFunding"], "properties": { "company": { "type": "string" }, "domain": { "type": "string" }, "monthlyVisits": { "type": "number", "description": "from Similarweb" }, "totalFunding": { "type": "string", "description": "from Harmonic" } } } } } } }' ``` ## Production checklist * Give Agent a specific `query` that names the unit of work and the desired source quality. * Use `input.data` for known records instead of embedding rows in the prompt. * Use `input.exclusion` for records that should not be returned again. * Add `outputSchema` whenever downstream code consumes the result. * Use `maxItems` on arrays when you need predictable scope and cost. * Store the returned run `id` so you can poll, replay events, inspect costs, or continue from the run later. # Exa Agent Source: https://exa.ai/docs/agent/quickstart Run deep research, list-building, and enrichment workflows that return structured outputs. Exa Agent is an async, usage-based endpoint for high-compute tasks like list building, enrichment, and deep research. It handles complex reasoning and can return many structured output fields. Think of it as a context agent: you describe the data you want and the shape it should come back in, and Exa Agent orchestrates the tool calls needed to get there. A single run can fan out many searches across different angles, read and condense the pages behind them, split list building into subtasks that run in parallel, verify each candidate against your criteria, enrich contacts, and query any [Exa Connect](/docs/agent/connect/overview) data partners you attach. You get the assembled context back as one grounded, structured result instead of orchestrating each `/search` and `/contents` call yourself. Each run can return a natural-language answer, schema-validated JSON, field-level grounding, metadata, and a cost breakdown. You can retrieve completed runs later, list past runs, replay events, or continue from a previous run. Prefer MCP? Exa Agent and [Exa Connect](/docs/agent/connect/overview) are available in [Exa MCP](/docs/get-started/exa-mcp#exa-agent). Enable `tools=agent_run` to run multi-step research, list-building, enrichment, and structured output from Claude, Cursor, and other MCP clients. ## When to use Exa Agent Use Exa Agent when a workflow needs more than a single search or extraction call, or when you would otherwise write your own loop of searches, page reads, and verification steps to assemble the data: * Build lists from open-ended criteria, then enrich each result * Research entities across many fields with citations * Run multi-hop tasks like "find companies, then find their decision makers" * Produce structured JSON from a long-running web research task * Combine web research with premium data partners in one grounded answer * Continue from a previous run with a follow-up request like "find 10 more results" Exa Agent is higher-latency and async by design. For a single low-latency search where you orchestrate the calls yourself, start with the [Search API](/docs/search/quickstart). ## Quickstart This example starts a run that builds a structured list of people matching your criteria. It returns JSON in `output.structured`. ### 1. Install the Exa SDK ```bash Python theme={null} pip install exa-py ``` ```bash JavaScript theme={null} npm install exa-js ``` ### 2. Set your API key ```bash theme={null} export EXA_API_KEY="your-api-key" ``` ```powershell theme={null} setx EXA_API_KEY "your-api-key" ``` ### 3. Create a run ```python Python theme={null} import json from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query="Find engineering leaders at AI infrastructure companies that raised a Series A or B in the last 6 months.", output_schema={ "type": "object", "properties": { "people": { "type": "array", "maxItems": 10, "items": { "type": "object", "properties": { "name": {"type": "string"}, "job_title": {"type": "string"}, "linkedin_url": {"type": "string", "format": "uri"}, }, "required": ["name", "job_title", "linkedin_url"], }, } }, "required": ["people"], }, effort="auto", ) print(json.dumps(run.model_dump(), indent=2)) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "Find engineering leaders at AI infrastructure companies that raised a Series A or B in the last 6 months.", outputSchema: { type: "object", properties: { people: { type: "array", maxItems: 10, items: { type: "object", properties: { name: { type: "string" }, job_title: { type: "string" }, linkedin_url: { type: "string", format: "uri" } }, required: ["name", "job_title", "linkedin_url"] } } }, required: ["people"] }, effort: "auto" }); console.log(JSON.stringify(run, null, 2)); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Find engineering leaders at AI infrastructure companies that raised a Series A or B in the last 6 months.", "effort": "auto", "outputSchema": { "type": "object", "properties": { "people": { "type": "array", "maxItems": 10, "items": { "type": "object", "properties": { "name": { "type": "string" }, "job_title": { "type": "string" }, "linkedin_url": { "type": "string", "format": "uri" } }, "required": ["name", "job_title", "linkedin_url"] } } }, "required": ["people"] } }' ``` Add `Accept: text/event-stream` when creating a run to receive server-sent events as the run is queued, started, and completed. See [Stream events](#stream-events) for more details. ### 4. Poll for completion If you do not stream events, save the returned `id` and poll the run until it reaches a terminal status. ```python Python theme={null} import json from exa_py import Exa exa = Exa() run_id = "agent_run_01j..." run = exa.agent.runs.poll_until_finished( run_id, poll_interval=4000, ) print(json.dumps(run.model_dump(), indent=2)) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const runId = "agent_run_01j..."; const run = await exa.agent.runs.pollUntilFinished(runId, { pollInterval: 4000 }); console.log(JSON.stringify(run, null, 2)); ``` ```bash cURL theme={null} RUN_ID="agent_run_01j..." while true; do RUN_JSON="$(curl -s "https://api.exa.ai/agent/runs/$RUN_ID" \ -H "Authorization: Bearer $EXA_API_KEY")" STATUS="$(echo "$RUN_JSON" | python3 -c 'import json,sys; print(json.load(sys.stdin)["status"])')" echo "status=$STATUS" if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ] || [ "$STATUS" = "cancelled" ]; then echo "$RUN_JSON" break fi sleep 4 done ``` Completed runs include: * `output.text`: a natural-language answer * `output.structured`: validated JSON when you provide `outputSchema` * `output.grounding`: citations for text or structured fields, when emitted * `costDollars`: the run's cost breakdown Exa Agent is also available through the OpenAI-compatible Responses API. Point the OpenAI SDK at `https://api.exa.ai`, use `model: "exa-agent"`, and choose synchronous, streaming, or background execution. See [OpenAI SDK compatibility](/docs/integrations/openai-sdk#agent-via-responses-api). ## Verify and enrich a specific entity Beyond list building, use Exa Agent to inspect a single known entity, verify a claim against authoritative sources, and return structured enrichment. This example checks whether a company's official website has a publicly accessible pricing page, and enriches the result with pricing details when they are available. The schema requires only `domain` and `verdict`; everything else is optional enrichment. ```python Python theme={null} import json from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query="Inspect the official website redbarnrobotics.com and determine whether it has a publicly accessible pricing or plans page. A dedicated pricing page counts as present even if it only says 'Contact sales'.", system_prompt="Judge only the company specified in the query. Use present only when a public pricing or plans page is found. Use absent only after successfully inspecting the website and finding no such page. If the website is unreachable, blocked, fails to render, or cannot be inspected reliably, use cannot_verify. Never use absent when inspection failed. Use only the company's official website as evidence.", effort="low", output_schema={ "type": "object", "additionalProperties": False, "required": ["domain", "verdict"], "properties": { "domain": {"type": "string", "const": "redbarnrobotics.com"}, "verdict": { "type": "string", "enum": ["present", "absent", "cannot_verify"], }, "pricing_page_url": {"type": ["string", "null"], "format": "uri"}, "displays_numeric_prices": {"type": ["boolean", "null"]}, "pricing_model": { "type": ["string", "null"], "enum": [ "free", "subscription", "usage_based", "one_time", "custom_quote", "mixed", "other", None, ], }, "starting_price": {"type": ["number", "null"], "minimum": 0}, "currency": { "type": ["string", "null"], "description": "ISO 4217 code such as USD or EUR.", }, "billing_period": { "type": ["string", "null"], "enum": [ "monthly", "annual", "one_time", "usage_based", "variable", "other", None, ], }, "has_free_plan": {"type": ["boolean", "null"]}, "has_free_trial": {"type": ["boolean", "null"]}, "reasoning": {"type": ["string", "null"], "maxLength": 300}, }, }, ) run = exa.agent.runs.poll_until_finished(run.id) print(json.dumps(run.output.structured if run.output else None, indent=2)) ``` ```typescript TypeScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "Inspect the official website redbarnrobotics.com and determine whether it has a publicly accessible pricing or plans page. A dedicated pricing page counts as present even if it only says 'Contact sales'.", systemPrompt: "Judge only the company specified in the query. Use present only when a public pricing or plans page is found. Use absent only after successfully inspecting the website and finding no such page. If the website is unreachable, blocked, fails to render, or cannot be inspected reliably, use cannot_verify. Never use absent when inspection failed. Use only the company's official website as evidence.", effort: "low", outputSchema: { type: "object", additionalProperties: false, required: ["domain", "verdict"], properties: { domain: { type: "string", const: "redbarnrobotics.com" }, verdict: { type: "string", enum: ["present", "absent", "cannot_verify"] }, pricing_page_url: { type: ["string", "null"], format: "uri" }, displays_numeric_prices: { type: ["boolean", "null"] }, pricing_model: { type: ["string", "null"], enum: [ "free", "subscription", "usage_based", "one_time", "custom_quote", "mixed", "other", null ] }, starting_price: { type: ["number", "null"], minimum: 0 }, currency: { type: ["string", "null"], description: "ISO 4217 code such as USD or EUR." }, billing_period: { type: ["string", "null"], enum: [ "monthly", "annual", "one_time", "usage_based", "variable", "other", null ] }, has_free_plan: { type: ["boolean", "null"] }, has_free_trial: { type: ["boolean", "null"] }, reasoning: { type: ["string", "null"], maxLength: 300 } } } }); const completedRun = await exa.agent.runs.pollUntilFinished(run.id); console.log(JSON.stringify(completedRun.output?.structured, null, 2)); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Inspect the official website redbarnrobotics.com and determine whether it has a publicly accessible pricing or plans page. A dedicated pricing page counts as present even if it only says '"'"'Contact sales'"'"'.", "systemPrompt": "Judge only the company specified in the query. Use present only when a public pricing or plans page is found. Use absent only after successfully inspecting the website and finding no such page. If the website is unreachable, blocked, fails to render, or cannot be inspected reliably, use cannot_verify. Never use absent when inspection failed. Use only the company'"'"'s official website as evidence.", "effort": "low", "outputSchema": { "type": "object", "additionalProperties": false, "required": ["domain", "verdict"], "properties": { "domain": { "type": "string", "const": "redbarnrobotics.com" }, "verdict": { "type": "string", "enum": ["present", "absent", "cannot_verify"] }, "pricing_page_url": { "type": ["string", "null"], "format": "uri" }, "displays_numeric_prices": { "type": ["boolean", "null"] }, "pricing_model": { "type": ["string", "null"], "enum": ["free", "subscription", "usage_based", "one_time", "custom_quote", "mixed", "other", null] }, "starting_price": { "type": ["number", "null"], "minimum": 0 }, "currency": { "type": ["string", "null"], "description": "ISO 4217 code such as USD or EUR." }, "billing_period": { "type": ["string", "null"], "enum": ["monthly", "annual", "one_time", "usage_based", "variable", "other", null] }, "has_free_plan": { "type": ["boolean", "null"] }, "has_free_trial": { "type": ["boolean", "null"] }, "reasoning": { "type": ["string", "null"], "maxLength": 300 } } } }' ``` Schemas for verification workflows should account for uncertainty. Make fields that may not be verifiable nullable and leave them out of `required`, so the agent can return `null` instead of fabricating a value. The `verdict` enum distinguishes a failed inspection (`cannot_verify`) from actual negative evidence (`absent`): a site that could not be reached is not evidence that the page does not exist. ## Stream events Streaming keeps the create request open and sends Server-Sent Events (SSE) until the run completes. See [Event format](#event-format) for the event types and payloads. Set `stream=True` in Python, `stream: true` in JavaScript, or send `Accept: text/event-stream` over HTTP: ```python Python theme={null} from exa_py import Exa exa = Exa() events = exa.agent.runs.create( query="Find five recently launched developer tools for evaluating AI agents.", stream=True, ) for event in events: print(event.event, event.data) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const events = await exa.agent.runs.create({ query: "Find five recently launched developer tools for evaluating AI agents.", stream: true }); for await (const event of events) { console.log(event.event, event.data); } ``` ```bash cURL theme={null} curl -N -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "Accept: text/event-stream" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Find five recently launched developer tools for evaluating AI agents." }' ``` ### Event format Each SSE frame contains an event ID, event name, and JSON payload: ```text theme={null} id: 1 event: agent_run.created data: {"id":"agent_run_01j...","status":"queued","createdAt":"2026-05-07T21:21:52.051Z"} ``` The stream may also contain comment lines such as `: keep-alive`. SSE clients ignore comments automatically; custom parsers should do the same. ### Event types | Event | `data` payload | How to use it | | --------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `agent_run.created` | `{ id, status: "queued", createdAt }` | Save the run ID as soon as the request is accepted. | | `agent_run.started` | `{ id, status: "running" }` | Mark the run as actively processing. | | `agent_run.completed` | The completed Agent run object | Read the final answer from `data.output.text` or `data.output.structured`, and citations from `data.output.grounding`. | | `agent_run.failed` | `{ id, status: "failed", error }` | Surface `error.code` and `error.message`; no completed output is available. | | `agent_run.cancelled` | `{ id, status: "cancelled", ... }` | Stop consuming the stream and handle the run as cancelled. | Events associated with the same research step include a `callId`. It corresponds to `item.call_id` in tool progress events. Use it to group search traces, sources, and tool progress. Some search-trace descriptions are generated asynchronously and can arrive after the source or tool event they describe, so do not correlate them by arrival order alone. Treat `agent_run.source.added` as a live preview rather than a complete citation list. The terminal run's `output.grounding` is the authoritative grounding output. ### Replay stored events For non-ZDR runs, [`GET /agent/runs/{id}/events`](/docs/reference/agent-api/list-run-events) returns stored events as paginated JSON. Send `Accept: text/event-stream` to replay the stored events as SSE, and `Last-Event-ID` to skip events your client has already processed: ```bash cURL theme={null} curl -N "https://api.exa.ai/agent/runs/agent_run_01j.../events" \ -H "Accept: text/event-stream" \ -H "Last-Event-ID: 12" \ -H "Authorization: Bearer $EXA_API_KEY" ``` The replay endpoint sends the events stored at request time and then closes; it does not continue following a running run. ZDR runs do not retain events and cannot be replayed. For forward compatibility, ignore event names your application does not recognize and continue until a terminal event arrives. ## Return structured JSON Use `outputSchema` to return schema-validated JSON in `output.structured`. `outputSchema` supports the [JSON Schema specification](https://json-schema.org/). To request contact information, describe the desired contact fields in `outputSchema`. Use standard JSON Schema shapes such as `{ "type": "string", "format": "email" }` for email addresses, `{ "type": "string", "format": "phone" }` for phone numbers, and `{ "type": "string", "format": "uri" }` for URLs. Bound list sizes with `maxItems` when possible so the maximum contact-enrichment cost is predictable. ```python Python theme={null} import json from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query="Find AI infrastructure companies that raised a Series A or B in the last 6 months.", effort="auto", output_schema={ "type": "object", "properties": { "companies": { "type": "array", "items": { "type": "object", "properties": { "name": {"type": "string"}, "round": {"type": "string"}, "website": {"type": "string"}, }, "required": ["name", "round"], }, } }, "required": ["companies"], }, ) run = exa.agent.runs.poll_until_finished( run.id, ) print(json.dumps(run.output.structured if run.output else None, indent=2)) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "Find AI infrastructure companies that raised a Series A or B in the last 6 months.", effort: "auto", outputSchema: { type: "object", properties: { companies: { type: "array", items: { type: "object", properties: { name: { type: "string" }, round: { type: "string" }, website: { type: "string" } }, required: ["name", "round"] } } }, required: ["companies"] } }); const completedRun = await exa.agent.runs.pollUntilFinished(run.id); console.log(JSON.stringify(completedRun.output?.structured, null, 2)); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Find AI infrastructure companies that raised a Series A or B in the last 6 months.", "effort": "auto", "outputSchema": { "type": "object", "properties": { "companies": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "round": { "type": "string" }, "website": { "type": "string" } }, "required": ["name", "round"] } } }, "required": ["companies"] } }' ``` ## Process input rows Use `input.data` when you have an existing set of data that you want to enrich. You can add more fields to each data entity, surface more entities based on the data you bring in, or both. For complete row-enrichment examples, see [Agent examples](/docs/agent/examples#enrich-input-rows-code). ## Process exclusions Use `input.exclusion` to exclude certain entries from being surfaced in the run. In the example below, we want to look for the top 10 cutest animals, but we exclude goats and pandas from the run because we already know how cute they are. ```python Python theme={null} import json from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query="Find the top 10 cutest animals. Return each animal's common name and a source URL.", input={ "exclusion": [ {"animal": "goat"}, {"animal": "panda"}, ] }, ) print(json.dumps(run.model_dump(), indent=2)) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "Find the top 10 cutest animals. Return each animal's common name and a source URL.", input: { exclusion: [ { animal: "goat" }, { animal: "panda" } ] } }); console.log(JSON.stringify(run, null, 2)); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Find the top 10 cutest animals. Return each animal'"'"'s common name and a source URL.", "input": { "exclusion": [ { "animal": "goat" }, { "animal": "panda" } ] } }' ``` ## Connect data sources The index is already available on every run. Use `dataSources` only to attach [Exa Connect](/docs/agent/connect/overview) partners. Each entry selects a `provider`. When a property in your `outputSchema` references a specific source (e.g., "from Similarweb"), Exa Agent calls the matching provider tool instead of guessing from a web page. ```json theme={null} { "dataSources": [ { "provider": "similarweb" }, { "provider": "fiber" } ] } ``` See [Exa Connect](/docs/agent/connect/overview) for the full list of data partners, with examples for each. ## Continue from a previous run Use `previousRunId` to ask follow-ups to a previous response. Each follow-up starts a new run with its own ID. `previousRunId` carries context into the new run; it is not reused as the new run's ID. ```python Python theme={null} import json from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query="Narrow that list to companies hiring in San Francisco.", previous_run_id="agent_run_01j...", ) print(json.dumps(run.model_dump(), indent=2)) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "Narrow that list to companies hiring in San Francisco.", previousRunId: "agent_run_01j..." }); console.log(JSON.stringify(run, null, 2)); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Narrow that list to companies hiring in San Francisco.", "previousRunId": "agent_run_01j..." }' ``` ## Find a run ID List recent runs and inspect their statuses: ```python Python theme={null} from exa_py import Exa exa = Exa() runs = exa.agent.runs.list( limit=10, ) for run in runs.data: query = (run.request or {}).get("query", "") print(f"{run.id}\t{run.status}\t{run.created_at}\t{query}") ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const list = await exa.agent.runs.list({ limit: 10 }); for (const run of list.data) { const query = run.request?.query ?? ""; console.log(`${run.id}\t${run.status}\t${run.createdAt}\t${query}`); } ``` ```bash cURL theme={null} curl -s "https://api.exa.ai/agent/runs?limit=10" \ -H "Authorization: Bearer $EXA_API_KEY" ``` ## Pricing Costs are usage-based and priced by component: | Component | Price | | ------------------- | ----------------- | | Agent Compute Units | `1 ACU = $0.10` | | Search tool calls | `$0.005 / search` | Contact enrichment is separate from the core pricing components above: email contact enrichment is `$0.02 / email`, and phone number contact enrichment is `$0.07 / phone number`. `usage.agentComputeUnits` measures model computation across the full run. Complex queries, especially ones with a large `input.data` field, need more reasoning steps and tool calls and consume more ACUs. See [Agent limits](/docs/admin/billing#agent-limits) for concurrency and rate limits. ### Effort Use `effort` to choose a cost and reasoning level for each run. The supported values are `minimal`, `low`, `medium`, `high`, `xhigh`, `auto`, and `max`; the default is `auto`. Fixed efforts have a predictable per-request price, while `auto` and beta `max` are metered by usage: | Effort | Price | | --------- | ---------------------------------------------- | | `minimal` | `$0.012 / request` | | `low` | `$0.025 / request` | | `medium` | `$0.10 / request` | | `high` | `$0.50 / request` | | `xhigh` | `$1.00 / request` | | `auto` | Metered; up to the default `$5` cap | | `max` | **Beta**, metered; up to the default `$20` cap | Agent Max is the highest-effort tier for work where completeness and thoroughness matter more than latency or cost, including large list building, deep multi-source research, and criteria that are hard to verify. It is in public beta: requests with `effort: "max"` must include `Exa-Beta: agent-max-effort-2026-07-27`. The header accepts a comma-separated list of beta tokens. `budget.maxCostDollars` is an optional per-run ceiling for `auto` and `max`. It accepts `$1`–`$100`; the shipped maximum is `$100`, though the server may configure a lower maximum. The default cap is `$5` for `auto` and `$20` for `max`. This is a ceiling rather than a fixed price: runs that finish early cost less. Budget is not accepted for fixed efforts. ### Choosing an effort mode Fixed effort modes work well when you want predictable per-request pricing for standard research. Use `auto` for variable-scope work like list building, where the number of entities can vary from request to request. | Effort | Best for | Suggested schema complexity | Runtime expectation | | --------- | ------------------------------------------------------------------ | -------------------------------------------------------------- | -------------------------- | | `minimal` | Lowest-cost lookups, very narrow factual tasks, short answers | One or two fields, shallow schema | Cheapest, least exhaustive | | `low` | Simple lookups, narrow factual tasks, short answers | A few fields, shallow schema | Fast, light research | | `medium` | Default starting point for most standard research tasks | Moderate field count, simple nested objects | Balanced quality/runtime | | `high` | Harder research, more citations, stricter completeness | Larger schemas or more nuanced fields | Slower, more thorough | | `xhigh` | High-value tasks where completeness matters more than cost/latency | Complex schemas, many fields, difficult verification | Slowest fixed effort | | `auto` | Variable-scope work, list building, unknown task difficulty | Flexible; useful when entity count or work required is unknown | Variable | | `max` | Highest-effort research (beta) | Complex schemas, many fields, difficult verification | Longest-running | Start with `medium` for standard single-entity research. Drop to `low` or `minimal` when cost and latency matter more than completeness. Move up to `high` or `xhigh` when the output schema is larger, fields need verification, or the task needs deeper reasoning. Use `auto` when you don't know the scope ahead of time, such as list building or workflows that may return many entities. Runtime varies by query difficulty, schema complexity, and external source availability. Treat effort modes as quality/cost/runtime tradeoffs rather than strict latency guarantees. ### Run with max effort ```python Python theme={null} from exa_py import Exa exa = Exa() run = exa.beta.agent.runs.create( query="Find all companies building browser automation tools in the United States.", effort="max", budget={"maxCostDollars": 10}, betas=["agent-max-effort-2026-07-27"], ) print(run) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.beta.agent.runs.create({ query: "Find all companies building browser automation tools in the United States.", effort: "max", budget: { maxCostDollars: 10 }, betas: ["agent-max-effort-2026-07-27"] }); console.log(run); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -H "Exa-Beta: agent-max-effort-2026-07-27" \ -d '{ "query": "Find all companies building browser automation tools in the United States.", "effort": "max", "budget": { "maxCostDollars": 10 } }' ``` The SDK samples require an `exa-py` or `exa-js` version with Agent Max support. ## Zero Data Retention Exa Agent supports [Zero Data Retention](/docs/admin/security/zero-data-retention) (ZDR). ZDR is enabled per team. [Contact us](mailto:sales@exa.ai) to enable it for your account. When ZDR is enabled for your team: * Create runs with streaming (`Accept: text/event-stream`) to consume output live, or poll asynchronous runs within the retention window. * Run data is available while the run executes and for up to 10 minutes after it reaches a terminal state. After that window, the run cannot be retrieved. * `previousRunId` is not available. * Exa Connect `dataSources` are not available; requests that include them return a `400` error. ## Next steps Explore news, code, company, and people sources across the public web. Attach premium partner databases to a run. Best practices for using Exa Agent. Examples of using Exa Agent. # Batch API Source: https://exa.ai/docs/batch/quickstart Run Exa API requests asynchronously in batches. The Batch API is available for Enterprise customers after Exa enables it for your team. Contact [sales@exa.ai](mailto:sales@exa.ai) to discuss Enterprise access and enablement. The Batch API lets you submit many Exa API requests at once and retrieve their results later as a JSONL file. Instead of sending thousands of individual requests and managing rate limits and retries yourself, you send a single batch, poll its status, and download all the results in one file. Use it for offline enrichment, backfills, or any other job that does not need an immediate response. Full request and response schemas are in the [API reference](/docs/reference/batches/create-a-batch). The Batch API is in beta. Include the `Exa-Beta: batches-2026-06-06` header on every request. ## Supported requests Each batch item must be a `POST` request to one of these routes: | Route | Use case | | ------------- | -------------------------------------- | | `/search` | Run Exa search requests asynchronously | | `/agent/runs` | Run Exa Agent requests asynchronously | Each item needs a batch-unique `customId`. The same `customId` is returned in the results file so you can map output rows back to your input data. ## Create a batch ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/batches" \ -H "Authorization: Bearer $EXA_API_KEY" \ -H "Exa-Beta: batches-2026-06-06" \ -H "Content-Type: application/json" \ -d '{ "requests": [ { "customId": "row-1", "method": "POST", "url": "/search", "body": { "query": "Latest AI infrastructure funding rounds" } }, { "customId": "row-2", "method": "POST", "url": "/agent/runs", "body": { "query": "Summarize recent vector database launches" } } ], "metadata": { "project": "weekly-digest" } }' ``` The response contains the batch ID and initial status: ```json theme={null} { "id": "batch_01j7x9v0m2n4p6q8r0s2t4v6w8", "object": "batch", "status": "in_progress", "requestCounts": { "total": 2, "completed": 0, "failed": 0 }, "createdAt": "2026-06-06T12:00:00.000Z", "expiresAt": null, "endedAt": null, "resultsUrl": null, "metadata": { "project": "weekly-digest" } } ``` ## Check status Poll the batch until it reaches a terminal status: ```bash cURL theme={null} curl -s "https://api.exa.ai/batches/batch_01j7x9v0m2n4p6q8r0s2t4v6w8" \ -H "Authorization: Bearer $EXA_API_KEY" \ -H "Exa-Beta: batches-2026-06-06" ``` Batch statuses are: | Status | Meaning | | ------------- | --------------------------------------------------------- | | `in_progress` | The batch is running | | `completed` | All requests have finished and results are available | | `cancelling` | Cancellation was requested and in-flight work is draining | | `cancelled` | The batch was cancelled | | `expired` | Results are no longer available | When the batch completes, `resultsUrl` contains a download URL for the JSONL results file, and `expiresAt` is set to the end of the results retention window. `resultsUrl` is a short-lived presigned URL. Re-fetch the batch to get a fresh URL whenever you need to download results again. ## List batches ```bash cURL theme={null} curl -s "https://api.exa.ai/batches?limit=100" \ -H "Authorization: Bearer $EXA_API_KEY" \ -H "Exa-Beta: batches-2026-06-06" ``` The response is cursor-paginated: `data` holds up to `limit` batches, and when `hasMore` is `true`, pass `nextCursor` as the `cursor` query parameter to fetch the next page. Pass `status=completed` to list only completed batches: ```bash theme={null} curl -s "https://api.exa.ai/batches?status=completed" \ -H "Authorization: Bearer $EXA_API_KEY" \ -H "Exa-Beta: batches-2026-06-06" ``` `completed` is the only supported value; any other value returns an error. Completed listings are ordered by expiry and use their own cursor, so keep sending `status=completed` on every page — completed and unfiltered cursors are not interchangeable. ```json theme={null} { "object": "list", "data": [], "hasMore": false, "nextCursor": null } ``` ## Download results ```bash cURL theme={null} curl "$RESULTS_URL" -o results.jsonl ``` Each JSONL line contains the original `customId` and either a `response` or an `error`: ```json theme={null} { "customId": "row-1", "response": { "statusCode": 200, "body": { "results": [] } } } { "customId": "row-2", "error": { "code": "API_ERROR", "message": "request failed" } } ``` ## Cancel a batch ```bash cURL theme={null} curl -X POST "https://api.exa.ai/batches/batch_01j7x9v0m2n4p6q8r0s2t4v6w8/cancel" \ -H "Authorization: Bearer $EXA_API_KEY" \ -H "Exa-Beta: batches-2026-06-06" ``` ## Delete a batch ```bash cURL theme={null} curl -X DELETE "https://api.exa.ai/batches/batch_01j7x9v0m2n4p6q8r0s2t4v6w8" \ -H "Authorization: Bearer $EXA_API_KEY" \ -H "Exa-Beta: batches-2026-06-06" ``` ## Access To enable the Batch API for a team, contact [sales@exa.ai](mailto:sales@exa.ai). # Changelog Source: https://exa.ai/docs/changelog Product updates and announcements from Exa. ## Dynamic Highlights (research preview) Dynamic Highlights selects excerpts across the complete result set instead of treating each page independently. It gives more of the shared context budget to useful sources and gives less context to sources that only repeat information already returned. * **Single-turn RAG**: about 49% better token efficiency and 2.4% higher downstream quality with Exa Auto across coding and general QA evaluations. * **Agents**: about 30% fewer tokens across complete agent trajectories and 1% higher quality across BrowseComp, WideSearch, and internal company and people evaluations. Requests that set `dynamic: true` require the `Exa-Beta: dynamic-highlights-2026-08-28` header. [Read the Dynamic Highlights guide →](/docs/contents/quickstart) ## Publication research We significantly expanded and improved research over academic publications. * **350M publications**: search across an index of 350 million publications. * **Richer organization and people results**: searches now return both organizations and their affiliated people, each as a detailed, enriched profile spanning publications, top collaborators, research areas, and funding. * **Agentic people and organization search**: agents can now search over people and organizations. * **Public retrieval benchmark**: we released a public benchmark for publication retrieval. * **New `publication` search category**: query scholarly results with `category: "publication"`, which replaces the `research paper` category. * **Deprecated categories**: the `pdf`, `github`, and `tweet` search categories are being deprecated. * **`startCrawlDate` / `endCrawlDate`**: deprecated parameters are now ignored for all teams while remaining accepted for compatibility. Query it via the API with the `publication` [search category](/docs/search/quickstart), or [try it in the dashboard →](https://dashboard.exa.ai/playground/search?type=instant). ## Exa Agent and Exa Connect in MCP Exa Agent is now available within Exa MCP. Use it from Claude, Cursor, or any other MCP client when the task needs more than a single search call. Enable the Agent tool with `https://mcp.exa.ai/mcp?tools=agent_run`, then call `agent_run` to run the agent through completion and return its output. Exa Connect data sources are available through the Agent flow, so you can attach premium data partners when a run needs more than web search alone. [Read the Exa MCP guide →](/docs/get-started/exa-mcp) · [Read the Exa Agent guide →](/docs/agent/quickstart) · [Announcement tweet →](https://x.com/ExaAILabs/status/2072389192458592672) ## Introducing Exa Connect Exa Connect gives Exa Agent live access to the world's public and private data. It launched with Similarweb, Fiber.ai, Baselayer, Financial Datasets, Affiliate.com, Particle, Jinko, and Additional Partners. You attach them via `dataSources` on `POST /agent/runs`. [Read the Exa Connect guide →](/docs/agent/connect/overview) · [Announcement tweet →](https://x.com/ExaAILabs/status/2069842203577651283) ## Introducing Exa Agent We released a new class of frontier web research agents that are accessible via API. Exa Agent API supports parameters including a natural-language query, `effort` mode, `outputSchema` for structured outputs, and `input.data` to build upon an existing dataset. [Read the Exa Agent API guide →](/docs/agent/quickstart) ## API Deprecation Notice We retired a few legacy items from the Exa API: * **`/research` endpoint**: replaced by `/search` with `type: "deep-reasoning"`. * **`resolvedSearchType` and `highlightScores` (response fields)**: returned `null` from April 15, removed May 1. * **`startCrawlDate` / `endCrawlDate` (deprecated request parameters)**: silently ignored from April 15. [Migrate to Deep search →](/docs/reference/search) ## Introducing Exa Monitors Monitors run Exa searches on a schedule and deliver results to your webhook, deduplicated against previous runs so you only get new content. * **Track topics over time**: competitor news, funding rounds, regulatory changes, research papers. * **Structured results**: return plain text or typed JSON via `outputSchema`. * **Flexible scheduling**: run on an interval (minimum 1 hour) or trigger manually. [Read the Monitors API guide →](/docs/monitors/quickstart) ## Exa Deep Revamp Exa Deep is faster, cheaper, and now supports structured outputs with field-level grounding. * **New `deep-reasoning` type** for higher-effort tasks (12-50s); `deep` runs in 4-12s. * **20% lower price** for regular `deep` search. * **Structured outputs** via `outputSchema`, with `output.content` and `output.grounding` (field-level citations and confidence) in the response. See the [Exa Pricing Update](#exa-pricing-update) below for full pricing. [Read the Search API reference →](/docs/reference/search) ## Exa Pricing Update We simplified and lowered pricing. Contents for the first 10 search results are now included for free, and the new pricing applies automatically with no action needed. * **Search with contents**: \$7 per 1k requests (10 results, text + highlights included); \$1 per 1k additional results. * **Summaries**: \$1 per 1k, on both search and contents. * **Exa Deep**: \$12 per 1k requests; **Deep (Reasoning)** \$15 per 1k. * **Contents endpoint**: \$1 per 1k pages per content type. [View current pricing →](https://exa.ai/pricing) ## Introducing Exa Instant Search Exa Instant is our fastest search type, combining improved neural search quality with sub-150ms latency. Enable it with `type="instant"`. * **Built for real-time**: chat apps, voice AI, coding agents, autocomplete, and live suggestions. * **State-of-the-art quality** at the lowest latency we offer. [Read the Search API guide →](/docs/search/quickstart) · [Try it in the dashboard →](https://dashboard.exa.ai/playground/search?type=instant) ## Highlights, content freshness, and MCP updates Three improvements to content extraction and access: * **`maxCharacters` for highlights**: now the preferred way to control highlight length. `numSentences` and `highlightsPerUrl` are deprecated. * **`maxAgeHours` for content freshness**: age-based control replacing boolean `livecrawl` (`0` always crawls, `-1` cache-only, `24` crawls if older than 24h). * **Exa MCP free tier**: try it unauthenticated at 3 QPS and 150 calls/day; add an API key for full access. [Content freshness docs →](/docs/contents/quickstart#content-freshness) · [Exa MCP →](/docs/get-started/exa-mcp) ## Introducing Exa Company Search Company search now uses a fine-tuned retrieval model and entity-matching pipeline. Use `type="auto"`, `category="company"`. * **Accurate across attributes**: industry, geography, funding stage, and employee count. * **Structured entity data**: results return typed company info (workforce, HQ, financials, web traffic). * **Use cases**: sales prospecting, market research, and supply chain workflows. [Read the Companies & People Search docs →](/docs/search/data/companies-people) · [Read the benchmark blog →](https://exa.ai/blog/company-search-benchmarks) ## Introducing Exa People Search People search now spans 1B+ public profiles via a hybrid retrieval system. The `linkedin` category is replaced by the new `people` category. * **Broader coverage**: profiles across the whole web, not just LinkedIn. * **Better accuracy**: fine-tuned embeddings for role, skill, and company queries. * **Use cases**: sales, recruiting, and market research. [Read the Companies & People Search docs →](/docs/search/data/companies-people) · [Read the benchmark blog →](https://exa.ai/blog/people-search-benchmark) ## JS SDK: highlights restored Highlights are back in the JavaScript SDK as of `exa-js` v2.0.11, returning key sentences with relevance scores. Pass `highlights: true` or `highlights: { maxCharacters, query }` in search and contents calls. [Read the JavaScript SDK docs →](/docs/sdks/quickstart) ## New Deep Search Type Exa Deep finds better results by running multiple searches at once and returning high-quality context for each result. Enable it with `type="deep"`. * **Query expansion**: send one query and we generate variations, or supply your own with `additionalQueries`. * **Parallel search and smart ranking** across your query and all variations. * **Detailed summaries** for each result. [Read the Search API reference →](/docs/reference/search) ## Added Language Filtering Exa now detects your query language and returns results only in that language. Enabled by default for all users, with no setup required. [Read the Search API guide →](/docs/search/quickstart) ## SDK changes: highlights removed and contents returned by default A major SDK version with breaking changes: * **Contents by default**: search now includes page contents; opt out for faster searches. * **Highlights removed from SDKs**: later restored in the JS SDK; see [JS SDK: highlights restored](#js-sdk-highlights-restored). * **`use_autoprompt` deprecated**: removed from all API responses. [Read the Python SDK docs →](/docs/sdks/quickstart) ## Domain Path Filter Support `includeDomains` and `excludeDomains` now support finer targeting: * **Path-specific filtering**: e.g. `exa.ai/blog` or `linkedin.com/company`. * **Subdomain wildcards**: e.g. `*.substack.com`. Useful for scoping searches to blogs, product catalogs, or directories. [Read the Search API reference →](/docs/reference/search) ## Geolocation Filter Support The new `userLocation` parameter biases results toward a user's region, passed as an [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country code (e.g. `"us"`, `"fr"`). Useful for multi-regional apps, regional-language content, and local discovery. [Read the Search API reference →](/docs/reference/search) ## New Fast Search Type Exa Fast uses streamlined search models with p50 latency below 425ms. Enable it with `type="fast"`. * **Same Exa index** of high-quality content as neural search. * **Full parameter compatibility** with other search types. * **Built for** fast web grounding, agentic workflows, and low-latency products. [Read the Search API guide →](/docs/search/quickstart) · [Try it in the dashboard →](https://dashboard.exa.ai/playground/search?q=blog%20post%20about%20AI\&filters=%7B%22text%22%3A%22true%22%2C%22type%22%3A%22fast%22%2C%22livecrawl%22%3A%22never%22%7D) ## Score Deprecation in Auto Search A new Auto search architecture can no longer produce meaningful relevance scores, so the `score` field is being removed from Auto search results. * **Auto search**: no longer returns `score`; results are already ranked by relevance. * **Neural search**: scores are unchanged. Set `type="neural"` if you depend on them. [Read the Search API reference →](/docs/reference/search) ## Markdown Contents as Default All endpoints now return clean markdown by default, which is better for LLMs, RAG, and general text processing. No action needed. * **`includeHtmlTags=false` (default)**: content processed into clean markdown. * **`includeHtmlTags=true`**: raw HTML without markdown processing. Either way, boilerplate like ads and navigation is stripped. [Read the Contents docs →](/docs/contents/quickstart) ## New Livecrawl Option: Preferred Historical entry: the `livecrawl` string parameter is now deprecated. For new integrations, use `maxAgeHours` with `livecrawlTimeout`. See [Content Freshness](/docs/contents/quickstart#content-freshness). The deprecated `livecrawl: "preferred"` option attempts a fresh crawl but falls back to cached content when crawling fails (unlike `"always"`, which errors). Ideal for production apps that want fresh content without failing on temporarily unavailable sites. [Read the Content Freshness docs →](/docs/contents/quickstart#content-freshness) ## Contents Endpoint Status Changes `/contents` now returns a per-URL `statuses` field instead of a single HTTP error, so you can handle each URL's outcome individually. The endpoint only errors on internal issues. * **`status`**: `"success"` or `"error"` per URL. * **`error.tag`**: e.g. `CRAWL_NOT_FOUND`, `CRAWL_TIMEOUT`, `SOURCE_NOT_AVAILABLE`, with an `httpStatusCode`. [Read the error codes reference →](/docs/admin/error-codes) ## Auto search as Default Auto search is now the default, automatically routing each query to the best search method. No action needed; set `type="neural"` to keep the previous behavior. [Learn about Exa's search types →](/docs/search/quickstart) # Contents API Source: https://exa.ai/docs/contents/quickstart Extract text, highlights, and summaries from any URL. Exa Contents returns clean page content from URLs, handling JavaScript-rendered pages, PDFs, and complex layouts automatically. All contents features are also available in [Exa Search](/docs/search/quickstart) for returned URLs, at no extra charge up to 10 results per search (\$1/1000 pages afterwards). We recommend using Search in this way instead of Contents for web search tool use cases. For search results feeding AI context, request `contents: { highlights: true }` on `/search` — Exa sizes each result's excerpts to its relevance. See [Highlights](/docs/search/highlights). ## Make your first request Pass one or more URLs or document IDs and request highlights for the parts relevant to your task. In HTTP requests, provide them in `ids`: ```python Python theme={null} from exa_py import Exa exa = Exa() result = exa.get_contents( ["https://exa.ai/blog/dynamic-highlights"], highlights={"query": "token efficiency and quality results"}, ) print(result.results[0].highlights) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const result = await exa.getContents( ["https://exa.ai/blog/dynamic-highlights"], { highlights: { query: "token efficiency and quality results" } } ); console.log(result.results[0].highlights); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/contents" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "ids": ["https://exa.ai/blog/dynamic-highlights"], "highlights": { "query": "token efficiency and quality results" } }' ``` ```json theme={null} { "requestId": "e492118ccdedcba5088bfc4357a8a125", "results": [ { "id": "https://exa.ai/blog/dynamic-highlights", "title": "Dynamic Highlights", "url": "https://exa.ai/blog/dynamic-highlights", "highlights": [ "With a 12k character budget, relative to existing highlights, Dynamic Highlights achieves a 40% average token efficiency gain with a notable quality increase..." ] } ], "statuses": [ { "id": "https://exa.ai/blog/dynamic-highlights", "status": "success", "source": "cached" } ], "costDollars": { "total": 0.001 } } ``` Each item in `results` includes page metadata and the content view you requested. Check `statuses` for the success or failure of every URL. ## Output shapes Highlights return relevant passages copied from the page. Start here for agents, RAG, and factual lookups because highlights keep context smaller than full text. Set `highlights: true` to enable highlights. An additional `query` parameter is recommended when using Contents to focus content extraction from the page: ```python Python theme={null} result = exa.get_contents( ["https://example.com/research-paper"], highlights={"query": "methodology and results"}, ) ``` ```javascript JavaScript theme={null} const result = await exa.getContents( ["https://example.com/research-paper"], { highlights: { query: "methodology and results" } } ); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/contents" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "ids": ["https://example.com/research-paper"], "highlights": { "query": "methodology and results" } }' ``` See [Highlights](/docs/search/highlights) for Dynamic Highlights and guidance on allocating context across several pages. Full text returns the clean page body as markdown. Use it when the task depends on broad context, document structure, or details that highlights may leave out. Full pages can be large, so use `maxCharacters` when you need a limit: ```python Python theme={null} result = exa.get_contents( ["https://example.com/technical-report"], text={"max_characters": 10000}, ) ``` ```javascript JavaScript theme={null} const result = await exa.getContents( ["https://example.com/technical-report"], { text: { maxCharacters: 10000 } } ); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/contents" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "ids": ["https://example.com/technical-report"], "text": { "maxCharacters": 10000 } }' ``` Summary makes a language model call for each page. Use it when you need a generated overview or fields extracted into a JSON schema. ```python Python theme={null} result = exa.get_contents( ["https://example.com/company"], summary={"query": "Summarize the product, customers, and pricing"}, ) ``` ```javascript JavaScript theme={null} const result = await exa.getContents( ["https://example.com/company"], { summary: { query: "Summarize the product, customers, and pricing" } } ); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/contents" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "ids": ["https://example.com/company"], "summary": { "query": "Summarize the product, customers, and pricing" } }' ``` To extract fields instead of prose, pass a JSON schema in `summary.schema`. The summary comes back as a JSON string matching the schema; parse it to read the fields: ```json theme={null} { "ids": ["https://example.com/company"], "summary": { "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Company Information", "type": "object", "properties": { "name": { "type": "string", "description": "The company name" }, "industry": { "type": "string", "description": "Primary industry" }, "foundedYear": { "type": "number", "description": "Year the company was founded" } }, "required": ["name"] } } } ``` Pick one content view per request. Requesting highlights, text, and summary together returns and bills each view separately. ## Content freshness `maxAgeHours` controls how fresh the extracted page content must be. | Value | Behavior | | ---------------- | -------------------------------------------------------------------------------- | | Omit | Use cached content when available and fetch the page when needed | | Positive integer | Use cached content if it is newer than this many hours, otherwise fetch the page | | `0` | Always fetch fresh content | | `-1` | Only use cached content | Most requests should omit this field. Set it when stale page content would be unusable, such as for prices, availability, or frequently updated pages. Pair a low `maxAgeHours` with `livecrawlTimeout` (milliseconds) to cap how long a fresh fetch may take. The `livecrawl` string parameter (`"always"`, `"preferred"`, `"fallback"`, `"never"`) is deprecated in favor of `maxAgeHours`: | Old `livecrawl` value | Equivalent | | --------------------- | -------------------------------------------------------------- | | `"always"` | `maxAgeHours: 0` | | `"never"` | `maxAgeHours: -1` | | `"fallback"` | Omit `maxAgeHours` | | `"preferred"` | No direct equivalent; use a low value such as `maxAgeHours: 1` | ## Crawl subpages Set `subpages` to follow links from each starting URL. Add `subpageTarget` when you want Exa to prioritize particular site sections: ```python Python theme={null} result = exa.get_contents( ["https://docs.example.com"], subpages=10, subpage_target=["api", "reference", "guides"], highlights=True, ) ``` ```javascript JavaScript theme={null} const result = await exa.getContents( ["https://docs.example.com"], { subpages: 10, subpageTarget: ["api", "reference", "guides"], highlights: true } ); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/contents" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "ids": ["https://docs.example.com"], "subpages": 10, "subpageTarget": ["api", "reference", "guides"], "highlights": true }' ``` ## Images and favicons Set `extras.imageLinks` to the number of image URLs you want from each page. Results also include the site's `favicon` and a representative `image` URL when available. On `/search`, this option sits at `contents.extras.imageLinks`. ## Next steps See every request parameter and response field. Compare regular and Dynamic Highlights for agent and RAG context. Find relevant pages before extracting their content. Use Exa from Python or JavaScript. # Build with Exa Skill Source: https://exa.ai/docs/get-started/agent-skills/build-with-exa An agent skill to help developers implement any part of the Exa API platform. Use this skill to teach your agent to build applications and agents with Exa's APIs, following best practices. Create a key in the dashboard. New accounts start with free credits. Set your key as `EXA_API_KEY` in your agent environment. ## Setup **Option A: Install this skill directly:** ```bash theme={null} npx skills add exa-labs/agent-skills --skill "build-with-exa" ``` **Option B: Copy this prompt into your coding agent.** The following prompt installs the skill and verifies your API key without printing it: ```text Copy this setup prompt into your agent theme={null} Set up the Exa build-with-exa agent skill on this machine. Goal: - Install the build-with-exa skill so my coding agent can use it to build applications and agents with Exa's full API platform. - Get an Exa API key working WITHOUT ever exposing, printing, or pasting the key into this chat. Selected agent: - Claude Code, Codex, Cursor, or any Agent-Skills-compatible agent - Global install directories: ~/.claude/skills (Claude Code), ~/.codex/skills (Codex), ~/.agents/skills (Cursor / other) - Project-local install directories: .claude/skills (Claude Code), .agents/skills (Codex / Cursor / other) Skill source: - SKILL.md URL: https://raw.githubusercontent.com/exa-labs/agent-skills/main/skills/build-with-exa/SKILL.md What to do: 1. Install the skill FIRST, before any key setup. Prefer a project-local install when working inside a repo; otherwise use the matching global directory listed above. Create the chosen skills directory and download the skill: mkdir -p /build-with-exa && curl -fsSL "https://raw.githubusercontent.com/exa-labs/agent-skills/main/skills/build-with-exa/SKILL.md" -o /build-with-exa/SKILL.md Then verify that /build-with-exa/SKILL.md exists. 2. Check whether an Exa API key is already available FROM YOUR OWN COMMAND-RUNNING ENVIRONMENT — use the same tool/shell you will run the skill with, not by asking me to echo it. The skill resolves the key from EXA_API_KEY first, then from the file ~/.config/exa/key, so check both without ever printing a value: printf '%s\n' "${EXA_API_KEY:+env-set}"; [ -s ~/.config/exa/key ] && printf 'file-set\n' Your shell is likely non-interactive and does NOT auto-source interactive profiles like ~/.zshrc or ~/.bashrc, so a key I set there can look present to me but empty to you. If neither shows, the key may still live in an interactive profile your shell skips: find which file WITHOUT printing its value using `grep -l EXA_API_KEY ~/.zshrc ~/.zshenv ~/.bashrc ~/.profile ~/.config/fish/config.fish 2>/dev/null` (lists names only — NEVER run a plain `grep`/`cat`/`echo` on a profile, since an `export EXA_API_KEY=...` line would leak the secret into our chat). Then `source` that file inside your command and re-run the presence test above; if it shows, prepend that same `source ...;` to every later command that needs the key. 3. Only if no key is resolvable anywhere, set one up WITHOUT hand-editing any shell profile and WITHOUT pasting the key into this chat. Tell me to create/copy a key at https://dashboard.exa.ai/api-keys, then in my own terminal either export EXA_API_KEY myself or write it to ~/.config/exa/key with mode 600 — never ask me to paste the key into chat. Then wait for me to confirm it is done before continuing. 4. Smoke-test the key from your own shell — resolve it from the env var or the file, and print only the status code: KEY="${EXA_API_KEY:-$(cat ~/.config/exa/key 2>/dev/null)}" curl -s -o /dev/null -w "%{http_code}\n" -X POST https://api.exa.ai/search \ -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{"query":"exa.ai","numResults":1}' Keep the endpoint, headers, and body exactly as written (do not guess the schema). It must return 200, not 401/429. If you needed a `source ...;` prefix in step 2 to see an env key, prepend that here too. 5. Tell me how to restart or rescan my agent so it discovers the skill. Hard rule throughout: the key is a secret. Only ever inspect it via a presence/length check (`${EXA_API_KEY:+set}`, `[ -s ~/.config/exa/key ]`) or an HTTP status code — never print, `echo`, `cat`, or `grep`-with-output any file or variable that may contain it, and never try to "redact" a key file with a regex. If a key is ever exposed, tell me to rotate it at https://dashboard.exa.ai/api-keys. ``` ## View source Read the build-with-exa skill definition before installing. ## Related Browse every Exa skill and install them all at once. Source for every skill, including raw `SKILL.md` files. # Exa Contents Skill Source: https://exa.ai/docs/get-started/agent-skills/exa-contents Extract page content with Exa Contents when you already have the URLs. Use this skill to teach your agent to call Exa Contents with cURL or raw HTTP, using best practices. Create a key in the dashboard. New accounts start with free credits. Set your key as `EXA_API_KEY` in your agent environment. ## Setup **Option A: Install this skill directly:** ```bash theme={null} npx skills add exa-labs/agent-skills --skill "exa-contents" ``` **Option B: Copy this prompt into your coding agent.** The following prompt installs the skill and verifies your API key without printing it: ```text Copy this setup prompt into your agent theme={null} Set up the Exa exa-contents agent skill on this machine. Goal: - Install the exa-contents skill so my coding agent can use it to call Exa Contents directly with cURL or raw HTTP. - Get an Exa API key working WITHOUT ever exposing, printing, or pasting the key into this chat. Selected agent: - Claude Code, Codex, Cursor, or any Agent-Skills-compatible agent - Global install directories: ~/.claude/skills (Claude Code), ~/.codex/skills (Codex), ~/.agents/skills (Cursor / other) - Project-local install directories: .claude/skills (Claude Code), .agents/skills (Codex / Cursor / other) Skill source: - SKILL.md URL: https://raw.githubusercontent.com/exa-labs/agent-skills/main/skills/exa-contents/SKILL.md What to do: 1. Install the skill FIRST, before any key setup. Prefer a project-local install when working inside a repo; otherwise use the matching global directory listed above. Create the chosen skills directory and download the skill: mkdir -p /exa-contents && curl -fsSL "https://raw.githubusercontent.com/exa-labs/agent-skills/main/skills/exa-contents/SKILL.md" -o /exa-contents/SKILL.md Then verify that /exa-contents/SKILL.md exists. 2. Check whether an Exa API key is already available FROM YOUR OWN COMMAND-RUNNING ENVIRONMENT — use the same tool/shell you will run the skill with, not by asking me to echo it. The skill resolves the key from EXA_API_KEY first, then from the file ~/.config/exa/key, so check both without ever printing a value: printf '%s\n' "${EXA_API_KEY:+env-set}"; [ -s ~/.config/exa/key ] && printf 'file-set\n' Your shell is likely non-interactive and does NOT auto-source interactive profiles like ~/.zshrc or ~/.bashrc, so a key I set there can look present to me but empty to you. If neither shows, the key may still live in an interactive profile your shell skips: find which file WITHOUT printing its value using `grep -l EXA_API_KEY ~/.zshrc ~/.zshenv ~/.bashrc ~/.profile ~/.config/fish/config.fish 2>/dev/null` (lists names only — NEVER run a plain `grep`/`cat`/`echo` on a profile, since an `export EXA_API_KEY=...` line would leak the secret into our chat). Then `source` that file inside your command and re-run the presence test above; if it shows, prepend that same `source ...;` to every later command that needs the key. 3. Only if no key is resolvable anywhere, set one up WITHOUT hand-editing any shell profile and WITHOUT pasting the key into this chat. Tell me to create/copy a key at https://dashboard.exa.ai/api-keys, then in my own terminal either export EXA_API_KEY myself or write it to ~/.config/exa/key with mode 600 — never ask me to paste the key into chat. Then wait for me to confirm it is done before continuing. 4. Smoke-test the key from your own shell — resolve it from the env var or the file, and print only the status code: KEY="${EXA_API_KEY:-$(cat ~/.config/exa/key 2>/dev/null)}" curl -s -o /dev/null -w "%{http_code}\n" -X POST https://api.exa.ai/contents \ -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{"urls":["https://exa.ai"],"text":true}' Keep the endpoint, headers, and body exactly as written (do not guess the schema). It must return 200, not 401/429. If you needed a `source ...;` prefix in step 2 to see an env key, prepend that here too. 5. Tell me how to restart or rescan my agent so it discovers the skill. Hard rule throughout: the key is a secret. Only ever inspect it via a presence/length check (`${EXA_API_KEY:+set}`, `[ -s ~/.config/exa/key ]`) or an HTTP status code — never print, `echo`, `cat`, or `grep`-with-output any file or variable that may contain it, and never try to "redact" a key file with a regex. If a key is ever exposed, tell me to rotate it at https://dashboard.exa.ai/api-keys. ``` ## View source Read the exa-contents skill definition before installing. ## Related Browse every Exa skill and install them all at once. Source for every skill, including raw `SKILL.md` files. # Exa Search Skill Source: https://exa.ai/docs/get-started/agent-skills/exa-search Find relevant web pages and return synthesized content in under two seconds with Exa Search. Use this skill to teach your agent to call Exa Search with cURL or raw HTTP, using best practices. Create a key in the dashboard. New accounts start with free credits. Set your key as `EXA_API_KEY` in your agent environment. ## Setup **Option A: Install this skill directly:** ```bash theme={null} npx skills add exa-labs/agent-skills --skill "exa-search" ``` **Option B: Copy this prompt into your coding agent.** The following prompt installs the skill and verifies your API key without printing it: ```text Copy this setup prompt into your agent theme={null} Set up the Exa exa-search agent skill on this machine. Goal: - Install the exa-search skill so my coding agent can use it to call Exa Search directly with cURL or raw HTTP. - Get an Exa API key working WITHOUT ever exposing, printing, or pasting the key into this chat. Selected agent: - Claude Code, Codex, Cursor, or any Agent-Skills-compatible agent - Global install directories: ~/.claude/skills (Claude Code), ~/.codex/skills (Codex), ~/.agents/skills (Cursor / other) - Project-local install directories: .claude/skills (Claude Code), .agents/skills (Codex / Cursor / other) Skill source: - SKILL.md URL: https://raw.githubusercontent.com/exa-labs/agent-skills/main/skills/exa-search/SKILL.md What to do: 1. Install the skill FIRST, before any key setup. Prefer a project-local install when working inside a repo; otherwise use the matching global directory listed above. Create the chosen skills directory and download the skill: mkdir -p /exa-search && curl -fsSL "https://raw.githubusercontent.com/exa-labs/agent-skills/main/skills/exa-search/SKILL.md" -o /exa-search/SKILL.md Then verify that /exa-search/SKILL.md exists. 2. Check whether an Exa API key is already available FROM YOUR OWN COMMAND-RUNNING ENVIRONMENT — use the same tool/shell you will run the skill with, not by asking me to echo it. The skill resolves the key from EXA_API_KEY first, then from the file ~/.config/exa/key, so check both without ever printing a value: printf '%s\n' "${EXA_API_KEY:+env-set}"; [ -s ~/.config/exa/key ] && printf 'file-set\n' Your shell is likely non-interactive and does NOT auto-source interactive profiles like ~/.zshrc or ~/.bashrc, so a key I set there can look present to me but empty to you. If neither shows, the key may still live in an interactive profile your shell skips: find which file WITHOUT printing its value using `grep -l EXA_API_KEY ~/.zshrc ~/.zshenv ~/.bashrc ~/.profile ~/.config/fish/config.fish 2>/dev/null` (lists names only — NEVER run a plain `grep`/`cat`/`echo` on a profile, since an `export EXA_API_KEY=...` line would leak the secret into our chat). Then `source` that file inside your command and re-run the presence test above; if it shows, prepend that same `source ...;` to every later command that needs the key. 3. Only if no key is resolvable anywhere, set one up WITHOUT hand-editing any shell profile and WITHOUT pasting the key into this chat. Tell me to create/copy a key at https://dashboard.exa.ai/api-keys, then in my own terminal either export EXA_API_KEY myself or write it to ~/.config/exa/key with mode 600 — never ask me to paste the key into chat. Then wait for me to confirm it is done before continuing. 4. Smoke-test the key from your own shell — resolve it from the env var or the file, and print only the status code: KEY="${EXA_API_KEY:-$(cat ~/.config/exa/key 2>/dev/null)}" curl -s -o /dev/null -w "%{http_code}\n" -X POST https://api.exa.ai/search \ -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{"query":"exa.ai","numResults":1}' Keep the endpoint, headers, and body exactly as written (do not guess the schema). It must return 200, not 401/429. If you needed a `source ...;` prefix in step 2 to see an env key, prepend that here too. 5. Tell me how to restart or rescan my agent so it discovers the skill. Hard rule throughout: the key is a secret. Only ever inspect it via a presence/length check (`${EXA_API_KEY:+set}`, `[ -s ~/.config/exa/key ]`) or an HTTP status code — never print, `echo`, `cat`, or `grep`-with-output any file or variable that may contain it, and never try to "redact" a key file with a regex. If a key is ever exposed, tell me to rotate it at https://dashboard.exa.ai/api-keys. ``` ## View source Read the exa-search skill definition before installing. ## Related Browse every Exa skill and install them all at once. Source for every skill, including raw `SKILL.md` files. # Agent Skills Source: https://exa.ai/docs/get-started/agent-skills/overview Install Exa skills in Claude Code, Codex, and other coding agents. Exa skills teach coding agents how to search, retrieve content, and build with Exa’s APIs. Find them in the open-source [exa-labs/agent-skills](https://github.com/exa-labs/agent-skills) repository. Each skill contains markdown files that follow the open [Agent Skills](https://agentskills.io) standard, so the same files install into any compatible agent. ## Install Install every Exa skill at once: ```bash theme={null} npx skills add exa-labs/agent-skills ``` Create a key in the dashboard. New accounts start with free credits. Set your key as `EXA_API_KEY` in your agent environment. Or open a skill page below and copy its setup prompt into your agent. The prompt installs that skill and verifies your API key without printing it. ## Skills Each skill page includes a one-line description, a copyable setup prompt, and a link to the raw `SKILL.md` source. Build applications and agents with Exa's full API platform. Call Exa Search directly with cURL or raw HTTP. Call Exa Contents directly with cURL or raw HTTP. ## Related Source for every skill, including raw `SKILL.md` files. Connect Claude, Cursor, VS Code, and other clients to Exa over MCP. # Exa MCP Source: https://exa.ai/docs/get-started/exa-mcp Connect ChatGPT, Codex, Claude, Grok, Cursor, and any other MCP client to Exa's web search, page fetching, Exa Agent, and Exa Connect tools. Use Exa MCP to improve the built-in web search in ChatGPT, Claude, and MCP-compatible tools with Exa's search capabilities, including web search, code search, [Exa Agent](/docs/agent/quickstart), and [Exa Connect](/docs/agent/connect/overview). Exa offers a hosted server that works in any MCP client: ```text theme={null} https://mcp.exa.ai/mcp ``` No API key is required to get started. Exa MCP is open source and available on [GitHub](https://github.com/exa-labs/exa-mcp-server). ## Install
Exa is an official plugin in OpenAI's plugin directory, which includes the hosted MCP server plus Exa's `search` and `exa-agent` skills. Go to [chatgpt.com/plugins/exa](https://chatgpt.com/plugins/exa?open_in_app). It opens **Exa** in OpenAI's plugin directory, which is the same directory for ChatGPT and for Codex. Select the plus button to install. Sign in to Exa when prompted, either during install or the first time Codex or ChatGPT uses it. Opening Plugins in Codex, viewing the Exa plugin, and authorizing access Skills load in chats and CLI sessions started after the install, so open a fresh one and ask for something that needs the web. That's it. The plugin includes both Exa's MCP integration and skills, so there's no separate MCP or skill setup required. See [Exa in Codex and ChatGPT](/docs/integrations/chatgpt-codex) for the full setup and workflow guide. ### Claude Code CLI Install Exa from the terminal: ```bash theme={null} claude plugin install exa@claude-plugins-official ``` You can also type `/plugin` in Claude Code, search for **Exa**, and install it. Start a new Claude Code session and ask for something that needs the web. ### Desktop, Web & Cowork Claude Desktop, Web, and Cowork all use Exa's official connector. Select the plus button in a new chat, choose **Add connector**, and search for **Exa**. Open Exa, select **Connect to Claude**, and authorize access when prompted. Opening the connector directory in Claude, finding Exa, connecting it, and authorizing access Start a new chat and ask for something that needs current information from the web. See [Exa in Claude Code, Web, and Desktop](/docs/integrations/claude-web-desktop) for the full setup and workflow guide. Claude Team and Enterprise admins can provision the connector for everyone through their identity provider instead: see [Enterprise Managed Auth](/docs/admin/mcp-enterprise-managed-auth). Exa is available on the [Grok Build](https://docs.x.ai/build/overview) marketplace. In Grok Build, run `/marketplace`. Find **exa** in the list and press `i`. Run `/mcp`, select **exa**, and press `i` to sign in to your Exa account in the browser. New accounts get free credits at signup. Install Exa MCP from the [Cursor marketplace](https://cursor.com/marketplace/exa), or add it to `~/.cursor/mcp.json`: ```json theme={null} { "mcpServers": { "exa": { "url": "https://mcp.exa.ai/mcp" } } } ``` Use the [one-click install](https://vscode.dev/redirect/mcp/install?name=exa\&config=%7B%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fmcp.exa.ai%2Fmcp%22%7D), or add it to `.vscode/mcp.json` in your project: ```json theme={null} { "servers": { "exa": { "type": "http", "url": "https://mcp.exa.ai/mcp" } } } ``` Most clients use the standard `mcpServers` shape: ```json theme={null} { "mcpServers": { "exa": { "url": "https://mcp.exa.ai/mcp" } } } ``` Where the config lives and what the URL key is called varies by client: | Client | Where to add it | URL key | | -------------------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------- | | [fx by Vercel](/docs/integrations/vercel/fx) | `/mcp add --transport http exa https://mcp.exa.ai/mcp` in the fx shell (saved to `~/.fx/mcp.json`) | `url` | | OpenCode | `opencode.json` (under `mcp`, with `"type": "remote"`) | `url` | | Kiro | `~/.kiro/settings/mcp.json` (under `mcpServers`) | `url` | | Windsurf | `~/.codeium/windsurf/mcp_config.json` (under `mcpServers`) | `serverUrl` | | Google Antigravity | Agent panel → Manage MCP Servers → View Raw config (under `mcpServers`) | `serverUrl` | | Zed | Zed `settings.json` (under `context_servers`) | `url` | | Gemini CLI | `~/.gemini/settings.json` (under `mcpServers`) | `httpUrl` | | Warp | Settings → MCP Servers → Add MCP Server (top-level `exa`) | `url` | | v0 by Vercel | Prompt Tools → Add MCP | paste the URL directly | If your client doesn't support remote MCP servers, use the `mcp-remote` bridge: ```json theme={null} { "mcpServers": { "exa": { "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.exa.ai/mcp"] } } } ``` Or run the local [npm package](https://www.npmjs.com/package/exa-mcp-server) with your [Exa API key](https://dashboard.exa.ai/api-keys): ```json theme={null} { "mcpServers": { "exa": { "command": "npx", "args": ["-y", "exa-mcp-server"], "env": { "EXA_API_KEY": "your_api_key" } } } } ```
## Authentication Exa MCP supports three authentication modes: | Mode | Use it for | Setup | | ------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | Keyless | Free rate-limited usage without sign-in or API key | Connect to `https://mcp.exa.ai/mcp` | | OAuth | Interactive clients, marketplace installs, production usage | Connect to `https://mcp.exa.ai/mcp?login` to sign in to Exa in the browser. Usage belongs to your Exa team. | | API key | Clients without MCP OAuth | Connect to `https://mcp.exa.ai/mcp` with the `x-api-key` header set to your API key | ### Sign in with OAuth ChatGPT, Claude, and other marketplace installs prompt you to sign in when needed. In any client that supports MCP OAuth, you can request the same flow by connecting to: ```text theme={null} https://mcp.exa.ai/mcp?login ``` Your client discovers Exa's authorization server, opens the browser sign-in, and manages access. ### Use an API key Create a key in the dashboard. New accounts start with free credits. Add the `x-api-key` header to the MCP server configuration: ```text theme={null} x-api-key: YOUR_EXA_API_KEY ``` ## Available Tools | Tool | Availability | Use it for | | ------------------------- | ------------------------------- | ------------------------------------------------------------------------- | | `web_search_exa` | Enabled by default | Search the web and return relevant, ready-to-use content | | `web_fetch_exa` | Enabled by default | Read clean content from one or more known URLs | | `web_search_advanced_exa` | Availiable when opted in | Configure web search with advanced filters and controls | | `agent_run` | Available with OAuth or API key | Run multi-step research, list-building, enrichment, and structured output | Use the `tools` URL parameter to choose what your client sees. For example, to enable all tools: ```text theme={null} https://mcp.exa.ai/mcp?tools=web_search_exa,web_fetch_exa,web_search_advanced_exa,agent_run ``` An explicit `tools` list replaces the defaults, so include every tool you want enabled including web search and fetch. ## Exa Agent Use [Exa Agent](/docs/agent/quickstart) for research that takes more than one search—for example, building a list, checking each item against criteria, or returning structured results. Agent runs are usage-based, so `agent_run` requires OAuth or an API key. This URL starts OAuth and adds Agent alongside the default tools: ```text theme={null} https://mcp.exa.ai/mcp?login&tools=web_search_exa,web_fetch_exa,agent_run ``` If you use an API key, omit `login` and add the key as described in [Authentication](#authentication). Ask for the research in plain language. Your assistant passes the request to `agent_run` with a `query`, and Exa Agent works out what to search, reads the sources, and checks what it finds against the request. Ask your Agent to provide an `outputSchema` only when your application needs the findings in a consistent JSON format. You can share one with your assistant in the system prompt, or have it generate one for you. When the research completes, the tool call hands your assistant the full research package: * The written findings * The sources behind them * Validated JSON if you provided `outputSchema` * Usage and cost Your assistant writes its reply from this package, so tell it what you want done with the output. You can ask it to summarize the findings, compare them, save them to a file, or anything else. Research that outlasts a single MCP call doesn't fail: the tool reports `status: "running"` with an `id` while the run continues on Exa. Your assistant calls `agent_run` again with that `id` as `runId` to pick the same run back up. | Field | Use it to | | ----------------- | -------------------------------------------------------------------- | | `systemPrompt` | Give Agent extra guidance for researching or judging results | | `outputSchema` | Return the answer in a specific JSON format | | `input.data` | Enrich rows or entities you already have | | `input.exclusion` | Skip results you already know about | | `dataSources` | Add up to five [Exa Connect](/docs/agent/connect/overview) providers | | `previousRunId` | Build a new request on completed research | | `effort` | Choose how much research Agent should do | Use `runId` to keep waiting on current work. Use `previousRunId` to ask a new follow-up based on finished work. See the [Exa Agent guide](/docs/agent/quickstart) for output schema patterns, effort modes, data sources, and pricing. ## Advanced search Use `web_search_advanced_exa` when the request needs explicit category or domain filters, date ranges, text constraints, geo-targeting, query expansion, summaries, highlights, freshness control, or subpage crawling. For ordinary searches, keep `web_search_exa`; it gives the model a smaller tool surface and requires less configuration. Advanced Search does not require authentication, although authenticated connections use your own plan and rate limits. Enable it alongside the default tools with: ```text theme={null} https://mcp.exa.ai/mcp?tools=web_search_exa,web_fetch_exa,web_search_advanced_exa ``` The MCP tool presents common [Search API](/docs/reference/search) controls as tool-friendly fields such as `includeDomains`, `startPublishedDate`, `enableHighlights`, and `maxAgeHours`. Read the tool schema in your client for the exact field names. ## Troubleshooting The connection is using Exa's free rate limits. Sign in with OAuth or add your own API key, then reconnect so requests use your team's plan and limits. Create a key in the dashboard. New accounts start with free credits. `agent_run` is not enabled by default and cannot use the free rate limits. Add it to the `tools` URL parameter, then connect with `?login` or configure an API key. See [Exa Agent](#exa-agent) for the complete URL. Confirm that your client supports MCP OAuth and connect to `https://mcp.exa.ai/mcp?login`. Restart the client after changing the URL. If the client cannot complete MCP OAuth, use an API key instead. An explicit `tools` parameter replaces the default tool list. Check that every tool you want is present in the URL, then restart your MCP client so it fetches the tool list again. Use the built-in connector: select **+** (or **Add connectors**) → **Connectors** tab → search for **Exa** → select **+**. Common config locations: * Cursor: `~/.cursor/mcp.json` * fx: `~/.fx/mcp.json` * VS Code: `.vscode/mcp.json` (in project root) * Claude desktop (macOS): `~/Library/Application Support/Claude/claude_desktop_config.json` * Claude desktop (Windows): `%APPDATA%\Claude\claude_desktop_config.json` ## Resources Exa MCP source code. Run Exa MCP locally with the npm package. Portable skills that pair with Exa MCP. Full setup and workflow guide for the Exa plugin. # Developer quickstart Source: https://exa.ai/docs/get-started/quickstart Get an API key, then use Exa from your code or your agent.
## 1. Get an API key Create a key in the dashboard. New accounts start with free credits. ```bash theme={null} export EXA_API_KEY="your-api-key" ``` ```powershell theme={null} setx EXA_API_KEY "your-api-key" ```
## 2. Choose how you'll use Exa Exa fits into your application two ways: call the APIs from your own code, or connect an agent you already use. Use Search, Contents, and Exa Agent from your own code. Install an SDK below and make your first request. Connect ChatGPT, Claude, Codex, or Cursor to Exa's search and research tools. No API key is required. Building with the APIs? Pick where to start: | Start with | Use it for | | --------------------------------------- | ------------------------------------------------------------------------- | | [Search](/docs/search/quickstart) | Finding relevant web pages & returning synthesized content in under 2s | | [Deep Search](/docs/search/deep-search) | Higher-quality search where an LLM iteratively finds better results | | [Agent](/docs/agent/quickstart) | Long-running asynchronous research, list building, enrichment, or reports | | [Contents](/docs/contents/quickstart) | Extracting page content when you already have the URL(s) |
## 3. Install an SDK ```bash Python theme={null} pip install exa-py ``` ```bash JavaScript theme={null} npm install exa-js ```
## 4. Make your first request ```python Python theme={null} from exa_py import Exa exa = Exa() results = exa.search( "best blog posts about vector databases", contents={"highlights": True}, ) for result in results.results: print(result.title, result.url) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const { results } = await exa.search( "best blog posts about vector databases", { contents: { highlights: true } }, ); for (const result of results) { console.log(result.title, result.url); } ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "best blog posts about vector databases", "contents": { "highlights": true } }' ``` ## Next steps Find relevant pages and return clean content or structured outputs. Build long-running research, list-building, and enrichment workflows. Extract clean content from pages you already know. Connect any MCP client to Exa's web search, page fetching, and Exa Agent tools.
# Start building with Exa Source: https://exa.ai/docs/index A powerful web search tool designed for agents. Everything optimized to get you token-efficient, accurate results.
If you’re an LLM, read LLMs.txt
Add Exa to your agent

Get started in under a minute

Examples

```python theme={null} from exa_py import Exa exa = Exa() result = exa.search( "companies selling AI voice agents to dental practices", contents={"highlights": True}, ) for hit in result.results: print(hit.title, hit.url) print(hit.highlights) ```
```javascript theme={null} import Exa from "exa-js"; const exa = new Exa(); const result = await exa.search( "companies selling AI voice agents to dental practices", { contents: { highlights: true } }, ); for (const hit of result.results) { console.log(hit.title, hit.url); console.log(hit.highlights); } ```
```bash theme={null} curl -s -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "companies selling AI voice agents to dental practices", "contents": { "highlights": true } }' ```
```python theme={null} from exa_py import Exa exa = Exa() result = exa.search( "Who is Anthropic's CEO, when was the company founded, and where is it headquartered?", type="deep", output_schema={ "type": "object", "required": ["company", "ceo", "founded_year", "headquarters"], "properties": { "company": {"type": "string"}, "ceo": {"type": "string"}, "founded_year": {"type": "number"}, "headquarters": {"type": "string"}, }, }, ) print(result.output.content) ```
```javascript theme={null} import Exa from "exa-js"; const exa = new Exa(); const result = await exa.search( "Who is Anthropic's CEO, when was the company founded, and where is it headquartered?", { type: "deep", outputSchema: { type: "object", required: ["company", "ceo", "founded_year", "headquarters"], properties: { company: { type: "string" }, ceo: { type: "string" }, founded_year: { type: "number" }, headquarters: { type: "string" }, }, }, }, ); console.log(result.output?.content); ```
```bash theme={null} curl -s -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Who is Anthropic'\''s CEO, when was the company founded, and where is it headquartered?", "type": "deep", "outputSchema": { "type": "object", "required": ["company", "ceo", "founded_year", "headquarters"], "properties": { "company": { "type": "string" }, "ceo": { "type": "string" }, "founded_year": { "type": "number" }, "headquarters": { "type": "string" } } } }' ```
```python theme={null} from exa_py import Exa from openai import OpenAI exa = Exa() openai = OpenAI() messages = [{"role": "user", "content": "What's the latest on AI chips?"}] completion = openai.chat.completions.create( model="gpt-5.6", 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.chat.completions.create(model="gpt-5.6", messages=messages) print(completion.choices[0].message.content) ```
```javascript theme={null} import Exa from "exa-js"; import { OpenAI } from "openai"; const exa = new Exa(); 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", 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", messages }); console.log(completion.choices[0].message.content); ```
```bash theme={null} # 1. Offer the model an Exa search tool and let it pick the query. USER_MSG='{ "role": "user", "content": "What'\''s the latest on AI chips?" }' TOOLS='[{ "type": "function", "function": { "name": "web_search", "description": "Search the web and return the most relevant pages.", "parameters": { "type": "object", "properties": { "query": { "type": "string" } }, "required": ["query"] } }}]' ASSISTANT="$(curl -s "https://api.openai.com/v1/chat/completions" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -d "{ \"model\": \"gpt-5.6\", \"messages\": [$USER_MSG], \"tools\": $TOOLS }" \ | python3 -c 'import json,sys; print(json.dumps(json.load(sys.stdin)["choices"][0]["message"]))')" # 2. Run the tool call against Exa. SEARCH_BODY="$(echo "$ASSISTANT" | python3 -c ' import json, sys args = json.loads(json.load(sys.stdin)["tool_calls"][0]["function"]["arguments"]) print(json.dumps({"query": args["query"], "contents": {"highlights": True}}))')" RESULTS="$(curl -s -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d "$SEARCH_BODY")" # 3. Hand the results back to the model for the final answer. TOOL_MSG="$(echo "$ASSISTANT" | python3 -c ' import json, sys assistant = json.load(sys.stdin) print(json.dumps({"role": "tool", "tool_call_id": assistant["tool_calls"][0]["id"], "content": sys.argv[1]}))' "$RESULTS")" curl -s "https://api.openai.com/v1/chat/completions" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -d "{ \"model\": \"gpt-5.6\", \"messages\": [$USER_MSG, $ASSISTANT, $TOOL_MSG] }" \ | python3 -c 'import json,sys; print(json.load(sys.stdin)["choices"][0]["message"]["content"])' ```
```python theme={null} from exa_py import Exa exa = Exa() result = exa.get_contents( ["https://www.anthropic.com/pricing"], highlights={"query": "enterprise plan features and pricing"}, ) print(result.results[0].highlights) ```
```javascript theme={null} import Exa from "exa-js"; const exa = new Exa(); const result = await exa.getContents( ["https://www.anthropic.com/pricing"], { highlights: { query: "enterprise plan features and pricing", }, }, ); console.log(result.results[0].highlights); ```
```bash theme={null} curl -s -X POST "https://api.exa.ai/contents" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "ids": ["https://www.anthropic.com/pricing"], "highlights": { "query": "enterprise plan features and pricing" } }' ```
```python theme={null} import json from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query="Find 10 seed-stage companies building infrastructure for AI coding agents.", output_schema={ "type": "object", "required": ["companies"], "properties": { "companies": { "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["name", "website"], "properties": { "name": {"type": "string"}, "website": {"type": "string", "format": "uri"}, }, }, } }, }, effort="auto", ) run = exa.agent.runs.poll_until_finished(run.id) print(json.dumps(run.output.structured if run.output else None, indent=2)) ```
```javascript theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "Find 10 seed-stage companies building infrastructure for AI coding agents.", outputSchema: { type: "object", required: ["companies"], properties: { companies: { type: "array", maxItems: 10, items: { type: "object", required: ["name", "website"], properties: { name: { type: "string" }, website: { type: "string", format: "uri" }, }, }, }, }, }, effort: "auto", }); const completedRun = await exa.agent.runs.pollUntilFinished(run.id); console.log(JSON.stringify(completedRun.output?.structured, null, 2)); ```
```bash theme={null} RUN_ID="$(curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Find 10 seed-stage companies building infrastructure for AI coding agents.", "effort": "auto", "outputSchema": { "type": "object", "required": ["companies"], "properties": { "companies": { "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["name", "website"], "properties": { "name": { "type": "string" }, "website": { "type": "string", "format": "uri" } } } } } } }' | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')" while true; do RUN_JSON="$(curl -s "https://api.exa.ai/agent/runs/$RUN_ID" \ -H "Authorization: Bearer $EXA_API_KEY")" STATUS="$(printf '%s' "$RUN_JSON" | python3 -c 'import json,sys; print(json.load(sys.stdin)["status"])')" if [ "$STATUS" = "completed" ]; then printf '%s' "$RUN_JSON" | python3 -c 'import json,sys; print(json.dumps(json.load(sys.stdin)["output"]["structured"], indent=2))' break elif [ "$STATUS" = "failed" ] || [ "$STATUS" = "cancelled" ]; then printf '%s' "$RUN_JSON" break fi sleep 4 done ```
```python theme={null} from exa_py import Exa exa = Exa() run = exa.agent.runs.create( query=( "Analyze how the EU AI Act affects startups selling AI products in Europe. " "Cover key dates, obligations, and practical risks, citing official sources." ), effort="medium", ) run = exa.agent.runs.poll_until_finished(run.id) print(run.output.text if run.output else None) ```
```javascript theme={null} import Exa from "exa-js"; const exa = new Exa(); const run = await exa.agent.runs.create({ query: "Analyze how the EU AI Act affects startups selling AI products in Europe. Cover key dates, obligations, and practical risks, citing official sources.", effort: "medium", }); const completedRun = await exa.agent.runs.pollUntilFinished(run.id); console.log(completedRun.output?.text); ```
```bash theme={null} RUN_ID="$(curl -s -X POST "https://api.exa.ai/agent/runs" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Analyze how the EU AI Act affects startups selling AI products in Europe. Cover key dates, obligations, and practical risks, citing official sources.", "effort": "medium" }' | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')" while true; do RUN_JSON="$(curl -s "https://api.exa.ai/agent/runs/$RUN_ID" \ -H "Authorization: Bearer $EXA_API_KEY")" STATUS="$(printf '%s' "$RUN_JSON" | python3 -c 'import json,sys; print(json.load(sys.stdin)["status"])')" if [ "$STATUS" = "completed" ]; then printf '%s' "$RUN_JSON" | python3 -c 'import json,sys; print(json.load(sys.stdin)["output"]["text"])' break elif [ "$STATUS" = "failed" ] || [ "$STATUS" = "cancelled" ]; then printf '%s' "$RUN_JSON" break fi sleep 4 done ```
Open in playground Open in playground Read the guide Open in playground Open in playground Open in playground

Available APIs

# Baseten Source: https://exa.ai/docs/integrations/baseten Ground open-source models on Baseten Model APIs with Exa web search through Baseten Hosted Tools. Exa is a web search provider in [Baseten Hosted Tools](https://www.baseten.co/blog/introducing-baseten-hosted-tools/). Baseten Model APIs serve open-source models, and Hosted Tools let those models search the web without you wiring up a tool loop: you add an Exa tool selector to a standard request, Baseten runs the model and the Exa searches together in a server-side loop, and you get back a grounded answer in the same response. No Exa API key is needed. Baseten passes Exa's cost through to your Baseten bill with no markup. ## Use the Exa web search tools Set the `x-baseten-server-tools: true` header, and add one or more Exa selectors to your `tools` array. Only the `type` is needed; Baseten expands the tool schema automatically, and the model decides when to search, what to search for, and which pages to read. Server-side tools work on Baseten's [Chat Completions](https://docs.baseten.co/reference/inference-api/chat-completions), [Messages](https://docs.baseten.co/reference/inference-api/messages), and Responses endpoints, buffered or streaming. ```python Python theme={null} from openai import OpenAI client = OpenAI( api_key="", base_url="https://inference.baseten.co/v1", default_headers={"x-baseten-server-tools": "true"}, ) response = client.chat.completions.create( model="zai-org/GLM-5.3-Fast", messages=[ {"role": "user", "content": "What were the major AI announcements this week?"} ], tools=[ {"type": "baseten__exa__web_search_exa"}, {"type": "baseten__exa__web_fetch_exa"}, ], extra_body={"baseten": {"tool_settings": {"max_react_iterations": 5}}}, ) print(response.choices[0].message.content) ``` ```javascript JavaScript theme={null} import OpenAI from "openai"; const client = new OpenAI({ apiKey: "", baseURL: "https://inference.baseten.co/v1", defaultHeaders: { "x-baseten-server-tools": "true" }, }); const response = await client.chat.completions.create({ model: "zai-org/GLM-5.3-Fast", messages: [ { role: "user", content: "What were the major AI announcements this week?" }, ], tools: [ { type: "baseten__exa__web_search_exa" }, { type: "baseten__exa__web_fetch_exa" }, ], baseten: { tool_settings: { max_react_iterations: 5 } }, }); console.log(response.choices[0].message.content); ``` ```bash cURL theme={null} curl https://inference.baseten.co/v1/chat/completions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -H "x-baseten-server-tools: true" \ -d '{ "model": "zai-org/GLM-5.3-Fast", "messages": [ { "role": "user", "content": "What were the major AI announcements this week?" } ], "tools": [ { "type": "baseten__exa__web_search_exa" }, { "type": "baseten__exa__web_fetch_exa" } ], "baseten": { "tool_settings": { "max_react_iterations": 5 } } }' ``` Three Exa tools are available. Give the model search plus fetch when it should discover sources and then read the pages it picks. | Selector | What the model gets | | --------------------------------------- | ------------------------------------------------------------------------------------- | | `baseten__exa__web_search_exa` | [Exa search](/docs/search/quickstart): relevant results with page content for a query | | `baseten__exa__web_search_advanced_exa` | Search with domain filters, subpage crawling, and an optional summary per result | | `baseten__exa__web_fetch_exa` | [Full page contents](/docs/contents/quickstart) for a URL the model already has | Selectors take no extra fields; the model fills in the tool arguments from Exa's schema. Use the system prompt to steer search policy, such as when to search, whether to fetch primary sources, and how to cite. Use `baseten.tool_settings` to bound the loop: | Setting | Use it to | | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `max_react_iterations` | Cap model iterations per request (default 12, range 2 to 20). The last iteration is reserved for the answer, so `N` allows `N - 1` rounds of tool calls. | | `max_tool_calls_per_iteration` | Cap server-side tool calls in one iteration (default 10, range 1 to 10) | ## How results come back The final answer arrives through the endpoint's normal field. Completed Exa calls are recorded per protocol: `tool_use` and `tool_result` blocks on Messages, `mcp_call` items on Responses, and `baseten.iterations[].continuation_messages` on Chat Completions. Streaming requests receive each search call and result as server-sent events while the loop runs, so you can show progress before the answer lands. The `baseten.request.server_tool_calls[]` array reports the outcome of every Exa call in the request. ## Pricing Exa calls bill to your Baseten account at Exa's rate with no markup, in addition to the model's token costs: about \$0.007 per search and \$0.001 per fetched URL. Exa reports the charge for each call at runtime, so individual calls can deviate from these figures. Billed tool calls appear in Baseten workspace settings under Billing → Usage, grouped by provider. See [Baseten's pricing table](https://docs.baseten.co/inference/model-apis/web-search#pricing) for current rates. Hosted Tools are in early access on Baseten with a 25 requests per minute limit per organization. Try Exa search in the [Baseten playground](https://app.baseten.co/model-apis/zai-org/GLM-5.3-Fast/playground), or contact Baseten to raise the limit for production workloads. ## Resources Runnable Messages, Responses, and Chat Completions examples with server-side tools. Tool catalog, loop settings, `tool_choice` formats, and response shapes. # Browserbase Source: https://exa.ai/docs/integrations/browserbase Combine Exa company search with Browserbase browser automation for job application workflows. Use Exa to find companies and careers pages, then use Browserbase and Stagehand to inspect and interact with those pages. ## Install Install the packages used by the Browserbase Exa template: ```bash npm theme={null} npm install @browserbasehq/stagehand dotenv exa-js zod ``` ## Configure environment variables Set the API keys used by Exa and Browserbase: ```bash .env theme={null} BROWSERBASE_API_KEY=your-browserbase-api-key EXA_API_KEY=your-exa-api-key ``` ## Search and interact with a page The following example follows the template's workflow: search for companies, find a careers page, open it in a Browserbase session, extract the job description, and let a Stagehand agent interact with the page. ```typescript quickstart.ts theme={null} import "dotenv/config"; import { Stagehand } from "@browserbasehq/stagehand"; import Exa from "exa-js"; import { z } from "zod"; const exa = new Exa(process.env.EXA_API_KEY); const companies = await exa.search("AI startups in SF", { category: "company", type: "auto", numResults: 5, contents: { text: true }, }); const company = companies.results[0]; if (!company?.url) { throw new Error("No matching company found"); } const companyDomain = new URL(company.url).hostname.replace("www.", ""); const careers = await exa.search(`${companyDomain} careers page`, { excludeDomains: ["linkedin.com"], type: "deep", numResults: 5, contents: { text: true }, }); const careersUrl = careers.results[0]?.url; if (!careersUrl) { throw new Error("No careers page found"); } const stagehand = new Stagehand({ env: "BROWSERBASE", model: "google/gemini-2.5-pro", }); try { await stagehand.init(); const page = stagehand.context.pages()[0]; await page.goto(careersUrl); const jobDescription = await stagehand.extract( "Extract the job title, requirements, responsibilities, and other important details from this page.", z.object({ jobTitle: z.string(), requirements: z.array(z.string()), responsibilities: z.array(z.string()), details: z.string(), }), ); const agent = stagehand.agent({ mode: "hybrid", model: "google/gemini-3-flash-preview", systemPrompt: "Interact with the page without submitting an application.", }); const result = await agent.execute({ instruction: `Review this job posting and identify the next application step. Job details: ${JSON.stringify(jobDescription)}`, maxSteps: 10, }); console.log(result); } finally { await stagehand.close(); } ``` The template includes the complete workflow for extracting job details, generating tailored responses, and filling application forms. See the [TypeScript implementation](https://github.com/browserbase/templates/tree/dev/typescript/exa-browserbase) or [Python implementation](https://github.com/browserbase/templates/tree/dev/python/exa-browserbase). # Exa in Codex and ChatGPT Source: https://exa.ai/docs/integrations/chatgpt-codex Search the web, read any page, and research with Exa directly from Codex and ChatGPT. Install the Exa plugin once to give Codex and ChatGPT access to the live web through Exa. Search for up-to-date information, read the sources that matter, and run deeper research without leaving your conversation or coding session. ## Install Exa Go to [chatgpt.com/plugins/exa](https://chatgpt.com/plugins/exa?open_in_app). It opens **Exa** in OpenAI's plugin directory, which is the same directory for ChatGPT and for Codex. Select the plus button to install. Sign in to Exa when prompted, either during install or the first time Codex or ChatGPT uses it. Opening Plugins in Codex, viewing the Exa plugin, and authorizing access Skills load in chats and CLI sessions started after the install, so open a fresh one and ask for something that needs the web. That's it. The plugin includes both Exa's MCP integration and skills, so there's no separate MCP or skill setup required. ## Build with what's on the web right now The libraries, APIs, and tools you build with change every day. With Exa installed, Codex can search for the latest documentation, issues, changelogs, and real-world examples while it works. From inside your repo: ```text theme={null} We're on Tailwind v3. Search for the Tailwind v4 upgrade guide, read it, then migrate this project to v4. ``` Codex can search with Exa, read the relevant sources, and use what it finds to make the change in your codebase. The same works whenever the answer might be somewhere outside your repo: * "Search the `tokio-tungstenite` issues and changelog for this error before you try to fix it." * "Find real examples of Postgres advisory locks in Rust and recommend the pattern that fits this worker pool." * "Read the current Stripe webhook docs and check our implementation against them." * "Search for the latest migration guide for this dependency, then upgrade it." ## Search, read, and research The Exa plugin gives Codex and ChatGPT three ways to work with the web. Search in natural language and get the content of the best pages back, not a list of links. Read a page you point it at, whether that's docs, a changelog, an issue, or a blog post. Work through a question that takes more than one search, and answer it with citations. ## Research without leaving ChatGPT Exa works in ChatGPT too. Ask a question that needs fresh information and use Exa to search and research the web from the conversation. ```text theme={null} Compare the managed offerings, licensing, and pricing of the main open source vector databases. Use current primary sources and cite them. ``` Instead of relying only on information already in context, ChatGPT can use Exa to find and read the sources needed for the task. Use it for competitive research, technical research, market mapping, company research, or anything else where the answer lives across the web. ## MCP + skills, together Under the hood, the plugin combines two parts of Exa's agent stack. [Exa MCP](/docs/get-started/exa-mcp) gives Codex and ChatGPT tools for accessing Exa. It's the connection between the agent and Exa's search and research capabilities. [Exa skills](/docs/get-started/agent-skills/overview) give the agent additional instructions for using those capabilities in useful workflows, including web research and [Exa Agent](/docs/agent/quickstart). You don't need to configure either separately when you install the plugin. ## Prefer MCP directly? The plugin is the recommended way to use Exa with Codex and ChatGPT. If you're configuring Codex manually or using another MCP client, you can connect directly to Exa's hosted MCP server: ```bash theme={null} codex mcp add exa --url https://mcp.exa.ai/mcp ``` See [Exa MCP](/docs/get-started/exa-mcp) for other clients, configuration options, and available tools. Add the Exa plugin from the ChatGPT marketplace. # Exa in Claude Code, Web, and Desktop Source: https://exa.ai/docs/integrations/claude-web-desktop Search the web and read any page with Exa directly from Claude Install Exa in Claude Code or connect it to Claude Web, Desktop, and Cowork to give Claude access to current information from the web. Claude can search in natural language, read the pages that matter, and use those sources while it works. ## Install Exa
In a new Claude chat, select the plus button, choose **Add connector**, and search for **Exa**. Open Exa, select **Connect to Claude**, and authorize access when prompted. Opening the connector directory in Claude, finding Exa, connecting it, and authorizing access Start a new chat and ask for something that needs current information from the web. Install Exa from the terminal: ```bash theme={null} claude plugin install exa@claude-plugins-official ``` You can also type `/plugin` in Claude Code, search for **Exa**, and install it. Open a new Claude Code session so the plugin loads, then ask for something that needs the web. Opening a new Claude Code session and asking for something that needs the web
Both options make Exa available without editing an MCP configuration file. ## Work with what's on the web right now In Claude Code, Exa can search current documentation, issues, changelogs, and real-world examples while working in your repository. The same integration gives Claude Web, Desktop, and Cowork current news, research, company information, product details, and other sources that may not already be in context. ```text theme={null} We're on Tailwind v3. Use Exa to find and read the official Tailwind v4 upgrade guide, then migrate this project to v4. ``` Claude Code can use what it finds to make the change in your codebase. In other Claude clients, it can use the same sources in answers, artifacts, and Cowork tasks. The same pattern works whenever the answer depends on current or specific web sources: * "Find the latest release notes for this dependency and summarize the breaking changes." * "Search for recent primary research on inference-time scaling and compare the methods." * "Read the current Stripe webhook documentation and explain the recommended retry behavior." * "Find official pricing pages for these products and compare their entry-level plans." ## Search, read, and research The Exa integration gives Claude tools to search and read the web, which it can combine across a longer research task. Search in natural language and get relevant page content, not only a list of links. Read a page you point to, including documentation, research, changelogs, issues, and articles. Run multiple searches, inspect useful pages, and combine the evidence into a sourced response. ## Research without leaving Claude Ask for the result you want and tell Claude what kinds of sources matter: ```text theme={null} Compare the managed offerings, licensing, and pricing of the main open source vector databases. Use current primary sources and cite them. ``` Claude can use Exa throughout the conversation to find and read the sources needed for the task. Use this for technical research, competitive analysis, market mapping, company research, or any question whose answer lives across the web. ## Use Exa in Cowork The same connector is available in Cowork. Give Claude a task that depends on outside information, and it can search or read pages while working with your files and other connected tools. ```text theme={null} Review this competitive brief, verify every pricing claim against the current vendor pages with Exa, and update the document with citations. ``` ## Prefer MCP directly? If you’re configuring Claude manually or using another MCP client, you can connect directly to Exa’s hosted MCP server: ```bash theme={null} claude mcp add --transport http exa https://mcp.exa.ai/mcp ``` See [Exa MCP](/docs/get-started/exa-mcp) for other clients, configuration options, and available tools. Add Exa in Claude's connector directory. # CrewAI Source: https://exa.ai/docs/integrations/crewai Learn how to add Exa retrieval capabilities to your CrewAI agents. New to Exa? Get started in under a minute. *** [CrewAI](https://crewai.com/) is a framework for orchestrating AI agents that work together to accomplish complex tasks. In this guide, we'll create a crew of two agents that generate a newsletter based on Exa's search results. We'll go over how to: 1. Create a custom Exa-powered CrewAI tool 2. Set up agents and assign them specific roles that use the Exa-powered search tool 3. Organize the agents into a crew that will write a newsletter CrewAI also ships a built-in [`ExaSearchTool`](https://docs.crewai.com/en/tools/search-research/exasearchtool) you can drop in without writing a custom wrapper. The custom tool below is useful if you want full control over how results are formatted; either approach works. *** ## Get started Install the crewAI core, crewAI tools and Exa Python SDK libraries. ```Python Python theme={null} pip install crewai 'crewai[tools]' exa_py ``` We set up a [custom tool](https://docs.crewai.com/concepts/tools) using the crewAI [@tool decorator ](https://docs.crewai.com/concepts/tools#utilizing-the-tool-decorator). Within the tool, we can initialize the Exa class from the [Exa Python SDK](https://github.com/exa-labs/exa-py), make a request, and return a parsed out result. ```Python Python theme={null} from crewai_tools import tool from exa_py import Exa import os exa_api_key = os.getenv("EXA_API_KEY") @tool("Exa search and get contents") def search_and_get_contents_tool(question: str) -> str: """Tool using Exa's Python SDK to run semantic search and return result highlights.""" exa = Exa(api_key=exa_api_key) response = exa.search( question, type="auto", num_results=10, contents={"highlights": True} ) parsedResult = ''.join([ f'{eachResult.title}' f'{eachResult.url}' f'{"".join(eachResult.highlights)}' for (idx, eachResult) in enumerate(response.results) ]) return parsedResult ``` Make sure your API keys are initialized properly. For this demonstration, the environment variable names are `OPENAI_API_KEY` and `EXA_API_KEY` for OpenAI and Exa keys respectively. Create a key in the dashboard. New accounts start with free credits. Import the relevant crewAI modules. Then, define `exa_tools` to reference the custom search method we defined above. ```Python Python theme={null} from crewai import Task, Crew, Agent exa_tools = search_and_get_contents_tool ``` We then set up[ two agents](https://docs.crewai.com/concepts/Agents/) and place them in a [crew together](https://docs.crewai.com/concepts/Crews/): * One to research with Exa (providing the custom tool defined above) * Another to write a newsletter as an output (using an LLM) ```Python Python theme={null} # Creating a senior researcher agent with memory and verbose mode researcher = Agent( role='Researcher', goal='Get the latest research on {topic}', verbose=True, memory=True, backstory=( "Driven by curiosity, you're at the forefront of" "innovation, eager to explore and share knowledge that could change" "the world." ), tools=[exa_tools], allow_delegation=False ) article_writer = Agent( role='Writer', goal='Write a great newsletter article on {topic}', verbose=True, memory=True, backstory=( "Driven by a love of writing and passion for" "innovation, you are eager to share knowledge with" "the world." ), tools=[exa_tools], allow_delegation=False ) ``` Next, we'll define [tasks](https://docs.crewai.com/concepts/Tasks/) for each agent and create the crew overall using all of the components we've set up above. ```Python Python theme={null} research_task = Task( description=( "Identify the latest research in {topic}." "Your final report should clearly articulate the key points," ), expected_output='A comprehensive 3 paragraphs long report on the {topic}.', tools=[exa_tools], agent=researcher, ) write_article = Task( description=( "Write a newsletter article on the latest research in {topic}." "Your article should be engaging, informative, and accurate." "The article should address the audience with a greeting to the newsletter audience \"Hi readers!\", plus a similar signoff" ), expected_output='A comprehensive 3 paragraphs long newsletter article on the {topic}.', agent=article_writer, ) crew = Crew( agents=[researcher, article_writer], tasks=[research_task, write_article], memory=True, cache=True, max_rpm=100, share_crew=True ) ``` Finally, we kick off the crew by providing a research topic as our input query. ```Python Python theme={null} response = crew.kickoff(inputs={'topic': 'Latest AI research'}) print(response) ``` The crew writes the newsletter from the content the Exa search tool returned. # ElevenLabs Source: https://exa.ai/docs/integrations/elevenlabs Add Exa web search to ElevenLabs voice agents. *** ElevenLabs voice agents can search the web mid-conversation using Exa as a **webhook tool**. When the agent decides it needs current information, ElevenLabs makes an HTTP POST directly to Exa's `/search` endpoint — no server or middleware required on your side. There are two ways to connect Exa to ElevenLabs: | Approach | Setup | Flexibility | | ------------------------------------ | --------------------------------- | ------------------------------------------------------------- | | **Webhook tool** (recommended) | Configure via API or dashboard | Full control over search params, content options, and headers | | **Built-in Exa integration** (alpha) | One-click in ElevenLabs dashboard | Simpler but limited configuration | This guide covers the webhook tool approach, which gives you full control over how Exa is called. You can also configure the integration through the [ElevenLabs dashboard](https://elevenlabs.io/app/conversational-ai). ## How it works 1. User speaks to the voice agent 2. The LLM decides to call `web_search` based on the tool description 3. ElevenLabs POSTs to `https://api.exa.ai/search` with headers and body you configured 4. LLM-determined params (the search `query`) get merged with your constant values (`type`, `numResults`, `contents`) 5. Exa results flow back to the LLM, which responds conversationally No server, no callback URL, no listener. ElevenLabs is the HTTP client calling Exa directly. Tool calls have a 20-second timeout. ## Prerequisites * An [Exa API key](https://dashboard.exa.ai/api-keys) * An [ElevenLabs API key](https://elevenlabs.io/app/settings/api-keys) Create a key in the dashboard. New accounts start with free credits. ## Get started Use the ElevenLabs [Create Tool API](https://elevenlabs.io/docs/api-reference/tools/create) to register a webhook tool that points to Exa's search endpoint. The key concept: properties with `constant_value` are fixed (sent on every request), while properties with `description` are determined by the LLM at runtime. ```bash bash theme={null} curl -s -X POST "https://api.elevenlabs.io/v1/convai/tools" \ -H "xi-api-key: $ELEVENLABS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "tool_config": { "type": "webhook", "name": "web_search", "description": "Search the web using Exa. Use this when the user asks anything that needs current or factual information.", "api_schema": { "url": "https://api.exa.ai/search", "method": "POST", "request_headers": { "x-api-key": "YOUR_EXA_API_KEY", "Content-Type": "application/json", "x-exa-integration": "elevenlabs" }, "request_body_schema": { "type": "object", "properties": { "query": { "type": "string", "description": "Natural language search query. Be specific." }, "type": { "type": "string", "constant_value": "instant" }, "numResults": { "type": "integer", "constant_value": 5 }, "contents": { "type": "object", "properties": { "highlights": { "type": "boolean", "constant_value": true } } } }, "required": ["query"] } } } }' ``` This creates a tool where: * `query` — the LLM fills this based on conversation context * `type: "instant"` — uses Exa's fastest search mode (\~150ms) * `numResults: 5` — returns 5 results per search * `contents.highlights: true` — returns token-efficient highlighted snippets (best for voice latency) Save the returned `id` — you'll need it to wire the tool to an agent. If you already have an agent, you can skip step 2 and add the tool to your existing agent in the ElevenLabs dashboard under **Agent > Tools**, or via the [Update Agent API](https://elevenlabs.io/docs/api-reference/agents/update). The tool won't do anything until it's attached to an agent. Create a conversational agent and attach the webhook tool by its ID. ```bash bash theme={null} curl -s -X POST "https://api.elevenlabs.io/v1/convai/agents/create" \ -H "xi-api-key: $ELEVENLABS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Exa Search Assistant", "conversation_config": { "agent": { "prompt": { "prompt": "You are a helpful voice assistant with real-time web search powered by Exa. When users ask questions that need current information, use the web_search tool.\n\nGuidelines:\n- Search proactively for time-sensitive or factual questions.\n- Summarize results conversationally — do not read URLs aloud.\n- Cite sources naturally.\n- Keep responses concise — this is voice.", "tool_ids": ["YOUR_TOOL_ID"] }, "first_message": "Hey! I can search the web for you in real-time. What would you like to know?" } } }' ``` The response includes an `agent_id`. Open the agent in the ElevenLabs dashboard to test it: ```text theme={null} https://elevenlabs.io/app/conversational-ai/agents/YOUR_AGENT_ID ``` Add the agent to any webpage with two lines of HTML: ```html html theme={null} ``` ## Full Python example This script creates both the webhook tool and agent in one run: ```python python theme={null} import os import requests ELEVENLABS_API_KEY = os.environ["ELEVENLABS_API_KEY"] EXA_API_KEY = os.environ["EXA_API_KEY"] BASE = "https://api.elevenlabs.io/v1/convai" HEADERS = {"xi-api-key": ELEVENLABS_API_KEY, "Content-Type": "application/json"} # 1. Create webhook tool tool_resp = requests.post(f"{BASE}/tools", headers=HEADERS, json={ "tool_config": { "type": "webhook", "name": "web_search", "description": ( "Search the web using Exa. Use this when the user asks anything " "that needs current or factual information." ), "api_schema": { "url": "https://api.exa.ai/search", "method": "POST", "request_headers": { "x-api-key": EXA_API_KEY, "Content-Type": "application/json", "x-exa-integration": "elevenlabs", }, "request_body_schema": { "type": "object", "properties": { "query": { "type": "string", "description": "Natural language search query. Be specific.", }, "type": {"type": "string", "constant_value": "instant"}, "numResults": {"type": "integer", "constant_value": 5}, "contents": { "type": "object", "properties": { "highlights": { "type": "boolean", "constant_value": True, } }, }, }, "required": ["query"], }, }, } }) tool_resp.raise_for_status() tool_id = tool_resp.json()["id"] print(f"Tool created: {tool_id}") # 2. Create agent agent_resp = requests.post(f"{BASE}/agents/create", headers=HEADERS, json={ "name": "Exa Search Assistant", "conversation_config": { "agent": { "prompt": { "prompt": ( "You are a helpful voice assistant with real-time web search " "powered by Exa. When users ask questions that need current " "information, use the web_search tool.\n\n" "Guidelines:\n" "- Search proactively for time-sensitive or factual questions.\n" "- Summarize results conversationally — do not read URLs aloud.\n" "- Cite sources naturally.\n" "- Keep responses concise — this is voice." ), "tool_ids": [tool_id], }, "first_message": "Hey! I can search the web for you. What would you like to know?", } }, }) agent_resp.raise_for_status() agent_id = agent_resp.json()["agent_id"] print(f"Agent created: {agent_id}") print(f"Dashboard: https://elevenlabs.io/app/conversational-ai/agents/{agent_id}") ``` Run it: ```bash bash theme={null} export ELEVENLABS_API_KEY="your-key" export EXA_API_KEY="your-key" python elevenlabs_exa_webhook.py ``` ## Customizing search parameters The webhook tool body schema maps directly to [Exa's search API](/docs/reference/search). Here are common configurations: ### Search type Control the speed/quality tradeoff with the `type` constant: | Type | Latency | Best for | | --------- | ------- | --------------------------------- | | `instant` | \~150ms | Voice conversations (recommended) | | `auto` | \~1s | General use | For voice agents, start with `instant`. Use `auto` when you want Exa to choose the best current search mode for each query. ### Content options Choose how results are returned via the `contents` object: ```json json theme={null} { "contents": { "type": "object", "properties": { "highlights": { "type": "boolean", "constant_value": true } } } } ``` * **`highlights`** — Token-efficient excerpts. Use this when you want relevant snippets without overwhelming the LLM context. Pass `true` for the highest-quality default. * **`text`** — Full page markdown. Use when the agent needs complete page content. Set `maxCharacters` to limit length. * **`summary`** — LLM-generated summary of each page. Higher latency but provides synthesized content. For voice agents, `highlights: true` is the recommended default — it balances relevance with response speed. ### Filtering results Add domain or date filters as constants: ```json json theme={null} { "includeDomains": { "type": "array", "constant_value": ["reuters.com", "apnews.com", "bbc.com"] } } ``` ```json json theme={null} { "startPublishedDate": { "type": "string", "constant_value": "2025-01-01T00:00:00.000Z" } } ``` ### Number of results Adjust `numResults` based on your use case. For voice, 3-5 results keep responses fast. For research-oriented agents, 10+ gives broader coverage. ## Schema reference ElevenLabs webhook tools use a JSON schema with these property types: * **`constant_value`** — Fixed value sent on every request. The LLM never sees or modifies it. Works for strings, numbers, booleans. * **`description`** — The LLM determines the value at runtime based on this description. Use for dynamic params like `query`. * **Nested objects** — Use `type: "object"` with `properties` to build nested structures like `contents.highlights`. Each parameter has a mode toggle in the dashboard — **Fixed** or **LLM**: ElevenLabs webhook tool parameter configuration showing Fixed vs LLM mode toggles Parameters set to **Fixed** (marked with `constant_value` in the API) are sent as-is on every request. Parameters set to **LLM** (marked with `description`) let the model choose the value at runtime. Keep as many parameters Fixed as possible — every LLM-determined parameter adds a tool-calling step that increases response latency. For the full ElevenLabs webhook tool schema, see the [ElevenLabs server tools documentation](https://elevenlabs.io/docs/conversational-ai/customization/tools/server-tools). ## Built-in Exa integration (alpha) ElevenLabs also offers a built-in Exa integration available in the agent dashboard under **Tools > Integrations**. This is simpler to set up, but customizing search parameters is harder compared to the webhook tool approach. For full control over search type, content options, and filtering, the webhook tool approach described above is recommended. # Exa for Google Sheets Source: https://exa.ai/docs/integrations/exa-for-sheets Use Exa Agent and Exa formulas inside Google Sheets. **Multiple Google accounts:** The add-on must run under the first (default) Google account in your browser profile. If you are logged into multiple accounts, you may not be able to save or load your API key. To fix this, open Sheets in an incognito window with only one account, or sign out of extra accounts so the account you want is the default. [Learn more](https://developers.google.com/apps-script/guides/projects#fix_issues_with_multiple_google_accounts). Use Exa inside Google Sheets to research the web, generate tables, and fill missing data. The add-on gives you two ways to work: * **Exa Agent** for full tables and multi-cell tasks * **`=EXA(...)`** for one answer in one cell ## Install Go to the [Exa AI add-on](https://workspace.google.com/marketplace/app/exa_ai/465545439521) in the Google Workspace Marketplace and click **Install**. Open a new or existing spreadsheet. Go to **Extensions → Exa AI → Open Sidebar**. Create a key in the dashboard. New accounts start with free credits. Paste the key in the sidebar. Open **Exa Agent** and start using Exa in your sheet. ## Exa Agent Exa Agent lets you use Exa across multiple cells in Google Sheets. Use it when you want to: * generate a full table from one prompt * fill missing cells in an existing table * continue a table by adding new rows * enrich a list with web data ### Generate a table Use **Generate table** when you want Exa to create a new table. 1. Open the sidebar. 2. Go to **Exa Agent**. 3. Choose **Generate table**. 4. Write what you want. 5. Click **Generate table**. Example prompt: ```text theme={null} Find top 40 AI companies and return company name, website URL, CEO, founding date, headquarters, and a short description. ``` Exa researches the web and writes the table into your sheet. By default, the table starts at the selected cell. You can choose another start cell in **More options**. ### Fill cells Use **Fill cells** when you already have a table and want Exa to fill missing data. 1. Select the blank cells in your sheet. 2. Open **Exa Agent**. 3. Choose **Fill cells**. 4. Click **Fill selected cells**. Exa looks at the table around your selection and fills the blanks. Select blank cells in a table that already has clear headers before you use **Fill cells**. Example: | Company | Website | CEO | Headquarters | | ------- | ---------------------------------------- | ------------- | ------------- | | Apple | [https://apple.com](https://apple.com) | | | | Google | [https://google.com](https://google.com) | Sundar Pichai | Mountain View | Select the blank cells for Apple, then click **Fill selected cells**. Exa uses the company name and the nearby rows as context. ### Continue rows You can also select blank rows under a table. If your table ends at rank 55 and you select the next two blank rows, Exa can continue the table with rank 56 and rank 57. Exa uses the existing rows as examples, keeps the same columns, and avoids repeating items already in the table. ## `=EXA(...)` Use `=EXA(...)` when you want one answer in one cell. It searches the web, reads the top results, and returns a concise answer. ```text theme={null} =EXA("what you want", cell) ``` | Parameter | Required | Description | | --------- | -------- | ---------------------------------------------------------------- | | `prompt` | Yes | What information you want (e.g., `"Return only the CEO name"`). | | `context` | No | Cell reference or text to enrich (e.g., a company name in `A2`). | Examples: ```text theme={null} =EXA("Return only the company website URL", A2) =EXA("Return only the CEO name", A2) =EXA("Return only the headquarters", A2) =EXA("Return the Amazon rating of this product", A2) ``` The second argument is the context. You can drag the formula down a column to run it for many rows. Use `=EXA(...)` for simple one-cell answers. Use **Exa Agent** when you want to create or fill a whole table. ## `=EXA_ANSWER(...)` Advanced AI answers with full control over output format. Use this when you need system prompts, structured JSON output, citations, or a specific search type. ```text theme={null} =EXA_ANSWER(prompt, [prefix], [suffix], [includeCitations], [systemPrompt], [outputSchema], [returnRawJson], [type]) ``` | Parameter | Required | Default | Description | | ------------------ | -------- | -------- | ------------------------------------------------------------------------------------------------------- | | `prompt` | Yes | — | The main question or prompt. | | `prefix` | No | `""` | Text added before the prompt. | | `suffix` | No | `""` | Text added after the prompt. | | `includeCitations` | No | `FALSE` | If `TRUE`, appends numbered source citations. | | `systemPrompt` | No | `""` | System instructions to control output format (e.g., `"only return a number"`). | | `outputSchema` | No | `""` | JSON schema for structured output. [Generate schemas here](https://dashboard.exa.ai/playground/answer). | | `returnRawJson` | No | `FALSE` | If `TRUE` and `outputSchema` is set, returns the full JSON instead of extracting the value. | | `type` | No | `"deep"` | Search type: `"auto"`, `"neural"`, `"fast"`, or `"deep"`. | Examples: ```text theme={null} =EXA_ANSWER("OpenAI CEO", "", "", FALSE, "only return a name") =EXA_ANSWER("Modal AI headcount", "", "", FALSE, "only return a number") =EXA_ANSWER("ceo of exa.ai", "", "", FALSE, "", "{""type"":""object"",""properties"":{""name"":{""type"":""string""}}}") ``` ## `=EXA_SEARCH(...)` Searches the web and returns a vertical list of URLs. Supports domain filtering, category filtering, content highlights, and synthesized output via `outputSchema`. ```text theme={null} =EXA_SEARCH(query, [numResults], [searchType], [prefix], [suffix], [includeDomainsStr], [excludeDomainsStr], [category], [highlightsMaxChars], [outputSchemaJson]) ``` | Parameter | Required | Default | Description | | -------------------- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `query` | Yes | — | The search query. | | `numResults` | No | `1` | Number of results (1–10). | | `searchType` | No | `"auto"` | `"auto"`, `"neural"`, or `"keyword"`. | | `prefix` | No | `""` | Text added before the query. | | `suffix` | No | `""` | Text added after the query. | | `includeDomainsStr` | No | `""` | Comma-separated domains to include (e.g., `"linkedin.com,crunchbase.com"`). | | `excludeDomainsStr` | No | `""` | Comma-separated domains to exclude. | | `category` | No | `""` | Filter by type: `"company"`, `"publication"`, `"news"`, `"personal site"`, `"financial report"`, `"people"`. | | `highlightsMaxChars` | No | `0` | If > 0, requests content highlights with this character limit per result. | | `outputSchemaJson` | No | `""` | JSON string for `outputSchema` (e.g., `"{""type"":""text"",""description"":""summarize""}"`). When set, returns synthesized output text instead of URLs. | Examples: ```text theme={null} =EXA_SEARCH("AI startups", 5, "auto", "", "", "linkedin.com,crunchbase.com") =EXA_SEARCH("transformer architecture", 5, "auto", "", "", "", "", "publication") ``` ## `=EXA_CONTENTS(...)` Extracts the text content from a URL. ```text theme={null} =EXA_CONTENTS(url) ``` | Parameter | Required | Description | | --------- | -------- | ------------------------------------------------- | | `url` | Yes | The full URL (must start with `http` or `https`). | ## `=EXA_FINDSIMILAR(...)` Finds URLs similar to a reference URL, with optional domain and text filters. ```text theme={null} =EXA_FINDSIMILAR(url, [numResults], [includeDomainsStr], [excludeDomainsStr], [includeTextStr], [excludeTextStr]) ``` | Parameter | Required | Default | Description | | ------------------- | -------- | ------- | --------------------------------------- | | `url` | Yes | — | The reference URL. | | `numResults` | No | `1` | Number of results (1–10). | | `includeDomainsStr` | No | `""` | Comma-separated domains to include. | | `excludeDomainsStr` | No | `""` | Comma-separated domains to exclude. | | `includeTextStr` | No | `""` | Phrase that must appear in results. | | `excludeTextStr` | No | `""` | Phrase that must not appear in results. | ## Batch Use **Batch** when you want to work with many Exa formula cells at once. Batch can: * refresh selected cells with Exa formulas * convert selected Exa formulas into normal values Convert formulas to values when you want to keep the current results and stop the formulas from running again. ## When to use what | Task | Use | | ----------------------------------------------------- | -------------------------- | | Create a full table from a prompt | Exa Agent → Generate table | | Fill blank cells in a table | Exa Agent → Fill cells | | Continue a table with new rows | Exa Agent → Fill cells | | Get one value in one cell | `=EXA(...)` | | Get an answer with system prompt or structured output | `=EXA_ANSWER(...)` | | Search and get a list of URLs | `=EXA_SEARCH(...)` | | Extract text from a URL | `=EXA_CONTENTS(...)` | | Find pages similar to a URL | `=EXA_FINDSIMILAR(...)` | | Refresh many Exa formulas | Batch | | Save formula results as plain text | Batch → Convert to values | ## Notes * Exa API requests count against your usage quota. Use **Batch → Convert to values** to freeze results and stop formulas from recalculating. * The add-on automatically retries up to 3 times with exponential backoff when rate limited (HTTP 429). * Start with small batches (10–20 rows) before scaling to hundreds. ## Links * [Install Exa AI for Google Sheets](https://workspace.google.com/marketplace/app/exa_ai/465545439521) * [Get an Exa API key](https://dashboard.exa.ai/api-keys) * [GitHub repository](https://github.com/exa-labs/exa-for-sheets) * [Privacy Policy](https://exa.ai/exa-for-sheets/privacy-policy) # Exa in Slack Source: https://exa.ai/docs/integrations/exa-slack Install Exa in Slack and tag @Exa in any channel or thread for cited research, list-building, and enrichment answers. Bring Exa into your team's Slack. Tag **@Exa** in any channel or thread with a research question, list-building task, or enrichment request. Exa searches the web, reads sources, and replies in-thread with cited answers. ## Get started ### Installation 1. Go to [Dashboard > Management > Exa in Slack](https://dashboard.exa.ai/integrations/slack), then click **Install**. The Exa in Slack page in the Exa dashboard, with the Install button 2. Slack's OAuth flow opens. Pick the workspace you want Exa in, then click **Allow**. Slack's OAuth approval screen for the Exa app, showing the "App is not approved by Slack" notice, a workspace picker, the requested permissions, and the Allow button The red **"App is not approved by Slack"** notice is expected and safe to ignore. It only means Exa isn't in the public Slack Marketplace, not that anything is wrong. 3. Once installed, invite @Exa to a channel (or DM it directly) and start asking questions. ## How to use Exa from Slack In any channel Exa has been added to, mention @Exa with your question: ```text theme={null} @Exa find all Series A fintech startups in SF ``` Exa replies to your question in-thread. ### Follow-ups Once Exa has answered in a thread, just reply in that thread to continue the conversation. No need to mention @Exa again. Exa remembers the conversation, so follow-ups build on the previous answer. Anyone in the thread can follow up. ### Direct messages You can also message Exa directly in a DM. No mention is needed there at all. Each message you send starts a new request, answered in a thread under that message. Reply in the thread to continue that conversation. ### Cancelling a run While a run is in progress, reply in the thread and ask Exa to stop the run. No mention needed. ```text theme={null} Stop the current run ``` ### Exa Connect providers Exa automatically includes [Exa Connect](/docs/agent/connect/overview) data providers when they are relevant to your question. To use a specific provider, mention it in your message: ```text theme={null} @Exa find me all AI infrastructure startups that raised funding this quarter using Fiber.ai ``` For a list of all available data providers, just ask Exa. ## Examples ### News and current events Learn about the latest info on anything. Exa answering a question about the latest news on a topic in a Slack thread, with dated results in a table ### Large list building Prefix the request with `!max` for exhaustive list building. Exa running a !max list-building request in a Slack thread and returning a table of results ## Keywords Use these in a thread Exa is in. Commands can follow an `@Exa` mention or start the message directly: | Keyword | Function | | ----------------- | ---------------------------------------------------------------------------------------------------- | | `!max ` | Runs this request at max effort, designed for building very large lists. | | `mute` | Stops Exa from responding to un-mentioned replies in the thread. Explicit @Exa mentions still work. | | `unmute` | Resumes thread follow-ups after a `mute`. | | `sleep` | Stops Exa from working in the thread entirely. Mention @Exa to wake it up. | | `aside ` | Posts a side comment that Exa ignores, useful for talking to teammates in a thread Exa is following. | | `help` | Shows usage instructions. | ## Permissions The Exa app for Slack requests the following scopes: | Permission | Slack access | Why Exa needs it | | ---------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `app_mentions:read` | View messages that directly mention @Exa | Start a request when someone mentions Exa in a channel or thread | | `assistant:write` | Act as an App Agent in Slack | Use Slack's agent experience and stream answers into DMs and channel threads | | `channels:history` | View messages in public channels Exa has been added to | Receive public-channel thread replies so follow-ups work without another mention | | `channels:read` | View basic information about public channels | Find public channels that already contain Exa when choosing where to sync a web session | | `chat:write` | Send messages as the Exa app | Post thread roots, answers, progress updates, confirmations, and web-synced messages | | `chat:write.customize` | Customize an app-authored message's name and avatar | Show the web participant's name and profile image on messages synchronized from the web app | | `files:read` | View files shared in conversations Exa has been added to | Read files attached to questions | | `files:write` | Upload, edit, and delete files as the Exa app | Attach result files, such as exported tables, to answers | | `groups:history` | View messages in private channels Exa has been added to | Receive private-channel thread replies so follow-ups work without another mention | | `groups:read` | View basic information about private channels Exa has been added to | Find eligible private channels and verify membership when choosing a web-session sync destination | | `im:history` | View messages in direct messages with Exa | Receive DM requests and follow-up replies | | `im:write` | Start direct messages | Open a verified user's Exa DM when they choose it as a web-session sync destination | | `users:read` | View people and their basic Slack profiles | Resolve mentions to names and use a web participant's Slack profile image on synchronized messages | | `users:read.email` | View workspace members' email addresses | Match Slack and Exa accounts for team attribution and customized web-message profile images | `channels:read`, `groups:read`, and `im:write` enable destination discovery for web-to-Slack sync. Existing installations can continue using their current Slack threads without these scopes, but must reconnect before using the corresponding destination. `chat:write.customize` is optional at runtime: without it, web-synced messages keep the standard Exa app identity and include the participant's name in the message body. Exa only receives messages from channels it has been explicitly invited to and from its own DMs. ## Pricing Runs started from Slack are billed to your Exa team. See [pricing](https://exa.ai/pricing) for details. ## Privacy For details on how Exa handles your data, see the [Exa privacy policy](https://exa.ai/privacy-policy). # Grok Build Source: https://exa.ai/docs/integrations/grok-build Use Exa web search in Grok Build. Install the Exa plugin from the Grok Build marketplace and sign in with your Exa account. Exa is available as a plugin on the [Grok Build](https://docs.x.ai/build/overview) marketplace. It gives Grok real-time web search, page reading, and a deep research skill. ## Installation Install the Grok CLI (see the [Grok Build docs](https://docs.x.ai/build/overview) for details): ```bash theme={null} curl -fsSL https://x.ai/cli/install.sh | bash ``` Then sign in to your xAI account: ```bash theme={null} grok login ``` Start Grok Build by running `grok`, then open the marketplace: ```text theme={null} /marketplace ``` Find **exa** in the list and press `i` to install it. Open the MCP servers tab with `/mcp`, select **exa**, and press `i` to sign in. Your browser opens the Exa sign-in page. New accounts get free credits at signup. Once exa shows **ready**, ask Grok anything that needs the web. ## What you get * **web\_search\_exa**: real-time web search. Supports natural language queries and category filters like news, companies, people, research papers, and GitHub. * **web\_fetch\_exa**: reads any URL and returns the page content as clean markdown. * **exa-search skill**: a deep research skill. Ask Grok for a deep dive on a topic and it runs multiple searches, reads the best sources, and answers with citations. ## Example prompts * "Search for recent news about xAI" * "Read [https://exa.ai](https://exa.ai) and summarize it" * "Do a deep dive on open source inference engines" # Gumloop Source: https://exa.ai/docs/integrations/gumloop Use Exa search and contents inside Gumloop flows. [Gumloop](https://www.gumloop.com/) includes Exa as a built-in MCP integration. Add it to an agent or Agent Node to search the web, extract pages, find related sources, and produce citation-backed answers inside a workflow. ## Add Exa to a Gumloop agent Open your agent's configuration, then select **Add tools** → **Connect an app with MCP**. Search for **Exa**, select the integration, and complete the authentication flow. Open the connected Exa integration and enable only the tools the agent needs. This makes tool selection clearer and prevents the agent from calling unrelated actions. Ask the agent: ```text theme={null} Find five recent articles about AI regulation and summarize the key changes with source links. ``` Review the run to confirm that the agent called Exa and returned cited sources. ## Available tools | Tool | Use it for | | ------------------------ | ----------------------------------------------------------- | | **Search** | Find relevant pages with neural or keyword search. | | **Get Contents** | Extract full text, summaries, and metadata from known URLs. | | **Find Similar** | Discover pages related to a source URL. | | **Answer** | Generate a grounded answer with citations. | | **Create Research Task** | Start longer-running research. | | **Get Research Task** | Retrieve a research task's status and result. | For a conversational agent, enable Search, Get Contents, and Answer first. Add the remaining tools only when the workflow requires them. ## Use Exa in a workflow ### Agent Node Add an **Agent Node** to a deterministic Gumloop flow and attach Exa as one of its tools. The node can decide whether to search, retrieve complete pages, or chain several Exa calls before passing its output to the next workflow step. This works well for: * enriching CRM or spreadsheet rows with current web evidence * monitoring news and sending a sourced summary to Slack or email * researching companies before routing records to a sales workflow * comparing products and writing the result to a document ### Reusable custom MCP node For a single repeatable action, create a dedicated node: 1. Open the node library and find Exa. 2. Select **Create a node with AI**. 3. Describe one action, such as `Search for funding announcements from the past seven days`. 4. Test the generated node, verify its inputs and outputs, then save it. Use an Agent Node when the task requires dynamic planning or multiple tools. Use a custom MCP node when the same Exa operation should run predictably on every item. ## Prompt patterns ```text theme={null} Search for official announcements about [topic] published this week. Return the date, publisher, summary, and source URL for each result. ``` ```text theme={null} Given this company name and domain, find its product description, latest funding announcement, and two recent news sources. ``` ```text theme={null} Get the full contents of this URL and extract the pricing tiers as JSON. ``` ## Troubleshooting Reopen the agent's MCP tools, confirm that Exa is connected, and enable the required tool. A connected integration can still have individual tools disabled. Make the request explicit about whether it should search, read known URLs, find similar pages, or answer from sources. Disable Exa tools the agent does not need for that workflow. Replace the general-purpose agent step with a custom Exa MCP node whose inputs and task are fixed. ## Resources Review Gumloop's current tools and Agent Node workflow. Understand the Exa tools exposed through MCP. Learn how to shape search queries and returned contents. # Hermes Agent Source: https://exa.ai/docs/integrations/hermes-agent Give Hermes Agent live web search and page contents with Exa. [Hermes Agent](https://github.com/NousResearch/hermes-agent) includes Exa as a native backend for its model-callable `web_search` and `web_extract` tools. Use Exa for both capabilities, or pair it with another Hermes web provider. ## Connect your Exa account Create a key in the dashboard. New accounts start with free credits. Run the tool setup wizard: ```bash theme={null} hermes tools ``` Open **Web Search & Extract**, choose Exa, and select the API-key-backed option. When prompted, enter your Exa API key. Hermes stores secrets in `~/.hermes/.env` and provider selection in `~/.hermes/config.yaml`. Start Hermes and ask it to search, then read one of the results: ```text theme={null} Search the web for the latest Exa product updates, then read the most relevant result. ``` Hermes should call `web_search`, followed by `web_extract` when it needs the page itself. ## Configure manually Add your key to the Hermes environment file: ```bash ~/.hermes/.env theme={null} EXA_API_KEY=your-exa-api-key ``` Then select Exa for both web capabilities: ```yaml ~/.hermes/config.yaml theme={null} web: search_backend: "exa" extract_backend: "exa" ``` You can use the shared fallback instead: ```yaml ~/.hermes/config.yaml theme={null} web: backend: "exa" ``` The per-capability settings take precedence over `web.backend`. This lets you use Exa only for search or only for extraction when combining providers. ## Tools Hermes gets | Tool | Exa behavior | | ------------- | -------------------------------------------------------------------------------- | | `web_search` | Searches with Exa and returns ranked pages with titles, URLs, and text snippets. | | `web_extract` | Retrieves readable content from one or more URLs through Exa Contents. | Hermes truncates long extracted pages to its configured character budget and stores the complete text on disk. Change the default with `web.extract_char_limit`, or let the agent request a larger `char_limit` for an individual call. Hermes can use Exa through its keyless free-provider pool without an API key. That pool is rate-limited and may rotate among providers. Configure `EXA_API_KEY` and select the API-key-backed Exa option when you need requests to use your Exa account consistently. ## Troubleshooting Run `hermes tools` and explicitly select Exa. If you configure files manually, check that `web.search_backend`, `web.extract_backend`, or `web.backend` is set to `exa`. Add the key to `~/.hermes/.env`, then restart Hermes so it reloads the environment. Hermes can configure search and extraction independently. Set both `web.search_backend` and `web.extract_backend` to `exa`. ## Resources Review Hermes provider selection, caching, and extraction behavior. Learn how Exa searches, filters, and returns page contents. Understand the extraction API behind `web_extract`. # LangChain Source: https://exa.ai/docs/integrations/langchain How to use Exa's integration with LangChain to perform RAG. New to Exa? Get started in under a minute. *** LangChain is a framework for building applications that combine LLMs with data, APIs and other tools. Use Exa's LangChain integration to perform RAG: 1. Set up Exa's LangChain integration and use Exa to retrieve relevant content 2. Connect this content to a toolchain that uses OpenAI's LLM for generation See a YouTube tutorial of a very similar setup by the LangChain team [here](https://www.youtube.com/watch?v=dA1cHGACXCo). See the full reference from LangChain [here](https://python.langchain.com/docs/integrations/providers/exa%5Fsearch/). *** ## Get started Install the core OpenAI and Exa LangChain libraries ```Bash Bash theme={null} pip install langchain-openai langchain-exa ``` Ensure API keys are initialized properly. For LangChain libraries, the environment variable names are `OPENAI_API_KEY` and `EXA_API_KEY` for OpenAI and Exa keys respectively. Create a key in the dashboard. New accounts start with free credits. Set up a Retriever tool using `ExaSearchRetriever`. This is a retriever that connects to Exa Search to find relevant documents via semantic search. First import the relevant libraries and instantiate the ExaSearchRetriever. ```Python Python theme={null} # load the environment variables import os from dotenv import load_dotenv load_dotenv() from langchain_exa import ExaSearchRetriever from langchain_core.prompts import PromptTemplate from langchain_core.runnables import RunnableLambda # Define our retriever to use Exa Search, grabbing 3 results and parsing highlights from each result retriever = ExaSearchRetriever(api_key=os.getenv("EXA_API_KEY"), k=3, highlights=True) ``` We use a LangChain [PromptTemplate](https://python.langchain.com/v0.1/docs/modules/model%5Fio/prompts/quick%5Fstart/#prompttemplate) to define a template of placeholder to parse out URLs and Highlights from the Exa retriever. ```Python Python theme={null} # Define a document prompt template using XML-like tags document_prompt = PromptTemplate.from_template(""" {url} {highlights} """) ``` We use a [Runnable Lambda](https://api.python.langchain.com/en/latest/runnables/langchain%5Fcore.runnables.base.RunnableLambda.html) to parse out the URL and Highlights attributes from the Exa Search results then pass this to the prompt template above ```Python Python theme={null} # Create a Runnable Lambda that parses highlights and URL attributes from the retriever and passes to our document prompt from above document_chain = RunnableLambda( lambda document: { "highlights": document.metadata["highlights"], "url": document.metadata["url"] } ) | document_prompt ``` Complete the retrieval chain by stitching together the Exa retriever, the parser and a short lambda function - this is crucial for passing the result as a single string as context for the LLM in the next step. ```Python Python theme={null} # Define the retrieval chain - Exa search results => grab attributes and parse into XML => join into a single string to feed as context in next steps retrieval_chain = retriever | document_chain.map() | (lambda docs: "\n".join([i.text for i in docs])) ``` In this step, we define the system prompt with Query and Context template inputs to be grabbed from the user and Exa Search respectively. First, once again import the relevant libraries and components from LangChains libraries ```Python Python theme={null} from langchain_core.runnables import RunnablePassthrough, RunnableParallel from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI from langchain_core.output_parsers import StrOutputParser ``` Then we define a generation prompt - the prompt template that is used with context from Exa to perform RAG. ```Python Python theme={null} # Define core prompt template generation_prompt = ChatPromptTemplate.from_messages([ ("system", "You are an expert research assistant. You use xml-formatted context to research people's questions."), ("human", """ Please answer the following query based on the provided context. Please cite your sources at the end of your response.: Query: {query} --- {context} """) ]) ``` We set the generation [LLM to OpenAI](https://python.langchain.com/v0.1/docs/integrations/chat/openai/), then connect everything with a [RunnableParallel](https://python.langchain.com/v0.1/docs/expression%5Flanguage/primitives/parallel/) parallel connection. The generation prompt, containing the query and context, is then passed to the LLM and [parsed for better output representation](https://api.python.langchain.com/en/latest/output%5Fparsers/langchain%5Fcore.output%5Fparsers.string.StrOutputParser.html). ```Python Python theme={null} # Use OpenAI for generation llm = ChatOpenAI(api_key=os.getenv("OPENAI_API_KEY")) # Simple string parsing for the output output_parser = StrOutputParser() # Connect the chain, including parallel connection for query from user and context from Exa retriever chain in step 2. chain = RunnableParallel({ "query": RunnablePassthrough(), "context": retrieval_chain, }) | generation_prompt | llm | output_parser ``` Let's [invoke](https://python.langchain.com/v0.1/docs/expression%5Flanguage/interface/#invoke) the chain: ```Python Python theme={null} result = chain.invoke("Latest research on climate change innovation") print(result) ``` And have a look at the output (newlines parsed): ```Stdout Stdout theme={null} 'Based on the provided context, the latest research on climate change innovation reveals several important findings: 1. Innovation in response to climate change: A study examined how innovation responds to climate change by analyzing a panel dataset of 70 countries. The study found that the number of climate-change-related innovations is positively correlated with increasing levels of carbon dioxide emissions from gas and liquid fuels, mainly from natural gases and petroleum. However, it is negatively correlated with increases in carbon dioxide emissions from solid fuel consumption, mainly from coal, and other greenhouse gas emissions. The research also highlighted that government investment does not always influence decisions to develop and patent climate technologies. This study contributes to the environmental innovation literature by providing insights on how innovation reacts to changes in major climate change factors. 2. Climate tech funding and attention: During the period of 2010-2022, outside of the US, China, EU, and India, only 8% of total climate venture capital activity came from the rest of the world. This concentration of funding and attention in specific regions may be hindering the reach of climate tech solutions to low-income communities and developing countries, which are already feeling the effects of climate change but lack the necessary resources to address them effectively. 3. Research funding allocation: A study from the University of Sussex Business School analyzed research funding for climate and energy research from 1990 to 2020. The research found that 36% of funding was allocated to climate adaptation, while 28% went to studying how to clean up the energy system. Other significant shares of funding were allocated to transport and mobility (13%), geoengineering (12%), and industrial decarbonization (11%). The majority of the funding went to researchers in wealthy, Western countries, which may not be the most vulnerable to the immediate impacts of climate change. Sources: 1. Study on innovation response to climate change: https://www.sciencedirect.com/science/article/pii/S0040162516302542 2. Climate tech funding and attention: https://www.sbs.ox.ac.uk/oxford-answers/climate-tech-opportunity-save-planet 3. Research funding allocation for climate and energy research: https://www.protocol.com/bulletins/climate-research-funding-adaptation' ``` Optionally, you may stream the output of the chain. ```Python Python theme={null} for chunk in chain.stream("Latest research on climate change innovation"): print(chunk, end="|", flush=True) # Or asynchronously async def run_async(): async for chunk in chain.astream("Latest research on climate change innovation"): print(chunk, end="|", flush=True) import asyncio asyncio.run(run_async()) ``` Outputs in a stream. [Learn more](https://python.langchain.com/v0.1/docs/expression%5Flanguage/streaming/) about the `.stream` method, including handling chunks and parsing outputs. # LlamaIndex Source: https://exa.ai/docs/integrations/llamaindex A quick-start guide on how to add Exa retrieval to a LlamaIndex Agent Application. New to Exa? Get started in under a minute. *** LlamaIndex is a framework for building LLM applications powered by structured data. In this guide, we'll use Exa's LlamaIndex integration to: 1. Specify Exa's Search and Retrieve Highlight Tool as a LlamaIndex retriever 2. Set up an OpenAI Agent that uses this tool in its response generation *** ## Get started Install the llama-index, llama-index core, llama-index-tools-exa libraries. OpenAI dependencies are within the core library, so we don't need to specify that. ```Python Python theme={null} pip install llama-index llama-index-core llama-index-tools-exa ``` Also ensure API keys are initialized properly. The following code uses the `EXA_API_KEY` as the relevant environment variable name. Create a key in the dashboard. New accounts start with free credits. Import the relevant Exa integration library and instantiate LlamaIndex's `ExaToolSpec`. ```Python Python theme={null} from llama_index.tools.exa import ExaToolSpec import os exa_tool = ExaToolSpec( api_key=os.environ["EXA_API_KEY"], ) ``` For this example, we are only interested in passing the [search\_and\_retrieve\_highlights](https://docs.llamaindex.ai/en/stable/api_reference/tools/exa/) method to our agent, so we specify this using the `.to_tool_list` LlamaIndex method. We also pass `current_date`, a simple utility so our agent knows the current date. ```Python Python theme={null} print('Tools that are provide by Exa LlamaIndex integration:') print('\n'.join(map(str, (exa_tool.spec_functions)))) search_and_retrieve_highlights_tool = exa_tool.to_tool_list( spec_functions=["search_and_retrieve_highlights", "current_date"] ) ``` Set up the [OpenAIAgent](https://docs.llamaindex.ai/en/stable/examples/agent/Chatbot%5FSEC/), passing the filtered down toolset from above. ```Python Python theme={null} from llama_index.agent.openai import OpenAIAgent agent = OpenAIAgent.from_tools( search_and_retrieve_highlights_tool, verbose=True, ) ``` We can then use the chat method to interact with the agent. ```Python Python theme={null} agent.chat( "Can you summarize the news from the last month related to the US stock market?" ) ``` The agent calls the Exa tools it was given, then answers from the results. The exact output varies with the query and the publication dates of the pages Exa returns. Review Exa search parameters and response fields. Explore LlamaIndex tools and agent configuration. # Stripe Projects Source: https://exa.ai/docs/integrations/marketplaces/stripe-projects Integrate Exa from your terminal using the Stripe Projects CLI. [Stripe Projects](https://projects.dev) lets you and your coding agents provision third-party services from the terminal, with no dashboards and no copy-pasting keys. A single command creates an Exa account and syncs an API key into your project. ## Prerequisites Install the Stripe CLI and the Projects plugin: ```bash theme={null} brew install stripe/stripe-cli/stripe && stripe plugin install projects ``` For other platforms and full CLI setup, see [Stripe Projects](https://projects.dev). ## Get started From your project directory, initialize a project, add Exa, and pull credentials: ```bash theme={null} stripe projects init stripe projects add exa/api stripe projects env --pull ``` Your `.env` now contains an `EXA_API_KEY`. The [Exa SDKs](/docs/sdks/quickstart) and [Quickstart](/docs/search/quickstart) read this variable automatically, so your code works without any changes. The key is provisioned in an Exa account you own. Manage usage, keys, and billing anytime from the [Exa Dashboard](https://dashboard.exa.ai). ## Link an existing Exa team Already have an Exa account? Connect it first so the API key is provisioned under your existing team: ```bash theme={null} stripe projects link exa stripe projects add exa/api ``` `stripe projects link` opens Exa so you can authenticate and associate your team with your Stripe account. Open the linked Exa Dashboard anytime with `stripe projects open exa`. ## Provision from your coding agent `stripe projects init` writes a Stripe Projects [Agent Skill](https://projects.dev) into your project, so you can let your agent (Claude Code, Cursor, Codex, and others) run the flow for you: ```text theme={null} Use Stripe Projects to add Exa and wire up the API key. ``` ## Next steps * [Quickstart](/docs/search/quickstart): make your first Exa search with our SDKs. * [Stripe Projects docs](https://docs.stripe.com/projects): full CLI reference, environments, and billing. * [Exa Dashboard](https://dashboard.exa.ai): manage API keys, usage, and billing. * [Provider catalog](https://projects.dev): browse all Stripe Projects providers. # n8n Source: https://exa.ai/docs/integrations/n8n Use Exa search and contents inside n8n workflows. The official [Exa node for n8n](https://github.com/exa-labs/n8n-integration) adds web search, content extraction, grounded answers, and Exa Agent runs to visual workflows. Use it as a regular workflow step or connect it to an n8n AI Agent as a tool. ## Install the Exa node The package name is `n8n-nodes-exa-official`. Search for **Exa** in the n8n node picker. If it is not available on your instance, an instance owner can install `n8n-nodes-exa-official` by following n8n's [community node installation guide](https://docs.n8n.io/integrations/community-nodes/installation/). The node requires n8n 1.60 or newer and Node.js 20.15 or newer. Create a key in the dashboard. New accounts start with free credits. Add an **Exa API** credential in n8n and paste your key. Select that credential on each Exa node that should use the account. ## Run a search 1. Add a trigger to a workflow. 2. Add the **Exa** node. 3. Choose **Search**. 4. Enter a query and select a search type. 5. Choose a response format: * **Results** for ranked pages * **Text** for a synthesized answer * **Structured** for JSON matching your schema 6. Run the node and pass its output to the next workflow step. Search can also return text, highlights, summaries, links, and images from each result. Domain filters, publication dates, categories, `maxAgeHours`, and subpage crawling are available in the node's optional fields. ## Available resources | Resource | Operations | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | | **Search** | Search the web using `auto`, `instant`, `fast`, `deep-lite`, `deep`, or `deep-reasoning`, with optional synthesis and structured output. | | **Contents** | Retrieve cleaned text, highlights, summaries, links, and images for a list of URLs. | | **Answer** | Generate a grounded answer with citations and optional structured output. | | **Agent** | Create, inspect, list, stream, poll, and cancel multi-step Agent runs. | ## Use Exa with an n8n AI Agent Connect an Exa node to an **AI Agent** node through its tool input. Parameters that the model should supply can use n8n's `$fromAI()` expression: ```javascript theme={null} {{ $fromAI("query", "What should Exa search for?", "string") }} ``` Search and Answer work well as grounding tools. Use the Agent resource when the task requires multi-step research, list building, structured enrichment, or premium [Exa Connect](/docs/agent/connect/overview) data. ## Wait for an Agent run When creating an Agent run, **Wait for Completion** supports: * **Stream** to hold one server-sent events connection open until the run finishes * **Poll** to check the run on an interval For long-running or asynchronous workflows, turn **Wait for Completion** off, save the returned run `id`, and use **Get Run** later. The run continues on Exa after the n8n step finishes. ## Troubleshooting Ask an instance owner to install the verified community package `n8n-nodes-exa-official`. Community-node availability can depend on how your n8n instance is hosted. Confirm that the selected credential contains an active key from the [Exa dashboard](https://dashboard.exa.ai/api-keys) and that the key has available credits. Disable **Wait for Completion**, persist the returned run `id`, and retrieve the result in a later step with **Get Run**. ## Resources Review the current operations, compatibility, and source. Build multi-step research and enrichment workflows. Write better queries and choose the right search mode. # OpenAI SDK Compatibility Source: https://exa.ai/docs/integrations/openai-sdk Use Exa's endpoints as a drop-in replacement for OpenAI - supporting both chat completions and responses APIs. New to Exa? Get started in under a minute. *** ## Overview Exa provides OpenAI-compatible endpoints that work with the OpenAI SDK: | Endpoint | OpenAI Interface | Models Available | Use Case | | ------------------- | -------------------- | ---------------- | ----------------------------------------------------- | | `/chat/completions` | Chat Completions API | `exa` | Traditional chat interface | | `/responses` | Responses API | `exa-agent` | Agent API (async research, enrichment, list-building) | `/chat/completions` routes to [`/answer`](/docs/reference/answer), and `/responses` routes to the [Agent API](/docs/agent/quickstart). See [Agent via Responses API](#agent-via-responses-api) below. ## Answer To use Exa's `/answer` endpoint via the chat completions interface: 1. Replace base URL with `https://api.exa.ai` 2. Replace API key with your Exa API key 3. Replace model name with `exa`. See the full [`/answer`](/docs/reference/answer) endpoint reference. For custom routing behavior, contact [hello@exa.ai](mailto:hello@exa.ai). ```python Python theme={null} import os from openai import OpenAI client = OpenAI( base_url="https://api.exa.ai", # use exa as the base url api_key=os.environ["EXA_API_KEY"], ) completion = client.chat.completions.create( model="exa", messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "What are the latest developments in quantum computing?"} ], # use extra_body to pass extra parameters to the /answer endpoint extra_body={ "text": True # include full text from sources } ) print(completion.choices[0].message.content) # print the response content print(completion.choices[0].message.citations) # print the citations ``` ```javascript JavaScript theme={null} import OpenAI from "openai"; const openai = new OpenAI({ baseURL: "https://api.exa.ai", // use exa as the base url apiKey: process.env.EXA_API_KEY, }); async function main() { const completion = await openai.chat.completions.create({ model: "exa", messages: [ { role: "system", content: "You are a helpful assistant." }, { role: "user", content: "What are the latest developments in quantum computing?", }, ], store: true, stream: true, extra_body: { text: true, // include full text from sources }, }); for await (const chunk of completion) { console.log(chunk.choices[0].delta.content); } } main(); ``` ```bash cURL theme={null} curl -s https://api.exa.ai/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "model": "exa", "messages": [ { "role": "system", "content": "You are a helpful assistant." }, { "role": "user", "content": "What are the latest developments in quantum computing?" } ], "text": true }' ``` ## Agent via Responses API Exa's [`/responses`](https://api.exa.ai/responses) endpoint exposes the [Agent API](/docs/agent/quickstart) through the OpenAI Responses interface, so the OpenAI SDKs work against it unchanged. Set `model: "exa-agent"` and choose an execution mode: | Mode | Request | Behavior | | ----------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------- | | Synchronous | default (no `stream`/`background`) | The request blocks and returns the completed `response` object. | | Streaming | `stream: true` | The request streams OpenAI Responses events (SSE) as the run progresses, ending with `response.completed`. | | Background | `background: true` | The request returns immediately with an `in_progress` response; poll `GET /responses/{id}` for the result. | Set `reasoning.effort` (`minimal`, `low`, `medium`, `high`, `xhigh`, `auto`, `max`) to trade cost against depth, and cancel a run with `POST /responses/{id}/cancel`. For `max`, set `Exa-Beta: agent-max-effort-2026-07-27` as a client default header. The [Agent guide](/docs/agent/quickstart) covers the run model, output shape, and effort pricing that back this surface. `high`, `xhigh`, and `max` `reasoning.effort` runs too long for a synchronous request and return `400`. Use `stream: true` or `background: true` for those runs. `/responses` has no `budget` field; max uses its default per-run cap. Use `previous_response_id` to continue a completed Responses run. ### Synchronous The request blocks until the run completes and returns the terminal `response` object. ```python Python theme={null} import os from openai import OpenAI client = OpenAI( base_url="https://api.exa.ai", api_key=os.environ["EXA_API_KEY"], ) response = client.responses.create( model="exa-agent", input="Find the top 5 AI startups founded in 2025 with their funding amounts", reasoning={"effort": "medium"}, ) print(response.output_text) ``` ```javascript JavaScript theme={null} import OpenAI from "openai"; const openai = new OpenAI({ baseURL: "https://api.exa.ai", apiKey: process.env.EXA_API_KEY, }); async function main() { const response = await openai.responses.create({ model: "exa-agent", input: "Find the top 5 AI startups founded in 2025 with their funding amounts", reasoning: { effort: "medium" }, }); console.log(response.output_text); } main(); ``` ```bash cURL theme={null} curl -s -X POST 'https://api.exa.ai/responses' \ -H "Authorization: Bearer $EXA_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "model": "exa-agent", "input": "Find the top 5 AI startups founded in 2025 with their funding amounts", "reasoning": { "effort": "medium" } }' ``` ### Streaming Set `stream: true` to receive Responses stream events over SSE. Events carry a monotonic `sequence_number` and finish with `response.completed`; there is no `[DONE]` sentinel. The stream may include `: keep-alive` comment lines, which SSE clients ignore. ```python Python theme={null} import os from openai import OpenAI client = OpenAI( base_url="https://api.exa.ai", api_key=os.environ["EXA_API_KEY"], ) with client.responses.stream( model="exa-agent", input="Find the top 5 AI startups founded in 2025 with their funding amounts", ) as stream: for event in stream: if event.type == "response.output_text.delta": print(event.delta, end="", flush=True) final = stream.get_final_response() print("\n\n", final.output_text) ``` ```javascript JavaScript theme={null} import OpenAI from "openai"; const openai = new OpenAI({ baseURL: "https://api.exa.ai", apiKey: process.env.EXA_API_KEY, }); async function main() { const stream = await openai.responses.create({ model: "exa-agent", input: "Find the top 5 AI startups founded in 2025 with their funding amounts", stream: true, }); for await (const event of stream) { if (event.type === "response.output_text.delta") { process.stdout.write(event.delta); } } } main(); ``` ```bash cURL theme={null} curl -N -X POST 'https://api.exa.ai/responses' \ -H "Authorization: Bearer $EXA_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: text/event-stream' \ -d '{ "model": "exa-agent", "input": "Find the top 5 AI startups founded in 2025 with their funding amounts", "stream": true }' ``` ### Background Set `background: true` to start a run without holding the connection open, then poll `GET /responses/{id}` until it reaches a terminal status. To stream instead of polling, use [Streaming](#streaming). ```python Python theme={null} import os import time from openai import OpenAI client = OpenAI( base_url="https://api.exa.ai", api_key=os.environ["EXA_API_KEY"], ) response = client.responses.create( model="exa-agent", input="Find the top 5 AI startups founded in 2025 with their funding amounts", background=True, ) # Poll until complete while response.status in ("queued", "in_progress"): time.sleep(5) response = client.responses.retrieve(response.id) print(response.output_text) ``` ```javascript JavaScript theme={null} import OpenAI from "openai"; const openai = new OpenAI({ baseURL: "https://api.exa.ai", apiKey: process.env.EXA_API_KEY, }); async function main() { let response = await openai.responses.create({ model: "exa-agent", input: "Find the top 5 AI startups founded in 2025 with their funding amounts", background: true, }); // Poll until complete while (response.status === "queued" || response.status === "in_progress") { await new Promise((r) => setTimeout(r, 5000)); response = await openai.responses.retrieve(response.id); } console.log(response.output_text); } main(); ``` ```bash cURL theme={null} # Create a background run curl -s -X POST 'https://api.exa.ai/responses' \ -H "Authorization: Bearer $EXA_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "model": "exa-agent", "input": "Find the top 5 AI startups founded in 2025 with their funding amounts", "background": true }' # Poll with the returned response ID curl -s 'https://api.exa.ai/responses/resp_agent_run_...' \ -H "Authorization: Bearer $EXA_API_KEY" ``` ## Chat wrapper Exa provides a Python wrapper that automatically enhances any OpenAI chat completion with RAG capabilities. With one line of code, you can turn any OpenAI chat completion into an Exa-powered RAG system that handles search, chunking, and prompting automatically. ```python Python theme={null} import os from openai import OpenAI from exa_py import Exa # Initialize clients openai = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) exa = Exa(api_key=os.environ["EXA_API_KEY"]) # Wrap the OpenAI client exa_openai = exa.wrap(openai) # Use exactly like the normal OpenAI client completion = exa_openai.chat.completions.create( model="gpt-5.6-sol", messages=[{"role": "user", "content": "What is the latest climate tech news?"}] ) print(completion.choices[0].message.content) ``` The wrapped client works exactly like the native OpenAI client, except it automatically improves your completions with relevant search results when needed. The wrapper supports any parameters from the `exa.search()` function. ```python theme={null} completion = exa_openai.chat.completions.create( model="gpt-5.6-sol", messages=messages, use_exa="auto", # "auto", "required", or "none" num_results=5, # defaults to 3 result_max_len=1024, # defaults to 2048 characters include_domains=["arxiv.org"], category="publication", start_published_date="2019-01-01" ) ``` # OpenClaw Source: https://exa.ai/docs/integrations/openclaw Give OpenClaw live web search and page contents with Exa. [OpenClaw](https://openclaw.ai/) supports Exa as a native `web_search` provider. Once selected, every OpenClaw agent can use Exa search modes, date filters, and content extraction through the built-in web tool. ## Set up Exa ```bash theme={null} openclaw plugins install @openclaw/exa-plugin openclaw gateway restart ``` Create a key in the dashboard. New accounts start with free credits. For a gateway installation, add the key to `~/.openclaw/.env`: ```bash ~/.openclaw/.env theme={null} EXA_API_KEY=your-exa-api-key ``` Restart the gateway after changing its environment. Run: ```bash theme={null} openclaw configure --section web ``` Choose **Exa** as the web search provider. OpenClaw stores the provider selection in its configuration and reads the credential from `EXA_API_KEY`. ## Configure manually You can select Exa directly in OpenClaw's JSON5 configuration: ```json5 theme={null} { tools: { web: { search: { provider: "exa", }, }, }, } ``` To store the key in configuration instead of the gateway environment: ```json5 theme={null} { plugins: { entries: { exa: { config: { webSearch: { apiKey: "exa-...", }, }, }, }, }, } ``` Prefer `EXA_API_KEY` or an OpenClaw SecretRef over committing an API key to a configuration file. ## What agents can request OpenClaw exposes Exa through `web_search`. | Parameter | Purpose | | ---------------------------- | --------------------------------------------------------------------------------------------- | | `query` | The web search query. | | `count` | Number of results, up to 100 and subject to the selected search type's limit. | | `type` | Exa search mode, including `auto`, `neural`, `fast`, `instant`, `deep`, and `deep-reasoning`. | | `freshness` | Restrict results to a recent day, week, month, or year. | | `date_after` / `date_before` | Restrict results with `YYYY-MM-DD` boundaries. | | `contents` | Return full text, highlights, or summaries with each result. | If `contents` is omitted, OpenClaw requests highlights by default. The agent can ask for a different content shape when it needs complete pages or summaries: ```javascript theme={null} await web_search({ query: "transformer architecture explained", type: "neural", contents: { text: { maxCharacters: 5000 }, highlights: { numSentences: 3 }, summary: true, }, }); ``` OpenClaw caches web search results for 15 minutes by default. Change `tools.web.search.cacheTtlMinutes` or set it to `0` when every request must be fresh. ## Troubleshooting Install `@openclaw/exa-plugin`, restart the gateway, and run `openclaw configure --section web` again. Check that `EXA_API_KEY` is available to the gateway process, not only your interactive shell. For a gateway install, place it in `~/.openclaw/.env` and restart the gateway. OpenClaw caches results independently of Exa. Lower `tools.web.search.cacheTtlMinutes` or set it to `0`, then use Exa content freshness options when requesting page contents. ## Resources Review the current plugin configuration and tool parameters. Compare Exa search modes and response formats. Control indexed and live-retrieved page contents. # OpenHuman Source: https://exa.ai/docs/integrations/openhuman Give the OpenHuman agent live web search with Exa, either managed or with your own Exa API key. [OpenHuman](https://tinyhumans.gitbook.io/openhuman) by TinyHumans is a desktop AI assistant with a native web search tool the agent calls on its own. Exa is the search provider behind that tool. | Approach | Setup | Runs on | | --------------------- | -------------------- | ----------------------------------------------------------------------- | | **OpenHuman Managed** | None | OpenHuman's backend, powered by Exa. No API key. | | **Exa provider** | Paste an Exa API key | Your machine, straight to `https://api.exa.ai` on your own Exa account. | ## OpenHuman Managed Managed search is the default. Choose **Simple** during onboarding and the agent can search the web immediately. OpenHuman onboarding asking how to run OpenHuman, with the Simple option selected **Managed is the fastest way to get Exa results.** No key to create, store, or rotate, no credentials on your machine, and search is billed on your OpenHuman subscription. ## Exa provider Configure Exa directly to run search on your own Exa account and give the agent Exa's search and page-contents tools. ### Get your Exa API key Create a key in the dashboard. New accounts start with free credits. ### Add Exa in OpenHuman 1. Open **Connections**, then select **Search engine** under **API keys**. The OpenHuman Connections page with Search engine selected under API keys, showing the search engine list with OpenHuman Managed active 2. Select **Exa**. The Exa engine option selected in the OpenHuman Search engine panel, showing the Needs API key badge 3. Paste your key into **Exa API key** and select **Save**. The Exa API key field in OpenHuman with a key entered and the Save button visible The OpenHuman Search engine panel with Exa selected and marked Configured ### Configuration The panel writes to OpenHuman's `config.toml`. Set the same values in the file or the environment instead: ```toml config.toml theme={null} [search] engine = "exa" # required max_results = 5 # optional, 1-20 timeout_secs = 15 # optional [search.exa] api_key = "your-exa-api-key" # required ``` ```bash theme={null} OPENHUMAN_SEARCH_ENGINE=exa EXA_API_KEY=your-exa-api-key ``` `EXA_API_KEY` and `OPENHUMAN_EXA_API_KEY` both override `search.exa.api_key`. When both are set, `OPENHUMAN_EXA_API_KEY` takes precedence. ### Tools the agent gets | Tool | Returns | | ------------------ | ------------------------------------------------------------------- | | `web_search_tool` | Web search, served by Exa. | | `exa_search` | Ranked pages with titles, URLs, publish dates, and optional text. | | `exa_get_contents` | Full contents of given URLs, with optional summaries or highlights. | The agent sets Exa's [search parameters](/docs/search/quickstart) per call, so plain instructions are enough to steer search mode, domains, dates, and categories. ## Troubleshooting OpenHuman found no key in the **Search engine** panel, the `EXA_API_KEY` and `OPENHUMAN_EXA_API_KEY` variables, or `search.exa.api_key`. Set it in one of them, and restart OpenHuman if you edited `config.toml` while it was running. The key is invalid or revoked. Check it in the [Exa dashboard](https://dashboard.exa.ai/api-keys), then **Clear** the stored key and save the correct one. Watch for pasted whitespace. `429` means a rate limit or exhausted quota: check usage in the [dashboard](https://dashboard.exa.ai). For `5xx`, retry, then see [error codes](/docs/admin/error-codes). Local-only sessions cannot use managed search. Configure the Exa provider with your own key. ## Resources Read OpenHuman's own reference for its search engines. Understand the search modes, filters, and content options behind the Exa tools. Get better results out of every query. # OpenRouter Source: https://exa.ai/docs/integrations/openrouter Ground any OpenRouter model with Exa web search through the openrouter:web_search server tool. Exa is the search engine behind [OpenRouter](https://openrouter.ai)'s web search. OpenRouter gives you one API for hundreds of models, and Exa gives those models live web access: any model without native search grounds through Exa by default, and any model can be pointed at Exa explicitly. No Exa API key is needed. OpenRouter runs the searches server-side and bills them to your OpenRouter credits. ## Use the web search server tool Add `openrouter:web_search` to your `tools` array and the model decides when to search, what to search for, and whether to search again within the same request. [Server tools](https://openrouter.ai/docs/guides/features/server-tools/web-search) are in beta on OpenRouter, and they replace the deprecated `web` plugin and `:online` model variants; see OpenRouter's [migration guide](https://openrouter.ai/docs/guides/features/server-tools/web-search#migrating-from-the-web-search-plugin) if you use either. ```javascript JavaScript theme={null} const response = await fetch("https://openrouter.ai/api/v1/chat/completions", { method: "POST", headers: { Authorization: "Bearer ", "Content-Type": "application/json", }, body: JSON.stringify({ model: "openai/gpt-5.2", messages: [ { role: "user", content: "What were the major AI announcements this week?" }, ], tools: [{ type: "openrouter:web_search" }], }), }); const data = await response.json(); console.log(data.choices[0].message.content); ``` ```python Python theme={null} import requests response = requests.post( "https://openrouter.ai/api/v1/chat/completions", headers={ "Authorization": "Bearer ", "Content-Type": "application/json", }, json={ "model": "openai/gpt-5.2", "messages": [ {"role": "user", "content": "What were the major AI announcements this week?"} ], "tools": [{"type": "openrouter:web_search"}], }, ) print(response.json()["choices"][0]["message"]["content"]) ``` ```bash cURL theme={null} curl https://openrouter.ai/api/v1/chat/completions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-5.2", "messages": [ { "role": "user", "content": "What were the major AI announcements this week?" } ], "tools": [{ "type": "openrouter:web_search" }] }' ``` With the default `engine: "auto"`, OpenRouter uses the provider's native search where a model has one and Exa everywhere else. Set `engine: "exa"` to keep one search behavior across every model: ```json theme={null} { "type": "openrouter:web_search", "parameters": { "engine": "exa", "mode": "auto", "max_results": 5, "max_total_results": 20, "allowed_domains": ["arxiv.org"], "excluded_domains": ["reddit.com"] } } ``` | Parameter | Use it to | | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mode` | Trade latency for depth: `instant`, `fast`, `auto` (default), `deep-lite`, `deep`, or `deep-reasoning`. The modes map to Exa's [search types](/docs/search/quickstart). | | `max_results` | Cap results per search call (default 5) | | `max_uses` | Cap how many times the model may search in one request | | `max_total_results` | Cap cumulative results across all searches in one request | | `max_characters` | Set an exact per-result character budget for highlights | | `search_context_size` | Use a preset budget instead: `low`, `medium`, or `high` | | `allowed_domains`, `excluded_domains` | Filter result domains. Exa supports both filters in the same request. | ## How results come back OpenRouter requests [Exa highlights](/docs/search/highlights) for each result rather than full page text: extractive excerpts sized adaptively, typically 2,000 to 4,000 characters per result, unless you set `max_characters` or `search_context_size`. The model reads the excerpts, and API callers receive them in standardized `url_citation` annotations on the response message. Within one result, `[...]` markers separate excerpts drawn from different parts of the page. ## Pricing Exa searches bill to your OpenRouter credits, in addition to the model's token costs for reading the results. The `instant`, `fast`, and `auto` modes cost \$0.007 per search, `deep-lite` and `deep` cost \$0.012, and `deep-reasoning` costs \$0.015. Each search includes up to 10 results, and additional results cost \$0.001 each. See [OpenRouter's web search docs](https://openrouter.ai/docs/guides/features/server-tools/web-search) for current rates. The response's `usage` object reports how many searches the model ran in `server_tool_use.web_search_requests`. ## Resources Full configuration reference for `openrouter:web_search`. How OpenRouter gives hundreds of models web search with Exa. # Tempo MPP GTM Enrichment Cookbook Source: https://exa.ai/docs/integrations/payments/mpp/gtm-enrichment Build a GTM enrichment workflow that pays per Exa search and contents request with Tempo MPP — no API key required. Use this cookbook to build a GTM enrichment agent or pipeline on top of Exa's `/search` and `/contents` endpoints, paid per request through the Machine Payments Protocol (MPP). MPP supports multiple payment methods; the examples here use stablecoins on [Tempo](https://tempo.xyz). No monthly subscription, no API key, and no seat-based pricing: fund a wallet with USDC.e and pay as you enrich leads or companies. MPP is currently supported on Exa's `/search` and `/contents` endpoints only. The Agent API (`/agent/runs`) and `/answer` require an Exa API key and go through the standard API key billing flow. ## What you'll build A lightweight enrichment pipeline that, given a list of company names or target descriptions: 1. Uses Exa `/search` with `type: "deep"` and `outputSchema` to find the official company page and extract key metadata. 2. Uses `contents.highlights` on the returned result to pull source snippets for funding, headquarters, employees, and product. 3. Emits a CSV or JSON enrichment record per input. This pattern works for lead-list enrichment, account research, and outbound personalization. Because it is composed of discrete `/search` + `/contents` calls, every step can be paid for with MPP. ## Prerequisites * A Tempo-compatible wallet funded with **USDC.e** on Tempo mainnet. * A safe way to load the wallet private key at runtime (see below; never commit the key or expose it in source code). * `mppx` (TypeScript) or `pympp` (Python) installed. For a command-line setup that doesn't need a raw private key, use the [Tempo Wallet CLI](/docs/integrations/payments/mpp/quickstart#pay-from-the-command-line). `tempo wallet login` creates or connects a wallet and may include free MPP Credits for new sign-ups. ## MPP setup ### Install the client ```bash TypeScript theme={null} npm install mppx viem ``` ```bash Python theme={null} pip install "pympp[tempo]" ``` ### Load your private key safely Never hardcode a private key. The examples below read `WALLET_PRIVATE_KEY` from your runtime environment for local development only. In production, load it from a secrets manager such as 1Password, AWS Secrets Manager, or HashiCorp Vault. ```bash TypeScript theme={null} # Set in your shell or CI secrets store; never commit this value export WALLET_PRIVATE_KEY="0x..." ``` ```bash Python theme={null} # Set in your shell or CI secrets store; never commit this value export WALLET_PRIVATE_KEY="0x..." ``` ### Make a paid search request ```typescript TypeScript theme={null} import { Mppx, tempo } from "mppx/client"; import { privateKeyToAccount } from "viem/accounts"; // In production, load this from a secrets manager — never commit the raw value. const account = privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as `0x${string}`); const mppx = Mppx.create({ methods: [tempo.charge({ account })], }); const response = await mppx.fetch("https://api.exa.ai/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: "Series A fintech companies with 50-200 employees", numResults: 5, contents: { highlights: true }, }), }); const data = (await response.json()) as { results: { title: string; url: string }[] }; console.log(data.results); console.log("Payment receipt:", response.headers.get("Payment-Receipt")); ``` ```python Python theme={null} import asyncio import os from mpp.client import Client from mpp.methods.tempo import ChargeIntent, TempoAccount, tempo async def main() -> None: # In production, load this from a secrets manager — never commit the raw value. account = TempoAccount.from_key(os.environ["WALLET_PRIVATE_KEY"]) method = tempo( account=account, chain_id=4217, intents={"charge": ChargeIntent()}, ) async with Client(methods=[method]) as client: response = await client.post( "https://api.exa.ai/search", json={ "query": "Series A fintech companies with 50-200 employees", "numResults": 5, "contents": {"highlights": True}, }, ) data = response.json() for result in data["results"]: print(result["url"], result["title"]) print("Payment receipt:", response.headers.get("Payment-Receipt")) asyncio.run(main()) ``` A successful response returns Exa results plus a `Payment-Receipt` header with the on-chain transaction hash. ### Make a paid contents request ```typescript TypeScript theme={null} const contentsResponse = await mppx.fetch("https://api.exa.ai/contents", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ urls: ["https://www.example.com"], text: true, summary: true, }), }); const contentsData = (await contentsResponse.json()) as { results: { url: string; text?: string; summary?: string }[]; }; console.log(contentsData.results[0]); ``` ```python Python theme={null} response = await client.post( "https://api.exa.ai/contents", json={ "urls": ["https://www.example.com"], "text": True, "summary": True, }, ) print(response.json()["results"][0]) ``` ## GTM enrichment recipe ### Enrich a list of companies Given a list of company names, search for each company's page and extract structured details. ```typescript TypeScript theme={null} interface CompanyEnrichment { name: string; url: string; title: string; industry?: string; headquarters?: string; funding?: string; summary?: string; highlights: string[]; } async function enrichCompanies(names: string[]): Promise { const enriched: CompanyEnrichment[] = []; for (const name of names) { const response = await mppx.fetch("https://api.exa.ai/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: `${name} official company`, type: "deep", numResults: 1, contents: { highlights: { query: "funding, headquarters, employees, product" }, }, outputSchema: { type: "object", properties: { company: { type: "object", properties: { name: { type: "string" }, url: { type: "string" }, industry: { type: "string" }, headquarters: { type: "string" }, funding: { type: "string" }, summary: { type: "string" }, }, required: ["name", "url"], }, }, required: ["company"], }, }), }); const data = (await response.json()) as { output?: { company?: CompanyEnrichment & { summary?: string } }; results?: { highlights?: string[] }[]; }; const company = data.output?.company; const highlights = data.results?.[0]?.highlights?.slice(0, 3) ?? []; if (!company) continue; enriched.push({ ...company, title: company.name, highlights, }); } return enriched; } ``` ```python Python theme={null} async def enrich_companies(names): enriched = [] for name in names: response = await client.post( "https://api.exa.ai/search", json={ "query": f"{name} official company", "type": "deep", "numResults": 1, "contents": { "highlights": {"query": "funding, headquarters, employees, product"} }, "outputSchema": { "type": "object", "properties": { "company": { "type": "object", "properties": { "name": {"type": "string"}, "url": {"type": "string"}, "industry": {"type": "string"}, "headquarters": {"type": "string"}, "funding": {"type": "string"}, "summary": {"type": "string"}, }, "required": ["name", "url"], } }, "required": ["company"], }, }, ) data = response.json() company = data.get("output", {}).get("company") highlights = [] if data.get("results"): highlights = data["results"][0].get("highlights", [])[:3] if not company: continue enriched.append({ "name": company["name"], "url": company["url"], "title": company["name"], "industry": company.get("industry"), "headquarters": company.get("headquarters"), "funding": company.get("funding"), "summary": company.get("summary"), "highlights": highlights, }) return enriched ``` ### Enrich a person profile This recipe uses `type: "deep"`, `contents.highlights`, and `outputSchema` to research a person and return a structured profile. ```typescript TypeScript theme={null} const response = await mppx.fetch("https://api.exa.ai/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: "Exa Labs founders contact and background", type: "deep", numResults: 5, contents: { highlights: { query: "email, title, education, work history, LinkedIn" }, }, outputSchema: { type: "object", properties: { people: { type: "array", items: { type: "object", properties: { name: { type: "string" }, title: { type: "string" }, company: { type: "string" }, email: { type: "string" }, linkedInUrl: { type: "string" }, summary: { type: "string" }, }, required: ["name"], }, }, }, required: ["people"], }, }), }); const data = (await response.json()) as { output?: { people: { name: string; title?: string; company?: string }[] }; }; console.log(data.output?.people); ``` ```python Python theme={null} response = await client.post( "https://api.exa.ai/search", json={ "query": "Exa Labs founders contact and background", "type": "deep", "numResults": 5, "contents": { "highlights": {"query": "email, title, education, work history, LinkedIn"} }, "outputSchema": { "type": "object", "properties": { "people": { "type": "array", "items": { "type": "object", "properties": { "name": {"type": "string"}, "title": {"type": "string"}, "company": {"type": "string"}, "email": {"type": "string"}, "linkedInUrl": {"type": "string"}, "summary": {"type": "string"}, }, "required": ["name"], }, } }, "required": ["people"], }, }, ) print(response.json().get("output", {}).get("people")) ``` This uses `type: "deep"` for richer reasoning and `outputSchema` to shape the response. Deep search is priced at \$0.012 per request, and `contents.highlights` adds \$0.001 per result. ### Structured output If you want JSON fields instead of raw text, use `outputSchema` in the search request. Exa returns an `output` object shaped to your schema. ```python Python theme={null} response = await client.post( "https://api.exa.ai/search", json={ "query": "Series A fintech companies with 50-200 employees", "type": "deep-lite", "numResults": 5, "outputSchema": { "type": "object", "properties": { "companies": { "type": "array", "items": { "type": "object", "properties": { "name": {"type": "string"}, "headcount": {"type": "string"}, "headquarters": {"type": "string"}, "fundingStage": {"type": "string"}, }, "required": ["name"], }, } }, "required": ["companies"], }, }, ) ``` ```javascript JavaScript theme={null} const response = await mppx.fetch("https://api.exa.ai/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: "Series A fintech companies with 50-200 employees", type: "deep-lite", numResults: 5, outputSchema: { type: "object", properties: { companies: { type: "array", items: { type: "object", properties: { name: { type: "string" }, headcount: { type: "string" }, headquarters: { type: "string" }, fundingStage: { type: "string" } }, required: ["name"] } } }, required: ["companies"] } }) }); ``` `outputSchema` works best with `deep-lite` or `deep` search types. It adds an LLM call on Exa's side, so it is priced as `deep-lite`/`deep`. ## Pricing and limits MPP uses the same per-request pricing as API key billing. MPP search requests are capped at 10 results. | Operation | Price | | -------------------------------------------------- | ------------------- | | `/search` with `type` `instant`, `auto`, or `fast` | \$0.007 per request | | `/search` with `type` `deep-lite` or `deep` | \$0.012 per request | | `/search` with `type` `deep-reasoning` | \$0.015 per request | | `contents.text` | \$0.001 per URL | | `contents.highlights` | \$0.001 per URL | | `contents.summary` | \$0.001 per result | See [Pay with MPP (Tempo)](/docs/integrations/payments/mpp/quickstart) for the full reference, including rate limits, network details, and payment headers. ## Production tips * **Fund the wallet with USDC.e only.** Exa sponsors the Tempo network fee, so the wallet does not need a separate gas token. * **Handle `402` responses.** The MPP SDK retries automatically, but a custom client should retry on `402` using the `WWW-Authenticate: Payment` challenge. * **Cache `/contents` results.** Contents are priced per URL. Cache by URL to avoid paying twice for the same company page. * **Watch the 10-result cap.** MPP search clamps `numResults` to 10. * **Never commit private keys.** Load `WALLET_PRIVATE_KEY` from a secrets manager, not source control. ## FAQ No. In the Exa codebase, MPP is wired only to `/search` and `/contents`. `/agent/runs` and `/answer` require an Exa API key and use standard API key billing. No. If a request includes `x-api-key` or `Authorization: Bearer`, the API key flow takes priority and MPP is bypassed. Exa returns `402` with a fresh `WWW-Authenticate: Payment` challenge and no results. Your client can retry with a new payment. No results are returned until settlement succeeds. You can reuse the same wallet, but we recommend separate wallets for development and production. Per-wallet QPS is 10 requests/second across all requests from that wallet. ## Next steps * [Pay with MPP (Tempo)](/docs/integrations/payments/mpp/quickstart): full MPP reference * [Exa Search API guide](/docs/search/quickstart): search parameter reference * [Exa Contents API guide](/docs/contents/quickstart): contents parameter reference * [Tempo MPP docs](https://mpp.dev/protocol): protocol and SDK details # Pay with MPP (Tempo) Source: https://exa.ai/docs/integrations/payments/mpp/quickstart Call Exa's Search and Contents APIs without an API key by paying per request with USDC.e on Tempo. ## What is MPP? MPP (Machine Payments Protocol) is an open, HTTP-native payment standard built on the `402 Payment Required` status code. It lets clients pay for API access per-request using multiple payment methods, including stablecoins on [Tempo](https://tempo.xyz), with no accounts, API keys, or subscriptions needed. The examples on this page use Tempo; Exa currently settles MPP payments in USDC.e on Tempo mainnet. Exa supports MPP on two endpoints: **`/search`** and **`/contents`**. When you send a request without an API key or payment credential, Exa responds with `402` and a `WWW-Authenticate: Payment` challenge describing the price and how to pay. Your client signs a payment, retries the request with an `Authorization: Payment` credential, and receives the results once the payment settles on-chain. This is ideal for **AI agents** that need to autonomously pay for web search without pre-provisioned credentials. MPP and API key access are independent. If your request includes an `x-api-key` header, the normal API key billing flow is used and MPP is bypassed entirely. ## Supported endpoints | Endpoint | Method | Description | | ----------- | ------ | --------------------------------------------------------------------------------------------------- | | `/search` | POST | Web search with all search types (`instant`, `auto`, `fast`, `deep`, `deep-lite`, `deep-reasoning`) | | `/contents` | POST | Content retrieval by URL or document ID | Other Exa endpoints do not accept MPP payments *yet*. ## Get started You need a Tempo-compatible wallet funded with USDC.e. Export your wallet's private key before running an example: ```bash theme={null} export WALLET_PRIVATE_KEY="0x..." ``` ### Install the client ```bash TypeScript theme={null} npm install mppx viem ``` ```bash Python theme={null} pip install "pympp[tempo]" ``` ### Make a paid search request Use the MPP client to sign and submit a payment for a search request: ```typescript TypeScript theme={null} import { Mppx, tempo } from "mppx/client"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as `0x${string}`); const mppx = Mppx.create({ methods: [tempo.charge({ account })], }); const response = await mppx.fetch("https://api.exa.ai/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: "best machine learning frameworks", numResults: 5, }), }); const data = await response.json(); console.log(data.results); console.log("Payment receipt:", response.headers.get("Payment-Receipt")); ``` ```python Python theme={null} import asyncio import os from mpp.client import Client from mpp.methods.tempo import ChargeIntent, TempoAccount, tempo async def main() -> None: account = TempoAccount.from_key(os.environ["WALLET_PRIVATE_KEY"]) method = tempo( account=account, chain_id=4217, intents={"charge": ChargeIntent()}, ) async with Client(methods=[method]) as client: response = await client.post( "https://api.exa.ai/search", json={"query": "best machine learning frameworks", "numResults": 5}, ) data = response.json() for result in data["results"]: print(result["url"], result["title"]) print("Payment receipt:", response.headers.get("Payment-Receipt")) asyncio.run(main()) ``` A successful run prints the search results and the `Payment-Receipt` header containing the on-chain transaction hash. ## Pay from the command line If you prefer not to manage a raw private key, use the Tempo Wallet CLI instead. `tempo wallet login` creates or connects a Tempo wallet, authorizes a local access key, and can include free MPP Credits for new sign-ups. ### Install and authenticate ```bash theme={null} curl -fsSL https://tempo.xyz/install | bash tempo add wallet tempo add request tempo wallet login ``` On a remote host without a local browser, use `tempo wallet login --no-browser` and open the printed URL on your device to authorize the CLI. ### Check balances and credits ```bash theme={null} tempo wallet whoami tempo wallet whoami --credits ``` ### Make a paid request ```bash theme={null} tempo request --max-spend 1.00 https://api.exa.ai/search \ --json '{"query": "Series A fintech companies", "numResults": 5}' ``` `tempo request` intercepts the `402 Payment Required` challenge, pays, and retries automatically. For full CLI reference, see the [Tempo Wallet CLI docs](https://tempo.xyz/developers/docs/cli/wallet) and [`tempo request` docs](https://tempo.xyz/developers/docs/cli/request). ## Gas fees Exa sponsors the Tempo network fee and pays it in USDC.e. Your wallet only needs enough USDC.e for the API charge; it does not need pathUSD or another gas-token balance. You do not configure a fee payer. Exa's payment challenge and the MPP SDK handle sponsorship automatically. ## Pricing MPP uses the same bundled pricing as API key billing. Exa calculates the price from the request parameters before processing the request. ### Search | Search type | Price for up to 10 results | | ------------------------- | -------------------------- | | `instant`, `auto`, `fast` | \$0.007 per request | | `deep-lite`, `deep` | \$0.012 per request | | `deep-reasoning` | \$0.015 per request | Adding `contents.summary` costs another **\$0.001 per result**. MPP search requests are capped at 10 results. If `numResults` is greater than 10, Exa uses 10 and prices the request for 10 results. If you need more, use [API key billing](/docs/search/quickstart). ### Contents Each requested content type costs \$0.001 per URL: | Content type | Price per URL | | ------------ | ------------- | | `text` | \$0.001 | | `highlights` | \$0.001 | | `summary` | \$0.001 | If you do not request `text`, `highlights`, or `summary`, Exa enables `text` by default. ### Pricing examples | Request | Price | | ----------------------------------------------- | ------- | | `/search` with `type: "auto"` | \$0.007 | | `/search` with 3 results and `contents.summary` | \$0.010 | | `/search` with `type: "deep"` | \$0.012 | | `/contents` for 2 URLs with `text: true` | \$0.002 | | `/contents` for 1 URL with `text` and `summary` | \$0.002 | ## How the payment flow works The SDK automates this flow, but you can inspect it directly over HTTP: 1. Send a request without an API key or payment credential. Exa returns `402` with a `WWW-Authenticate: Payment` challenge containing the price, token, recipient, network, and sponsorship details. 2. Sign the challenge and retry with `Authorization: Payment `. 3. Exa processes the request while settling the payment. After settlement confirms, Exa returns the results with a `Payment-Receipt` header. If settlement fails, Exa returns `402` with a fresh challenge and no results. ### Inspect a payment challenge You can inspect the price and payment details without a wallet: ```bash theme={null} curl -s -D - -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -d '{"query": "test query", "numResults": 3}' ``` Look for the `WWW-Authenticate: Payment` header in the `402` response. Unpaid discovery requests are rate-limited, so use this for debugging rather than polling. ## Payment reference Exa accepts MPP payments in USDC.e on Tempo mainnet. | Network | Identifier | Token | Asset | | ------------- | ------------- | ------ | -------------------------------------------- | | Tempo mainnet | `eip155:4217` | USDC.e | `0x20c000000000000000000000b9537d11c60e8b50` | USDC.e has 6 decimals. The challenge expresses prices in atomic units, so `7000` is \$0.007 and `1000000` is \$1.00. Exa supports MPP and [x402](/docs/integrations/payments/x402/quickstart) on the same endpoints. An unauthenticated `402` response can include both the MPP `WWW-Authenticate: Payment` challenge and the x402 `PAYMENT-REQUIRED` header. Use the headers for the payment protocol your client supports. ### Headers | Header | Direction | Description | | ------------------------------------- | ------------------- | ----------------------------------------------------------- | | `Authorization: Payment ` | Request | MPP payment credential | | `WWW-Authenticate: Payment` | `402` response | Price and payment instructions for the request | | `Payment-Receipt` | Successful response | Settlement receipt, including the on-chain transaction hash | ### Errors | Status | Description | | ------ | ------------------------------------------------------------------------------------- | | `402` | The payment credential is missing or invalid; the response includes a fresh challenge | | `402` | The payment amount does not match the request price, or settlement failed | | `429` | This IP sent too many unpaid discovery requests | | `429` | This wallet exceeded the paid-request rate limit | ### Rate limits MPP rate limits are shared with x402 and are separate from API key limits: | Limit | Threshold | Window | | -------------------------------- | ----------- | ---------- | | Unpaid discovery requests per IP | 5 requests | 60 seconds | | Paid requests per wallet | 10 requests | 1 second | ## FAQ If your request includes an `x-api-key` header, the API key flow takes priority and MPP is bypassed. They don't stack. It's one or the other per request. Your response is blocked. You receive a `402` with a fresh `WWW-Authenticate: Payment` challenge so your client can retry. No results are returned until settlement succeeds. Any Tempo-compatible EVM wallet the client SDK can sign with — a `viem` account with `mppx` (TypeScript), or an `eth-account` key with `pympp` (Python). For AI agents, use a wallet with a USDC.e balance on Tempo to cover request prices. ## Resources * [MPP protocol docs](https://mpp.dev/protocol): protocol details and authentication format * [mppx documentation](https://mpp.dev/sdk/typescript): MPP TypeScript SDK reference * [pympp documentation](https://mpp.dev/sdk/python): MPP Python SDK reference * [Tempo](https://tempo.xyz): Tempo network documentation * [Pay with x402](/docs/integrations/payments/x402/quickstart): pay for the same endpoints with x402 * [Exa Search API guide](/docs/search/quickstart): full search parameter reference * [Exa Contents API guide](/docs/contents/quickstart): full contents parameter reference # Nevermined Source: https://exa.ai/docs/integrations/payments/nevermined Autonomous agent payments for Exa via Nevermined x402 card delegation. A 7 USD purchase provisions or tops up an Exa API key with 7 USD of credits. Agents pay Exa with a credit card via [Nevermined](https://nevermined.ai)'s [x402 card-delegation](https://nevermined.ai/docs/specs/x402-card-delegation) scheme. Each **\$7 purchase** returns an Exa API key with **\$7 of Exa credits**. Use this Nevermined plan ID:
`27800462147494506865542649899724877617306579171265399959488097895839186996870`
This plan runs on Nevermined's live environment (live-prefixed API keys). The purchase is for API credits, not for a single search request.
For a first-time Nevermined payer, `POST /team-management/nevermined/purchase-key` provisions a new Exa API key and adds \$7 of credits. If the key runs out, mint a fresh x402 token with the same delegation and call the same endpoint again. Exa returns the same API key with another \$7 of credits added. ## Buy a key ```bash theme={null} POST https://admin-api.exa.ai/team-management/nevermined/purchase-key payment-signature: ``` * **Cost:** \$7 per purchase, charged to the card behind the delegation referenced by the x402 token. * **Response (new payer):** `{ status: "ok", apiKey: "…", expiresAt: null }` — a new Exa API key with \$7 of credits. * **Response (returning payer):** `{ status: "ok", apiKey: "…", expiresAt: null }` — the same Exa API key with \$7 more credits. * **Response (replayed token):** cached result, no new charge. * **Missing/invalid signature:** `402 Payment Required` with payment requirements in the body. ## How it works The payment side is handled by Nevermined; Exa only sees the signed x402 token. 1. **One-time setup (by the card owner):** enroll a card at [nevermined.app](https://nevermined.app), create a **delegation** on it (the spending permission: the owner sets a limit and duration, and can scope it to a specific API key), and issue a Nevermined API key for the agent. 2. **The agent finds its delegation.** The Nevermined SDK lets the agent discover the delegations its key can spend from and pick one with enough remaining budget (at least \$7). If none exists, the owner creates one in the dashboard, or a fully autonomous agent can create one through the SDK within the card's limits. 3. **The agent mints an x402 access token** for the plan ID above, on the card-delegation scheme, referencing the delegation by ID. Delegations must exist before minting; tokens cannot create them on the fly. 4. **The agent POSTs the token to the endpoint above** in the `payment-signature` header and receives the Exa API key from the response. 5. **The key works immediately** against the standard [Exa Search API](/docs/search/quickstart). For the complete agent-ready walkthrough (SDK methods, parameters, delegation discovery and creation, troubleshooting), follow Nevermined's Exa integration guide: [nevermined.ai/docs/integrations/exa](https://nevermined.ai/docs/integrations/exa) (agents: fetch [nevermined.ai/docs/integrations/exa.md](https://nevermined.ai/docs/integrations/exa.md)). ## What \$7 buys Credits are consumed at standard Exa API pricing. At current rates, \$7 of credits covers roughly: | Endpoint or feature | Price | Approximate usage | | -------------------------------------------------------- | ---------------------------------: | ------------------: | | Search (`instant`, `fast`, `auto`) with up to 10 results | \$7 / 1,000 requests | 1,000 requests | | Deep-Lite Search | \$10 / 1,000 requests | 700 requests | | Deep Search | \$12 / 1,000 requests | \~583 requests | | Deep-Reasoning Search | \$15 / 1,000 requests | \~466 requests | | Contents (`text`, `highlights`, or `summary`) | \$1 / 1,000 pages per content type | 7,000 pages | | AI page summaries on Search or Contents | \$1 / 1,000 pages | 7,000 summaries | | Additional results beyond the first 10 | \$1 / 1,000 results | 7,000 extra results | | Answer | \$5 / 1,000 requests | 1,400 requests | | Monitors | \$15 / 1,000 requests | \~466 requests | Search requests include text and highlights for up to 10 results. Extra results beyond 10 and AI summaries are billed separately.
For full pricing details, see [Exa pricing](https://exa.ai/pricing). ## When the key runs out Exa returns **`HTTP 402`** on the regular API endpoints once the API key's credits are exhausted: ```json theme={null} { "requestId": "...", "error": "You have exceeded your credits limit. Please top up to keep using Exa at dashboard.exa.ai", "tag": "NO_MORE_CREDITS" } ``` Mint a fresh x402 token with the same plan ID and delegation, then POST it again to the same `/purchase-key` endpoint. Exa adds another \$7 of credits to the same API key. ## References * [Nevermined Exa integration guide](https://nevermined.ai/docs/integrations/exa) * [x402 card-delegation spec](https://nevermined.ai/docs/specs/x402-card-delegation) * [Exa pricing](https://exa.ai/pricing) * [Exa Search API](/docs/search/quickstart) # World AgentKit Source: https://exa.ai/docs/integrations/payments/x402/agentkit Let verified human-backed AI agents access Exa for free using World AgentKit — no USDC needed. ## What is AgentKit? [World AgentKit](https://docs.world.org/agents/agent-kit) is a toolkit that lets AI agents prove they are backed by a real, verified human via [World ID](https://world.org). When integrated with [x402](/docs/integrations/payments/x402/quickstart), it enables a **free trial** path: agents registered in World's [AgentBook](https://docs.world.org/agents/agent-kit/integrate) can access Exa's `/search` and `/contents` endpoints without paying USDC. This works alongside the standard x402 payment flow. Each verified human gets **100 free requests per month** across all agents they back. Once exhausted, the agent falls back to the normal USDC payment path. Counters reset at the start of each calendar month (UTC). AgentKit free trial and x402 payment are both bypassed if your request includes an `x-api-key` or `Authorization: Bearer` header. The normal API key billing flow takes priority. ## How it works When a client hits `/search` or `/contents` without an API key, Exa responds with `402 Payment Required`. The response includes an `agentkit` extension in the `PAYMENT-REQUIRED` header containing a [CAIP-122](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-122.md) (Sign-In with Ethereum) challenge. The agent signs this challenge with its registered wallet, and Exa verifies: 1. **Signature check** — validates the SIWE signature against the wallet address (supports both EOA via EIP-191 and smart contract wallets via ERC-1271) 2. **AgentBook lookup** — resolves the wallet to an anonymous `humanId` via the AgentBook contract on World Chain (`eip155:480`), confirming a unique verified human delegated their identity to this agent 3. **Usage check** — if the human still has free trial uses remaining, access is granted; otherwise, falls back to requiring USDC payment ## Quickstart ### 1. Register your agent in AgentBook This is a one-time setup. You need the [World App](https://world.org/download) with a verified identity. ```bash theme={null} npx @worldcoin/agentkit-cli register ``` The CLI triggers a World App verification flow, then submits a registration transaction on World Chain. Once complete, any server using AgentKit can look up your wallet and confirm it is backed by a real person. ### 2. Send a request (get the challenge) ```bash theme={null} curl -s -D - -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -d '{"query": "fusion energy breakthroughs", "numResults": 5}' ``` The `402` response includes an `agentkit` extension inside the decoded `PAYMENT-REQUIRED` payload: ```json theme={null} { "x402Version": 2, "accepts": [ ... ], "extensions": { "agentkit": { "info": { "version": "1", "statement": "Verify your agent is backed by a real human to access Exa", "domain": "api.exa.ai", "uri": "https://api.exa.ai/search", "nonce": "abc123...", "issuedAt": "2026-04-11T01:30:00.000Z", "resources": ["https://api.exa.ai/search"] }, "supportedChains": [ { "chainId": "eip155:480", "type": "eip191" }, { "chainId": "eip155:480", "type": "eip1271" } ], "schema": { ... }, "_options": { "statement": "Verify your agent is backed by a real human to access Exa", "mode": { "type": "free-trial", "uses": 100 }, "network": "eip155:480" } } } } ``` ### 3. Sign the challenge and resubmit Construct a [SIWE message](https://eips.ethereum.org/EIPS/eip-4361) from the `info` fields (domain, uri, nonce, statement, etc.), sign it with your registered agent wallet using one of the `supportedChains` types, and send it in the `agentkit` header (base64-encoded JSON): ```bash theme={null} curl -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "agentkit: " \ -d '{"query": "fusion energy breakthroughs", "numResults": 5}' ``` If the agent is verified and has free trial uses remaining, Exa returns `200` with search results — no payment needed. ### Using the AgentKit x402 skill Instead of implementing the challenge-response flow manually, add the [agentkit-x402 skill](https://github.com/worldcoin/agentkit/blob/main/skills/agentkit-x402/SKILL.md) to your AI agent: ```bash theme={null} npx skills add worldcoin/agentkit agentkit-x402 ``` This skill automatically handles the full flow when the agent encounters a `402` response with an AgentKit extension. ## Free trial details * Each verified human gets **100 free requests per month** across all agents they back * Usage counters reset at the start of each calendar month (UTC) * Usage is tracked per human per endpoint (`/search` and `/contents` are counted separately) * Two agents backed by the same human share the same counter * Once free trial uses are exhausted for the month, the agent falls back to the standard [x402 payment flow](/docs/integrations/payments/x402/quickstart) * The same [10-result cap](/docs/integrations/payments/x402/quickstart#pricing) applies to free trial requests on `/search` * The free trial counter is not currently exposed in the API response — when uses are exhausted, the server responds with a standard `402` without granting free access ## Supported endpoints | Endpoint | x402 Payment | AgentKit Free Trial | | ----------- | :----------: | :-----------------: | | `/search` | Yes | Yes | | `/contents` | Yes | Yes | All other Exa endpoints are not supported via x402 or AgentKit free trial. ## Network details | Property | Value | | ---------------------- | --------------------------------------------------- | | AgentBook chain | World Chain | | Chain ID (CAIP-2) | `eip155:480` | | Verification | AgentBook contract on World Chain | | Supported wallet types | EOA (EIP-191) and smart contract wallets (ERC-1271) | ## FAQ Yes. The `PAYMENT-REQUIRED` response includes both payment pricing and the AgentKit challenge. Your client can choose either path. If free trial uses are exhausted, the agent can fall back to paying with USDC. The AgentKit verification fails silently and the request is treated as a standard `402` — your agent can still pay with USDC via the normal x402 flow. No. Usage is tracked per human (via the anonymous `humanId` from AgentBook), not per wallet. Two agents backed by the same World ID share the same counter. Standard x402 USDC payments can settle on **Base** (`eip155:8453`) or **Solana mainnet** (`solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`). AgentKit verification uses **World Chain** (`eip155:480`) for AgentBook lookups. These are independent — AgentKit doesn't require any on-chain payment. Both EOA (externally owned accounts) using EIP-191 signatures and smart contract wallets (e.g. Coinbase Smart Wallet, Safe) using ERC-1271. See the [World AgentKit SDK reference](https://docs.world.org/agents/agent-kit/sdk-reference) for details. ## Resources * [x402 payment guide](/docs/integrations/payments/x402/quickstart): standard USDC payment flow * [World AgentKit docs](https://docs.world.org/agents/agent-kit): full AgentKit documentation * [World AgentKit integration guide](https://docs.world.org/agents/agent-kit/integrate): AgentBook registration * [World AgentKit SDK reference](https://docs.world.org/agents/agent-kit/sdk-reference): SDK API reference * [AgentKit x402 skill](https://github.com/worldcoin/agentkit/blob/main/skills/agentkit-x402/SKILL.md): pre-built skill for AI agents * [x402 protocol docs](https://docs.x402.org): full x402 specification # Pay with x402 Source: https://exa.ai/docs/integrations/payments/x402/quickstart Use Exa's Search and Contents APIs without an API key. Pay per request with USDC on Base or Solana via the x402 protocol. ## What is x402? [x402](https://x402.org) is an open payment standard built on the HTTP `402 Payment Required` status code. It lets clients pay for API access per-request using USDC stablecoins on Base or Solana, with no accounts, API keys, or subscriptions needed. Exa supports x402 on two endpoints: **`/search`** and **`/contents`**. When you send a request without an API key or payment header, Exa responds with `402` and a `PAYMENT-REQUIRED` header containing pricing details and the supported payment networks. Your client signs a USDC payment, retries the request with a `PAYMENT-SIGNATURE` header, and receives the results once settlement confirms on-chain. This is ideal for **AI agents** that need to autonomously pay for web search without pre-provisioned credentials. x402 and API key access are independent. If your request includes an `x-api-key` or `Authorization: Bearer` header, the normal API key billing flow is used and x402 is bypassed entirely. ## Supported endpoints | Endpoint | Method | Description | | ----------- | ------ | --------------------------------------------------------------------------------------------------- | | `/search` | POST | Web search with all search types (`instant`, `auto`, `fast`, `deep`, `deep-lite`, `deep-reasoning`) | | `/contents` | POST | Content retrieval by URL or document ID | All other endpoints are **not** available via x402. ## How it works x402 payment flow sequence diagram: Client sends request to server, gets 402 with PAYMENT-REQUIRED header, creates payment payload, retries with PAYMENT-SIGNATURE, server verifies via facilitator, does work, settles on-chain, returns 200 with results and PAYMENT-RESPONSE ### Step 1: Discovery Send a request to a supported endpoint without an API key or payment header: ```bash theme={null} curl -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -d '{"query": "best machine learning frameworks", "numResults": 5}' ``` You'll receive a `402` response with a base64-encoded `PAYMENT-REQUIRED` header. Decoded, it looks like: ```json theme={null} { "x402Version": 2, "resource": { "url": "https://api.exa.ai/search", "description": "Exa /search endpoint" }, "accepts": [ { "scheme": "exact", "network": "eip155:8453", "amount": "7000", "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "payTo": "0x...", "maxTimeoutSeconds": 60, "extra": { "name": "USD Coin", "version": "2" } }, { "scheme": "exact", "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "amount": "7000", "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "payTo": "...", "maxTimeoutSeconds": 60, "extra": { "name": "USD Coin", "version": "2", "feePayer": "..." } } ] } ``` The `amount` is in USDC atomic units (6 decimals), so `"7000"` = \$0.007. The client can pay with any advertised `accepts` entry it supports. Solana entries include facilitator-provided fields such as `extra.feePayer`; use the exact entry from the `PAYMENT-REQUIRED` header when constructing the payment. ### Step 2: Pay and retry Sign the payment with your wallet and re-send the request with a `PAYMENT-SIGNATURE` header containing your base64-encoded payment payload. The x402 client SDKs handle this automatically. ### Step 3: Settlement Exa verifies your payment signature with the facilitator, then starts on-chain settlement **in parallel** with processing your request. The response is held until settlement confirms. On success, you receive: * HTTP `200` with your results * A `PAYMENT-RESPONSE` header containing the settlement receipt (base64-encoded), including the on-chain transaction hash If settlement fails, you get `402` with both `PAYMENT-RESPONSE` (error details) and `PAYMENT-REQUIRED` (so you can retry). ## Pricing x402 uses the same bundled pricing as API key billing. Prices are calculated upfront based on your request parameters (not actual results returned). ### Search (`/search`) | Search type | Base price (up to 10 results) | Per result beyond 10 | | ------------------------- | ----------------------------- | -------------------- | | `instant`, `auto`, `fast` | \$0.007 / request | N/A (capped at 10) | | `deep-lite` | \$0.012 / request | N/A (capped at 10) | | `deep` | \$0.012 / request | N/A (capped at 10) | | `deep-reasoning` | \$0.015 / request | N/A (capped at 10) | Adding `contents.summary` costs an additional **\$0.001 per result**. x402 requests are capped at **10 results maximum**. If you request more than 10, `numResults` is silently clamped to 10 and pricing is based on 10 results. ### Contents (`/contents`) Each content type is charged per page/URL: | Content type | Price per page | | ------------ | -------------- | | `text` | \$0.001 | | `highlights` | \$0.001 | | `summary` | \$0.001 | If you request no content types (no `text`, `highlights`, or `summary`), `text` is enabled by default. ### Examples | Request | Price | USDC atomic | | -------------------------------------------------- | ------- | ----------- | | `/search` with 10 results, `type: "auto"` | \$0.007 | 7000 | | `/search` with 5 results, `type: "fast"` | \$0.007 | 7000 | | `/search` with 3 results + summary, `type: "auto"` | \$0.010 | 10000 | | `/search` with 10 results, `type: "deep-lite"` | \$0.012 | 12000 | | `/search` with 10 results, `type: "deep"` | \$0.012 | 12000 | | `/contents` for 2 URLs with `text: true` | \$0.002 | 2000 | | `/contents` for 1 URL with `text` + `summary` | \$0.002 | 2000 | ## Quickstart ### Install dependencies ```bash JavaScript theme={null} npm install @x402/fetch @x402/core @x402/evm viem # For Solana support, also install: npm install @x402/svm @solana/kit @scure/base ``` ```bash Python theme={null} pip install "x402[requests,evm]" # For Solana support, also install: pip install "x402[svm]" "solana<0.40" ``` No install is needed for cURL, but you'll need to handle the 402 challenge and payment signing manually. The SDK approach is recommended for production use. Don't want to manage private keys? [Coinbase Agentic Wallets](https://docs.cdp.coinbase.com/agent-kit/core-concepts/wallet-management) provide TEE-isolated key management for AI agents. Your agent never sees the private key. The wallet is viem-compatible, so it works directly with `@x402/fetch`. ### Make a paid search request ```typescript JavaScript theme={null} import { wrapFetchWithPayment } from "@x402/fetch"; import { x402Client, x402HTTPClient } from "@x402/core/client"; import { ExactEvmScheme } from "@x402/evm/exact/client"; // For Solana support, also import: // import { ExactSvmScheme } from "@x402/svm/exact/client"; import { privateKeyToAccount } from "viem/accounts"; const signer = privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as `0x${string}`); const client = new x402Client(); client.register("eip155:*", new ExactEvmScheme(signer)); // Register a Solana signer too if you want the client to use Solana accept // entries such as `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`: // client.register("solana:*", new ExactSvmScheme(svmSigner)); const fetchWithPayment = wrapFetchWithPayment(fetch, client); const response = await fetchWithPayment("https://api.exa.ai/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: "best machine learning frameworks", numResults: 5, }), }); const data = await response.json(); console.log(data.results); // Check settlement receipt const httpClient = new x402HTTPClient(client); const receipt = httpClient.getPaymentSettleResponse( (name) => response.headers.get(name) ); console.log("Transaction:", receipt?.transaction); ``` ```python Python theme={null} import os import requests from eth_account import Account from x402 import x402ClientSync from x402.http.clients import wrapRequestsWithPayment from x402.mechanisms.evm.exact import register_exact_evm_client from x402.mechanisms.evm.signers import EthAccountSigner account = Account.from_key(os.environ["WALLET_PRIVATE_KEY"]) client = x402ClientSync() register_exact_evm_client( client, EthAccountSigner(account), networks="eip155:*", ) session = wrapRequestsWithPayment(requests.Session(), client) response = session.post("https://api.exa.ai/search", json={ "query": "best machine learning frameworks", "numResults": 5, }) data = response.json() for result in data["results"]: print(result["url"], result["title"]) print("Payment response:", response.headers.get("PAYMENT-RESPONSE")) ``` ```bash cURL theme={null} # Step 1: Discovery, get pricing info curl -s -o /dev/null -w "%{http_code}" -D - \ -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -d '{"query": "best machine learning frameworks", "numResults": 5}' # Returns 402 with PAYMENT-REQUIRED header containing base64-encoded pricing # Step 2: Sign the payment with your wallet (use the SDK for this) # Step 3: Retry with payment signature curl -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "PAYMENT-SIGNATURE: " \ -d '{"query": "best machine learning frameworks", "numResults": 5}' # Returns 200 with results + PAYMENT-RESPONSE header (settlement receipt) ``` cURL requires manual payment signing. For production, use the JavaScript or Python SDK which handles the full 402 > sign > retry flow automatically. ### Discovery mode (no wallet needed) Probe pricing without a wallet by sending unauthenticated requests: ```typescript JavaScript theme={null} const res = await fetch("https://api.exa.ai/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: "test query", numResults: 3 }), }); // res.status === 402 const paymentRequired = JSON.parse( atob(res.headers.get("PAYMENT-REQUIRED")!) ); console.log( paymentRequired.accepts.map(({ network, amount }) => ({ network, amount, })) ); ``` ```python Python theme={null} import base64, json, requests res = requests.post("https://api.exa.ai/search", json={ "query": "test query", "numResults": 3, }) # res.status_code == 402 pricing = json.loads(base64.b64decode(res.headers["PAYMENT-REQUIRED"])) print([(accept["network"], accept["amount"]) for accept in pricing["accepts"]]) ``` ```bash cURL theme={null} curl -s -D - -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -d '{"query": "test query", "numResults": 3}' # Look for the PAYMENT-REQUIRED header in the 402 response # Decode it: echo "" | base64 -d ``` ## Payment networks Exa advertises every currently supported network in the `accepts` array. Choose the entry that matches your wallet and registered x402 client scheme. | Network | Identifier | Token | Asset | | ------------------ | ----------------------------------------- | ----- | ---------------------------------------------- | | Base (Ethereum L2) | `eip155:8453` | USDC | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | | Solana mainnet | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | USDC | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` | Both use 6-decimal USDC (`1000000` = \$1.00) and settle on-chain via an x402 facilitator. ## Rate limits x402 has its own rate limiting separate from API key limits: | Limit | Threshold | Window | | ---------------------------------- | ------------------ | ---------- | | Unpaid discovery requests (per IP) | 5 requests | 60 seconds | | Paid requests (per wallet) | 10 requests/second | 1 second | After 5 unauthenticated `402` discovery requests from the same IP within 60 seconds, further requests return `429 Too Many Requests`. Making a successful paid request decrements the counter. Per-wallet QPS is enforced across all paid requests from the same wallet address. ## Headers reference ### Request headers | Header | Description | | ------------------- | ---------------------------------------- | | `PAYMENT-SIGNATURE` | Base64-encoded payment payload (x402 v2) | | `payment-signature` | Alias (also accepted) | | `x-payment` | Legacy alias (v1 compatibility) | ### Response headers | Header | When | Description | | ------------------ | -------------------------------------- | ----------------------------------------------------------------------------- | | `PAYMENT-REQUIRED` | `402` responses | Base64-encoded `PaymentRequired` object with pricing and payment instructions | | `PAYMENT-RESPONSE` | `200` or `402` (after payment attempt) | Base64-encoded settlement result with transaction hash or error | ## Error codes | Status | Tag | Description | | ------ | -------------------------- | ------------------------------------------------------------------ | | `402` | `X402_PAYMENT_REQUIRED` | No payment provided. Includes pricing in `PAYMENT-REQUIRED` header | | `402` | `X402_VERIFICATION_FAILED` | Payment signature did not pass facilitator verification | | `400` | `X402_INVALID_SIGNATURE` | Malformed or unparseable payment signature | | `429` | `X402_TOO_MANY_UNPAID` | Too many unpaid discovery requests from this IP | | `429` | `X402_WALLET_RATE_LIMITED` | Wallet exceeded 10 requests/second | | `500` | `X402_INTERNAL_ERROR` | Server-side error generating payment requirements | ## FAQ If your request includes an `x-api-key` header or `Authorization: Bearer` token, the API key flow takes priority and x402 is bypassed. They don't stack. It's one or the other per request. Your response is blocked. You receive a `402` with both `PAYMENT-RESPONSE` (containing the error) and `PAYMENT-REQUIRED` (so your client can retry). No results are returned until settlement succeeds. x402 requests enforce a maximum of 10 results per search. If you need more, use the API key flow with a paid plan. Any EVM-compatible wallet that can sign EIP-712 typed data on Base, or a Solana wallet supported by the x402 SVM client for `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`. The x402 SDK supports `viem`, `ethers`, Coinbase Wallet signers, and Solana SVM signers. For EVM-based AI agents, [Coinbase Agentic Wallets](https://docs.cdp.coinbase.com/agent-kit/core-concepts/wallet-management) offer TEE-isolated key management so your agent never handles raw private keys directly. ## Resources * [x402 protocol docs](https://docs.x402.org): full protocol specification * [x402 GitHub](https://github.com/coinbase/x402): open-source SDKs and examples * [@x402/fetch on npm](https://www.npmjs.com/package/@x402/fetch): fetch wrapper for automatic payment handling * [@x402/svm on npm](https://www.npmjs.com/package/@x402/svm): Solana/SVM exact payment support * [Exa Search API guide](/docs/search/quickstart): full search parameter reference * [Exa Contents API guide](/docs/contents/quickstart): full contents parameter reference # Pydantic AI Source: https://exa.ai/docs/integrations/pydantic-ai Give a Pydantic AI agent web research tools backed by the Exa search API. New to Exa? Get started in under a minute. *** [Pydantic AI](https://pydantic.dev/docs/ai/) is a Python agent framework from the team behind Pydantic. Its [harness](https://pydantic.dev/docs/ai/harness/exa-search/) ships an official Exa integration as two composable capabilities: * **`ExaSearch`**: web research tools backed by the Exa Search API: `web_search` (top results with their most relevant excerpts, plus an optional synthesized text summary), `get_page` (full-page retrieval for a specific URL), and opt-in `deep_search` (a synthesized, cited answer in one call). * **`ExaAgent`**: delegates long-running research to the [Exa Agent API](/docs/agent/quickstart) as deferred tool calls. A capability bundles the tools, per-tool output budgets, and short research guidance in the system prompt, so you don't have to wire a search API to a page fetcher and prompt the agent to research methodically yourself. See the full reference from Pydantic [here](https://pydantic.dev/docs/ai/harness/exa-search/). A walkthrough of three copy-paste research agents built on Pydantic AI and Exa. *** ## Get Started Install the harness with the Exa extra and set your `EXA_API_KEY` environment variable. ```Bash Bash theme={null} uv add "pydantic-ai-harness[exa]" ``` Create a key in the dashboard. New accounts start with free credits. Pass `ExaSearch` to an `Agent` via the `capabilities` parameter. Authentication comes from `EXA_API_KEY` by default. ```Python Python theme={null} from pydantic_ai import Agent from pydantic_ai_harness.exa import ExaSearch agent = Agent('anthropic:claude-sonnet-4-6', capabilities=[ExaSearch()]) result = agent.run_sync('What changed in the latest stable Python release?') print(result.output) ``` `ExaSearch` contributes two tools to the agent: | Tool | Purpose | | ------------ | ------------------------------------------------------------------------------------------------------------ | | `web_search` | Search the web and return the top `num_results` pages, each with title, URL, and its most relevant excerpts. | | `get_page` | Retrieve the full text of one specific URL — a promising `web_search` hit, or a URL the user provided. | `web_search` returns short excerpts (Exa highlights) rather than full page text, so surveying several sources stays cheap; the agent then reads a chosen page with `get_page`. A URL or question that returns no content, a rate limit, or a transient failure surfaces to the model as a `ModelRetry` so the run can recover; authentication failures (401/403) propagate as configuration errors. `deep_search` runs Exa's multi-step [deep search](/docs/search/quickstart) (`type='deep'`): Exa expands the question into multiple queries, searches, and returns an answer grounded in citations in one tool call. It invests more time and search depth than `web_search`, so it is off by default. Enable it explicitly: ```Python Python theme={null} from pydantic_ai_harness.exa import ExaSearch agent = Agent('anthropic:claude-sonnet-4-6', capabilities=[ExaSearch(include_deep_search=True)]) ``` When enabled, the capability's instructions tell the model to treat `deep_search` as an escalation from `web_search`, not a replacement. *** ## Configuration Every field of `ExaSearch` with its default: ```Python Python theme={null} from pydantic_ai_harness.exa import ExaSearch ExaSearch( num_results=5, # results per web_search call (1 to 100) max_text_chars=10_000, # get_page text cap, in characters (1 to 10,000) text_summary=False, # web_search also returns a synthesized text summary include_deep_search=False, # also expose the deep_search tool include_domains=[], # only search these domains (allowlist) exclude_domains=[], # never search these domains (denylist) guidance=None, # None = default instructions, '' = none, str = custom client=None, # ExaClient -- None builds exa_py.AsyncExa from EXA_API_KEY ) ``` `include_domains` and `exclude_domains` apply to `web_search` and `deep_search`, and are mutually exclusive. Out-of-range limits and setting both domain lists raise at construction. ### Text summary Set `text_summary` to have every `web_search` call also request a synthesized plain-text summary of the results. Pass `True` for an unconstrained summary, or a string describing the desired format: ```Python Python theme={null} from pydantic_ai_harness.exa import ExaSearch ExaSearch(text_summary='One concise sentence with the requested facts.') ``` The tool's return shape is unchanged: when Exa returns a summary it is prepended as a `Summary:` line. ### Structured citations Every tool returns a `ToolReturn`: `return_value` carries the readable text the model sees (including the `Sources:` blocks), and `metadata` carries the sources as structured `ExaSource` records (`{'url': ..., 'title': ...}`) under the `'sources'` key. Metadata is never sent to the model, so rendering citations needs no text parsing: ```Python Python theme={null} from pydantic_ai.messages import ModelRequest, ToolReturnPart for message in result.all_messages(): if isinstance(message, ModelRequest): for part in message.parts: if isinstance(part, ToolReturnPart) and part.metadata is not None: for source in part.metadata.get('sources', []): print(source['url'], source['title']) ``` ### Custom client The default client is `exa_py.AsyncExa`, configured from `EXA_API_KEY`. Pass any object satisfying the `ExaClient` protocol to configure authentication or the base URL explicitly, or to substitute a fake in tests: ```Python Python theme={null} from exa_py import AsyncExa from pydantic_ai_harness.exa import ExaSearch ExaSearch(client=AsyncExa(api_key='...')) ``` *** ## Exa agent runs The [Exa Agent API](/docs/agent/quickstart) runs open-ended research tasks asynchronously. The `ExaAgent` capability maps that lifecycle onto Pydantic AI's [deferred tool calls](https://pydantic.dev/docs/ai/deferred-tools/): its `exa_agent` tool creates the run and defers, carrying the Exa run ID in the deferred call's metadata. ```Python Python theme={null} from pydantic_ai import Agent from pydantic_ai_harness.exa import ExaAgent agent = Agent('anthropic:claude-sonnet-4-6', capabilities=[ExaAgent()]) ``` By default (`execution='inline'`) the capability resolves its own deferred calls within the agent run by polling the Exa run to completion, so the tool behaves like a regular (if slow) tool. With `execution='external'` the calls bubble up as `DeferredToolRequests` output for the host application to resolve out of band. Every field of `ExaAgent` with its default: ```Python Python theme={null} from pydantic_ai_harness.exa import ExaAgent ExaAgent( effort=None, # 'low' | 'medium' | 'high' | 'xhigh' | 'auto' -- None = API default execution='inline', # 'inline' polls to completion; 'external' bubbles DeferredToolRequests output_schema=None, # BaseModel class or dict schema for structured output system_prompt=None, # forwarded to the Exa agent run poll_interval=1000, # ms between polls when resolving inline timeout_ms=3_600_000, # ms to wait for a run when resolving inline guidance=None, # None = default instructions, '' = none, str = custom runs=None, # ExaAgentRuns -- None builds AsyncExa().agent.runs from EXA_API_KEY ) ``` *** ## Agent spec (YAML/JSON) Both capabilities work with Pydantic AI's [agent spec](https://pydantic.dev/docs/ai/agents/#agent-spec), so you can declare them in a config file instead of Python: ```yaml agent.yaml theme={null} model: anthropic:claude-sonnet-4-6 capabilities: - ExaSearch: num_results: 3 include_deep_search: true - ExaAgent: effort: low ``` ```Python Python theme={null} from pydantic_ai import Agent from pydantic_ai_harness.exa import ExaAgent, ExaSearch agent = Agent.from_file('agent.yaml', custom_capability_types=[ExaSearch, ExaAgent]) ``` Pass `custom_capability_types` so the spec loader knows how to instantiate the capabilities. Spec-loaded instances always build the default client from `EXA_API_KEY`. *** ## Next * [**Search API**](/docs/search/quickstart) - Semantic search with highlights, summaries, and deep search * [**Agent API**](/docs/agent/quickstart) - Open-ended asynchronous research runs * [**MCP Setup**](/docs/get-started/exa-mcp) - Exa's hosted MCP server * [**SDKs**](/docs/sdks/quickstart) - Python and JavaScript SDK docs # Snowflake Source: https://exa.ai/docs/integrations/snowflake Use Exa search and content retrieval from Snowflake stored procedures and Cortex Agents. Use Exa's search API directly from Snowflake to enrich your data with live web intelligence — without leaving SQL. This integration uses Snowflake's [External Access](https://docs.snowflake.com/en/developer-guide/external-network-access/external-network-access-overview) feature to securely call the Exa API from stored procedures, enabling two main workflows: 1. **Cortex Agent tools** — give a Snowflake Cortex Agent the ability to search the web and retrieve page contents 2. **Data enrichment** — enrich tables with CEO/founder info, latest news, and other web data in batch ## Prerequisites 1. An Exa API key 2. `ACCOUNTADMIN` role (required for creating External Access Integrations) 3. A Snowflake account with External Access support (paid accounts) Create a key in the dashboard. New accounts start with free credits. ## Setup Run the following SQL in a Snowflake worksheet. Replace `<>` with your actual key. ### Roles and database ```sql theme={null} USE ROLE ACCOUNTADMIN; CREATE ROLE IF NOT EXISTS exa_agent_role; GRANT DATABASE ROLE SNOWFLAKE.CORTEX_USER TO ROLE exa_agent_role; SET my_user = CURRENT_USER(); GRANT ROLE exa_agent_role TO USER IDENTIFIER($my_user); CREATE DATABASE IF NOT EXISTS exa_integration; CREATE SCHEMA IF NOT EXISTS exa_integration.tools; GRANT USAGE ON DATABASE exa_integration TO ROLE exa_agent_role; GRANT USAGE ON SCHEMA exa_integration.tools TO ROLE exa_agent_role; ``` ### Warehouse ```sql theme={null} CREATE WAREHOUSE IF NOT EXISTS exa_agent_wh WITH WAREHOUSE_SIZE = 'XSMALL' AUTO_SUSPEND = 60 AUTO_RESUME = TRUE INITIALLY_SUSPENDED = TRUE; GRANT USAGE, OPERATE ON WAREHOUSE exa_agent_wh TO ROLE exa_agent_role; ``` ### Network rule, secret, and external access ```sql theme={null} CREATE OR REPLACE NETWORK RULE exa_integration.tools.exa_api_rule MODE = EGRESS TYPE = HOST_PORT VALUE_LIST = ('api.exa.ai:443'); CREATE OR REPLACE SECRET exa_integration.tools.exa_api_key TYPE = GENERIC_STRING SECRET_STRING = '<>'; CREATE OR REPLACE EXTERNAL ACCESS INTEGRATION exa_api_access ALLOWED_NETWORK_RULES = (exa_integration.tools.exa_api_rule) ALLOWED_AUTHENTICATION_SECRETS = (exa_integration.tools.exa_api_key) ENABLED = TRUE; GRANT USAGE ON INTEGRATION exa_api_access TO ROLE exa_agent_role; ``` This creates: * A **network rule** allowing egress only to `api.exa.ai:443` * A **secret** storing your API key (encrypted, not visible in SQL) * An **external access integration** binding the rule and secret together ## Stored procedures ### EXA\_SEARCH Searches the web using Exa's search API. Returns results with titles, URLs, scores, and optionally text/highlights/summaries. ```sql theme={null} CREATE OR REPLACE PROCEDURE EXA_INTEGRATION.TOOLS.EXA_SEARCH( query STRING, num_results INTEGER DEFAULT 10, search_type STRING DEFAULT 'auto', category STRING DEFAULT NULL, include_domains STRING DEFAULT NULL, exclude_domains STRING DEFAULT NULL, start_published_date STRING DEFAULT NULL, end_published_date STRING DEFAULT NULL, include_text STRING DEFAULT NULL, get_contents BOOLEAN DEFAULT TRUE ) RETURNS STRING LANGUAGE PYTHON RUNTIME_VERSION = '3.10' PACKAGES = ('requests', 'snowflake-snowpark-python') EXTERNAL_ACCESS_INTEGRATIONS = (EXA_API_ACCESS) SECRETS = ('cred' = exa_integration.tools.exa_api_key) HANDLER = 'main' AS $$ import _snowflake import requests import json def main(session, query, num_results, search_type, category, include_domains, exclude_domains, start_published_date, end_published_date, include_text, get_contents): api_key = _snowflake.get_generic_secret_string('cred') url = "https://api.exa.ai/search" body = { "query": query, "numResults": num_results, "type": search_type, } if category: body["category"] = category if include_domains: body["includeDomains"] = [d.strip() for d in include_domains.split(",")] if exclude_domains: body["excludeDomains"] = [d.strip() for d in exclude_domains.split(",")] if start_published_date: body["startPublishedDate"] = start_published_date if end_published_date: body["endPublishedDate"] = end_published_date if include_text: body["includeText"] = [include_text] if get_contents: body["contents"] = { "text": {"maxCharacters": 3000}, "highlights": True, "summary": True } headers = { "x-api-key": api_key, "Content-Type": "application/json", "x-exa-integration": "snowflake-cortex-agent" } try: resp = requests.post(url, json=body, headers=headers, timeout=30) resp.raise_for_status() data = resp.json() results = [] for r in data.get("results", []): result = { "title": r.get("title", ""), "url": r.get("url", ""), "score": r.get("score"), "publishedDate": r.get("publishedDate", ""), } if get_contents: result["summary"] = r.get("summary", "") result["highlights"] = r.get("highlights", []) result["text"] = (r.get("text", "") or "")[:2000] results.append(result) return json.dumps({ "query": query, "numResults": len(results), "results": results }) except requests.exceptions.HTTPError as e: return json.dumps({"error": str(e), "status_code": e.response.status_code, "detail": e.response.text}) except Exception as e: return json.dumps({"error": str(e)}) $$; GRANT USAGE ON PROCEDURE EXA_INTEGRATION.TOOLS.EXA_SEARCH( STRING, INTEGER, STRING, STRING, STRING, STRING, STRING, STRING, STRING, BOOLEAN ) TO ROLE exa_agent_role; ``` #### Parameters | Parameter | Type | Default | Description | | ---------------------- | --------- | -------- | --------------------------------------------------------------------------------------- | | `query` | `STRING` | required | The search query | | `num_results` | `INTEGER` | `10` | Number of results (1–100) | | `search_type` | `STRING` | `'auto'` | Recommended values: `auto`, `instant`, `fast`, `deep-lite`, `deep`, or `deep-reasoning` | | `category` | `STRING` | `NULL` | `company`, `publication`, `news`, `personal_site`, `financial_report`, `people` | | `include_domains` | `STRING` | `NULL` | Comma-separated domains to restrict to | | `exclude_domains` | `STRING` | `NULL` | Comma-separated domains to exclude | | `start_published_date` | `STRING` | `NULL` | ISO 8601 date lower bound | | `end_published_date` | `STRING` | `NULL` | ISO 8601 date upper bound | | `include_text` | `STRING` | `NULL` | Phrase that must appear in results | | `get_contents` | `BOOLEAN` | `TRUE` | Fetch text, highlights, and summaries | ### EXA\_GET\_CONTENTS Retrieves page contents (text, highlights, summaries) for specific URLs. ```sql theme={null} CREATE OR REPLACE PROCEDURE EXA_INTEGRATION.TOOLS.EXA_GET_CONTENTS( urls STRING, get_text BOOLEAN DEFAULT TRUE, get_highlights BOOLEAN DEFAULT TRUE, get_summary BOOLEAN DEFAULT TRUE ) RETURNS STRING LANGUAGE PYTHON RUNTIME_VERSION = '3.10' PACKAGES = ('requests', 'snowflake-snowpark-python') EXTERNAL_ACCESS_INTEGRATIONS = (EXA_API_ACCESS) SECRETS = ('cred' = exa_integration.tools.exa_api_key) HANDLER = 'main' AS $$ import _snowflake import requests import json def main(session, urls, get_text, get_highlights, get_summary): api_key = _snowflake.get_generic_secret_string('cred') url = "https://api.exa.ai/contents" url_list = [u.strip() for u in urls.split(",")] contents = {} if get_text: contents["text"] = {"maxCharacters": 5000} if get_highlights: contents["highlights"] = True if get_summary: contents["summary"] = True body = { "urls": url_list, **contents } headers = { "x-api-key": api_key, "Content-Type": "application/json", "x-exa-integration": "snowflake-cortex-agent" } try: resp = requests.post(url, json=body, headers=headers, timeout=30) resp.raise_for_status() data = resp.json() results = [] for r in data.get("results", []): result = { "title": r.get("title", ""), "url": r.get("url", ""), } if get_text: result["text"] = (r.get("text", "") or "")[:3000] if get_highlights: result["highlights"] = r.get("highlights", []) if get_summary: result["summary"] = r.get("summary", "") results.append(result) return json.dumps({"numResults": len(results), "results": results}) except requests.exceptions.HTTPError as e: return json.dumps({"error": str(e), "status_code": e.response.status_code, "detail": e.response.text}) except Exception as e: return json.dumps({"error": str(e)}) $$; GRANT USAGE ON PROCEDURE EXA_INTEGRATION.TOOLS.EXA_GET_CONTENTS( STRING, BOOLEAN, BOOLEAN, BOOLEAN ) TO ROLE exa_agent_role; ``` ## Usage examples ### Basic search ```sql theme={null} USE WAREHOUSE exa_agent_wh; CALL EXA_INTEGRATION.TOOLS.EXA_SEARCH( 'latest developments in renewable energy', 5, -- num_results 'auto', -- search_type 'news', -- category NULL, NULL, '2025-01-01T00:00:00.000Z', -- start_published_date NULL, NULL, TRUE ); ``` ### Get contents for specific URLs ```sql theme={null} CALL EXA_INTEGRATION.TOOLS.EXA_GET_CONTENTS( 'https://example.com/article1,https://example.com/article2', TRUE, -- get_text TRUE, -- get_highlights TRUE -- get_summary ); ``` ### Company research ```sql theme={null} CALL EXA_INTEGRATION.TOOLS.EXA_SEARCH( 'Stripe CEO founder leadership', 5, 'auto', 'company', NULL, NULL, NULL, NULL, NULL, TRUE ); ``` ## Data enrichment Exa can enrich Snowflake tables with live web data — CEO/founder info, latest news, and more — all without leaving the platform. ### Create a prospect table ```sql theme={null} CREATE DATABASE IF NOT EXISTS exa_enrichment; CREATE SCHEMA IF NOT EXISTS exa_enrichment.demo; CREATE OR REPLACE TABLE exa_enrichment.demo.prospect_companies ( company_name STRING, website STRING, industry STRING, employee_count INTEGER, ceo_name STRING, ceo_title STRING, ceo_source_url STRING, latest_news_headline STRING, latest_news_summary STRING, latest_news_url STRING, latest_news_date STRING, leaders_enriched_at TIMESTAMP, news_enriched_at TIMESTAMP ); INSERT INTO exa_enrichment.demo.prospect_companies (company_name, website, industry, employee_count) VALUES ('Stripe', 'https://stripe.com', 'Fintech / Payments', 8000), ('Databricks', 'https://databricks.com', 'Data & AI Platform', 7000), ('Figma', 'https://figma.com', 'Design Tools', 1500), ('Anthropic', 'https://anthropic.com', 'AI Research', 1000), ('Vercel', 'https://vercel.com', 'Developer Platform', 600); ``` ### Enrich with CEO/founder info ```sql theme={null} CREATE OR REPLACE PROCEDURE exa_enrichment.demo.enrich_company_leaders( target_company STRING ) RETURNS STRING LANGUAGE PYTHON RUNTIME_VERSION = '3.10' PACKAGES = ('requests', 'snowflake-snowpark-python') EXTERNAL_ACCESS_INTEGRATIONS = (EXA_API_ACCESS) SECRETS = ('cred' = exa_integration.tools.exa_api_key) HANDLER = 'main' AS $$ import _snowflake import requests import json import re def main(session, target_company): api_key = _snowflake.get_generic_secret_string('cred') body = { "query": f"{target_company} CEO founder leadership team", "numResults": 5, "type": "auto", "category": "company", "contents": { "text": {"maxCharacters": 2000}, "highlights": True, "summary": True } } headers = { "x-api-key": api_key, "Content-Type": "application/json", "x-exa-integration": "snowflake-cortex-agent" } resp = requests.post( "https://api.exa.ai/search", json=body, headers=headers, timeout=30 ) resp.raise_for_status() data = resp.json() ceo_name = None ceo_title = None source_url = None ceo_keywords = ["ceo", "chief executive", "founder", "co-founder"] for r in data.get("results", []): text = ((r.get("text", "") or "") + " " + (r.get("summary", "") or "")).lower() title_text = (r.get("title", "") or "").lower() combined = text + " " + title_text for kw in ceo_keywords: idx = combined.find(kw) if idx == -1: continue original = (r.get("text", "") or "") + " " + (r.get("summary", "") or "") + " " + (r.get("title", "") or "") names = re.findall( r'(?:^|[\s,])([A-Z][a-z]+(?:\s[A-Z][a-z]+)+)', original[max(0, idx - 200):idx + 200] ) if names: ceo_name = names[0].strip() title_map = { "co-founder": "Co-Founder & CEO", "founder": "Founder & CEO", "chief executive": "CEO", "ceo": "CEO", } ceo_title = title_map.get(kw, "CEO") source_url = r.get("url", "") break if ceo_name: break if ceo_name: safe = lambda s: (s or "").replace("'", "''") session.sql(f""" UPDATE exa_enrichment.demo.prospect_companies SET ceo_name = '{safe(ceo_name)}', ceo_title = '{safe(ceo_title)}', ceo_source_url = '{safe(source_url)}', leaders_enriched_at = CURRENT_TIMESTAMP() WHERE company_name = '{safe(target_company)}' """).collect() return json.dumps({ "company": target_company, "ceo_name": ceo_name, "ceo_title": ceo_title, "source_url": source_url }) $$; ``` ### Enrich with latest news ```sql theme={null} CREATE OR REPLACE PROCEDURE exa_enrichment.demo.enrich_company_news( target_company STRING ) RETURNS STRING LANGUAGE PYTHON RUNTIME_VERSION = '3.10' PACKAGES = ('requests', 'snowflake-snowpark-python') EXTERNAL_ACCESS_INTEGRATIONS = (EXA_API_ACCESS) SECRETS = ('cred' = exa_integration.tools.exa_api_key) HANDLER = 'main' AS $$ import _snowflake import requests import json from datetime import datetime, timedelta def main(session, target_company): api_key = _snowflake.get_generic_secret_string('cred') one_month_ago = (datetime.utcnow() - timedelta(days=30)).strftime( "%Y-%m-%dT00:00:00.000Z" ) body = { "query": f"latest news about {target_company}", "numResults": 3, "type": "auto", "category": "news", "startPublishedDate": one_month_ago, "contents": { "text": {"maxCharacters": 1000}, "summary": True } } headers = { "x-api-key": api_key, "Content-Type": "application/json", "x-exa-integration": "snowflake-cortex-agent" } resp = requests.post( "https://api.exa.ai/search", json=body, headers=headers, timeout=30 ) resp.raise_for_status() data = resp.json() results = data.get("results", []) if not results: return json.dumps({"company": target_company, "status": "no_news_found"}) top = results[0] headline = (top.get("title", "") or "")[:500] summary = (top.get("summary", "") or "")[:2000] news_url = top.get("url", "") or "" pub_date = top.get("publishedDate", "") or "" safe = lambda s: (s or "").replace("'", "''") session.sql(f""" UPDATE exa_enrichment.demo.prospect_companies SET latest_news_headline = '{safe(headline)}', latest_news_summary = '{safe(summary)}', latest_news_url = '{safe(news_url)}', latest_news_date = '{safe(pub_date)}', news_enriched_at = CURRENT_TIMESTAMP() WHERE company_name = '{safe(target_company)}' """).collect() return json.dumps({ "company": target_company, "headline": headline, "url": news_url, "published_date": pub_date }) $$; ``` ### Run enrichment ```sql theme={null} -- Enrich a single company CALL exa_enrichment.demo.enrich_company_leaders('Stripe'); CALL exa_enrichment.demo.enrich_company_news('Stripe'); -- View enriched results SELECT company_name, ceo_name, ceo_title, latest_news_headline, latest_news_url, latest_news_date FROM exa_enrichment.demo.prospect_companies; ``` ## Parallelizing Exa requests When enriching large tables, calling Exa sequentially per row is slow. You can parallelize requests using Python's `concurrent.futures` inside a single stored procedure, sending multiple Exa API calls at once. ### Concurrent enrichment procedure ```sql theme={null} CREATE OR REPLACE PROCEDURE exa_enrichment.demo.enrich_all_parallel( max_workers INTEGER DEFAULT 10 ) RETURNS STRING LANGUAGE PYTHON RUNTIME_VERSION = '3.10' PACKAGES = ('requests', 'snowflake-snowpark-python') EXTERNAL_ACCESS_INTEGRATIONS = (EXA_API_ACCESS) SECRETS = ('cred' = exa_integration.tools.exa_api_key) HANDLER = 'main' AS $$ import _snowflake import requests import json from concurrent.futures import ThreadPoolExecutor, as_completed def exa_search(api_key, company_name, query_template, category="company", num_results=5): body = { "query": query_template.format(company=company_name), "numResults": num_results, "type": "auto", "category": category, "contents": { "text": {"maxCharacters": 2000}, "highlights": True, "summary": True } } headers = { "x-api-key": api_key, "Content-Type": "application/json", "x-exa-integration": "snowflake-cortex-agent" } resp = requests.post("https://api.exa.ai/search", json=body, headers=headers, timeout=30) resp.raise_for_status() return {"company": company_name, "results": resp.json().get("results", [])} def main(session, max_workers): api_key = _snowflake.get_generic_secret_string('cred') rows = session.sql( "SELECT company_name FROM exa_enrichment.demo.prospect_companies" ).collect() companies = [row["COMPANY_NAME"] for row in rows] results = {} with ThreadPoolExecutor(max_workers=max_workers) as pool: futures = { pool.submit(exa_search, api_key, name, "{company} CEO founder leadership"): name for name in companies } for future in as_completed(futures): company = futures[future] try: results[company] = future.result() except Exception as e: results[company] = {"company": company, "error": str(e)} return json.dumps({ "enriched": len([r for r in results.values() if "error" not in r]), "errors": len([r for r in results.values() if "error" in r]), "results": results }) $$; ``` ### Choosing parallelism | `max_workers` | Use Case | | ------------- | ----------------------------------------------------------------------------- | | `5` | Conservative — stays well within rate limits | | `10` | Good default for most enrichment jobs | | `20–50` | Large datasets with a high [rate limit](/docs/admin/billing#rate-limits) tier | ### Snowflake task scheduling For recurring enrichment jobs, wrap the procedure in a [Snowflake Task](https://docs.snowflake.com/en/user-guide/tasks-intro): ```sql theme={null} CREATE OR REPLACE TASK exa_enrichment.demo.daily_enrichment WAREHOUSE = exa_agent_wh SCHEDULE = 'USING CRON 0 8 * * * America/Los_Angeles' AS CALL exa_enrichment.demo.enrich_all_parallel(10); ALTER TASK exa_enrichment.demo.daily_enrichment RESUME; ``` This runs the parallel enrichment every day at 8 AM PT. Adjust the cron schedule and `max_workers` to fit your volume and rate limits. ## Cortex Agent integration You can also register the Exa stored procedures as tools for a [Snowflake Cortex Agent](https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-agents), giving it the ability to search the web conversationally. ```sql theme={null} CREATE DATABASE IF NOT EXISTS exa_agents; CREATE SCHEMA IF NOT EXISTS exa_agents.agents; GRANT USAGE ON DATABASE exa_agents TO ROLE exa_agent_role; GRANT USAGE ON SCHEMA exa_agents.agents TO ROLE exa_agent_role; GRANT CREATE AGENT ON SCHEMA exa_agents.agents TO ROLE exa_agent_role; USE DATABASE exa_agents; USE SCHEMA agents; USE WAREHOUSE exa_agent_wh; CREATE OR REPLACE AGENT exa_web_search_agent COMMENT = 'Agent with Exa web search and content retrieval' FROM SPECIFICATION $$ models: orchestration: auto orchestration: budget: seconds: 60 tokens: 32000 instructions: system: | You are a helpful research assistant with access to Exa, a web search engine built for AI. Always cite your sources with URLs. orchestration: | - Use EXA_SEARCH for questions requiring web search or external knowledge. - Use EXA_GET_CONTENTS when the user provides specific URLs. - For news queries, set category to "news". - For scholarly publications (research papers, preprints, journal articles), set category to "publication". - For company lookups, set category to "company". - When asked about recent events, set start_published_date to a recent date. - Always request contents (get_contents=TRUE) for substantive answers. tools: - tool_spec: type: generic name: exa_search description: > Search the web using Exa. Returns web pages with titles, URLs, text content, highlights, and summaries. Supports filtering by domain, date, category, and text content. input_schema: type: object properties: query: type: string description: The search query. num_results: type: integer description: Number of results (1-100). Default 10. search_type: type: string enum: ["auto", "instant", "fast", "deep-lite", "deep", "deep-reasoning"] category: type: string enum: ["company", "publication", "news", "personal_site", "financial_report", "people"] include_domains: type: string description: Comma-separated domains to restrict to. exclude_domains: type: string description: Comma-separated domains to exclude. start_published_date: type: string description: ISO 8601 date lower bound. end_published_date: type: string description: ISO 8601 date upper bound. include_text: type: string description: Phrase that must appear in results. get_contents: type: boolean description: Fetch page contents. Default true. required: - query - tool_spec: type: generic name: exa_get_contents description: > Retrieve text, highlights, and summaries for specific URLs. input_schema: type: object properties: urls: type: string description: Comma-separated URLs. get_text: type: boolean get_highlights: type: boolean get_summary: type: boolean required: - urls tool_resources: exa_search: type: procedure execution_environment: type: warehouse warehouse: EXA_AGENT_WH identifier: EXA_INTEGRATION.TOOLS.EXA_SEARCH exa_get_contents: type: procedure execution_environment: type: warehouse warehouse: EXA_AGENT_WH identifier: EXA_INTEGRATION.TOOLS.EXA_GET_CONTENTS $$; ``` Once created, open Snowflake Intelligence and try asking your agent: * "Search the web for recent AI agent frameworks" * "Find research papers about retrieval augmented generation" * "What are the top AI companies to watch?" ## Security The integration uses Snowflake's built-in security features: | Layer | Detail | | ------------------------------- | --------------------------------------------------------- | | **Roles** | `exa_agent_role` for scoped access to procedures and data | | **Network Rule** | Egress restricted to `api.exa.ai:443` only | | **Secret** | API key stored encrypted, not visible in SQL queries | | **External Access Integration** | Binds network rule + secret together | Grant access to additional users: ```sql theme={null} GRANT ROLE exa_agent_role TO USER analyst_user; ``` ## More resources * [Exa API Documentation](/docs/search/quickstart) * [Exa Dashboard](https://dashboard.exa.ai) — get your API key * [Snowflake External Access Docs](https://docs.snowflake.com/en/developer-guide/external-network-access/external-network-access-overview) * [Snowflake Cortex Agents Docs](https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-agents) # Anthropic Tool Calling Source: https://exa.ai/docs/integrations/tool-calling/anthropic Use Claude tool use to add Exa web search and page contents to your application. New to Exa? Get started in under a minute. *** Claude's [tool use](https://docs.anthropic.com/en/docs/build-with-claude/tool-use) allows models to call functions that you define in your code. The Exa SDKs ship ready-made web search and page reading tools for Anthropic, so you don't have to hand-write the tool schema, parse `tool_use` blocks, or format Exa results yourself. ## Get started ```bash Python theme={null} pip install anthropic exa_py ``` ```bash JavaScript theme={null} npm install @anthropic-ai/sdk exa-js ``` Set the `EXA_API_KEY` and `ANTHROPIC_API_KEY` environment variables. Visit the [Anthropic console](https://console.anthropic.com/settings/keys) and the [Exa dashboard](https://dashboard.exa.ai/api-keys) to generate your API keys. Create a key in the dashboard. New accounts start with free credits. Pass the tools in the request's `tools` list, then hand the assistant message to `handle_tool_use`. It executes every `tool_use` block in the message and returns the matching `tool_result` blocks, ready to send back in the next user message. `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} import anthropic from exa_py import Exa exa = Exa() # reads EXA_API_KEY from the environment claude = anthropic.Anthropic() messages = [{"role": "user", "content": "What's the latest on AI chips?"}] response = claude.messages.create( model="claude-sonnet-4-6", max_tokens=1024, messages=messages, tools=[exa.anthropic.web_search(), exa.anthropic.get_contents()], ) messages.append({"role": "assistant", "content": response.content}) messages.append( {"role": "user", "content": exa.anthropic.handle_tool_use(response)} ) response = claude.messages.create( model="claude-sonnet-4-6", max_tokens=1024, messages=messages, tools=[exa.anthropic.web_search(), exa.anthropic.get_contents()], ) print(response.content[0].text) ``` ```javascript JavaScript theme={null} import Anthropic from "@anthropic-ai/sdk"; import Exa from "exa-js"; const exa = new Exa(); // reads EXA_API_KEY from the environment const anthropic = new Anthropic(); const messages = [ { role: "user", content: "What's the latest on AI chips?" }, ]; let response = await anthropic.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, messages, tools: [exa.anthropic.webSearch(), exa.anthropic.getContents()], }); messages.push({ role: "assistant", content: response.content }); messages.push({ role: "user", content: await exa.anthropic.handleToolUse(response), }); response = await anthropic.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, messages, tools: [exa.anthropic.webSearch(), exa.anthropic.getContents()], }); console.log(response.content[0].text); ``` 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_use` blocks — that's how a search result turns into a follow-up page read. Calling the factories with no arguments gives Exa's recommended defaults: `type="auto"` with `contents={"highlights": True}` for search. Highlights return query-relevant excerpts — they do not cap page text at 10,000 characters. The contents factory returns page text; the SDK's 10,000-character limit applies only to `text`, and only when you omit `max_characters`. ## Configuring the tools Keyword arguments are regular Exa options, passed through when the tool runs — search options to `exa.search()`, contents options to `exa.get_contents()`: ```python Python theme={null} tools = [ exa.anthropic.web_search(category="news", contents={"text": True}), exa.anthropic.get_contents(summary=True, livecrawl="preferred"), ] ``` ```javascript JavaScript theme={null} const tools = [ exa.anthropic.webSearch({ category: "news", contents: { text: true } }), exa.anthropic.getContents({ summary: true, livecrawl: "preferred" }), ]; ``` The model picks the search `query` and the `urls` to read; everything else is bound when you create the tool, so it can't change what gets crawled or extracted. `name` (defaulting to `"web_search"` and `"get_contents"`) and `description` instead override the tool definition the model sees. Anthropic requires tool names to be unique, so a custom name lets the Exa tool run alongside Anthropic's built-in `web_search_20250305` server tool, which reserves the `web_search` name: ```python Python theme={null} response = claude.messages.create( model="claude-sonnet-4-6", max_tokens=1024, messages=messages, tools=[ exa.anthropic.web_search(name="exa_web_search"), {"type": "web_search_20250305", "name": "web_search", "max_uses": 5}, ], ) ``` ```javascript JavaScript theme={null} const response = await anthropic.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, messages, tools: [ exa.anthropic.webSearch({ name: "exa_web_search" }), { type: "web_search_20250305", name: "web_search", max_uses: 5 }, ], }); ``` ## Mixing in your own tools `handle_tool_use` answers every `tool_use` block in the message: a block naming a tool it can't resolve gets an `Error: unknown tool ""` result instead of being dropped, so the follow-up request never omits a required tool result. If you run your own tools alongside Exa's, replace those error results with your own 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 `tool_use` blocks 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} TOOLS = [ { "name": "exa_search", "description": "Perform a search query on the web, and retrieve the most relevant URLs/web data.", "input_schema": { "type": "object", "properties": { "query": { "type": "string", "description": "The search query to perform.", }, }, "required": ["query"], }, } ] def exa_search(query: str): return exa.search(query=query, type="auto", contents={"highlights": True}) def process_tool_use(response): results = [] for block in response.content: if block.type == "tool_use" and block.name == "exa_search": results.append( { "type": "tool_result", "tool_use_id": block.id, "content": str(exa_search(**block.input)), } ) return results ``` See the [SDK quickstart](/docs/sdks/quickstart) for search and contents options in Python and TypeScript. # OpenAI Tool Calling Source: https://exa.ai/docs/integrations/tool-calling/openai Use OpenAI tool calling to add Exa web search and page contents to your application. 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 ```bash Python theme={null} pip install openai exa_py ``` ```bash JavaScript theme={null} npm install openai exa-js ``` 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) to generate your API keys. Create a key in the dashboard. New accounts start with free credits. 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: `type="auto"` with `contents={"highlights": True}` for search. Highlights return query-relevant excerpts — they do not cap page text at 10,000 characters. The contents factory returns page text; the SDK's 10,000-character limit applies only to `text`, and only when you omit `max_characters`. ## 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))); ``` 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 Keyword arguments are regular Exa options, passed through when the tool runs — search options to `exa.search()`, contents options to `exa.get_contents()`: ```python Python theme={null} tools = [ exa.openai.web_search(category="news", contents={"text": True}), exa.openai.get_contents(summary=True, livecrawl="preferred"), ] ``` ```javascript JavaScript theme={null} const tools = [ exa.openai.webSearch({ category: "news", contents: { text: true } }), exa.openai.getContents({ summary: true, livecrawl: "preferred" }), ]; ``` The model picks the search `query` and the `urls` to read; everything else is bound when you create the tool, so it can't change what gets crawled or extracted. `name` (defaulting to `"web_search"` and `"get_contents"`) and `description` instead 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 ""` 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.", }, }, "required": ["query"], }, }, } ] def exa_search(query: str): return exa.search(query=query, 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](/docs/sdks/quickstart) for search and contents options in Python and TypeScript. # TrueFoundry Source: https://exa.ai/docs/integrations/truefoundry Connect Exa to the TrueFoundry MCP Gateway for centralized access controls, tool management, and usage monitoring. [TrueFoundry AI Gateway](https://truefoundry.com/ai-gateway) is an enterprise-grade proxy layer between your applications and LLM providers or MCP servers. It provides unified access to 1,000+ LLMs with centralized observability and governance. TrueFoundry provides Exa as an official remote server in its [MCP Gateway](https://www.truefoundry.com/mcp-gateway). Connect the Exa MCP server to give your teams one managed endpoint for web search, content fetching, and agentic research. The Exa server in TrueFoundry's official remote MCP catalog ## Add Exa to TrueFoundry 1. Open **MCP Servers** in the TrueFoundry sidebar and select **Add new MCP Server**. 2. Select **Connect Official Remote MCP Servers**. TrueFoundry's Add MCP Server picker with Connect Official Remote MCP Servers selected 3. Find **Exa** in the catalog and select **+ Add**. 4. Confirm the pre-filled server details: | Field | Value | | -------------- | ---------------------------------------------------------------------------------------------------------------------- | | Name | `exa` | | Description | Search Engine made for AIs by Exa | | URL | `https://mcp.exa.ai/mcp` | | Authentication | Optional (The MCP Server works without authentication. You only need an Exa API key if you reach the free rate limit.) | 5. Add the users or teams that should manage or use the server. Leave **Auth Data** off, then select **Update MCP Server**. Exa MCP server registration form with its name, URL, collaborators, and authentication settings Open the **Tools** tab and confirm that Exa's search, content fetching, and agentic research tools are available. ## Configure the Exa server The pre-filled URL exposes Exa's default toolset. Change it only if you need to restrict the available tools or use your own API key. ### Choose which tools are available Pass a comma-separated list of tool names in the `tools` query parameter: ```text theme={null} https://mcp.exa.ai/mcp?tools=web_search_exa,web_fetch_exa,agent_tools ``` You can enter the URL in the server form or use **Apply using YAML**: ```yaml theme={null} url: >- https://mcp.exa.ai/mcp?tools=web_search_exa,web_fetch_exa,agent_tools name: exa type: mcp-server/remote description: Search Engine made for AIs by Exa collaborators: - role_id: mcp-server-manager subject: user:you@your-company.com ``` You can find available tool names in the [Exa MCP documentation](/docs/get-started/exa-mcp). ### Use your Exa API key to bypass the free rate limit If you reach the free rate limit, add your Exa API key to the server URL: ```text theme={null} https://mcp.exa.ai/mcp?exaApiKey=YOUR_API_KEY ``` Create a key in the dashboard. New accounts start with free credits. ## Connect an MCP client Open the Exa server's **How To Use** tab and select your client. TrueFoundry generates the tenant-specific endpoint and ready-to-paste configuration for Cursor, Claude Code, VS Code, Windsurf, Codex, and other MCP clients. TrueFoundry's client-specific setup instructions for the Exa MCP server ## Test a tool Select **Try** beside an Exa tool, enter its inputs, then select **Execute Tool**. The playground displays the JSON response so you can verify the tool before using it in an agent. Testing an Exa tool in the TrueFoundry tool playground ## Manage and monitor tools * Toggle individual tools to control what MCP clients can call * Use **Tool Metrics** to review traffic, latency, and errors * Export invocation traces to your observability stack through OpenTelemetry Exa tools available from the TrueFoundry MCP server ## Resources Read TrueFoundry's guide to its Exa MCP server. Review Exa's tools, configuration, and usage examples. View the server source and releases on GitHub. # Vercel AI Gateway Source: https://exa.ai/docs/integrations/vercel/ai-gateway Use Exa web search through Vercel AI Gateway with the AI SDK. Use Exa web search through [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) with `gateway.tools.exaSearch()` from the `ai` package. You do not need an Exa API key; Vercel bills these requests through AI Gateway. See Vercel's [web search documentation](https://vercel.com/docs/ai-gateway/models-and-providers/web-search) for the complete reference. ## Install Install AI SDK 5 or later: ```bash install.sh theme={null} npm install ai ``` ## Authentication AI Gateway requires an API key or OIDC token. Create an `AI_GATEWAY_API_KEY` in the Vercel dashboard under **AI Gateway > API Keys**, then add it to your environment. ```bash .env theme={null} AI_GATEWAY_API_KEY=your-api-key-here ``` When you deploy your application on Vercel, you can use the automatically available `VERCEL_OIDC_TOKEN` instead. See Vercel's [authentication and BYOK documentation](https://vercel.com/docs/ai-gateway/authentication-and-byok). ## Quick start You can use Exa search with any supported model: ```typescript quickstart.ts theme={null} import { gateway, generateText, stepCountIs } from 'ai'; const { text } = await generateText({ model: 'openai/gpt-5.6-sol', prompt: 'What are the latest developments in AI this week?', tools: { exa_search: gateway.tools.exaSearch(), }, stopWhen: stepCountIs(3), }); console.log(text); ``` ## Streaming Use `streamText` to process generated text and search tool events as they arrive: ```typescript stream.ts theme={null} import { gateway, streamText } from 'ai'; const result = streamText({ model: 'openai/gpt-5.6-sol', prompt: 'What are the latest developments in AI this week?', tools: { exa_search: gateway.tools.exaSearch(), }, }); for await (const part of result.fullStream) { if (part.type === 'text-delta') { process.stdout.write(part.text); } else if (part.type === 'tool-call') { console.log('Tool call:', part.toolName); } else if (part.type === 'tool-result') { console.log('Search results received'); } } ``` In a Next.js route handler, return the stream to the client with `return result.toUIMessageStreamResponse()`. ## Configuration Pass options to `gateway.tools.exaSearch()` to tune your search: ```typescript configuration.ts theme={null} tools: { exa_search: gateway.tools.exaSearch({ type: 'fast', numResults: 5, category: 'news', includeDomains: ['reuters.com', 'bbc.com', 'nytimes.com'], contents: { highlights: true, maxAgeHours: 24, }, }), }, ``` The available options include: | Option | Description | | ------------------------------------------------------ | -------------------------------------------------------------- | | `type` | Search mode: `auto` (default), `fast`, or `instant`. | | `numResults` | Number of results to return, from 1 to 100. The default is 10. | | `category` | Content category. | | `includeDomains` / `excludeDomains` | Include or exclude specific domains. | | `startPublishedDate` / `endPublishedDate` | Filter results by publication date. | | `userLocation` | Two-letter ISO country code for location-aware search. | | `contents.text` | Return extracted page text. | | `contents.highlights` | Return relevant page highlights. | | `contents.maxAgeHours` | Set the maximum age of cached content. | | `contents.livecrawlTimeout` | Set the livecrawl timeout. | | `contents.subpages` / `contents.subpageTarget` | Crawl subpages and optionally target a subpage. | | `contents.extras.links` / `contents.extras.imageLinks` | Return links or image links from results. | See Vercel's [Exa web search reference](https://vercel.com/docs/ai-gateway/models-and-providers/web-search) for the full list of parameters and their behavior. ## Vercel eve agents Agents built with [eve](https://eve.dev) get the built-in `web_search` tool, and AI Gateway models run it on Exa by default, without the need for configuration or an Exa API key. To pin the provider explicitly, export it from `agent/tools/web_search.ts`: ```typescript agent/tools/web_search.ts theme={null} import { webSearch } from 'eve/tools'; export default webSearch({ provider: 'exa' }); ``` Models called through a direct provider instead of AI Gateway keep their native web search. See eve's [harness documentation](https://eve.dev/docs/concepts/default-harness#built-in-tools) for the full tool set. ## Pricing Exa web search is **free through August 31** on AI Gateway and eve, so you can build with it today at no cost. After that, Vercel bills requests through AI Gateway at the rates in Vercel's [web search documentation](https://vercel.com/docs/ai-gateway/models-and-providers/web-search). This integration currently supports Exa's standard search modes and content extraction controls. Deep synthesis modes and generated summaries are not exposed yet. Call Exa directly with an Exa API key through `@exalabs/ai-sdk`. Review the complete AI Gateway configuration and pricing reference. # AI SDK by Vercel Source: https://exa.ai/docs/integrations/vercel/ai-sdk Add Exa web search to AI SDK applications with the @exalabs/ai-sdk package. Use the `@exalabs/ai-sdk` package to add Exa web search to applications built with the AI SDK by Vercel. You provide an Exa API key, and the `webSearch()` tool handles search requests for your model. ## Install ```bash install.sh theme={null} npm install @exalabs/ai-sdk ``` ## Quick start ```typescript quickstart.ts theme={null} import { generateText, stepCountIs } from 'ai'; import { webSearch } from '@exalabs/ai-sdk'; import { openai } from '@ai-sdk/openai'; const { text } = await generateText({ model: openai('gpt-5-nano'), prompt: 'Tell me the latest developments in AI', system: 'Only use web search once per turn. Answer based on the information you have.', tools: { webSearch: webSearch(), }, stopWhen: stepCountIs(3), }); console.log(text); ``` Create a key in the dashboard. New accounts start with free credits. Set your key as `EXA_API_KEY` before you run the example. The package reads this environment variable automatically. ## Defaults `webSearch()` uses these defaults: * `type`: `auto` * `numResults`: `10` * `contents.text`: `3000` characters per result * `maxAgeHours`: the default cache fallback; set this option when you need stricter freshness ## Configure search Use the options below to tune search and content extraction: ```typescript configuration.ts theme={null} const { text } = await generateText({ model: openai('gpt-5-nano'), prompt: 'Find the top AI companies in Europe founded after 2018', tools: { webSearch: webSearch({ type: 'auto', numResults: 6, category: 'company', contents: { text: { maxCharacters: 1000 }, maxAgeHours: 1, summary: true, }, }), }, stopWhen: stepCountIs(5), }); console.log(text); ``` ### Search options | Option | Description | | ----------------------------------------- | ----------------------------------------------------------------------------------------------------- | | `type` | Search mode: `auto`, `fast`, `instant`, `deep-lite`, `deep`, or `deep-reasoning`. | | `category` | Content category: `company`, `publication`, `news`, `personal site`, `people`, or `financial report`. | | `numResults` | Number of results to return. | | `includeDomains` / `excludeDomains` | Include or exclude specific domains. | | `startPublishedDate` / `endPublishedDate` | Filter results by publication date in ISO 8601 format. | | `includeText` / `excludeText` | Require or exclude text in results. | | `userLocation` | Two-letter country code for location-aware search. | ### Content options | Option | Description | | ------------------------------------------------------ | -------------------------------------------------------------------------------------- | | `contents.text` | Return extracted text. Supports `maxCharacters` and `includeHtmlTags`. | | `contents.summary` | Return an AI-generated summary. Supports a `query`. | | `contents.maxAgeHours` | Use cached content only when it is within the specified age; otherwise, use livecrawl. | | `contents.livecrawlTimeout` | Set the livecrawl timeout. | | `contents.subpages` / `contents.subpageTarget` | Crawl subpages and optionally target a subpage. | | `contents.extras.links` / `contents.extras.imageLinks` | Return links or image links from results. | ## TypeScript support The package includes TypeScript types: ```typescript types.ts theme={null} import { webSearch, ExaSearchConfig, ExaSearchResult } from '@exalabs/ai-sdk'; const config: ExaSearchConfig = { numResults: 10, type: 'auto', }; const search = webSearch(config); ``` ## Related pages Use Exa web search without an Exa API key through Vercel's AI Gateway. View the source code and package details on GitHub. You can also find the package on [npm](https://www.npmjs.com/package/@exalabs/ai-sdk) and read the [Vercel AI SDK web search guide](https://ai-sdk.dev/cookbook/node/web-search-agent#exa). # fx by Vercel Labs Source: https://exa.ai/docs/integrations/vercel/fx Add Exa web search to fx, Vercel Labs' native coding agent, with the hosted Exa MCP server. [fx](https://fx.sh) is a native coding agent and CLI from Vercel Labs, and an MCP client. Add Exa's hosted MCP server to give it live web search and page reading. Installing fx, adding the Exa MCP server with /mcp add, and running a live Exa web search ## Installation ```bash theme={null} curl -fsSL https://fx.sh/setup.sh | bash ``` Then sign in with `fx login`. See the [fx docs](https://fx.sh/docs) for provider options. Start fx by running `fx`, then add the Exa MCP server from the interactive shell: ```text theme={null} /mcp add --transport http exa https://mcp.exa.ai/mcp ``` fx saves the server to `~/.fx/mcp.json` and reloads MCP. ```text theme={null} /mcp list ``` ## Configure by hand fx reads MCP servers only from `~/.fx/mcp.json`, so you can also add Exa there directly: ```json ~/.fx/mcp.json theme={null} { "mcp": { "exa": { "type": "http", "url": "https://mcp.exa.ai/mcp" } } } ``` Run `/mcp reload` to apply the change without restarting fx. The free plan covers casual use. To lift the rate limits, create an API key and add it to the config: Create a key in the dashboard. New accounts start with free credits. ```json ~/.fx/mcp.json theme={null} { "mcp": { "exa": { "type": "http", "url": "https://mcp.exa.ai/mcp", "header_env": { "x-api-key": "EXA_API_KEY" } } } } ``` `header_env` maps a header name to an environment variable, so the key stays out of the config file. ## Tool discovery fx discovers MCP tools lazily: the server's tools are not in the model's context until a turn needs them, so adding Exa costs nothing on turns that don't search the web. Review the available tools, configuration options, and other clients. # Monitors API Source: https://exa.ai/docs/monitors/quickstart Run recurring searches and receive newly discovered results by webhook. Monitors run Exa searches on a recurring schedule and deliver results to a webhook endpoint. Use Monitors to follow news, competitor announcements, funding rounds, regulatory changes, research publications, or any other topic that changes over time. ## How Monitors work On each run, Exa executes the configured search, filters by time, removes results or findings the monitor has already returned, and sends the new output to your webhook. Each monitor keeps its own run history, so write the query around the ongoing signal you want to track rather than adding a moving date range yourself. ## Create your first monitor Create a monitor with a search query, an interval, and the HTTPS endpoint that will receive updates: ```python Python theme={null} from exa_py import Exa exa = Exa() monitor = exa.monitors.create({ "name": "Battery recycling expansion", "search": { "query": "new battery recycling facilities announced in North America" }, "trigger": { "type": "interval", "period": "1d", }, "webhook": { "url": "https://example.com/webhooks/exa", "events": ["monitor.run.completed"], }, }) print(monitor.id) print(monitor.webhook_secret) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const monitor = await exa.monitors.create({ name: "Battery recycling expansion", search: { query: "new battery recycling facilities announced in North America" }, trigger: { type: "interval", period: "1d" }, webhook: { url: "https://example.com/webhooks/exa", events: ["monitor.run.completed"] } }); console.log(monitor.id); console.log(monitor.webhookSecret); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/monitors" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "name": "Battery recycling expansion", "search": { "query": "new battery recycling facilities announced in North America" }, "trigger": { "type": "interval", "period": "1d" }, "webhook": { "url": "https://example.com/webhooks/exa", "events": ["monitor.run.completed"] } }' ``` ```json theme={null} { "id": "01k4d9w6y3h7p2m8n5q1r0s4tv", "name": "Battery recycling expansion", "status": "active", "search": { "query": "new battery recycling facilities announced in North America" }, "trigger": { "type": "interval", "period": "1d" }, "outputSchema": null, "metadata": null, "webhook": { "url": "https://example.com/webhooks/exa", "events": ["monitor.run.completed"] }, "nextRunAt": null, "createdAt": "2026-09-05T20:00:00.000Z", "updatedAt": "2026-09-05T20:00:00.000Z", "webhookSecret": "" } ``` Store `webhookSecret` when you create the monitor. It is returned only once and is required to verify webhook signatures. ## Configure the output Every completed run returns newly discovered pages in `output.results`. Exa also synthesizes findings from each page into `output.content`: | Output shape | How to use | Returned value | | --------------- | ---------------------------- | -------------------------------------------- | | Text summary | Default | A string in `output.content` | | Structured JSON | Add an object `outputSchema` | JSON matching the schema in `output.content` | Sources for synthesized fields are returned automatically in `output.grounding`. Add `outputSchema` when downstream code needs consistent fields: ```json theme={null} { "outputSchema": { "type": "object", "properties": { "announcements": { "type": "array", "items": { "type": "object", "properties": { "company": { "type": "string" }, "location": { "type": "string" }, "announcement": { "type": "string" } }, "required": ["company", "location", "announcement"] } } }, "required": ["announcements"] } } ``` Keep citations and confidence out of the schema. They are returned separately in `output.grounding`. ## Add page content `search` accepts the same options as [Exa Search](/docs/search/quickstart): use `contents` to include highlights, full text, or summaries with each result, and `includeDomains` or `excludeDomains` to constrain sources. ```python Python theme={null} monitor = exa.monitors.create({ "name": "LLM Research Tracker", "search": { "query": "new large language model training techniques and architectures", "numResults": 10, "contents": { "highlights": True } }, "trigger": { "type": "interval", "period": "7d" }, "webhook": { "url": "https://example.com/webhooks/exa", "events": ["monitor.run.completed"] } }) ``` ```javascript JavaScript theme={null} const monitor = await exa.monitors.create({ name: "LLM Research Tracker", search: { query: "new large language model training techniques and architectures", numResults: 10, contents: { highlights: true } }, trigger: { type: "interval", period: "7d" }, webhook: { url: "https://example.com/webhooks/exa", events: ["monitor.run.completed"] } }); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/monitors" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "name": "LLM Research Tracker", "search": { "query": "new large language model training techniques and architectures", "numResults": 10, "contents": { "highlights": true } }, "trigger": { "type": "interval", "period": "7d" }, "webhook": { "url": "https://example.com/webhooks/exa", "events": ["monitor.run.completed"] } }' ``` ## Test your monitor Trigger a run immediately instead of waiting for the next scheduled time, then list its runs: ```python Python theme={null} exa.monitors.trigger(monitor.id) runs = exa.monitors.runs.list(monitor.id, limit=1) latest = runs.data[0] print(latest.id, latest.status) ``` ```javascript JavaScript theme={null} await exa.monitors.trigger(monitor.id); const runs = await exa.monitors.runs.list(monitor.id, { limit: 1 }); const latest = runs.data[0]; console.log(latest.id, latest.status); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/monitors/$MONITOR_ID/trigger" \ -H "Authorization: Bearer $EXA_API_KEY" curl -s "https://api.exa.ai/monitors/$MONITOR_ID/runs?limit=1" \ -H "Authorization: Bearer $EXA_API_KEY" ``` Run statuses are: | Status | Meaning | | ----------- | -------------------------------------------------------- | | `pending` | The run is queued | | `running` | The run is executing | | `completed` | The run finished; fetch it by ID to read its full output | | `failed` | The run failed; `failReason` says why | | `cancelled` | The run was cancelled | `output` is null until the run completes. ## Schedule runs The minimum interval is one hour. Use a single duration such as `1h`, `6h`, `1d`, or `7d`. The schedule is anchored to the monitor's creation time — a daily monitor created at 2:30 PM runs daily around 2:30 PM — but each run may be delayed by up to 30 minutes, so do not rely on an exact wall-clock delivery time. Omit `trigger` to create a manual-only monitor. Pausing a scheduled monitor also stops automatic runs while preserving manual triggers. Monitor runs do not overlap. If the next scheduled run starts while the previous one is still running, Exa cancels the previous run. ## Receive webhook updates Subscribe to `monitor.run.completed` when you only need finished runs. If you omit `events`, Exa sends monitor lifecycle events and run-created events as well. The completed-run payload includes the run status and output. Optional monitor `metadata` is echoed in webhook deliveries, which lets you route an update back to the correct customer, workspace, channel, or internal job. The output and timestamps are shortened below. ```json theme={null} { "id": "event_...", "object": "event", "type": "monitor.run.completed", "data": { "id": "01k...", "monitorId": "01k...", "status": "completed", "output": { "results": [ { "title": "New battery recycling facility announced", "url": "https://example.com/announcement" } ], "content": "...", "grounding": [ { "field": "content", "citations": [ { "title": "New battery recycling facility announced", "url": "https://example.com/announcement" } ], "confidence": "high" } ] }, "failReason": null, "metadata": { "workspace_id": "workspace_123" } }, "createdAt": "2026-09-05T20:00:00.000Z" } ``` Your webhook must use HTTPS and be the final destination because redirects are not followed. Verify `Exa-Signature` before processing the event. Every delivery includes an `Exa-Signature` header in the form `t=,v1=`. Construct `.`, compute its HMAC-SHA256 digest with the one-time `webhookSecret`, and compare the result with `v1` using a constant-time comparison. ```python Python theme={null} import hashlib import hmac def verify_webhook(payload: bytes, signature_header: str, secret: str) -> bool: parts = dict(part.split("=", 1) for part in signature_header.split(",")) signed_payload = parts["t"].encode() + b"." + payload expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts["v1"]) ``` ```javascript JavaScript theme={null} import crypto from "crypto"; function verifyWebhook(payload, signatureHeader, secret) { const parts = Object.fromEntries( signatureHeader.split(",").map((part) => part.split("=", 2)) ); const expected = crypto .createHmac("sha256", secret) .update(`${parts.t}.`) .update(payload) .digest("hex"); const actualBuffer = Buffer.from(parts.v1 ?? "", "hex"); const expectedBuffer = Buffer.from(expected, "hex"); return ( actualBuffer.length === expectedBuffer.length && crypto.timingSafeEqual(actualBuffer, expectedBuffer) ); } ``` ## Next steps See every search, schedule, output, metadata, and webhook field. Inspect a run's status, output, grounding, and failure reason. Configure queries, filters, highlights, full text, and freshness. Improve retrieval quality while keeping output focused. # Cancel a run Source: https://exa.ai/docs/reference/agent-api/cancel-a-run exa-spec.yaml POST /agent/runs/{id}/cancel Cancel a queued or running Agent run. If the run is still active, it transitions to `cancelled` immediately and terminates without returning any results. The run ends with status `cancelled` and `stopReason: cancelled`. You are billed for usage accrued before cancellation. If the run has already reached a terminal status (completed, failed, or cancelled), the endpoint returns the existing run unchanged. Create a key in the dashboard. New accounts start with free credits. # Create a run Source: https://exa.ai/docs/reference/agent-api/create-a-run exa-spec.yaml POST /agent/runs Create an asynchronous Agent run. The response returns the run object immediately unless you request server-sent events. Create a run with a natural-language `query`. Add `outputSchema` for validated structured JSON, `input.data` for rows to process, `input.exclusion` for records or entities to avoid, or `previousRunId` to continue from a completed run. Set `Accept: text/event-stream` to stream run events as the run is created, started, and completed. **Connect:** Pass `dataSources` to give the agent access to third-party data providers during the run. See the [Connect guide](/docs/agent/quickstart#connect-data-sources) for details. Create a key in the dashboard. New accounts start with free credits. # Delete a run Source: https://exa.ai/docs/reference/agent-api/delete-a-run exa-spec.yaml DELETE /agent/runs/{id} Delete a stored Agent run. Deleting a run removes the stored run from your team's Agent run history. Create a key in the dashboard. New accounts start with free credits. # Get an Agent run Source: https://exa.ai/docs/reference/agent-api/get-a-run exa-spec.yaml GET /agent/runs/{id} Retrieve an Agent run by ID. Use this endpoint to poll a run until it reaches `completed`, `failed`, or `cancelled`. Create a key in the dashboard. New accounts start with free credits. # List run events Source: https://exa.ai/docs/reference/agent-api/list-run-events exa-spec.yaml GET /agent/runs/{id}/events List stored Agent run events or replay them as server-sent events. By default, this endpoint returns a paginated JSON list of stored events. Set `Accept: text/event-stream` to replay stored events as SSE. For JSON pagination, use `cursor`. For SSE replay, use `Last-Event-ID`. Create a key in the dashboard. New accounts start with free credits. # List Agent runs Source: https://exa.ai/docs/reference/agent-api/list-runs exa-spec.yaml GET /agent/runs Retrieve a paginated list of Agent runs for your team. Runs are returned from newest to oldest. Use `limit` to control page size and `cursor` with the `nextCursor` from the previous response to fetch the next page. Create a key in the dashboard. New accounts start with free credits. # Stop a run Source: https://exa.ai/docs/reference/agent-api/stop-a-run exa-spec.yaml POST /agent/runs/{id}/stop Gracefully stop a running Agent run and keep the results gathered so far. If the run is still active, the agent wraps up and completes the run early with the results gathered so far. The run ends with status `completed` and `stopReason: stopped`. You are billed for usage accrued before the stop. If the run has already reached a terminal status (completed, failed, or cancelled), the endpoint returns the existing run unchanged. To terminate a run immediately without returning any results, use [cancel](/docs/reference/agent-api/cancel-a-run) instead. Only supported on `max` effort runs. `Exa-Beta: agent-max-effort-2026-07-27` must be passed as a request header. The header accepts a comma-separated list of beta tokens. Create a key in the dashboard. New accounts start with free credits. # Answer Source: https://exa.ai/docs/reference/answer exa-spec.yaml POST /answer Get an LLM answer to a question informed by Exa search results. `/answer` performs an Exa search and uses an LLM to generate either: 1. A direct answer for specific queries. (i.e. "What is the capital of France?" would return "Paris") 2. A detailed summary with citations for open-ended queries (i.e. "What is the state of ai in healthcare?" would return a summary with citations to relevant sources) The response includes both the generated answer and the sources used to create it. The endpoint also supports streaming (as `stream=True`), which will return tokens as they are generated. Alternatively, you can use the OpenAI compatible [chat completions interface](https://exa.ai/docs/integrations/openai-sdk#answer). Create a key in the dashboard. New accounts start with free credits. `/answer` supports structured output via the `outputSchema` parameter. Pass a [JSON Schema](https://json-schema.org/draft-07) object and the answer will be returned as structured JSON matching your schema instead of a plain string. # Cancel a batch Source: https://exa.ai/docs/reference/batches/cancel-a-batch exa-spec.yaml POST /batches/{id}/cancel Cancel an in-progress batch. The batch stops scheduling new requests and drains in-flight work before settling to `cancelled`. Cancelling a batch that is already `cancelling` or `cancelled` returns it unchanged; a batch that is `completed` or `expired` cannot be cancelled. Create a key in the dashboard. New accounts start with free credits. # Create a batch Source: https://exa.ai/docs/reference/batches/create-a-batch exa-spec.yaml POST /batches Submit a batch of Exa API requests to run asynchronously. Each item in `requests` is a `POST` to `/search` or `/agent/runs` with a batch-unique `customId`. The same `customId` is returned in the results file so you can map output rows back to your input data. See the [Batch API guide](/docs/batch/quickstart) for the end-to-end workflow. Create a key in the dashboard. New accounts start with free credits. # Delete a batch Source: https://exa.ai/docs/reference/batches/delete-a-batch exa-spec.yaml DELETE /batches/{id} Delete a batch in a terminal status. Deleting a batch removes it from your team's batch history. Only batches in a terminal status can be deleted. Create a key in the dashboard. New accounts start with free credits. # Get a batch Source: https://exa.ai/docs/reference/batches/get-a-batch exa-spec.yaml GET /batches/{id} Retrieve a batch by ID. Use this endpoint to poll a batch until it reaches `completed`, `cancelled`, or `expired`. When the batch completes, `resultsUrl` holds a short-lived presigned URL for the JSONL results file; re-fetch the batch to get a fresh URL. Create a key in the dashboard. New accounts start with free credits. # List batches Source: https://exa.ai/docs/reference/batches/list-batches exa-spec.yaml GET /batches Retrieve a paginated list of batches for your team. Batches are returned from newest to oldest. Use `limit` to control page size and `cursor` with the `nextCursor` from the previous response to fetch the next page. Pass `status=completed` to list only completed batches; completed listings use their own cursor, so keep sending `status=completed` on every page. Create a key in the dashboard. New accounts start with free credits. # Contents Source: https://exa.ai/docs/reference/get-contents exa-spec.yaml POST /contents Get the full page contents, summaries, and metadata for a list of URLs. Returns instant results from our cache, with automatic live crawling as fallback for uncached pages. *** Create a key in the dashboard. New accounts start with free credits. # Batch action on monitors Source: https://exa.ai/docs/reference/monitors/batch-monitors exa-spec.yaml POST /monitors/batch Perform a batch action on monitors matching the provided filters. Supported actions: - **delete**: Permanently remove matching monitors - **pause**: Pause matching monitors - **unpause**: Unpause matching monitors Use `dry_run: true` (the default) to preview which monitors would be affected before performing the action. Results are paginated via the `limit` parameter; loop until `has_more` is `false` to process all matching monitors. # Create a monitor Source: https://exa.ai/docs/reference/monitors/create-a-monitor exa-spec.yaml POST /monitors Creates a new Monitor to run recurring Exa searches on a schedule. Monitors automatically execute your search query on a recurring schedule and deliver results to your webhook endpoint with automatic deduplication: - **Date-based filtering** only fetches content since the last run - **Semantic deduplication** tracks previous outputs to surface only new developments The response includes a `webhookSecret` that is only returned once at creation time. Store it securely for webhook signature verification. # Delete a monitor Source: https://exa.ai/docs/reference/monitors/delete-a-monitor exa-spec.yaml DELETE /monitors/{id} Deletes a monitor. This cannot be undone. # Get a monitor Source: https://exa.ai/docs/reference/monitors/get-a-monitor exa-spec.yaml GET /monitors/{id} Retrieves a single monitor by its ID. # List monitors Source: https://exa.ai/docs/reference/monitors/list-monitors exa-spec.yaml GET /monitors Lists all monitors for the authenticated team. Supports filtering by status and cursor-based pagination. # Get a run Source: https://exa.ai/docs/reference/monitors/runs/get-a-run exa-spec.yaml GET /monitors/{id}/runs/{runId} Retrieves a single run by its ID, including the full output if the run is completed. # List runs Source: https://exa.ai/docs/reference/monitors/runs/list-runs exa-spec.yaml GET /monitors/{id}/runs Lists all runs for a monitor with cursor-based pagination. Runs are returned in reverse chronological order. # Trigger a monitor Source: https://exa.ai/docs/reference/monitors/trigger-a-monitor exa-spec.yaml POST /monitors/{id}/trigger Triggers a run immediately, regardless of the schedule. Works for monitors with status `active` or `paused`. # Update a monitor Source: https://exa.ai/docs/reference/monitors/update-a-monitor exa-spec.yaml PATCH /monitors/{id} Updates an existing monitor. All fields are optional. For `search`, you can send a partial object containing only the fields you want to change. Set `trigger` to `null` to remove the schedule. # Search Source: https://exa.ai/docs/reference/search exa-spec.yaml POST /search The search endpoint lets you search the web and extract contents from the results. Create a key in the dashboard. New accounts start with free credits. # Create API key Source: https://exa.ai/docs/reference/team-management/create-api-key team-management-spec.yaml POST /api-keys Create a new API key for your team with optional name and rate limit configuration. Create a key in the dashboard. New accounts start with free credits. The Team Management API is enabled per team. It authenticates with a service account API key, which is created from the **Service keys** tab on the [API keys page](https://dashboard.exa.ai/api-keys) once the feature is enabled for your team. Contact [support@exa.ai](mailto:support@exa.ai) to request access. The Create API Key endpoint allows you to programmatically generate new API keys for your team using your service API key. ## Optional Parameters * **name**: A descriptive name for the API key to help identify its purpose * **rateLimit**: Maximum number of requests per minute allowed for this API key # Delete API key Source: https://exa.ai/docs/reference/team-management/delete-api-key team-management-spec.yaml DELETE /api-keys/{id} Permanently delete an API key from your team. Create a key in the dashboard. New accounts start with free credits. The Team Management API is enabled per team. It authenticates with a service account API key, which is created from the **Service keys** tab on the [API keys page](https://dashboard.exa.ai/api-keys) once the feature is enabled for your team. Contact [support@exa.ai](mailto:support@exa.ai) to request access. ## Overview The Delete API Key endpoint permanently removes an API key from your team. ## Path Parameters * **id**: The unique identifier of the API key to delete. # Get API key Source: https://exa.ai/docs/reference/team-management/get-api-key team-management-spec.yaml GET /api-keys/{id} Retrieve details of a specific API key by its ID. Create a key in the dashboard. New accounts start with free credits. The Team Management API is enabled per team. It authenticates with a service account API key, which is created from the **Service keys** tab on the [API keys page](https://dashboard.exa.ai/api-keys) once the feature is enabled for your team. Contact [support@exa.ai](mailto:support@exa.ai) to request access. ## Overview The Get API Key endpoint allows you to retrieve detailed information about a specific API key using its unique identifier. ## Path Parameters * **id**: The unique identifier of the API key to retrieve ## Response Returns detailed information about the API key including: * **id**: Unique identifier * **name**: Descriptive name * **rateLimit**: Rate limit in requests per minute (if set) * **teamId**: Team ID this key belongs to * **createdAt**: When the key was created # Get API key usage Source: https://exa.ai/docs/reference/team-management/get-api-key-usage team-management-spec.yaml GET /api-keys/{id}/usage Retrieve usage analytics and billing data for a specific API key. Create a key in the dashboard. New accounts start with free credits. The Team Management API is enabled per team. It authenticates with a service account API key, which is created from the **Service keys** tab on the [API keys page](https://dashboard.exa.ai/api-keys) once the feature is enabled for your team. Contact [support@exa.ai](mailto:support@exa.ai) to request access. ## Overview The Get API Key Usage endpoint allows you to retrieve detailed billing and usage analytics for a specific API key over a given time period. This endpoint returns cost data from Exa's billing system, providing an authoritative view of what you're being billed for that API key. ## Path Parameters * **id**: The unique identifier of the API key to retrieve usage for ## Query Parameters * **start\_date** (optional): Start date for the usage period in ISO 8601 format (e.g., `2025-01-01T00:00:00Z` or `2025-01-01`). Defaults to 30 days ago. Must be within the last 6 months (180 days). * **end\_date** (optional): End date for the usage period in ISO 8601 format. Defaults to the current time. * **group\_by** (optional): Time granularity for grouping results (`hour`, `day`, or `month`). Currently reserved for future enhancements and does not change the response shape. Defaults to `day`. ## Response Returns detailed usage and billing information including: * **id**: Unique identifier of the API key * **api\_key\_id**: Unique identifier of the API key * **api\_key\_name**: Descriptive name of the API key (if set) * **team\_id**: Team ID this key belongs to * **period**: Object containing the start and end dates of the usage period * **total\_cost\_usd**: Total cost in USD for the specified period * **cost\_breakdown**: Array of cost breakdowns by price type, each containing: * **price\_id**: Unique identifier for the price * **price\_name**: Name of the price (e.g., "Neural Search", "Content Retrieval") * **quantity**: Total quantity consumed * **amount\_usd**: Cost in USD for this price type * **metadata**: Object containing report generation timestamp ## Important Notes * **6-Month Lookback Limit**: The billing system has a 6-month (180-day) lookback limit. Requests with `start_date` older than 180 days will return a 400 error. * **Zero Usage**: If the API key has no usage in the requested period, `total_cost_usd` will be 0 and `cost_breakdown` may be empty. * **Team Ownership**: The service API key used for authentication must belong to the same team as the requested API key. Cross-team access is not permitted. * **Date Formats**: Dates can be provided in ISO 8601 format with or without time components (e.g., `2025-01-01` or `2025-01-01T00:00:00Z`). ## Use Cases This endpoint is useful for: * Building API-key-level billing dashboards * Monitoring usage and costs for specific API keys * Creating automated alerts based on usage thresholds * Generating usage reports for internal cost allocation * Debugging billing questions for specific API keys # List API keys Source: https://exa.ai/docs/reference/team-management/list-api-keys team-management-spec.yaml GET /api-keys Retrieve all API keys belonging to your team with their metadata. Create a key in the dashboard. New accounts start with free credits. The Team Management API is enabled per team. It authenticates with a service account API key, which is created from the **Service keys** tab on the [API keys page](https://dashboard.exa.ai/api-keys) once the feature is enabled for your team. Contact [support@exa.ai](mailto:support@exa.ai) to request access. ## Overview The List API Keys endpoint returns all API keys associated with your team. This includes the key ID, name, rate limit, and creation timestamp for each key. ## Response Format The response includes an array of API key objects with the following information: * **id**: Unique identifier for the API key * **name**: Human-readable name (if provided during creation) * **rateLimit**: Rate limit in requests per minute (if set) * **createdAt**: ISO 8601 timestamp of when the key was created # Update API key Source: https://exa.ai/docs/reference/team-management/update-api-key team-management-spec.yaml PUT /api-keys/{id} Update the name and rate limit of an existing API key. Create a key in the dashboard. New accounts start with free credits. The Team Management API is enabled per team. It authenticates with a service account API key, which is created from the **Service keys** tab on the [API keys page](https://dashboard.exa.ai/api-keys) once the feature is enabled for your team. Contact [support@exa.ai](mailto:support@exa.ai) to request access. ## Overview The Update API Key endpoint allows you to modify an existing API key ## Path Parameters * **id**: The unique identifier of the API key to update. ## Optional Parameters * **name**: New descriptive name for the API key * **rateLimit**: New rate limit in requests per minute # SDK Quickstart Source: https://exa.ai/docs/sdks/quickstart Install and use the Exa Python and JavaScript SDKs The official Exa SDKs. Search the web, get page contents, and get answers with citations. Create a key in the dashboard. New accounts start with free credits. ## Install ```bash pip theme={null} pip install exa-py ``` ```bash uv theme={null} uv add exa-py ``` ```bash npm theme={null} npm install exa-js ``` ```bash pnpm theme={null} pnpm add exa-js ``` The Python SDK requires Python 3.9+. ## Authentication Set your API key as an environment variable: ```bash theme={null} export EXA_API_KEY="your-api-key" ``` ```powershell theme={null} setx EXA_API_KEY "your-api-key" ``` ## Getting started Initialize the client and run your first search: ```python Python theme={null} from exa_py import Exa exa = Exa() results = exa.search( "latest developments in fusion energy", type="auto", contents={"highlights": True}, ) for source in results.results: print(source.url, source.highlights) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const results = await exa.search("latest developments in fusion energy", { type: "auto", contents: { highlights: true, }, }); for (const source of results.results) { console.log(source.url, source.highlights); } ``` Both clients read your key from the `EXA_API_KEY` environment variable. To set it explicitly instead, pass it inline: `Exa(api_key="your-api-key")` or `new Exa("your-api-key")`. ## Recommended defaults | Decision | Recommended default | | -------------- | ----------------------------------------------------------------------------- | | Starting point | Use `search` | | Search type | Keep `auto` unless latency or synthesis needs require another type | | Page content | Start with `highlights: true` | | Known URLs | Use `get_contents` / `getContents` | | Freshness | Set `max_age_hours` / `maxAgeHours` only when stale content would be unusable | The two request types take the same content options in different places: | Method | Content option placement | | ------------------------------ | --------------------------------------------------------------------------- | | `search` | Inside `contents`, as in `exa.search(query, contents={"highlights": True})` | | `get_contents` / `getContents` | Directly on the request, as in `exa.get_contents(urls, highlights=True)` | ## Search Search finds relevant pages and returns their contents in one call. Use `highlights: true` for AI answers, RAG, and search previews. Exa sizes each result's excerpts to its relevance; set `max_characters` / `maxCharacters` only when your application requires a fixed limit. Filters, date ranges, and result counts: ```python Python theme={null} results = exa.search( "climate tech news", num_results=20, start_published_date="2024-01-01", include_domains=["techcrunch.com", "wired.com"], contents={"highlights": True} ) ``` ```javascript JavaScript theme={null} const result = await exa.search("interesting articles about space", { numResults: 10, includeDomains: ["nasa.gov", "space.com"], startPublishedDate: "2024-01-01", contents: { highlights: true, }, }); ``` ### Output schema ```python Python theme={null} structured_results = exa.search( "Who is the CEO of OpenAI?", type="deep", system_prompt="Prefer official sources and avoid duplicate results", output_schema={ "type": "object", "properties": { "leader": {"type": "string"}, "title": {"type": "string"}, "source_count": {"type": "number"} }, "required": ["leader", "title"] }, contents={"highlights": True} ) print(structured_results.output.content if structured_results.output else None) ``` ```javascript JavaScript theme={null} const structuredResult = await exa.search("Who is the CEO of OpenAI?", { type: "deep", systemPrompt: "Prefer official sources and avoid duplicate results", outputSchema: { type: "object", properties: { leader: { type: "string" }, title: { type: "string" }, sourceCount: { type: "number" }, }, required: ["leader", "title"], }, contents: { highlights: true, }, }); console.log(structuredResult.output?.content); ``` `output_schema` / `outputSchema` works with every search type and returns the synthesized value in `output.content`. Use `system_prompt` / `systemPrompt` for source preferences or emphasis. Grounding is returned automatically in `output.grounding`, so do not duplicate citations or confidence in your schema. Deep modes are recommended when the output requires research across several searches. Use `deep-lite` for lightweight research or `deep` for multi-step search and stronger synthesis. See the [Search guide](/docs/search/quickstart) for the full request options. ## Contents Extract highlights, full text, or summaries from URLs you already know. Start with highlights and add a query to focus them on the information you need. ```python Python theme={null} results = exa.get_contents( ["https://exa.ai/blog/dynamic-highlights"], highlights={"query": "token efficiency and result quality"}, ) ``` ```javascript JavaScript theme={null} const results = await exa.getContents(["https://exa.ai/blog/dynamic-highlights"], { highlights: { query: "token efficiency and result quality", }, }); ``` Use full text when you need broader context or document structure. See the [Contents guide](/docs/contents/quickstart) for output shapes, freshness controls, and subpage crawling. ## Answer Get answers to questions with citations. ```python Python theme={null} response = exa.answer("What caused the 2008 financial crisis?") print(response.answer) for chunk in exa.stream_answer("Explain quantum computing"): print(chunk, end="", flush=True) ``` ```javascript JavaScript theme={null} const response = await exa.answer("What caused the 2008 financial crisis?"); console.log(response.answer); for await (const chunk of exa.streamAnswer("Explain quantum computing")) { if (chunk.content) { process.stdout.write(chunk.content); } } ``` ## Async and types Python exposes `AsyncExa` for async operations, and the JavaScript SDK ships TypeScript types for every method. ```python Python theme={null} from exa_py import AsyncExa exa = AsyncExa() results = await exa.search( "machine learning startups", contents={"highlights": True} ) ``` ```typescript TypeScript theme={null} import Exa from "exa-js"; import type { SearchResponse, RegularSearchOptions } from "exa-js"; ``` ## Resources Python: [exa-py source](https://github.com/exa-labs/exa-py) and [PyPI package](https://pypi.org/project/exa-py/). JavaScript: [exa-js source](https://github.com/exa-labs/exa-js) and [npm package](https://www.npmjs.com/package/exa-js). ## Continue Return to the main Search guide for request patterns, filters, and deeper modes. Jump to the full `/search` request and response schema. Use Contents when you already know the URLs and want direct extraction. # Search Best Practices Source: https://exa.ai/docs/search/best-practices Tune retrieval quality, latency, context, and synthesis for production Search API integrations. This guide assumes you already have a working [Search API request](/docs/search/quickstart). It covers how to improve that request following Exa's recommended best practices. ## Start with the smallest useful request The best baseline is a natural-language query with `highlights: true`. Exa sizes each result's excerpts to its relevance, so there is no character budget to tune: ```python Python theme={null} result = exa.search( "Recent technical articles comparing hybrid and semantic retrieval for RAG systems", contents={"highlights": True}, ) ``` ```javascript JavaScript theme={null} const result = await exa.search( "Recent technical articles comparing hybrid and semantic retrieval for RAG systems", { contents: { highlights: true } } ); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Recent technical articles comparing hybrid and semantic retrieval for RAG systems", "contents": { "highlights": true } }' ``` This gives you ranked pages and token-efficient context per page, relevant to the query. Add additional parameters only as needed: | Parameter | Add it when | | -------------------------- | -------------------------------------------------------------------- | | `type` | Adjusting to meet a latency budget or depth requirement | | `numResults` | Fewer pages for a smaller context window, or more for broader recall | | `outputSchema` | Synthesizing results or structuring them into JSON | | `maxAgeHours` | Cached page content may be too old | | `highlights.maxCharacters` | Your application requires a fixed excerpt limit per page | | Domain or date filters | Results outside the constraint would be unusable | ## Search vs. Deep Search Standard search retrieves and ranks pages for a query. Deep Search runs a research process that can search iteratively, inspect what it found, refine the search, and synthesize a grounded result. | Need | Start with | | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | | Ranked pages for a well-formed query | `auto` or `fast` | | Difficult searches, synthesis across many results, or structured outputs that cannot be filled with a single search (3+ fields) | `deep` | | Long-running research, list building, or multi-hop enrichment | [Exa Agent](/docs/agent/quickstart) | Deep modes are recommended by default when using `outputSchema`. Read the [Deep Search guide](/docs/search/deep-search) for full instructions and examples. ## Improve retrieval quality When results need improvement, change one part of the request at a time. Describe the pages you want, not a bag of keywords. Include the subject and any source type, time period, or other detail that changes what a relevant result looks like. ```text theme={null} Benchmark papers evaluating long-context retrieval methods on legal documents ``` Look at the titles, URLs, publication dates, and highlights before changing the request. ```json theme={null} { "results": [ { "title": "Long-Context Retrieval Methods on Legal Documents", "url": "https://arxiv.org/abs/2608.00000", "publishedDate": "2026-08-26T00:00:00.000Z", "highlights": [ "We compare long-context retrieval methods across legal document benchmarks..." ] } ] } ``` The title and URL show the kind of source Exa retrieved, `publishedDate` shows its recency, and the highlight shows the evidence that matched the query. Refine the query to retrieve different pages, add date filters to narrow the time period, or fetch full text when you need more context from a useful result. Use `includeDomains`, `excludeDomains`, and publication-date filters only when a result that violates the constraint cannot be used. Put retrieval preferences in the query and, when synthesizing, put response instructions in `systemPrompt`. Use a faster mode for a latency requirement or a deep mode when the retrieval process itself needs iteration and reasoning. A different mode cannot repair an underspecified query. Keep a small set of representative queries while tuning. Compare result relevance and downstream task success across the set instead of optimizing for one example. Record `requestId`, `searchTime`, and `costDollars` so regressions are reproducible. ## Budget latency and context Each control spends a different resource: | Control | What it adds | | ------------------------- | ------------------------------------------------- | | More results | More pages, response data, and downstream context | | Full text | Broader page context and a larger payload | | `summary` | One additional language model call per result | | `outputSchema` | Synthesis across the retrieved results | | `contents.maxAgeHours: 0` | A fresh page fetch instead of cached content | | Deep search types | Iterative search, synthesis, and reasoning | For a real-time path where cached content is acceptable, combine the lowest-latency mode with highlights and cache-only content: ```python Python theme={null} result = exa.search( "Recent product updates from major AI labs", type="instant", contents={ "highlights": True, "max_age_hours": -1, }, ) ``` ```javascript JavaScript theme={null} const result = await exa.search("Recent product updates from major AI labs", { type: "instant", contents: { highlights: true, maxAgeHours: -1 } }); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Recent product updates from major AI labs", "type": "instant", "contents": { "highlights": true, "maxAgeHours": -1 } }' ``` Do not use this recipe when page freshness is part of correctness. Start from `auto` and default freshness unless the product has a measured latency target. To let Exa allocate one context budget across the whole result set — more from strong sources, less from redundant ones — see the [Dynamic Highlights research preview](/docs/search/highlights#dynamic-highlights). ## Tips for common use cases | If you need | Use | Avoid | | ------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------- | | Newer publications | Put the time window in the query or use publication-date filters | `maxAgeHours` | | Fresh content from pages that change | `contents.maxAgeHours` | Publication-date filters | | A preferred kind of source | Query phrasing; `systemPrompt` when synthesizing | A hard domain allowlist | | Results only from approved sources | `includeDomains` | Repeating `site:` in the query | | A small structured output | `outputSchema` with standard Search | Choosing Deep only because the output is JSON | | A multi-item researched output | `deep` with `outputSchema`, or [Exa Agent](/docs/agent/quickstart) | Expecting one retrieval pass to gather every item | | More context from a few pages | Search with highlights, then call Contents | Full text for every result | | Lower latency | Measure `fast` or `instant` with compact content | Adding freshness or synthesis controls by default | ## When to use another endpoint Use a different Exa endpoint when the task changes shape: | Task | Use | | --------------------------------------------------- | ------------------------------------- | | Long-running research, list building, or enrichment | [Exa Agent](/docs/agent/quickstart) | | The URLs are already known | [Contents](/docs/contents/quickstart) | | Run a search on a schedule | [Monitors](/docs/monitors/quickstart) | ## Next steps Every request parameter and response field. Core request shapes, filters, output, and freshness. Extract highlights or full text from pages you already know. Long-running research, list building, and enrichment. # Code & Docs Source: https://exa.ai/docs/search/data/code Find code, technical documentation, and implementation guidance with Exa Search. Use Exa Search for repositories, technical documentation, package information, and implementation guidance with natural-language queries. Read [WebCode: Search Evals for Coding Agents](https://exa.ai/blog/webcode) to learn how Exa evaluates retrieval for coding tasks. ## Use it for * Coding agents and code-generation tools * Developer search and documentation products * Debugging, migration, and configuration workflows * Technical research across repositories, docs, and package registries ## Example queries ### Discover libraries by capability Describe the capability, ecosystem, and constraints you care about. This retrieves candidates based on what they do instead of relying on an exact project name. ### Retrieve implementation documentation Name the product and the exact operation. Search can then prioritize API documentation and implementation guides over general discussion. ### Check version-specific changes Include the release channel or version when compatibility matters. This reduces results about older releases. ### Find reusable agent tooling State the artifact type and task rather than searching for a broad phrase such as “AI tools.” ## Make a request ```python Python theme={null} from exa_py import Exa exa = Exa() results = exa.search( "how to use Exa search in python", type="fast", num_results=10, contents={"highlights": True}, ) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const results = await exa.search( "how to use Exa search in python", { type: "fast", numResults: 10, contents: { highlights: true, }, } ); ``` ```bash cURL theme={null} curl -s -X POST https://api.exa.ai/search \ -H "Authorization: Bearer $EXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "how to use Exa search in python", "type": "fast", "numResults": 10, "contents": { "highlights": true } }' ``` ## Get structured data with Exa Agent For structured data that requires research across multiple sources, use an [Exa Agent task run](/docs/agent/quickstart). Describe the libraries, technical criteria, and output fields you need, and Agent returns schema-validated results with citations. Compare libraries, enrich repository records, or produce a structured list from multiple technical signals. # Companies & People Source: https://exa.ai/docs/search/data/companies-people Find companies, professional profiles, and the relationships between them with Exa Search. Use Exa Search for organizations and the people connected to them. These searches work best together: describe the company traits that qualify a person, or the people and roles that reveal how a company operates. See how Exa evaluates company retrieval and fact extraction. See how Exa evaluates targeted lookups and profile discovery. ## Use it for * Company, candidate, and expert discovery * Account research and stakeholder mapping * Market maps, investment research, and deal sourcing * Leadership, hiring, and organization research ## Write better queries Start with the entity you want, then add the traits and relationships that qualify it. Ask for the source type when it matters, such as company homepages, professional profiles, job postings, or personal websites. ### Discover companies by what they do Describe the customer, product, capability, stage, and geography that define the market. This finds candidates based on what they do instead of relying on a predefined company list. ### Find operating signals Name the signal and the company traits that matter. Search can retrieve job postings, pricing pages, product documentation, and reporting alongside company pages. ### Research funding activity Specify the round, industry, participant, and time window. ### Discover people by role and skills Combine the role, seniority, location, relevant skills, and source type you want. ### Qualify people by company traits Describe the person's relationship to the company and the traits that qualify the company. This works better than building a company list first. ### Find personal websites and public work Name the profession or research area and ask explicitly for personal websites, talks, interviews, or articles. ## Search both together Write one query that expresses the relationship you need. Exa can return company pages, professional profiles, hiring pages, and public references in the same result set. ## Make a request ```python Python theme={null} from exa_py import Exa exa = Exa() results = exa.search( "heads of security at Series B healthcare software companies that sell to hospitals", type="auto", num_results=10, ) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const results = await exa.search( "heads of security at Series B healthcare software companies that sell to hospitals", { type: "auto", numResults: 10, } ); ``` ```bash cURL theme={null} curl -s -X POST https://api.exa.ai/search \ -H "Authorization: Bearer $EXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "heads of security at Series B healthcare software companies that sell to hospitals", "type": "auto", "numResults": 10 }' ``` ## Get structured data with Exa Agent For structured data that requires research across multiple sources, use an [Exa Agent task run](/docs/agent/quickstart). Describe the companies, people, qualification criteria, and output fields you need, and Agent returns schema-validated results with citations. Build and qualify company or people lists, then enrich each record with fields gathered from multiple sources. # Financial Markets Source: https://exa.ai/docs/search/data/financial Find market data, filings, earnings calls, and economic releases with Exa Search. Use Exa Search for prices, filings, transcripts, and the reporting around them in one query. A question about a ticker can return the quote, the latest earnings call, and analyst coverage together. ## Included * Quotes and recent price history for stocks, crypto, forex, indices, futures, options, and commodities * Security profiles with key statistics and daily OHLCV history * Earnings call transcripts, with prepared remarks and Q\&A attributed to speakers * SEC filings, reported financials, and international filings * Analyst estimates, funding announcements, and economic releases ## Use it for * Equity and credit research * KYC, KYB, and adverse-media screening * Portfolio and policy monitoring * Deal sourcing and private-market research ## Example queries ### Look up a quote Name the ticker or company and the figure you want. A cashtag such as `$NVDA` also works. ### Read an earnings call Name the company and quarter to retrieve the transcript rather than coverage of it. ### Search filings Describe the disclosure you are looking for, not just the form type. The `financial report` category keeps results to filings and reports. ### Track private-market activity Specify the round, sector, and time window. ### Follow economic data Name the release and the figure you want from it. ## Make a request ```python Python theme={null} from exa_py import Exa exa = Exa() results = exa.search( "10-K risk factors that mention dependency on third-party AI models", type="auto", category="financial report", num_results=10, ) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const results = await exa.search( "10-K risk factors that mention dependency on third-party AI models", { type: "auto", category: "financial report", numResults: 10, } ); ``` ```bash cURL theme={null} curl -s -X POST https://api.exa.ai/search \ -H "Authorization: Bearer $EXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "10-K risk factors that mention dependency on third-party AI models", "type": "auto", "category": "financial report", "numResults": 10 }' ``` ## Get structured data with Exa Agent For structured data that requires research across multiple sources, use an [Exa Agent task run](/docs/agent/quickstart). Describe the securities, periods, criteria, and output fields you need, and Agent returns schema-validated results with citations. Screen companies, compare filings, or assemble a structured brief across a portfolio. # Legal & Public Records Source: https://exa.ai/docs/search/data/legal Find court opinions, patents, sanctions, government contracts, and other public records with Exa Search. Use Exa Search for primary legal sources and government records alongside the commentary written about them. ## Included * US court opinions, with full text, court, docket, and citation metadata * Granted US patents, with abstract, claims, description, inventors, and assignees * Statutes, regulations, and agency guidance * Sanctions lists and watchlists * Government contracts and procurement records * Census data and other public statistical records ## Use it for * Case-law research and legal RAG * Regulatory and policy monitoring * Prior-art and freedom-to-operate searches * Compliance screening and due diligence * Public-sector market research ## Example queries ### Find case law Describe the legal question and jurisdiction in plain language rather than a citation. ### Search patents Describe what the invention does, the way a claim would. ### Screen against sanctions Name the list and the entity class you are screening. ### Research government spending Name the buying agency or service category and the time window. ### Pull public statistics Name the dataset and geography. ## Make a request ```python Python theme={null} from exa_py import Exa exa = Exa() results = exa.search( "California appellate decisions on non-compete enforceability", type="auto", num_results=10, ) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const results = await exa.search( "California appellate decisions on non-compete enforceability", { type: "auto", numResults: 10, } ); ``` ```bash cURL theme={null} curl -s -X POST https://api.exa.ai/search \ -H "Authorization: Bearer $EXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "California appellate decisions on non-compete enforceability", "type": "auto", "numResults": 10 }' ``` ## Get structured data with Exa Agent For structured data that requires research across multiple sources, use an [Exa Agent task run](/docs/agent/quickstart). Describe the jurisdictions, record types, criteria, and output fields you need, and Agent returns schema-validated results with citations. Screen an entity across record types or trace a regulatory change through primary sources and coverage. # News Source: https://exa.ai/docs/search/data/news Find current reporting, industry coverage, and emerging stories with Exa Search. Use Exa Search for reporting from major publications, trade press, and niche outlets. New articles become searchable within minutes of publication. Combine a natural-language query with date filters when the publication window is a hard requirement. ## Use it for * Market and investment research * Cybersecurity and threat intelligence * Company, product, and competitor monitoring * Industry briefings and current-event research ## Example queries ### Follow a developing policy story Name the topic, source type, and publication window so the results focus on the current phase of the story. ### Find practitioner analysis Name the source type when you want analysis from practitioners rather than general news coverage. ### Discover discussions in a specific format Include the format and the subject in the query. This keeps the search open to episode pages and transcripts across the web. ### Research adverse media Describe both the negative signal and the entity class you are investigating. Avoid reducing the query to a company name plus the word “news.” ## Make a request ```python Python theme={null} from exa_py import Exa exa = Exa() results = exa.search( "news articles about AI regulation updates in the European Union", type="auto", num_results=10, ) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const results = await exa.search( "news articles about AI regulation updates in the European Union", { type: "auto", numResults: 10, } ); ``` ```bash cURL theme={null} curl -s -X POST https://api.exa.ai/search \ -H "Authorization: Bearer $EXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "news articles about AI regulation updates in the European Union", "type": "auto", "numResults": 10 }' ``` ## Get structured data with Exa Agent For structured data that requires research across multiple sources, use an [Exa Agent task run](/docs/agent/quickstart). Describe the stories, fields, and time window you need, and Agent returns schema-validated results with citations. Build a sourced news brief, compare coverage, or extract normalized facts from a developing story. # Data Index Source: https://exa.ai/docs/search/data/overview What Exa indexes across the public web and private data sources. Exa searches the public web and select private data sources, with coverage refreshed continuously. It includes: Explore news search use cases, examples, and best practices. News and articles: Blog posts and listicles: Podcast and video transcripts: Adverse media: Explore code search use cases, examples, and best practices. GitHub repos: API and developer documentation: Package registries, with precise version and release details: Agent skills directories: Learn how to find companies, people, and the relationships between them. Company discovery and operating signals: Professional profiles by role, skill, and location: People qualified by the companies they work for: Company and stakeholder research in one query: Explore quotes, filings, earnings calls, and market research use cases. Prices, analyst estimates, and financial reports: SEC filings, earnings calls, and international filings: Announced funding and other publicly-reported data: Published economic data and government statistics: Explore paper, patent, clinical, and regulatory research use cases. Research papers, patents, and grants: Clinical trials and drug interactions: Regulatory and health approvals: Explore case law, patents, sanctions, and public-records use cases. Legal and court records: Sanctions and watchlists: Public government contracts: Censuses and other public records: Learn how to query live sports data, forecasts, and local information. Live scores, standings, and schedules: Forecasts for any place and date: Local businesses, venues, and properties: Explore vulnerability, advisory, and vendor-risk use cases. Security advisories: CVE and GHSA vulnerability databases: Data subprocessor lists and trust pages: These guides cover common data patterns, but Exa also searches the broader public web across many sites, formats, and languages. # Research Publications Source: https://exa.ai/docs/search/data/research Find academic papers, patents, grants, clinical trials, and regulatory approvals with Exa Search. Use Exa Search for research publications and related records, including titles, abstracts, authors, venues, citations, publisher pages, preprints, and repository pages. Read [SOTA Search Over Academic Publications](https://exa.ai/blog/publications-search) to learn more about publication search quality. ## Included * Papers and preprints, including full-text chunks where a parsed full text is available * Patents, with abstracts, claims, inventors, and assignees * Grants and funding announcements * Clinical trials, drug labels, and interaction data * Regulatory and health approvals ## Use it for * Literature review and citation discovery * Prior-art and patent landscaping * Clinical and pharmaceutical research * Grant and funding-opportunity discovery ## Example queries ### Find papers on a topic Describe the method or finding rather than guessing at title keywords. The `publication` category keeps results to papers. ### Search clinical evidence Name the phase, intervention, and population so trial registrations and results pages rank above general coverage. ### Track regulatory approvals Name the regulator and the device or drug class you are watching. ### Run a prior-art search Describe the invention functionally, the way a claim would, instead of using a product name. ## Make a request ```python Python theme={null} from exa_py import Exa exa = Exa() results = exa.search( "papers on evaluation benchmarks for retrieval-augmented generation", type="auto", category="publication", num_results=10, ) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const results = await exa.search( "papers on evaluation benchmarks for retrieval-augmented generation", { type: "auto", category: "publication", numResults: 10, } ); ``` ```bash cURL theme={null} curl -s -X POST https://api.exa.ai/search \ -H "Authorization: Bearer $EXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "papers on evaluation benchmarks for retrieval-augmented generation", "type": "auto", "category": "publication", "numResults": 10 }' ``` ## Get structured data with Exa Agent For structured data that requires research across multiple sources, use an [Exa Agent task run](/docs/agent/quickstart). Describe the publications, inclusion criteria, and output fields you need, and Agent returns schema-validated results with citations. Build a literature map, screen papers against inclusion criteria, or gather fields from several publications into one table. # Cybersecurity Source: https://exa.ai/docs/search/data/security Find vulnerabilities, advisories, threat reporting, and trust documentation with Exa Search. Use Exa Search for vulnerability records, vendor advisories, and threat research from the sources security teams already read. ## Included * CVE and GHSA vulnerability records * Vendor security advisories and patch notes * Threat intelligence reports and incident write-ups * Trust pages, subprocessor lists, and compliance documentation * Security blogs, conference talks, and research ## Use it for * Vulnerability triage and exposure assessment * Threat intelligence and adversary tracking * Vendor risk and third-party security reviews * Security monitoring and alerting ## Example queries ### Triage a vulnerability class Name the product, version range, and severity. ### Find vendor advisories Describe the exploitation status and product class rather than one CVE ID. ### Review a vendor's security posture Name the document type and the vendor class. ### Research an adversary Name the group or campaign and the technique or sector you care about. ## Make a request ```python Python theme={null} from exa_py import Exa exa = Exa() results = exa.search( "critical CVEs affecting Apache Struts 6.x", type="auto", num_results=10, ) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const results = await exa.search( "critical CVEs affecting Apache Struts 6.x", { type: "auto", numResults: 10, } ); ``` ```bash cURL theme={null} curl -s -X POST https://api.exa.ai/search \ -H "Authorization: Bearer $EXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "critical CVEs affecting Apache Struts 6.x", "type": "auto", "numResults": 10 }' ``` ## Get structured data with Exa Agent For structured data that requires research across multiple sources, use an [Exa Agent task run](/docs/agent/quickstart). Describe the products, threat criteria, and output fields you need, and Agent returns schema-validated results with citations. Screen a vendor across advisories, breach reporting, and trust pages, or assemble normalized vulnerability data. # Sports, Weather & Places Source: https://exa.ai/docs/search/data/sports-weather-places Find live sports data, weather forecasts, and local places with Exa Search. Use Exa Search for live sports data, weather forecasts, and local information without integrating a separate API for each one. Ask a natural-language question with the team, place, and time window you care about. ## Write better queries Name the location or team precisely and include a date whenever the answer changes over time. Add the condition or attribute that matters to your task instead of asking for broad information. ### Included Available sports data: * **Scores**: games for a league on a day, including teams, scores, status, start time, and venue * **Standings**: current league tables with conference or division splits * **Schedules**: past results and upcoming games for a league or team Coverage includes the NBA, WNBA, NFL, MLB, NHL, MLS, college basketball and football, major European soccer leagues and UEFA competitions, cricket, F1, UFC, tennis, and golf. ### Ask for the league, team, and time ### Add the surrounding story Ask for the reporting you need alongside the live data. ### Included Forecasts include conditions, high and low temperatures, precipitation, wind, humidity, UV index, and sunrise and sunset in the place's local time. A query with no date returns today's forecast. Ask for a specific day or range to get one page per day, up to 16 days ahead or 92 days back. ### Name the place and day ### Ask about the condition that affects your plan ### Combine forecasts with reporting ### Included * Local business profiles, with addresses, hours, amenities, and reviews * Venues, attractions, and points of interest * Real estate listings and property records * Zoning decisions, permits, and planning records ### Describe the place like you would ask a local Combine the category, neighborhood, and attributes that matter. ### Name the record type and geography ### Compare places against practical constraints ## Make a request All three data types use the same Search endpoint. ```python Python theme={null} from exa_py import Exa exa = Exa() results = exa.search( "weather in San Francisco tomorrow", type="auto", num_results=5, ) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const results = await exa.search("weather in San Francisco tomorrow", { type: "auto", numResults: 5, }); ``` ```bash cURL theme={null} curl -s -X POST https://api.exa.ai/search \ -H "Authorization: Bearer $EXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "weather in San Francisco tomorrow", "type": "auto", "numResults": 5 }' ``` ## Get structured data with Exa Agent For structured data that requires research across multiple sources, use an [Exa Agent task run](/docs/agent/quickstart). Describe the places, teams, dates, criteria, and output fields you need, and Agent returns schema-validated results with citations. Compare places, assemble a game-day brief, or combine local details and conditions into structured results. # Deep Search Source: https://exa.ai/docs/search/deep-search Use iterative search, reasoning, and grounded synthesis for complex research tasks. Deep Search is the research mode of the Search API. It uses the same `/search` endpoint, but the retrieval process can issue multiple searches, inspect the evidence, refine its approach, and synthesize a grounded result. Use standard Search when you need ranked pages for a well-formed query. Use Deep when finding the answer requires research. ## How Deep Search works Deep Search adds a research loop before the final response: Exa starts with your `query` and may expand it into searches that cover different parts of the request. You can supply starting variations with `additionalQueries`. Deep searches for evidence, compares the findings with the request, and determines what is supported or still missing. When the evidence is incomplete or contradictory, Deep can issue a more targeted search instead of returning the first plausible pages. Deep selects the useful results and then uses the same synthesis path as other search types. When you provide `outputSchema`, the response includes structured `output.content` and field-level citations in `output.grounding`. This process is especially useful for lists and structured outputs. Each requested item may require a different search, and Deep can gather and check those results before producing the final structure. ## Choose a Deep mode | Type | Use it when | | ---------------- | ------------------------------------------------------------------------------------ | | `deep-lite` | You need lightweight query expansion and synthesis | | `deep` | The task needs iterative search, evidence gathering, or multiple structured items | | `deep-reasoning` | The task requires more deliberate reasoning across difficult or conflicting evidence | Start with `deep` for research workflows. Move to `deep-lite` when the task is simpler and latency matters. Instead of `deep-reasoning`, use [Exa Agent](/docs/agent/quickstart) for long-running research, list building, and multi-hop enrichment. Agent has more compute per run and returns grounded, structured results. See [Pricing](/docs/admin/pricing#deep-search) for current cost and latency guidance. ## Make a Deep request Set `type` on a normal Search API request: ```python Python theme={null} from exa_py import Exa exa = Exa() result = exa.search( "Compare how major database vendors support vector, keyword, and hybrid retrieval", type="deep", contents={"highlights": True}, ) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const result = await exa.search( "Compare how major database vendors support vector, keyword, and hybrid retrieval", { type: "deep", contents: { highlights: true } } ); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Compare how major database vendors support vector, keyword, and hybrid retrieval", "type": "deep", "contents": { "highlights": true } }' ``` Deep returns the selected search results under `results`. Add `outputSchema` when you also want a synthesized answer or structured dataset. ## Provide starting queries Deep normally decides which searches to run. Use `additionalQueries` when you already know distinct terminology, perspectives, or subproblems that the research should cover: ```python Python theme={null} result = exa.search( "Compare current approaches to inference-time scaling", additional_queries=[ "inference-time compute scaling benchmark", "test-time reasoning methods survey", "adaptive compute language models", ], type="deep", contents={"highlights": True}, ) ``` ```javascript JavaScript theme={null} const result = await exa.search( "Compare current approaches to inference-time scaling", { additionalQueries: [ "inference-time compute scaling benchmark", "test-time reasoning methods survey", "adaptive compute language models" ], type: "deep", contents: { highlights: true } } ); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Compare current approaches to inference-time scaling", "additionalQueries": [ "inference-time compute scaling benchmark", "test-time reasoning methods survey", "adaptive compute language models" ], "type": "deep", "contents": { "highlights": true } }' ``` The main `query` is always included. You can provide up to 10 additional queries, and the list is available only to Deep search types. Do not provide minor rephrasings just to increase search volume. Add queries when each one contributes a meaningfully different search direction. ## Guide behavior and output separately `systemPrompt` and `outputSchema` affect different parts of the request: * `systemPrompt` guides source preferences, novelty, deduplication, and Deep's research behavior. * `outputSchema` defines the final shape and triggers synthesis. The query should describe what to research. The system prompt should describe how to conduct and present that research. ```python Python theme={null} result = exa.search( "Find AI infrastructure companies that announced Series A or B funding in the last six months", type="deep", system_prompt="Prefer company announcements and investor portfolio pages. Exclude duplicate rounds.", output_schema={ "type": "object", "required": ["companies"], "properties": { "companies": { "type": "array", "maxItems": 8, "items": { "type": "object", "required": ["name", "round", "amount"], "properties": { "name": {"type": "string"}, "round": {"type": "string"}, "amount": {"type": "string"}, }, }, } }, }, ) ``` ```javascript JavaScript theme={null} const result = await exa.search( "Find AI infrastructure companies that announced Series A or B funding in the last six months", { type: "deep", systemPrompt: "Prefer company announcements and investor portfolio pages. Exclude duplicate rounds.", outputSchema: { type: "object", required: ["companies"], properties: { companies: { type: "array", maxItems: 8, items: { type: "object", required: ["name", "round", "amount"], properties: { name: { type: "string" }, round: { type: "string" }, amount: { type: "string" } } } } } } } ); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Find AI infrastructure companies that announced Series A or B funding in the last six months", "type": "deep", "systemPrompt": "Prefer company announcements and investor portfolio pages. Exclude duplicate rounds.", "outputSchema": { "type": "object", "required": ["companies"], "properties": { "companies": { "type": "array", "maxItems": 8, "items": { "type": "object", "required": ["name", "round", "amount"], "properties": { "name": { "type": "string" }, "round": { "type": "string" }, "amount": { "type": "string" } } } } } } }' ``` Prefer Deep when you need more than two structured items or each item must satisfy several requirements. Standard search types use the same synthesis path, but they do not perform the same iterative research before synthesis. ## Read the grounded response Structured responses separate generated values from their evidence: ```json theme={null} { "results": [ { "title": "Acme AI raises $30M Series B", "url": "https://acme.example/news/series-b" } ], "output": { "content": { "companies": [ { "name": "Acme AI", "round": "Series B", "amount": "$30M" } ] }, "grounding": [ { "field": "companies[0].amount", "citations": [ { "title": "Acme AI raises $30M Series B", "url": "https://acme.example/news/series-b" } ], "confidence": "high" } ] } } ``` Use `output.content` as the generated result and `output.grounding` to show or verify the sources supporting each field. Do not add citation or confidence fields to your own schema; Exa returns them automatically. `numResults` controls how many selected pages are returned in `results`. It does not set the number of searches Deep may perform. ## Stream the synthesis Set `stream: true` with `outputSchema` to receive the synthesized output over server-sent events: ```python Python theme={null} import os import requests response = requests.post( "https://api.exa.ai/search", headers={"Authorization": f"Bearer {os.environ['EXA_API_KEY']}"}, json={ "query": "Explain the competing technical approaches to long-context retrieval", "type": "deep", "stream": True, "outputSchema": { "type": "text", "description": "A grounded comparison organized by approach", }, }, stream=True, ) response.raise_for_status() for line in response.iter_lines(decode_unicode=True): if line: print(line) ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.exa.ai/search", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.EXA_API_KEY}` }, body: JSON.stringify({ query: "Explain the competing technical approaches to long-context retrieval", type: "deep", stream: true, outputSchema: { type: "text", description: "A grounded comparison organized by approach" } }) }); if (!response.ok || !response.body) { throw new Error(`Search failed: ${response.status}`); } const decoder = new TextDecoder(); for await (const chunk of response.body) { process.stdout.write(decoder.decode(chunk, { stream: true })); } ``` ```bash cURL theme={null} curl -N -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Explain the competing technical approaches to long-context retrieval", "type": "deep", "stream": true, "outputSchema": { "type": "text", "description": "A grounded comparison organized by approach" } }' ``` Consume the typed events until `done`. The final event contains the completed output and search time, with cost information when available. ## When to stay with standard Search Deep is unnecessary when one retrieval pass can satisfy the request: * You need relevant pages, not a researched conclusion. * The query already identifies a specific source or narrow topic. * Your application performs its own reasoning and only needs retrieval. * The request is on an interactive, autocomplete, or voice path. Use `auto` for the default quality and speed balance, or `fast` and `instant` for measured latency requirements. Build requests, choose result content, and apply filters. Improve quality, context, latency, and agent integrations. See every request parameter and response field. Review current Deep Search costs and latency. # Highlights Source: https://exa.ai/docs/search/highlights Return query-relevant excerpts from Exa Search results while controlling context size and latency. Highlights return extractive passages from each result that are relevant to your query. Use them when your application needs evidence from the page without the token cost of full text. Each result returns its selected passages in `results[].highlights`. ## Why highlights instead of full text Highlights come from Exa's in-house extraction model. The model reads each result against your query on every request and returns only the passages that answer it. You keep a fraction of the tokens of full page text with equal or better downstream answer quality. | Evaluation | Result | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | Accuracy (SimpleQA) | 500 characters of highlights match the accuracy of the first 8,000 characters of page text, with 16x fewer tokens | | Quality at larger budgets | 4,000 characters of highlights outscore 32,000 characters of full text | | Long technical docs | At a 500-character budget, highlights reach 60% accuracy on API references, SDK docs, specs, and papers; full text reaches 6% | | Search token usage | Highlights cut search tokens by 5x on average | The savings matter most in agent loops, where every round of search results competes with reasoning traces for context. Read [Exa Highlights: Quality, Token-Efficient Search](https://exa.ai/blog/highlights-for-agents) for the methodology and full results. ## Add highlights to Search Use `highlights: true` inside `contents` as the recommended default. Exa chooses how much text to return from each result based on its relevance to your query, so there is no character budget to tune. Set `maxCharacters` only when your application requires a fixed per-page limit. ```python Python theme={null} result = exa.search( "How are inference providers reducing transformer latency?", contents={"highlights": True}, ) ``` ```javascript JavaScript theme={null} const result = await exa.search( "How are inference providers reducing transformer latency?", { contents: { highlights: true } } ); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "How are inference providers reducing transformer latency?", "contents": { "highlights": true } }' ``` ## Dynamic Highlights Dynamic Highlights adjusts how much text it selects from each result based on what is most useful for your query. It can take more from strong sources and less from repetitive or irrelevant ones, reducing the total tokens returned. Use it when several results will feed the same agent or context window. Keep regular `highlights: true` when every page needs its own excerpt or a predictable per-page limit. In Exa's evaluations, Dynamic Highlights cut tokens by an average of 95% compared to full page content. At a 12,000-character budget it beat regular highlights with a 40% average token-efficiency gain and a 3.8% quality increase. Inside Exa Agent, it cut total agent token usage by 30% with a 2.1% average quality gain on benchmarks including BrowseComp and WideSearch. Read [Dynamic Highlights](https://exa.ai/blog/dynamic-highlights) for the evaluation results and design behind cross-result highlight selection. Enable it with `dynamic: true`: ```python Python theme={null} from exa_py.api import DYNAMIC_HIGHLIGHTS_BETA result = exa.search( "How did US household solar installation costs change over the past five years?", contents={ "highlights": { "dynamic": True, } }, betas=[DYNAMIC_HIGHLIGHTS_BETA], ) ``` ```javascript JavaScript theme={null} import Exa, { DYNAMIC_HIGHLIGHTS_BETA } from "exa-js"; const result = await exa.search( "How did US household solar installation costs change over the past five years?", { contents: { highlights: { dynamic: true } }, betas: [DYNAMIC_HIGHLIGHTS_BETA] } ); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -H "Exa-Beta: dynamic-highlights-2026-08-28" \ -d '{ "query": "How did US household solar installation costs change over the past five years?", "contents": { "highlights": { "dynamic": true } } }' ``` Dynamic Highlights is a research preview and requires the `Exa-Beta: dynamic-highlights-2026-08-28` request header. The SDKs send it when you pass `betas=[DYNAMIC_HIGHLIGHTS_BETA]` (Python) or `betas: [DYNAMIC_HIGHLIGHTS_BETA]` (JavaScript). The response uses the same `results[].highlights` shape as regular highlights. ## Next steps Build a Search request and choose the right output shape. Tune retrieval quality, latency, freshness, and context size. # Exa Search API Source: https://exa.ai/docs/search/quickstart Search the web in natural language and get clean, relevant page content in one request. Exa Search takes a natural language query and returns ranked web results with clean page content. ## Make your first request Start with a `query` in natural language and `contents: { highlights: true }`, which returns excerpts sized to each result's relevance. Other fields control how Exa searches and what each result includes; the rest of this page covers the ones you will actually use. ```python Python theme={null} from exa_py import Exa exa = Exa() result = exa.search( "recent techniques for improving retrieval in RAG systems", type="auto", contents={"highlights": True}, ) for item in result.results: print(item.title, item.url) print(item.highlights) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const result = await exa.search( "recent techniques for improving retrieval in RAG systems", { type: "auto", contents: { highlights: true } } ); for (const item of result.results) { console.log(item.title, item.url); console.log(item.highlights); } ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "recent techniques for improving retrieval in RAG systems", "type": "auto", "contents": { "highlights": true } }' ``` Search returns up to 10 results by default. Set `numResults` to request up to 100 results; fewer may be returned when fewer relevant pages are available. Search does not support pagination. Highlights and the list below are shortened. ```json theme={null} { "requestId": "c3174df2b9cd5afbc64cdf79f3719b19", "resolvedSearchType": "", "results": [ { "id": "https://arxiv.org/html/2608.21702", "title": "From Association to Causation: Improving Retrieval Precision ofRetrieval-Augmented Generation via Causal Relations and an Attention Mechanism", "url": "https://arxiv.org/html/2608.21702", "highlights": [ "Retrieval-Augmented Generation (RAG) grounds LLM generation on retrieved documents, but the standard terminal retrieval stage—dense-vector similarity, optionally followed by reranking—often returns documents that merely share keywords with the query without containing the needed information...\n..." ], "image": "https://arxiv.org/static/base/1.0.1/images/icons/smileybones-small.svg", "favicon": "https://arxiv.org/static/browse/0.3.4/images/icons/favicon-32x32.png" }, { "id": "https://www.thoughtworks.com/en-us/insights/blog/generative-ai/four-retrieval-techniques-improve-rag", "title": "Four retrieval techniques to improve RAG you need to know", "url": "https://www.thoughtworks.com/en-us/insights/blog/generative-ai/four-retrieval-techniques-improve-rag", "publishedDate": "2025-04-14T00:00:00.000Z", "highlights": [ "It's not surprising, then, that we've seen a range of different approaches emerge that attempt to address RAG's limitations over the last year or so.\n..." ], "image": "https://www.thoughtworks.com/content/dam/thoughtworks/images/illustration/brand/tw_illustration_5.jpg" } ], "searchTime": 1324.3, "costDollars": { "total": 0.007, "search": { "neural": 0.007 } } } ``` Results are ranked by relevance. Each one carries metadata such as title, URL, and publication date, plus whatever you requested under `contents`. ## Writing queries The `query` field is the only required field when using the Search API. Write queries in natural language. Include the subject and, when useful, the kind of source and time period you want. Queries can be broad and exploratory. `"Latest news on EU battery policy"` gives Exa enough intent to discover relevant pages; `"news"` does not. When the source type matters, name it in the query: ```text theme={null} Recent technical articles comparing hybrid and semantic retrieval for RAG systems ``` See [What's in Exa's Index](/docs/search/data/overview) for what Exa's index includes and how to search those content types. ## Choose a search type `type` selects a search mode, each tuned for a different balance of speed, search depth, and synthesis. `auto` is the default and works for most searches. | Type | Use it when | | ---------------- | ---------------------------------------------------------------- | | `auto` | You want the best default balance of quality and speed | | `fast` | The request is latency-sensitive | | `instant` | The request is on a real-time path such as autocomplete or voice | | `deep-lite` | The task needs lightweight research and synthesis | | `deep` | The task requires multi-step search and stronger synthesis | | `deep-reasoning` | Completeness and reasoning depth matter more than latency | Deep modes run a research process rather than a single retrieval pass. See [Deep Search](/docs/search/deep-search) for how that process works and how to use its additional controls. Instead of `deep-reasoning`, use [Exa Agent](/docs/agent/quickstart) for long-running research, list building, and multi-hop enrichment. Agent has more compute per run and returns grounded, structured results. ## Output shapes Every result includes metadata such as its title, URL, and publication date. Use `contents` to add highlights, full text, or a summary from the page. Highlights return the excerpts most relevant to your query. They give models and agents the evidence they need without filling the context window with unrelated parts of each page. This is the recommended output shape for most tasks. Start with bare `highlights: true`. Exa uses the query to select an appropriate amount of content from each result. ```python Python theme={null} result = exa.search( "How are inference providers reducing transformer latency?", contents={"highlights": True}, ) ``` ```javascript JavaScript theme={null} const result = await exa.search( "How are inference providers reducing transformer latency?", { contents: { highlights: true } } ); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "How are inference providers reducing transformer latency?", "contents": { "highlights": true } }' ``` See [Highlights](/docs/search/highlights) for Dynamic Highlights and guidance on when to enable it. Full text returns the clean page body. Use it when the task depends on broader context, document structure, or details that may fall outside query-focused excerpts. Full pages can be large. Limit both the number of results and the text returned per page. ```python Python theme={null} result = exa.search( "Technical postmortems of large-scale inference outages", num_results=5, contents={"text": {"max_characters": 10000}}, ) ``` ```javascript JavaScript theme={null} const result = await exa.search( "Technical postmortems of large-scale inference outages", { numResults: 5, contents: { text: { maxCharacters: 10000 } } } ); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "Technical postmortems of large-scale inference outages", "numResults": 5, "contents": { "text": { "maxCharacters": 10000 } } }' ``` Pick one content view per request. Requesting both highlights and text returns and bills two views of the same page. `summary` is a third option, but it adds a language model call for every result. `/search` and `/contents` accept the same content options in different places: * **`/search`** nests `highlights`, `text`, and `summary` inside the `contents` object: `"contents": { "highlights": true }` * **`/contents`** has no `contents` wrapper. Its body is the content options themselves, so the same fields sit at the top level next to `urls`: `"urls": [...], "highlights": true` ## Output schema Add `outputSchema` when you want Exa to synthesize the search results. It works with every search type and adds an `output` object to the response. The ranked pages remain in `results`. The generated value is returned in `output.content`, with field-level sources and confidence in `output.grounding`. Use `type: "text"` for generated prose. Add a `description` to specify its format or length. ```python Python theme={null} result = exa.search( "What changed in the latest EU battery policy?", output_schema={ "type": "text", "description": "Summarize the changes in three concise bullets", }, ) ``` ```javascript JavaScript theme={null} const result = await exa.search( "What changed in the latest EU battery policy?", { outputSchema: { type: "text", description: "Summarize the changes in three concise bullets" } } ); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "What changed in the latest EU battery policy?", "outputSchema": { "type": "text", "description": "Summarize the changes in three concise bullets" } }' ``` Use `type: "object"` for JSON that follows the properties and requirements you define. ```python Python theme={null} result = exa.search( "AI infrastructure companies that announced Series A or B funding in the past six months", output_schema={ "type": "object", "properties": { "companies": { "type": "array", "maxItems": 10, "items": { "type": "object", "properties": { "name": {"type": "string"}, "round": {"type": "string"}, "amount": {"type": "string"}, "announcedDate": { "type": "string", "description": "The funding announcement date", }, "leadInvestors": { "type": "array", "items": {"type": "string"}, }, }, "required": ["name", "round", "amount", "announcedDate"], }, } }, "required": ["companies"], }, ) ``` ```javascript JavaScript theme={null} const result = await exa.search( "AI infrastructure companies that announced Series A or B funding in the past six months", { outputSchema: { type: "object", properties: { companies: { type: "array", maxItems: 10, items: { type: "object", properties: { name: { type: "string" }, round: { type: "string" }, amount: { type: "string" }, announcedDate: { type: "string", description: "The funding announcement date" }, leadInvestors: { type: "array", items: { type: "string" } } }, required: ["name", "round", "amount", "announcedDate"] } } }, required: ["companies"] } } ); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "AI infrastructure companies that announced Series A or B funding in the past six months", "outputSchema": { "type": "object", "properties": { "companies": { "type": "array", "maxItems": 10, "items": { "type": "object", "properties": { "name": { "type": "string" }, "round": { "type": "string" }, "amount": { "type": "string" }, "announcedDate": { "type": "string", "description": "The funding announcement date" }, "leadInvestors": { "type": "array", "items": { "type": "string" } } }, "required": ["name", "round", "amount", "announcedDate"] } } }, "required": ["companies"] } }' ``` Use `systemPrompt` for instructions such as source preferences or emphasis; use `outputSchema` for the response shape. Python uses `system_prompt` and `output_schema`. Keep object schemas small: they support up to 2 levels of nesting and 10 properties. Do not add citation or confidence fields to the schema; Exa returns them automatically in `output.grounding`. ## Filter results Filters are hard constraints: add one when a result outside it would be unusable to you, and keep softer source preferences in the query text instead. The [API reference](/docs/reference/search) has the full set. ### Include domains or paths `includeDomains` restricts results to sources you trust. It accepts full domains, path prefixes such as `anthropic.com/news`, and subdomain wildcards such as `*.substack.com`. ```python Python theme={null} result = exa.search( "new model releases", include_domains=["openai.com", "anthropic.com/news"], contents={"highlights": True}, ) ``` ```javascript JavaScript theme={null} const result = await exa.search("new model releases", { includeDomains: ["openai.com", "anthropic.com/news"], contents: { highlights: true } }); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "new model releases", "includeDomains": ["openai.com", "anthropic.com/news"], "contents": { "highlights": true } }' ``` Put the path in the filter rather than repeating it as a `site:` operator in the query. ### Exclude domains or paths `excludeDomains` removes results from specific domains or paths. It supports the same path prefixes and subdomain wildcards as `includeDomains`. Use it when those sources would make a result unusable, not to express a preference. ```python Python theme={null} result = exa.search( "primary research on retrieval-augmented generation benchmarks", exclude_domains=["medium.com", "dev.to"], contents={"highlights": True}, ) ``` ```javascript JavaScript theme={null} const result = await exa.search( "primary research on retrieval-augmented generation benchmarks", { excludeDomains: ["medium.com", "dev.to"], contents: { highlights: true } } ); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "primary research on retrieval-augmented generation benchmarks", "excludeDomains": ["medium.com", "dev.to"], "contents": { "highlights": true } }' ``` ## Content freshness `contents.maxAgeHours` controls how fresh the content extracted from each result must be. It does not filter results by publication date. | Value | Behavior | | ---------------- | -------------------------------------------------------------------------------- | | Omit | Use cached content when available and fetch the page when needed | | Positive integer | Use cached content if it is newer than this many hours; otherwise fetch the page | | `0` | Always fetch fresh content | | `-1` | Only use cached content | Most searches should omit this field. Set it when stale page content would be unusable, such as for prices, availability, or a page that changes frequently. ```python Python theme={null} result = exa.search( "current pricing for serverless GPU providers", contents={ "highlights": True, "max_age_hours": 24, }, ) ``` ```javascript JavaScript theme={null} const result = await exa.search("current pricing for serverless GPU providers", { contents: { highlights: true, maxAgeHours: 24 } }); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "current pricing for serverless GPU providers", "contents": { "highlights": true, "maxAgeHours": 24 } }' ``` ## Next steps Token budgets, content freshness, structured output, and system prompts. Every request parameter and response field, with a live playground. You already have the URLs and want clean text, highlights, or summaries. You need long-running research, list building, or enrichment. # Exa Snapshot Source: https://exa.ai/docs/search/snapshot Pin Search and Contents to a stored version of a page at a datetime you choose. Exa Snapshot keeps stored versions of pages Exa has crawled. Send `snapshotAsOf` to pin the request to a datetime. Use this to backtest agents, run reproducible evals, and compare earlier versions of docs, pricing pages, policies, and filings. Exa Snapshot is available on pay as you go at 10 QPS, with a rolling 5-month index window. After 100 requests, [talk to sales](https://exa.ai/contact/sales) to continue. ## Search at a datetime On `/search`, put `snapshotAsOf` inside `contents`. ```python Python theme={null} from exa_py import Exa exa = Exa() result = exa.search( "latest stable Python release notes", num_results=3, contents={ "snapshot_as_of": "2026-07-01T00:00:00Z", "highlights": True, }, ) for r in result.results: print(r.title, r.url) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const result = await exa.search("latest stable Python release notes", { numResults: 3, contents: { snapshotAsOf: "2026-07-01T00:00:00Z", highlights: true } }); for (const r of result.results) { console.log(r.title, r.url); } ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/search" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "query": "latest stable Python release notes", "numResults": 3, "contents": { "snapshotAsOf": "2026-07-01T00:00:00Z", "highlights": true } }' ``` Exa discovers candidate URLs, then keeps only pages with a stored version at or before `snapshotAsOf`. ```json theme={null} { "requestId": "211fc1f57b87a792de082309ef3bce95", "results": [ { "id": "https://docs.python.org/3/whatsnew/changelog.html", "url": "https://docs.python.org/3/whatsnew/changelog.html", "title": "Changelog — Python 3.14.6 documentation", "highlights": [ "Changelog — Python 3.14.6 documentation\n...\n## Python 3.14.6 final¶\n...\nRelease date: 2026-06-10" ], "image": "https://docs.python.org/3.14/_images/social_previews/..." }, { "id": "https://docs.python.org/3/whatsnew/index.html", "url": "https://docs.python.org/3/whatsnew/index.html", "title": "What's New in Python — Python 3.14.6 documentation", "highlights": ["What's new in Python\n...\n- Python 3.14.6 final\n- Python 3.14.5 final"] }, { "id": "https://docs.python.org/3/whatsnew/3.14.html", "url": "https://docs.python.org/3/whatsnew/3.14.html", "title": "What's new in Python 3.14 — Python 3.14.6 documentation", "highlights": ["Python 3.14 is the latest stable release of the Python programming language..."] } ] } ``` ## Pin contents to a datetime Add `snapshotAsOf` at the top level of a `/contents` request. ```python Python theme={null} from exa_py import Exa exa = Exa() result = exa.get_contents( ["https://en.wikipedia.org/wiki/2026"], snapshot_as_of="2026-06-01T00:00:00Z", text=True, ) print(result.results[0].text[:300]) ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(); const result = await exa.getContents( ["https://en.wikipedia.org/wiki/2026"], { snapshotAsOf: "2026-06-01T00:00:00Z", text: true } ); console.log(result.results[0].text.slice(0, 300)); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/contents" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EXA_API_KEY" \ -d '{ "ids": ["https://en.wikipedia.org/wiki/2026"], "snapshotAsOf": "2026-06-01T00:00:00Z", "text": true }' ``` Exa returns the newest stored version at or before that datetime. ```json theme={null} { "requestId": "c05151f7df9cd9d8785e0acf0935355d", "results": [ { "id": "https://en.wikipedia.org/wiki/2026", "url": "https://en.wikipedia.org/wiki/2026", "title": "2026", "author": null, "text": "2026\n\n2026 (MMXXVI) is the current year, and is a common year starting on Thursday of the Gregorian calendar...", "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/..." } ], "statuses": [ { "id": "https://en.wikipedia.org/wiki/2026", "status": "success", "source": "cached" } ] } ``` IDs with no eligible version are omitted from `results` and reported in `statuses` with `"status": "error"` and `"tag": "CONTENT_NOT_CACHED"`. ## How snapshots work | Field | Where | Meaning | | -------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `snapshotAsOf` | Request | Cutoff instant as an ISO 8601 date-time (`2026-06-01T00:00:00Z`) or date (`2026-06-01`, midnight UTC). Exa returns the newest stored version at or before this instant. | For both endpoints: * Returned page content comes from that stored version. * Title, author, publication date, text, highlights, and summaries are generated only from that version. * Pages without an eligible version in the 5-month window are omitted. On Search, the cutoff bounds content, not ranking. Exa still uses current retrieval signals to discover candidate URLs. Use the results as evidence bounded by `snapshotAsOf`, not as an exact reconstruction of what a search would have ranked at that time. ## Limits and compatibility Pay as you go includes 10 QPS and a rolling 5 months of index access. A `snapshotAsOf` older than that window is rejected. After 100 requests, [talk to sales](https://exa.ai/contact/sales) to continue. Do not combine `snapshotAsOf` with options that can reach the live web or expand to other pages. Omit `livecrawl`, `livecrawlTimeout`, `maxAgeHours`, and `subpages` entirely; requests that set any of them alongside `snapshotAsOf` are rejected with `INVALID_REQUEST`. Exa Snapshot on Search supports `auto`, `fast`, and `instant`. It does not support `deep-lite`, `deep`, or `deep-reasoning`. Exa Snapshot does not support the `category` parameter on Search. ## Common uses Use Exa Snapshot when the task depends on what Exa had stored by a specific datetime: * Backtest an agent without exposing it to later page updates. * Run an evaluation against a repeatable content boundary. * Compare earlier versions of documentation, pricing, policies, or filings. # Get an event Source: https://exa.ai/docs/websets/api/events/get-an-event exa-spec.yaml GET /v0/events/{id} Get a single Event by id. You can subscribe to Events by creating a Webhook. # List all events Source: https://exa.ai/docs/websets/api/events/list-all-events exa-spec.yaml GET /v0/events List all events that have occurred in the system. You can paginate through the results using the `cursor` parameter. # Event types Source: https://exa.ai/docs/websets/api/events/types Learn about the events that occur within the Webset API The Websets API uses events to notify you about changes in your Websets. You can monitor these events through our [events endpoint](/docs/websets/api/events/list-all-events) or by setting up [webhooks](/docs/websets/api/webhooks/create-a-webhook). Events are retained for 60 days before being automatically deleted. ## Webset * `webset.created` - Emitted when a new Webset is created. * `webset.deleted` - Emitted when a Webset is deleted. * `webset.paused` - Emitted when a Webset's operations are paused. * `webset.idle` - Emitted when a Webset has no running operations. ## Search * `webset.search.created` - Emitted when a new search is initiated. * `webset.search.updated` - Emitted when search progress is updated. * `webset.search.completed` - Emitted when a search finishes finding all items. * `webset.search.canceled` - Emitted when a search is manually canceled. ## Item * `webset.item.created` - Emitted when a new item has been added to the Webset. * `webset.item.enriched` - Emitted when an item's enrichment is completed. ## Import * `import.created` - Emitted when a new import is initiated. * `import.completed` - Emitted when an import has been completed. ## Export * `webset.export.created` - Emitted when a new export is initiated. * `webset.export.completed` - Emitted when an export has been completed. ## Monitor * `monitor.created` - Emitted when a new monitor is created. * `monitor.updated` - Emitted when a monitor's configuration is updated. * `monitor.deleted` - Emitted when a monitor is deleted. * `monitor.run.created` - Emitted when a monitor run starts. * `monitor.run.completed` - Emitted when a monitor run finishes. Each event includes: * A unique `id` * The event `type` * A `data` object containing the full resource that triggered the event * A `createdAt` timestamp You can use these events to: * Track the progress of searches and enrichments * Build real-time dashboards * Trigger workflows when new items are found * Monitor the status of your exports # Create an import Source: https://exa.ai/docs/websets/api/imports/create-an-import exa-spec.yaml POST /v0/imports Creates a new import to upload your data into Websets. Imports can be used to: - **Enrich**: Enhance your data with additional information using our AI-powered enrichment engine - **Search**: Query your data using Websets' agentic search with natural language filters - **Exclude**: Prevent duplicate or already known results from appearing in your searches Once the import is created, you can upload your data to the returned `uploadUrl` until `uploadValidUntil` (by default 1 hour). # Delete an import Source: https://exa.ai/docs/websets/api/imports/delete-import exa-spec.yaml DELETE /v0/imports/{id} Deletes a import. # Get an import Source: https://exa.ai/docs/websets/api/imports/get-import exa-spec.yaml GET /v0/imports/{id} Gets a specific import. # List imports Source: https://exa.ai/docs/websets/api/imports/list-imports exa-spec.yaml GET /v0/imports Lists all imports for the Webset. # Update an import Source: https://exa.ai/docs/websets/api/imports/update-import exa-spec.yaml PATCH /v0/imports/{id} Updates a import configuration. # Create a Websets monitor Source: https://exa.ai/docs/websets/api/monitors/create-a-monitor exa-spec.yaml POST /v0/monitors Creates a new `Monitor` to continuously keep your Websets updated with fresh data. Monitors automatically run on your defined schedule to ensure your Websets stay current without manual intervention: - **Find new content**: Execute `search` operations to discover fresh items matching your criteria - **Update existing content**: Run `refresh` operations to update items contents and enrichments - **Automated scheduling**: Configure `cron` expressions and `timezone` for precise scheduling control # Delete a Websets monitor Source: https://exa.ai/docs/websets/api/monitors/delete-monitor exa-spec.yaml DELETE /v0/monitors/{id} Deletes a monitor. # Get a Websets monitor Source: https://exa.ai/docs/websets/api/monitors/get-monitor exa-spec.yaml GET /v0/monitors/{id} Gets a specific monitor. # List Websets monitors Source: https://exa.ai/docs/websets/api/monitors/list-monitors exa-spec.yaml GET /v0/monitors Lists all monitors for the Webset. # Get a monitor run Source: https://exa.ai/docs/websets/api/monitors/runs/get-monitor-run exa-spec.yaml GET /v0/monitors/{monitor}/runs/{id} Gets a specific monitor run. # List monitor runs Source: https://exa.ai/docs/websets/api/monitors/runs/list-monitor-runs exa-spec.yaml GET /v0/monitors/{monitor}/runs Lists all runs for the Monitor. # Update a Websets monitor Source: https://exa.ai/docs/websets/api/monitors/update-monitor exa-spec.yaml PATCH /v0/monitors/{id} Updates a monitor configuration. # Get team info Source: https://exa.ai/docs/websets/api/teams/get-team-info exa-spec.yaml GET /v0/teams/me Retrieve information about your team including concurrency usage and limits. ## Overview The Get Team Info endpoint returns information about the authenticated team, including the team's current concurrency usage and configured limits. This is useful for monitoring your Websets API usage and understanding your rate limits. ## Response The response includes: * **object**: Always "team" * **id**: Your team's unique identifier * **name**: Your team's name * **concurrency**: Current usage showing active and queued requests * **limits**: Your team's concurrency limits ### Concurrency Fields The `concurrency` object shows your current request state: * **active**: Number of requests currently being processed * **queued**: Number of requests waiting to be processed ### Limits Fields The `limits` object shows your team's configured limits: * **maxConcurrent**: Maximum number of requests that can be processed simultaneously (null means unlimited) * **maxQueued**: Maximum number of requests that can wait in the queue (null means unlimited) # List webhook attempts Source: https://exa.ai/docs/websets/api/webhooks/attempts/list-webhook-attempts exa-spec.yaml GET /v0/webhooks/{id}/attempts List all attempts made by a Webhook ordered in descending order. # Create a webhook Source: https://exa.ai/docs/websets/api/webhooks/create-a-webhook exa-spec.yaml POST /v0/webhooks Creates a Webhook that delivers the selected events to your URL as they occur. The response includes the signing `secret` used to verify deliveries; it is only returned on creation. **Redirects are not followed.** Webhook deliveries are sent directly to the registered URL. If your endpoint responds with a 3xx redirect, the delivery will be treated as a failure. Always register the final destination URL. # Delete a webhook Source: https://exa.ai/docs/websets/api/webhooks/delete-a-webhook exa-spec.yaml DELETE /v0/webhooks/{id} Deletes a Webhook. Its status becomes `inactive`, which stops future webhook deliveries to its URL. # Get a webhook Source: https://exa.ai/docs/websets/api/webhooks/get-a-webhook exa-spec.yaml GET /v0/webhooks/{id} Returns a Webhook by id, including its status, subscribed events, target URL, and metadata. The signing `secret` is not returned. # List webhooks Source: https://exa.ai/docs/websets/api/webhooks/list-webhooks exa-spec.yaml GET /v0/webhooks Returns the active Webhooks for your team. You can paginate through the results using the `cursor` parameter. # Update a webhook Source: https://exa.ai/docs/websets/api/webhooks/update-a-webhook exa-spec.yaml PATCH /v0/webhooks/{id} Updates the target URL, subscribed events, or metadata of a Webhook. Omitted fields are left unchanged. **Redirects are not followed.** If you update the URL, ensure it is the final destination. Endpoints that respond with a 3xx redirect will be treated as delivery failures. # Verifying signatures Source: https://exa.ai/docs/websets/api/webhooks/verifying-signatures Learn how to securely verify webhook signatures to ensure requests are from Exa When you receive a webhook from Exa, you should verify that it came from us to ensure the integrity and authenticity of the data. Exa signs all webhook payloads with a secret key that's unique to your webhook endpoint. ## How Webhook Signatures Work Exa uses HMAC SHA256 to sign webhook payloads. The signature is included in the `Exa-Signature` header, which contains: * A timestamp (`t=`) indicating when the webhook was sent * One or more signatures (`v1=`) computed using the timestamp and payload The signature format looks like this: ```text theme={null} Exa-Signature: t=1234567890,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd ``` ## Verification Process To verify a webhook signature: 1. Extract the timestamp and signatures from the `Exa-Signature` header 2. Create the signed payload by concatenating the timestamp, a period, and the raw request body 3. Compute the expected signature using HMAC SHA256 with your webhook secret 4. Compare your computed signature with the provided signatures ```python Python theme={null} import hmac import hashlib import time def verify_webhook_signature(payload, signature_header, webhook_secret): """ Verify the signature of a webhook payload. Args: payload (str): The raw request body as a string signature_header (str): The Exa-Signature header value webhook_secret (str): Your webhook secret Returns: bool: True if signature is valid, False otherwise """ try: # Parse the signature header pairs = [pair.split('=', 1) for pair in signature_header.split(',')] timestamp = None signatures = [] for key, value in pairs: if key == 't': timestamp = value elif key == 'v1': signatures.append(value) if not timestamp or not signatures: return False # Optional: Check if timestamp is recent (within 5 minutes) current_time = int(time.time()) if abs(current_time - int(timestamp)) > 300: print("Warning: Webhook timestamp is more than 5 minutes old") # Create the signed payload signed_payload = f"{timestamp}.{payload}" # Compute the expected signature expected_signature = hmac.new( webhook_secret.encode('utf-8'), signed_payload.encode('utf-8'), hashlib.sha256 ).hexdigest() # Compare with provided signatures return any(hmac.compare_digest(expected_signature, sig) for sig in signatures) except Exception as e: print(f"Error verifying signature: {e}") return False # Example usage in a Flask webhook endpoint from flask import Flask, request, jsonify import os app = Flask(__name__) @app.route('/webhook', methods=['POST']) def handle_webhook(): # Get the raw payload and signature payload = request.get_data(as_text=True) signature_header = request.headers.get('Exa-Signature', '') webhook_secret = os.environ.get('WEBHOOK_SECRET') # Verify the signature if not verify_webhook_signature(payload, signature_header, webhook_secret): return jsonify({'error': 'Invalid signature'}), 400 # Process the webhook webhook_data = request.get_json() print(f"Received {webhook_data['type']} event") return jsonify({'status': 'success'}), 200 ``` ```javascript JavaScript/Node.js theme={null} const crypto = require('crypto'); function verifyWebhookSignature(payload, signatureHeader, webhookSecret) { /** * Verify the signature of a webhook payload. * * @param {string} payload - The raw request body as a string * @param {string} signatureHeader - The Exa-Signature header value * @param {string} webhookSecret - Your webhook secret * @returns {boolean} True if signature is valid, false otherwise */ try { // Parse the signature header const pairs = signatureHeader.split(',').map(pair => pair.split('=')); const timestamp = pairs.find(([key]) => key === 't')?.[1]; const signatures = pairs .filter(([key]) => key === 'v1') .map(([, value]) => value); if (!timestamp || signatures.length === 0) { return false; } // Optional: Check if timestamp is recent (within 5 minutes) const currentTime = Math.floor(Date.now() / 1000); if (Math.abs(currentTime - parseInt(timestamp)) > 300) { console.warn('Warning: Webhook timestamp is more than 5 minutes old'); } // Create the signed payload const signedPayload = `${timestamp}.${payload}`; // Compute the expected signature const expectedSignature = crypto .createHmac('sha256', webhookSecret) .update(signedPayload) .digest('hex'); // Compare with provided signatures using timing-safe comparison return signatures.some(sig => crypto.timingSafeEqual( Buffer.from(expectedSignature, 'hex'), Buffer.from(sig, 'hex') ) ); } catch (error) { console.error('Error verifying signature:', error); return false; } } // Example usage in an Express.js webhook endpoint const express = require('express'); const app = express(); // Important: Use raw body parser for webhook verification app.use('/webhook', express.raw({ type: 'application/json' })); app.post('/webhook', (req, res) => { const payload = req.body.toString(); const signatureHeader = req.headers['exa-signature'] || ''; const webhookSecret = process.env.WEBHOOK_SECRET; // Verify the signature if (!verifyWebhookSignature(payload, signatureHeader, webhookSecret)) { return res.status(400).json({ error: 'Invalid signature' }); } // Process the webhook const webhookData = JSON.parse(payload); console.log(`Received ${webhookData.type} event`); res.json({ status: 'success' }); }); ``` ```java Java theme={null} import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.time.Instant; import java.util.ArrayList; import java.util.List; public class WebhookTest { /** * Verify the signature of a webhook payload. * * @param payload The raw request body as a string * @param signatureHeader The Exa-Signature header value * @param webhookSecret Your webhook secret * @return true if signature is valid, false otherwise */ public static boolean verifyWebhookSignature(String payload, String signatureHeader, String webhookSecret) { try { // Parse the signature header String[] pairs = signatureHeader.split(","); String timestamp = null; List signatures = new ArrayList<>(); for (String pair : pairs) { String[] keyValue = pair.split("=", 2); if (keyValue.length == 2) { String key = keyValue[0]; String value = keyValue[1]; if ("t".equals(key)) { timestamp = value; } else if ("v1".equals(key)) { signatures.add(value); } } } if (timestamp == null || signatures.isEmpty()) { return false; } // Optional: Check if timestamp is recent (within 5 minutes) long currentTime = Instant.now().getEpochSecond(); long webhookTime = Long.parseLong(timestamp); if (Math.abs(currentTime - webhookTime) > 300) { System.out.println("Warning: Webhook timestamp is more than 5 minutes old"); } // Create the signed payload String signedPayload = timestamp + "." + payload; // Compute the expected signature String expectedSignature = computeHmacSha256(signedPayload, webhookSecret); // Compare with provided signatures using timing-safe comparison return signatures.stream().anyMatch(sig -> timingSafeEquals(expectedSignature, sig)); } catch (Exception e) { System.err.println("Error verifying signature: " + e.getMessage()); return false; } } /** * Compute HMAC SHA256 signature. */ private static String computeHmacSha256(String data, String key) throws NoSuchAlgorithmException, InvalidKeyException { Mac mac = Mac.getInstance("HmacSHA256"); SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); mac.init(secretKeySpec); byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); return bytesToHex(hash); } /** * Convert byte array to hexadecimal string. */ private static String bytesToHex(byte[] bytes) { StringBuilder result = new StringBuilder(); for (byte b : bytes) { result.append(String.format("%02x", b)); } return result.toString(); } /** * Timing-safe string comparison to prevent timing attacks. */ private static boolean timingSafeEquals(String a, String b) { if (a.length() != b.length()) { return false; } int result = 0; for (int i = 0; i < a.length(); i++) { result |= a.charAt(i) ^ b.charAt(i); } return result == 0; } // Example usage and test public static void main(String[] args) { System.out.println("🚀 === Exa Webhook Signature Verification Test ===\n"); // Test with a known payload and signature String testPayload = "{\"type\":\"webset.created\",\"data\":{\"id\":\"ws_test\"}}"; String testSecret = "test_webhook_secret"; String testTimestamp = String.valueOf(Instant.now().getEpochSecond()); try { // Create test signature String signedPayload = testTimestamp + "." + testPayload; String testSignature = computeHmacSha256(signedPayload, testSecret); String testHeader = "t=" + testTimestamp + ",v1=" + testSignature; System.out.println("📋 Test Data:"); System.out.println(" • Payload: " + testPayload); System.out.println(" • Secret: " + testSecret); System.out.println(" • Timestamp: " + testTimestamp); System.out.println(" • Generated Signature: " + testSignature); System.out.println(" • Header: " + testHeader); System.out.println(); System.out.println("🧪 Running Tests..."); // Test verification boolean isValid = verifyWebhookSignature(testPayload, testHeader, testSecret); System.out.println(" ✓ Valid signature verification: " + (isValid ? "✅ PASSED" : "❌ FAILED")); // Test with invalid signature String invalidHeader = "t=" + testTimestamp + ",v1=invalid_signature"; boolean isInvalid = verifyWebhookSignature(testPayload, invalidHeader, testSecret); System.out.println(" ✓ Invalid signature rejection: " + (!isInvalid ? "✅ PASSED" : "❌ FAILED")); // Test with missing timestamp String noTimestampHeader = "v1=" + testSignature; boolean noTimestamp = verifyWebhookSignature(testPayload, noTimestampHeader, testSecret); System.out.println(" ✓ Missing timestamp rejection: " + (!noTimestamp ? "✅ PASSED" : "❌ FAILED")); // Test with empty header boolean emptyHeader = verifyWebhookSignature(testPayload, "", testSecret); System.out.println(" ✓ Empty header rejection: " + (!emptyHeader ? "✅ PASSED" : "❌ FAILED")); // Test with malformed header boolean malformedHeader = verifyWebhookSignature(testPayload, "invalid-header-format", testSecret); System.out.println(" ✓ Malformed header rejection: " + (!malformedHeader ? "✅ PASSED" : "❌ FAILED")); System.out.println(); // Example webhook processing if (isValid) { System.out.println("🎉 === Processing Valid Webhook ==="); System.out.println(" Processing webhook payload: " + testPayload); // Here you would parse the JSON and handle the webhook event System.out.println(" Webhook processed successfully!"); System.out.println(); System.out.println("🔒 Security verification complete! Your webhook signature verification is working correctly."); } } catch (Exception e) { System.err.println("❌ Test failed with error: " + e.getMessage()); e.printStackTrace(); } } } ``` ***
## Security Best Practices Following these practices will help ensure your webhook implementation is secure and robust: * **Always Verify Signatures** - Never process webhook data without first verifying the signature. This prevents attackers from sending fake webhooks to your endpoint. * **Use Timing-Safe Comparison** - When comparing signatures, use functions like `hmac.compare_digest()` in Python or `crypto.timingSafeEqual()` in Node.js to prevent timing attacks. * **Check Timestamp Freshness** - Consider rejecting webhooks with timestamps that are too old (e.g., older than 5 minutes) to prevent replay attacks. * **Store Secrets Securely** - Store your webhook secrets in environment variables or a secure secret management system. Never hardcode them in your application. **Important**: The webhook secret is only returned when you [create a webhook](/docs/websets/api/webhooks/create-a-webhook) - make sure to save it securely as it cannot be retrieved later. * **Use HTTPS** - Always use HTTPS endpoints for your webhooks to ensure the data is encrypted in transit. * **Register the Final URL** - Webhook deliveries do not follow HTTP redirects (3xx responses). If your endpoint redirects, the delivery will be treated as a failure. Always register the URL that directly handles the payload. ***
## Troubleshooting ### Invalid Signature Errors If you're getting signature verification failures: 1. **Check the raw payload**: Make sure you're using the raw request body, not a parsed JSON object 2. **Verify the secret**: Ensure you're using the correct webhook secret from when the webhook was created 3. **Check header parsing**: Make sure you're correctly extracting the timestamp and signatures from the header 4. **Encoding issues**: Ensure consistent UTF-8 encoding throughout the verification process ### Testing Signatures Locally You can test your signature verification logic using the webhook secret and a sample payload: ```python Python theme={null} # Test with a known payload and signature test_payload = '{"type":"webset.created","data":{"id":"ws_test"}}' test_timestamp = "1234567890" test_secret = "your_webhook_secret" # Create test signature import hmac import hashlib signed_payload = f"{test_timestamp}.{test_payload}" test_signature = hmac.new( test_secret.encode('utf-8'), signed_payload.encode('utf-8'), hashlib.sha256 ).hexdigest() test_header = f"t={test_timestamp},v1={test_signature}" # Verify it works is_valid = verify_webhook_signature(test_payload, test_header, test_secret) print(f"Test signature valid: {is_valid}") # Should print True ``` ***
## What's Next? * Learn about [webhook events](/docs/websets/api/events/types) and their payloads * Set up [webhook retries and monitoring](/docs/websets/api/webhooks/attempts/list-webhook-attempts) * Explore [webhook management endpoints](/docs/websets/api/webhooks/create-a-webhook) # Cancel a running webset Source: https://exa.ai/docs/websets/api/websets/cancel-a-running-webset exa-spec.yaml POST /v0/websets/{id}/cancel Cancels all operations being performed on a Webset. Any enrichment or search will be stopped and the Webset will be marked as `idle`. # Create a webset Source: https://exa.ai/docs/websets/api/websets/create-a-webset exa-spec.yaml POST /v0/websets Creates a new Webset with optional search, import, and enrichment configurations. The Webset will automatically begin processing once created. You can specify an `externalId` to reference the Webset with your own identifiers for easier integration. # Delete a webset Source: https://exa.ai/docs/websets/api/websets/delete-a-webset exa-spec.yaml DELETE /v0/websets/{id} Deletes a Webset. Once deleted, the Webset and all its Items will no longer be available. # Cancel a running enrichment Source: https://exa.ai/docs/websets/api/websets/enrichments/cancel-a-running-enrichment exa-spec.yaml POST /v0/websets/{webset}/enrichments/{id}/cancel All running enrichments will be canceled. You can not resume an Enrichment after it has been canceled. # Create an enrichment Source: https://exa.ai/docs/websets/api/websets/enrichments/create-an-enrichment exa-spec.yaml POST /v0/websets/{webset}/enrichments Create an Enrichment for a Webset. # Delete an enrichment Source: https://exa.ai/docs/websets/api/websets/enrichments/delete-an-enrichment exa-spec.yaml DELETE /v0/websets/{webset}/enrichments/{id} When deleting an Enrichment, any running enrichments will be canceled and all existing `enrichment_result` generated by this Enrichment will no longer be available. # Get an enrichment Source: https://exa.ai/docs/websets/api/websets/enrichments/get-an-enrichment exa-spec.yaml GET /v0/websets/{webset}/enrichments/{id} Returns an Enrichment configured on a Webset, including its status, description, format, and options. # Update an enrichment Source: https://exa.ai/docs/websets/api/websets/enrichments/update-an-enrichment exa-spec.yaml PATCH /v0/websets/{webset}/enrichments/{id} Update an Enrichment configuration for a Webset. # Get a webset Source: https://exa.ai/docs/websets/api/websets/get-a-webset exa-spec.yaml GET /v0/websets/{id} Returns a Webset by `id` or `externalId`, including its status, searches, imports, enrichments, and monitors. Pass `expand=items` to include up to the first 100 items in the response. # Delete an item Source: https://exa.ai/docs/websets/api/websets/items/delete-an-item exa-spec.yaml DELETE /v0/websets/{webset}/items/{id} Deletes an Item from the Webset. This will cancel any enrichment process for it. # Get an item Source: https://exa.ai/docs/websets/api/websets/items/get-an-item exa-spec.yaml GET /v0/websets/{webset}/items/{id} Returns a Webset Item. # List all items for a webset Source: https://exa.ai/docs/websets/api/websets/items/list-all-items-for-a-webset exa-spec.yaml GET /v0/websets/{webset}/items Returns a list of Webset Items. You can paginate through the Items using the `cursor` parameter. # List all websets Source: https://exa.ai/docs/websets/api/websets/list-all-websets exa-spec.yaml GET /v0/websets Returns a list of Websets. You can paginate through the results using the `cursor` parameter. You can filter results using the `search` parameter to find Websets by ID, external ID, or title. # Preview a webset Source: https://exa.ai/docs/websets/api/websets/preview-a-webset exa-spec.yaml POST /v0/websets/preview Preview how a search query will be decomposed before creating a webset. This endpoint performs the same query analysis that happens during webset creation, allowing you to see the detected entity type, generated search criteria, and available enrichment columns in advance. Use this to help users understand how their search will be interpreted before committing to a full webset creation. # Cancel a running search Source: https://exa.ai/docs/websets/api/websets/searches/cancel-a-running-search exa-spec.yaml POST /v0/websets/{webset}/searches/{id}/cancel Cancels a currently running Search. You can cancel all searches at once by using the `websets/:webset/cancel` endpoint. # Create a search Source: https://exa.ai/docs/websets/api/websets/searches/create-a-search exa-spec.yaml POST /v0/websets/{webset}/searches Creates a new Search for the Webset. The default behavior is to reuse the previous Search results and evaluate them against the new criteria. # Get a search Source: https://exa.ai/docs/websets/api/websets/searches/get-a-search exa-spec.yaml GET /v0/websets/{webset}/searches/{id} Gets a Search by id # Update a webset Source: https://exa.ai/docs/websets/api/websets/update-a-webset exa-spec.yaml POST /v0/websets/{id} Updates the `title` or `metadata` of a Webset. Searches, imports, and enrichments are managed through their own endpoints. # Exclude Results Source: https://exa.ai/docs/websets/dashboard/exclude-results Avoid duplicate results in your new searches by excluding URLs from previous Websets or CSV files.
## Overview The Exclude Results feature ensures you don't get duplicate results when creating new searches. By specifying URLs to exclude based on previous Websets or uploaded CSV files, you can focus on discovering fresh, unique results that complement your existing data.
## How it works Exclude results options when creating a Webset 1. Begin creating a new Webset 2. Below the criteria in the sidepanel, click "Exclude" 3. Select from past Websets or upload a CSV with URLs to exclude. You can select multiple sources to exclude from. 4. Start your search, with only new results that don't match your exclusions The maximum number of results you can exclude is determined by your plan.
## When to use exclusions * Finding leads that aren't already in your CRM * Following up on previous searches with refined criteria * Excluding results you already know about # Get started Source: https://exa.ai/docs/websets/dashboard/get-started Welcome to the Websets Dashboard! Find anything you want on the web, no matter how complex.
## 1. Sign up Websets is now generally available at [https://websets.exa.ai/](https://websets.exa.ai/)! If you'd like to ask us about it, [book a call here](https://scheduler.default.com/exa/websets-intro/30min).
## 2. Get started Websets is very easy to use. Websets landing page 1. Describe what you want in plain English - make it as complicated as you'd like! Webset preview of generated criteria 2. Confirm your criteria and data category look good. 3. Confirm how many results you want, then start your search.
## 3. Inside your Webset In brief, Websets does the following: 1. Break down what you're asking for 2. Find promising data that might satisfy your ask 3. Verify all criteria using AI agents and finding parallel sources 4. Adjust search based on feedback you provide our agent If you're not satisfied with the initial results you see, refine the criteria in "Edit criteria" or inside the chat.
## 4. Interacting with your Webset Once the Webset is complete, you can interact with the components! Click on a result to see: Webset result detail with AI summary, matched criteria, and sources 1. Its AI-generated summary 2. The criteria it met to be included in the Webset 3. The sources that informed the matching (you can click through the sources here) You can manually delete results, to clean up your Webset before exporting it.
## 5. Add more result criteria and custom columns Adding an enrichment column 1. **Add enrichments:** You can create custom enrichment columns, asking for any information you want. Think contact information (email & phone number), revenue, employee count, sentiment analysis, summary of the paper, etc. Fill in: * The name of the column (e.g. 'Revenue') * The column type (e.g. 'Number') * Instructions for Websets to find the data (e.g. 'Find the annual revenue of the company') * Or click "fill in for me" for the instructions to be generated automatically by our agent
## 6. Share and export your Webset 1. Click export to download your Webset as a CSV file. 2. Click share to get a link for your Webset.
## 7. Search history If you click on the sidebar icon in the top left, you'll see your full history with all past Websets in the left panel. # Import from CSV Source: https://exa.ai/docs/websets/dashboard/import-from-csv Turn your existing CSV data into a Webset
## Overview The Import from CSV feature allows you to transform your existing CSV files containing URLs into fully-functional Websets. This is perfect when you already have a list of websites, companies, or resources that you want to enrich with additional data or apply search criteria to filter.
## How it works CSV import flow for creating a Webset 1. Click "Start from CSV" to select your CSV file 2. Select which column contains the URLs you want to analyze 3. Review how your data will be imported before proceeding 4. Your URLs are transformed into a Webset with enrichments and metadata
## CSV preparation Ensure your CSV file has a URL column * For People searches: URLs must be LinkedIn profile URLs (e.g., [https://linkedin.com/in/username](https://linkedin.com/in/username)) * For Company search: URLs must be company homepage URLs (e.g., [https://example.com](https://example.com)) * For other searches: use any type of URL If you do not have URLs, Websets will attempt to infer URLs based on the information in each CSV row and any extra info you provide. The maximum number of results you can import is determined by your plan. ## What happens next? Once imported, your CSV becomes a full Webset where you can: ### Enrich with custom columns Add any information you want about each URL: * Contact information (emails, phone numbers) * Company metrics (revenue, employee count) * Content analysis (sentiment, topics, summaries) * Custom data specific to your use case ### Apply search criteria Filter your imported URLs based on specific criteria: * Company stage or size * Industry or sector * Geographic location * Content type or topic # How to Use Imports Source: https://exa.ai/docs/websets/imports-and-search A step-by-step guide to importing URLs into Websets -- enriching your list, scoring it against criteria, discovering new matches, and combining all three. When you already have a list of URLs (companies, people, products, etc.), you can **import** them into a Webset. Depending on how you set up the Webset, your imported items can be enriched, evaluated against criteria, or combined with web discovery results. This guide walks through every configuration with exact API calls you can copy-paste. Just replace `$EXA_API_KEY` with your API key. ## Our Example: 5 IT Consulting Suppliers Throughout this guide, we'll use the same list of 5 companies as our import: | Company | URL | Notes | | ------------ | -------------------------------- | ---------------------------------------------------- | | Accenture | `https://www.accenture.com` | Global IT consulting, US HQ | | Infosys | `https://www.infosys.com` | IT services, large US presence | | Wipro | `https://www.wipro.com` | IT services, offices in US | | EPAM Systems | `https://www.epam.com` | Software engineering, US-listed | | Persol Group | `https://www.persol-group.co.jp` | Staffing company, Japan-focused, minimal US presence | We picked these because 4 of the 5 clearly match typical IT consulting criteria (US office, IT services). **Persol Group** is the outlier -- it's a Japanese staffing company with minimal US presence, so it should fail US-focused criteria. Our criteria for the examples below: 1. "The company has an office in the United States" 2. "The company provides IT consulting or staff augmentation services" *** ## Config 1: Import Only -- Enrich Without Filtering **Live example:** [View this webset on the dashboard](https://websets.exa.ai/websets/webset_01kmnrshyh3bdart13q1ehdtdj) **Use when:** You have a list of URLs and just want to enrich them. No scoring, no filtering -- every item is kept. ### API Calls ```bash theme={null} # Step 1: Create a CSV import with your supplier URLs curl -s -X POST "https://api.exa.ai/websets/v0/imports" \ -H "Authorization: Bearer $EXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "format": "csv", "count": 5, "size": 128, "entity": { "type": "company" }, "title": "IT Consulting Suppliers" }' # Response includes an `uploadUrl` and an import `id` # Step 2: Upload your CSV to the presigned URL from Step 1 curl -X PUT "" \ -H "Content-Type: text/csv" \ --data-binary @suppliers.csv # suppliers.csv contains: url\nhttps://www.accenture.com\nhttps://www.infosys.com\n... # Step 3: Create a Webset that uses this import (enrichments only, no search/criteria) # The import is automatically scheduled for processing when the Webset is created. curl -s -X POST "https://api.exa.ai/websets/v0/websets" \ -H "Authorization: Bearer $EXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "import": [ { "source": "import", "id": "" } ], "enrichments": [ { "description": "What services does this company provide?", "format": "text" }, { "description": "Number of employees", "format": "number" } ] }' ``` ### What We See in the Live Webset All **5 items** appear in the Webset. No filtering happens because there are no criteria. | Supplier | In Webset? | Source | Evaluations | Enrichments | Why? | | ------------ | ---------- | -------- | ----------- | ----------- | ----------------------------------------- | | Accenture | **Yes** | `import` | 0 | 2 | Imported, no criteria to evaluate against | | Infosys | **Yes** | `import` | 0 | 2 | Imported, no criteria to evaluate against | | Wipro | **Yes** | `import` | 0 | 2 | Imported, no criteria to evaluate against | | EPAM Systems | **Yes** | `import` | 0 | 2 | Imported, no criteria to evaluate against | | Persol Group | **Yes** | `import` | 0 | 2 | Imported, no criteria to evaluate against | Every item has `source: "import"` and `evaluations: []`. All 5 are kept and enriched regardless of whether they'd pass any criteria -- because there are no criteria in this config. Persol Group's URL (`persol-group.co.jp`) resolved to "PERSOL Vietnam Japan Desk" in the entity data -- the system still imports and enriches it, it just resolved to a regional subsidiary page. *** ## Config 2: Search Only -- Web Discovery **Live example:** [View this webset on the dashboard](https://websets.exa.ai/websets/webset_01kmnrn5e1jr7gp22x8vk53wbz) **Use when:** You don't have a list -- you want to discover new companies from the web that match your criteria. ### API Call ```python Python theme={null} import os import requests response = requests.post( "https://api.exa.ai/websets/v0/websets", headers={"Authorization": f"Bearer {os.environ['EXA_API_KEY']}"}, json={ "search": { "query": "IT consulting and staff augmentation companies", "entity": {"type": "company"}, "criteria": [ {"description": "The company has an office in the United States"}, { "description": "The company provides IT consulting or staff augmentation services" }, ], "count": 25, }, "enrichments": [ { "description": "What services does this company provide?", "format": "text", }, {"description": "Number of employees", "format": "number"}, ], }, ) response.raise_for_status() webset = response.json() ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.exa.ai/websets/v0/websets", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.EXA_API_KEY}` }, body: JSON.stringify({ search: { query: "IT consulting and staff augmentation companies", entity: { type: "company" }, criteria: [ { description: "The company has an office in the United States" }, { description: "The company provides IT consulting or staff augmentation services" } ], count: 25 }, enrichments: [ { description: "What services does this company provide?", format: "text" }, { description: "Number of employees", format: "number" } ] }) }); if (!response.ok) { throw new Error(`Webset creation failed: ${response.status}`); } const webset = await response.json(); ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/websets/v0/websets" \ -H "Authorization: Bearer $EXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "search": { "query": "IT consulting and staff augmentation companies", "entity": { "type": "company" }, "criteria": [ { "description": "The company has an office in the United States" }, { "description": "The company provides IT consulting or staff augmentation services" } ], "count": 25 }, "enrichments": [ { "description": "What services does this company provide?", "format": "text" }, { "description": "Number of employees", "format": "number" } ] }' ``` ### What We See in the Live Webset The system searched the web and found **35 companies** that pass both criteria. Every item has `source: "search"` with full evaluations explaining why it matched. | Our 5 Suppliers | In Webset? | Why? | | ---------------------- | ---------- | ----------------------------------------------------------------------- | | Accenture | **Yes** | The web search independently discovered Accenture as a matching company | | Infosys | **No** | Not discovered by this particular web search | | Wipro | **No** | Not discovered by this particular web search | | EPAM Systems | **No** | Not discovered by this particular web search | | Persol Group | **No** | Not discovered by this particular web search | | *(34 other companies)* | **Yes** | Found by web search, passed both criteria | The web search happened to find Accenture among its 35 results -- but the other 4 suppliers were not discovered. This is expected: search-only websets only return what the web crawl finds, not a predetermined list. Examples of other discovered companies: Artech, TurnKey Staffing, DataArt, Insight Global, and others. *** ## Config 3: Scoped Search -- Score Your List Against Criteria **Live example:** [View this webset on the dashboard](https://websets.exa.ai/websets/webset_01kmnrsnkmksyb5e5d31e6bw5w) **Use when:** You have a supplier list and want to **evaluate each one against criteria**. Only the ones that pass are returned. This is the "score my list" use case. ### API Calls ```python Python theme={null} import os import requests # Create a CSV import and upload it as shown in Config 1, then use its ID here. response = requests.post( "https://api.exa.ai/websets/v0/websets", headers={"Authorization": f"Bearer {os.environ['EXA_API_KEY']}"}, json={ "search": { "query": "IT consulting and staff augmentation companies", "entity": {"type": "company"}, "criteria": [ {"description": "The company has an office in the United States"}, { "description": "The company provides IT consulting or staff augmentation services" }, ], "count": 25, "scope": [ {"source": "import", "id": ""}, ], }, "enrichments": [ { "description": "What services does this company provide?", "format": "text", }, {"description": "Number of employees", "format": "number"}, ], }, ) response.raise_for_status() webset = response.json() ``` ```javascript JavaScript theme={null} // Create a CSV import and upload it as shown in Config 1, then use its ID here. const response = await fetch("https://api.exa.ai/websets/v0/websets", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.EXA_API_KEY}` }, body: JSON.stringify({ search: { query: "IT consulting and staff augmentation companies", entity: { type: "company" }, criteria: [ { description: "The company has an office in the United States" }, { description: "The company provides IT consulting or staff augmentation services" } ], count: 25, scope: [ { source: "import", id: "" } ] }, enrichments: [ { description: "What services does this company provide?", format: "text" }, { description: "Number of employees", format: "number" } ] }) }); if (!response.ok) { throw new Error(`Webset creation failed: ${response.status}`); } const webset = await response.json(); ``` ```bash cURL theme={null} # Step 1: Create a CSV import and upload it (same as Config 1, Steps 1-2) # ... (see Config 1 for the full import flow) # You'll get back an # Step 2: Create a Webset with a scoped search -- evaluates each imported URL against criteria # The import is automatically scheduled for processing when the Webset is created. curl -s -X POST "https://api.exa.ai/websets/v0/websets" \ -H "Authorization: Bearer $EXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "search": { "query": "IT consulting and staff augmentation companies", "entity": { "type": "company" }, "criteria": [ { "description": "The company has an office in the United States" }, { "description": "The company provides IT consulting or staff augmentation services" } ], "count": 25, "scope": [ { "source": "import", "id": "" } ] }, "enrichments": [ { "description": "What services does this company provide?", "format": "text" }, { "description": "Number of employees", "format": "number" } ] }' ``` ### What We See in the Live Webset The webset contains **4 items**. Each of our 5 suppliers was evaluated against the criteria -- only the ones that passed both criteria appear. | Supplier | In Webset? | Source | Has Evaluations? | Why? | | ------------ | ----------------- | -------- | ---------------- | ---------------------------------------------------------------------- | | Accenture | **Yes** | `search` | Yes (2) | Passed: has US office, provides IT consulting | | Infosys | **Yes** | `search` | Yes (2) | Passed: has US office, provides IT services | | Wipro | **Yes** | `search` | Yes (2) | Passed: has US office, provides IT services | | EPAM Systems | **Yes** | `search` | Yes (2) | Passed: US-listed, provides software engineering services | | Persol Group | **No -- dropped** | -- | -- | Failed "has an office in the United States" -- primarily Japan-focused | We imported 5 suppliers but only 4 appear in the results. **Persol Group was evaluated and didn't pass**, so it's filtered out. Every visible item has `source: "search"` with full `evaluations` showing the reasoning for each criterion. Items that fail criteria are **dropped from the results**. If you need to keep all items and just see which ones pass/fail, use Config 1 (import only, no filtering) as a separate webset alongside Config 3. *** ## Config 4: Scoped Search + Web Discovery -- Score Your List AND Find New Matches **Live example:** [View this webset on the dashboard](https://websets.exa.ai/websets/webset_01kmpbj5wjcsh1yqn2cfhx2v7h) **Use when:** You have a supplier list you want to score against criteria, but you also want to discover additional companies from the web that match the same criteria. This is a two-step process: first create a webset with a scoped search, then add a regular web search to the same webset. ### API Calls ```python Python theme={null} import os import requests # Create a CSV import and upload it as shown in Config 1, then use its ID here. headers = {"Authorization": f"Bearer {os.environ['EXA_API_KEY']}"} webset_response = requests.post( "https://api.exa.ai/websets/v0/websets", headers=headers, json={ "search": { "query": "IT consulting and staff augmentation companies", "entity": {"type": "company"}, "criteria": [ {"description": "The company has an office in the United States"}, { "description": "The company provides IT consulting or staff augmentation services" }, ], "count": 25, "scope": [ {"source": "import", "id": ""}, ], }, "enrichments": [ { "description": "What services does this company provide?", "format": "text", }, {"description": "Number of employees", "format": "number"}, ], }, ) webset_response.raise_for_status() webset_id = webset_response.json()["id"] search_response = requests.post( f"https://api.exa.ai/websets/v0/websets/{webset_id}/searches", headers=headers, json={ "query": "IT consulting and staff augmentation companies", "entity": {"type": "company"}, "criteria": [ {"description": "The company has an office in the United States"}, { "description": "The company provides IT consulting or staff augmentation services" }, ], "count": 25, "behavior": "append", }, ) search_response.raise_for_status() ``` ```javascript JavaScript theme={null} // Create a CSV import and upload it as shown in Config 1, then use its ID here. const headers = { "Content-Type": "application/json", Authorization: `Bearer ${process.env.EXA_API_KEY}` }; const websetResponse = await fetch( "https://api.exa.ai/websets/v0/websets", { method: "POST", headers, body: JSON.stringify({ search: { query: "IT consulting and staff augmentation companies", entity: { type: "company" }, criteria: [ { description: "The company has an office in the United States" }, { description: "The company provides IT consulting or staff augmentation services" } ], count: 25, scope: [ { source: "import", id: "" } ] }, enrichments: [ { description: "What services does this company provide?", format: "text" }, { description: "Number of employees", format: "number" } ] }) } ); if (!websetResponse.ok) { throw new Error(`Webset creation failed: ${websetResponse.status}`); } const webset = await websetResponse.json(); const searchResponse = await fetch( `https://api.exa.ai/websets/v0/websets/${webset.id}/searches`, { method: "POST", headers, body: JSON.stringify({ query: "IT consulting and staff augmentation companies", entity: { type: "company" }, criteria: [ { description: "The company has an office in the United States" }, { description: "The company provides IT consulting or staff augmentation services" } ], count: 25, behavior: "append" }) } ); if (!searchResponse.ok) { throw new Error(`Search creation failed: ${searchResponse.status}`); } ``` ```bash cURL theme={null} # Step 1: Create a CSV import and upload it (same as Config 1, Steps 1-2) # ... (see Config 1 for the full import flow) # You'll get back an # Step 2: Create a Webset with a scoped search -- evaluates each imported URL against criteria # The import is automatically scheduled for processing when the Webset is created. curl -s -X POST "https://api.exa.ai/websets/v0/websets" \ -H "Authorization: Bearer $EXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "search": { "query": "IT consulting and staff augmentation companies", "entity": { "type": "company" }, "criteria": [ { "description": "The company has an office in the United States" }, { "description": "The company provides IT consulting or staff augmentation services" } ], "count": 25, "scope": [ { "source": "import", "id": "" } ] }, "enrichments": [ { "description": "What services does this company provide?", "format": "text" }, { "description": "Number of employees", "format": "number" } ] }' # Response includes a webset `id` -- save it as # Step 3: Wait for the scoped search to complete, then add a web search to discover new matches curl -s -X POST "https://api.exa.ai/websets/v0/websets//searches" \ -H "Authorization: Bearer $EXA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "IT consulting and staff augmentation companies", "entity": { "type": "company" }, "criteria": [ { "description": "The company has an office in the United States" }, { "description": "The company provides IT consulting or staff augmentation services" } ], "count": 25, "behavior": "append" }' ``` ### What We See in the Live Webset The webset contains **29 items** -- 4 from our imported suppliers (scored and passed) plus 25 web-discovered companies. Both sets are evaluated against criteria. | Supplier | In Webset? | Source | Has Evaluations? | Why? | | ------------------------------- | ----------------- | -------- | ---------------- | -------------------------------------------------------------- | | Accenture | **Yes** | `search` | Yes (2) | Passed scoped search: has US office, provides IT consulting | | Infosys | **Yes** | `search` | Yes (2) | Passed scoped search: has US office, provides IT services | | Wipro | **Yes** | `search` | Yes (2) | Passed scoped search: has US office, provides IT services | | EPAM Systems | **Yes** | `search` | Yes (2) | Passed scoped search: US-listed, provides software engineering | | Persol Group | **No -- dropped** | -- | -- | Failed scoped search: no US office | | *(25 web-discovered companies)* | **Yes** | `search` | Yes (2 each) | Found by web search, passed both criteria | The scoped search evaluates your imported list against criteria (dropping Persol Group), and the appended web search discovers 25 additional companies. The result is a single webset with both your scored imports and new web discoveries. The web search uses `"behavior": "append"` so it adds to the existing results rather than replacing them. If the web search discovers a company that was already in the scoped search results (e.g., Accenture), the duplicate is automatically handled. *** ## Quick Reference | Configuration | What it does | All items kept? | Items get scored? | | ------------------------------------ | -------------------------------------- | --------------------------------- | ---------------------------------------------- | | **1. Import Only** | Enrich your list | Yes -- all kept | No | | **2. Search Only** | Discover new matches from the web | N/A (no imports) | Yes -- only passing items returned | | **3. Scoped Search** | Score your list against criteria | No -- failures are dropped | Yes | | **4. Scoped Search + Web Discovery** | Score your list + discover new matches | No -- import failures are dropped | Yes -- both imports and discoveries are scored | ## Which Config Should I Use? * **"I just want to enrich my list, no filtering"** -- Config 1 * **"I don't have a list, find me companies"** -- Config 2 * **"Score my list, drop the ones that don't match"** -- Config 3 * **"Score my list AND find new companies that match"** -- Config 4 # Websets Source: https://exa.ai/docs/websets/quickstart Build verified, enriched datasets from the web. ## What Are Websets? A Webset starts with a natural-language query and a target item count. Add criteria that every result must satisfy and enrichment fields to populate for each accepted item. Results arrive asynchronously through the dashboard, API, or webhooks. You can also build websets visually in the [Dashboard](/docs/websets/dashboard/get-started), no code required. Starting a new list-building or enrichment workflow? Use [Exa Agent](/docs/agent/quickstart). Use this guide to maintain or extend an existing Websets integration. The Websets API requires a paid Websets plan; Search API credits and Websets credits are separate. ## How It Works 1. **Define a search:** Provide a natural-language query, a result count, and optional verification criteria and enrichments. 2. **Search and verify:** Websets finds candidates and checks each one against your criteria. Only matching results become items. 3. **Run enrichments:** For each verified item, Websets searches for the additional data you requested, such as a CEO name, funding amount, or contact information. 4. **Receive results:** Poll for status, use webhooks for updates, or check the dashboard as items arrive. ## Key Capabilities | Feature | What It Does | | ------------------------- | --------------------------------------------------------------------------------- | | **Criteria verification** | Each result is checked against rules you define, so you only get relevant matches | | **Enrichments** | Extract specific data points (text, numbers, dates, booleans) for every result | | **Monitors** | Schedule recurring searches to keep your webset updated automatically | | **Webhooks** | Get real-time HTTP callbacks as items are added or enriched | | **Imports** | Bring your own URLs and run enrichments on them | ## Human Quickstart Create a key in the dashboard. New accounts start with free credits. Install the SDK: ```bash Python theme={null} pip install exa-py ``` ```bash JavaScript theme={null} npm install exa-js ``` Then make your first request: ```python Python theme={null} from exa_py import Exa from exa_py.websets.types import CreateWebsetParameters, CreateEnrichmentParameters import os exa = Exa(api_key=os.getenv("EXA_API_KEY")) webset = exa.websets.create( params=CreateWebsetParameters( search={ "query": "Top AI research labs focusing on large language models", "count": 5 }, enrichments=[ CreateEnrichmentParameters( description="LinkedIn profile of VP of Engineering or related role", format="text", ), ], ) ) print(f"Webset created with ID: {webset.id}") print(f"View your Webset at: {webset.dashboard_url}") # Wait until Webset completes processing webset = exa.websets.wait_until_idle(webset.id) # Retrieve Webset Items items = exa.websets.items.list(webset_id=webset.id) for item in items.data: print(f"Item: {item.model_dump_json(indent=2)}") ``` ```javascript JavaScript theme={null} import Exa from "exa-js"; const exa = new Exa(process.env.EXA_API_KEY); const webset = await exa.websets.create({ search: { query: "Top AI research labs focusing on large language models", count: 10 }, enrichments: [ { description: "Estimate the company's founding year", format: "number" } ], }); console.log(`Webset created with ID: ${webset.id}`); console.log(`View your Webset at: ${webset.dashboardUrl}`); const idleWebset = await exa.websets.waitUntilIdle(webset.id, { timeout: 60000, pollInterval: 2000, onPoll: (status) => console.log(`Current status: ${status}...`) }); const items = await exa.websets.items.list(webset.id, { limit: 10 }); for (const item of items.data) { console.log(`Item: ${JSON.stringify(item, null, 2)}`); } ``` ```bash cURL theme={null} curl -s -X POST "https://api.exa.ai/websets/v0/websets/" \ -H "accept: application/json" \ -H "content-type: application/json" \ -H "Authorization: Bearer ${EXA_API_KEY}" \ -d '{ "search": { "query": "Top AI research labs focusing on large language models", "count": 5 }, "enrichments": [ {"description": "Find the company'\''s founding year", "format": "number"} ] }' ``` See [Zero Data Retention](/docs/admin/security/zero-data-retention) for product availability. ## Next * [**Dashboard Guide**](./dashboard/get-started) - Step-by-step guide to using Websets in the dashboard * [**How It Works**](./api/how-it-works) - Deep dive into the event-driven architecture * [**Websets API Reference**](./api/websets/create-a-webset) - Full API reference for all endpoints