# typesearch
> Search API for AI agents: our own continuously refreshed news index, a calibrated relevance score on every
> result, structured answers per result, and drop-in compatibility with the Exa, Tako and Perplexity SDKs.
> Base URL: https://api.typesearch.ai · OpenAPI: /openapi.json · Full docs as one file: /llms-full.txt
> Agents calling the API: start with /.well-known/skill.md (which endpoint to use, what it costs, a request for each).
## Products
- [Search API](https://typesearch.ai/products/search): Ranked, verified results from our own index. From $1.00 / 1k requests.
- [Contents API](https://typesearch.ai/products/contents): Metadata and a short verbatim excerpt for any URL. $0.20 / 1k pages.
- [Similar API](https://typesearch.ai/products/similar): Everything written about the same story. $2.20 / 1k requests.
- [Live site search](https://typesearch.ai/products/site-search): Search any site, right now. $2.40 / 1k requests.
- [Custom index](https://typesearch.ai/products/custom-index): Your agent searches the pages you choose. $2.00 / 1k URLs a month.
- [Charts API](https://typesearch.ai/products/charts): Ask in plain words, get the right chart. From $1.50 / 1k charts.
- [Monitors](https://typesearch.ai/products/monitors): Webhooks when your topic moves. $2.80 / 1k requests (planned, coming soon).
- [News API for AI agents](https://typesearch.ai/news-api): real-time news search built for agents and LLMs: coverage in 130+ countries and 30+ languages, an example request and prices per mode. Spanish: https://typesearch.ai/es/api-de-noticias
- [Argentina news API](https://typesearch.ai/news-api/argentina): news from outlets based in Argentina that publish in Spanish. Filter with countries: ["AR"], languages: ["es"]. Spanish: https://typesearch.ai/es/api-de-noticias/argentina
- [Brazil news API](https://typesearch.ai/news-api/brazil): news from outlets based in Brazil that publish in Portuguese. Filter with countries: ["BR"], languages: ["pt"]. Spanish: https://typesearch.ai/es/api-de-noticias/brasil
- [Spain news API](https://typesearch.ai/news-api/spain): news from outlets based in Spain that publish in Spanish. Filter with countries: ["ES"], languages: ["es"]. Spanish: https://typesearch.ai/es/api-de-noticias/espana
## Integrations
- [All integrations](https://typesearch.ai/integrations): what works today with each framework and SDK, and how. Spanish: https://typesearch.ai/es/integraciones
- MCP server: https://api.typesearch.ai/mcp (Streamable HTTP, stateless; key in Authorization: Bearer or x-api-key). Tools: search_news, get_contents, find_similar, create_chart; search_news takes an optional index (idx_…) to search a custom index instead of the news index, and create_chart turns a question into a chart card (image and embed URLs, with sources). Claude Code: `claude mcp add --transport http typesearch https://api.typesearch.ai/mcp --header "Authorization: Bearer $TYPESEARCH_API_KEY"`
- [MCP server](https://typesearch.ai/mcp): News search as tools for Claude, Cursor, VS Code, Codex and any MCP client. Setup: Remote server · one click.
- [LangChain](https://typesearch.ai/docs/integrations/langchain): A news search tool and retriever for LangChain agents, in Python and JavaScript. Setup: langchain-exa, pointed at typesearch.
- [Vercel AI SDK](https://typesearch.ai/docs/integrations/vercel-ai-sdk): A news search tool for generateText, streamText and agents. Setup: A tool() over the HTTP API.
- [LlamaIndex](https://typesearch.ai/docs/integrations/llamaindex): News search as a tool for LlamaIndex agents and workflows. Setup: A FunctionTool over the HTTP API.
- [n8n](https://typesearch.ai/docs/integrations/n8n): News in your workflows, and as a tool for the AI Agent node. Setup: The HTTP Request node.
- [Exa SDKs](https://typesearch.ai/docs/integrations/exa-compatible): exa-py, exa-js and the tools built on them that let you set the base URL, such as langchain-exa. Setup: Change the base URL and the key.
- [Tako SDK](https://typesearch.ai/docs/migration/tako): The Tako SDK, Python and JavaScript, against our compatibility layer. Setup: Change the host and the key.
- [Perplexity SDK](https://typesearch.ai/docs/migration/perplexity): The Perplexity Search API SDK, Python and JavaScript, against our compatibility layer. Setup: Change the base URL and the key.
- [HTTP API and OpenAPI](https://typesearch.ai/docs/api-reference): Plain JSON over HTTPS from any language, described by an OpenAPI 3.1 document you can feed to any client generator. Setup: OpenAPI 3.1 · JSON · SSE.
## Open source
- [Open-source apps and SDKs](https://typesearch.ai/open-source): MIT-licensed apps to clone or deploy in one click, and the SDKs, MCP server and plugins. GitHub: https://github.com/typesearch-ai. Spanish: https://typesearch.ai/es/open-source
- [Fact check](https://github.com/typesearch-ai/fact-check): Paste a text: each checkable claim is searched in recent coverage and marked supported, contradicted or unverified, with the quote that decides it and a link to the article. Next.js and the Vercel AI SDK. Use case: https://typesearch.ai/use-cases/fact-checking
- [News briefing](https://github.com/typesearch-ai/news-briefing): Pick a country, a language and a topic: typesearch finds the day’s articles and the model writes the briefing, with every sentence linked to the article it comes from. Next.js and the Vercel AI SDK. Use case: https://typesearch.ai/use-cases/briefings
## Changelog
- [Changelog](https://typesearch.ai/changelog): new products, endpoints and behavior in the API, newest first. RSS: https://typesearch.ai/changelog/rss.xml. Spanish: https://typesearch.ai/es/novedades
## Pricing and comparisons
- [Pricing](https://typesearch.ai/pricing): pay per request, no seats or minimums. Spanish: https://typesearch.ai/es/precios
- Search, USD per 1,000 requests, by mode: ultra $1.00, fast $1.40, normal $2.20 (reads the top 4), deep $5.60 (reads 8, with highlights). Each query in a multi-query request is one search of its mode.
- Similar $2.20 per 1,000 requests ($4.60 in deep mode, which reads 8); live site search $2.40; contents $0.20 per 1,000 pages, $0.40 with a query. Cached repeats and failed requests are free.
- Custom index: $2.00 per 1,000 URLs a month, billed daily and prorated from the same credit; every URL is re-read daily, sitemaps and feeds are checked hourly. Up to 100 URLs (3 indexes) pay as you go, 10,000 (50 indexes) with the monthly credit plan, unlimited on Enterprise. Searches over it (POST /v1/search with index: "idx_…") cost the same as the mode.
- Charts (POST /v1/charts, and create_chart on the MCP server): ask in plain words and get a chart card that picks the chart type (12: line, area, bar, bar_horizontal, stacked_bar, pie, donut, scatter, funnel, timeline, kpi, table), the key figures, a title that states the finding and the source of every value; or send your own data. From a query it first works out what you expect to see (returned as interpretation, with other readings to ask for) and gets that data, checked against the sources: race standings, wins in a season, poll or election results, what an outlet published, coverage, share of voice, tone, outlets, timelines or published figures. If the exact data is not published it shows the closest thing and says how close (rung), and a stored chart keeps improving until the data arrives; stream: true sends step, result and improved events. Four themes (light, dark, editorial, electric), an embeddable iframe, a PNG and an SVG. From a query: the search it needs (its mode, per query) plus $0.50 per 1,000 charts, so $1.50 per 1,000 with ultra and $1.90 with fast. From your own data: $0.30 per 1,000. Embeds, images, type or theme changes (PATCH) and repeats within 10 minutes are free; failed charts are not billed. Tako's Search, which returns chart cards, lists at $7 per 1,000.
- New accounts get $5 in free credit, valid for 30 days.
- Against Exa: an ultra search (headlines only) lists at $1.00 per 1,000 and a fast one at $1.40, against $7 for a search on Exa. Normal and deep read pages before ranking and cost more, so the gap in those modes is smaller. Some search APIs (Perplexity, Parallel) list their most basic search at the price of our ultra mode and for less than our fast mode. Every comparison, mode by mode and with sources, is on the compare pages.
- [Alternatives](https://typesearch.ai/alternatives): Exa, Tavily, Perplexity, Tako and Bing News compared side by side.
- [Google News API](https://typesearch.ai/google-news-api): there is no official Google News API; the real options for getting news into an app or an agent, compared.
- [typesearch vs Exa](https://typesearch.ai/compare/exa): the Exa alternative that works with the Exa SDK: change the base URL and the key. Prices for comparable work, mode by mode, on the page.
- [typesearch vs Tako](https://typesearch.ai/compare/tako): the Tako alternative that works with the Tako SDK: change the base URL and the key. Prices for comparable work, mode by mode, on the page.
- [typesearch vs Perplexity](https://typesearch.ai/compare/perplexity): the Perplexity alternative that works with the Perplexity SDK: change the base URL and the key. Prices for comparable work, mode by mode, on the page.
- [typesearch vs Tavily](https://typesearch.ai/compare/tavily): the Tavily alternative for news search: one request to rewrite. Prices mode by mode on the page.
- [typesearch vs Bing News Search API](https://typesearch.ai/compare/bing-news-search): a replacement for the Bing News Search API, retired by Microsoft on August 11, 2025.
## Compared with news APIs
- [typesearch vs NewsAPI.org](https://typesearch.ai/compare/newsapi-org): A news API with a 5-year archive. Plans, limits and news features compared, each with its source and date (checked 2026-09-25).
- [typesearch vs GNews](https://typesearch.ai/compare/gnews): A low-cost news API with full text. Plans, limits and news features compared, each with its source and date (checked 2026-09-25).
- [typesearch vs NewsData.io](https://typesearch.ai/compare/newsdata): A news API with a 10-year archive and AI tags. Plans, limits and news features compared, each with its source and date (checked 2026-09-25).
- [typesearch vs NewsCatcher](https://typesearch.ai/compare/newscatcher): An enterprise news API with rich NLP. Plans, limits and news features compared, each with its source and date (checked 2026-09-25).
- [typesearch vs Event Registry](https://typesearch.ai/compare/event-registry): A news API with events, concepts and a 2014+ archive. Plans, limits and news features compared, each with its source and date (checked 2026-09-25).
- [typesearch vs mediastack](https://typesearch.ai/compare/mediastack): A simple, low-cost news API. Plans, limits and news features compared, each with its source and date (checked 2026-09-25).
- [typesearch vs Perigon](https://typesearch.ai/compare/perigon): A news context platform with rich enrichment. Plans, limits and news features compared, each with its source and date (checked 2026-09-25).
- [typesearch vs APITube](https://typesearch.ai/compare/apitube): A low-cost news API with many filters. Plans, limits and news features compared, each with its source and date (checked 2026-09-25).
- [typesearch vs SerpApi](https://typesearch.ai/compare/serpapi): Google News results, scraped. Plans, limits and news features compared, each with its source and date (checked 2026-09-25).
- [typesearch vs Brave Search API](https://typesearch.ai/compare/brave): A web search API with a news endpoint. Plans, limits and news features compared, each with its source and date (checked 2026-09-25).
- [typesearch vs Linkup](https://typesearch.ai/compare/linkup): A web search API for AI, with sourced answers. Plans, limits and news features compared, each with its source and date (checked 2026-09-25).
- [typesearch vs Parallel](https://typesearch.ai/compare/parallel): A web search API for agents, with monitors. Plans, limits and news features compared, each with its source and date (checked 2026-09-25).
## Lists of alternatives
- [Best Exa alternatives for news search in 2026](https://typesearch.ai/alternatives/exa)
- [Best Tavily alternatives for news search in 2026](https://typesearch.ai/alternatives/tavily)
- [Best Perplexity Search API alternatives for news in 2026](https://typesearch.ai/alternatives/perplexity)
- [Best NewsAPI.org alternatives in 2026](https://typesearch.ai/alternatives/newsapi-org)
## Use cases
- [News API use cases for AI agents](https://typesearch.ai/use-cases): six jobs done with news search, each with the mode, the request and what it costs. Spanish: https://typesearch.ai/es/casos-de-uso
- [Media monitoring](https://typesearch.ai/use-cases/media-monitoring): Brands, people, competitors and topics, checked on a schedule — a Google Alerts alternative for developers.
- [Financial news](https://typesearch.ai/use-cases/financial-news): Company, ticker and macro news for trading and research agents, with typed answers on every article.
- [Risk and compliance](https://typesearch.ai/use-cases/risk-compliance): Adverse media screening, supplier risk and regulatory watch, with verbatim evidence for every flag.
- [Daily briefings](https://typesearch.ai/use-cases/briefings): The day’s coverage per topic, deduplicated, with verbatim quotes your model can cite.
- [Fact-checking](https://typesearch.ai/use-cases/fact-checking): Check a recent claim against what was actually reported, with the quote that supports or contradicts it.
- [Local and multilingual news](https://typesearch.ai/use-cases/multilingual-news): Local press in the language it was written in, starting with Latin America, filtered by country and language.
## For publishers
- [For publishers](https://typesearch.ai/publishers): what typesearch takes from news sites, what it shows and stores, the opt-out signals it honors, and a form to opt out, remove pages or talk about licensing. Spanish: https://typesearch.ai/es/medios · Portuguese: https://typesearch.ai/pt/imprensa
- [Our crawler](https://typesearch.ai/bot): TypesearchBot and Typesearch-User, their user agents, robots.txt rules and IP addresses.
# For AI agents (/docs/agents)
typesearch publishes machine-readable guides so an agent — or the assistant writing your code — can learn the
API without a human in between. All of them are generated from the same source as the API and this site, so
prices, modes and endpoints stay current.
| File | What it is |
| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| [`/.well-known/skill.md`](/.well-known/skill.md) | The API guide for agents: what typesearch covers, which endpoint to use, what it costs, a request for every endpoint, errors and limits. |
| [`/.well-known/skills/index.json`](/.well-known/skills/index.json) | The same guide as an [Agent Skill](https://agentskills.io), for agents that discover and install skills. |
| [`/llms.txt`](/llms.txt) | An index of the site and the docs ([llmstxt.org](https://llmstxt.org)). |
| [`/llms-full.txt`](/llms-full.txt) | All the docs as one Markdown file. |
| [`/openapi.json`](/openapi.json) | Every request and response field, with types and defaults. The authority on details. |
Every page of these docs is also available as Markdown: **Copy Markdown** at the top of the page, or open it
straight in ChatGPT, Claude or Cursor from the menu next to it.
## Give it to your agent [#give-it-to-your-agent]
* **Coding assistants** (Claude Code, Cursor, Codex…): point them at `https://typesearch.ai/.well-known/skill.md`
before they write code against the API.
* **Agents that call the API at runtime**: add the skill to the agent's instructions, and the key as
`TYPESEARCH_API_KEY` ([Authentication](/docs/authentication)).
* **MCP clients**: connect the [MCP server](/docs/integrations/mcp) instead — the tools describe themselves.
## Rules we ask agents to follow [#rules-we-ask-agents-to-follow]
* Cite the link of every fact taken from a result.
* Quote only the excerpts the API returned, verbatim and short. Never reconstruct article text.
* Treat results with a `score` between 0.35 and 0.65 as leads, not facts.
* Start with `fast`, and say so when escalating to a more expensive mode.
# Authentication (/docs/authentication)
## Send your key [#send-your-key]
Send your key as a bearer token, or in `x-api-key` — whichever your HTTP client or SDK already uses:
```http
Authorization: Bearer ts_live_…
```
```http
x-api-key: ts_live_…
```
Both are accepted on every endpoint, including the [compatibility layers](/docs/migration/exa), so the
official Exa, Tako and Perplexity SDKs work without changes.
## Keep keys on the server [#keep-keys-on-the-server]
A key authorises spending on your account. Call typesearch from your backend or your agent’s runtime,
never from a browser or a mobile app, and keep keys out of version control.
We store a hash of each key, never the key itself: it is shown once, when you create it. If a key leaks,
revoke it and create a new one.
## Errors [#errors]
| Status | `code` | What it means |
| ------ | ----------------- | --------------------------------------- |
| 401 | `missing_api_key` | Neither header was sent. |
| 401 | `invalid_api_key` | The key does not exist or is malformed. |
| 401 | `revoked_api_key` | The key was revoked. Create a new one. |
See [Errors](/docs/errors) for the error format and every other code.
# Errors (/docs/errors)
Errors use [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details, with
`Content-Type: application/problem+json`:
```json
{
"type": "urn:typesearch:error:invalid_request",
"title": "Bad request",
"status": 400,
"detail": "max_results: Too big: expected number to be <=50",
"code": "invalid_request",
"request_id": "req_8fKq2mZr1xYt",
"errors": [{ "path": "max_results", "message": "Too big: expected number to be <=50" }]
}
```
Use `code` in your code — it is stable. `title`, `detail` and `message` are for people and can change.
`title` and `detail` come in English; send `Accept-Language: es` to get them in Spanish. Every response,
successful or not, carries an `X-Request-Id` header: include it when you contact support.
## Codes [#codes]
| Status | `code` | What to do |
| ------ | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400 | `invalid_json`, `invalid_request` | Fix the request. `errors` lists each invalid field. Unknown fields are rejected too. |
| 400 | `invalid_url`, `invalid_site`, `site_not_found`, `unsupported` | Fix the URL or site. |
| 401 | `missing_api_key`, `invalid_api_key`, `revoked_api_key` | See [Authentication](/docs/authentication). |
| 402 | `budget_too_small` | Raise `max_tokens`. |
| 402 | `insufficient_credits` | No credit left on your account. Top up in the [dashboard](https://app.typesearch.ai/billing). |
| 402 | `spend_limit_reached` | The key reached its monthly spend limit. Raise it in the [dashboard](https://app.typesearch.ai/api-keys). |
| 403 | `robots_disallowed` | The site’s robots.txt disallows it, or it couldn’t be read (we retry after 10 minutes). |
| 403 | `source_unavailable` | The source isn’t available right now. We don’t say why, and it may be temporary. Try another source. |
| 404 | `job_not_found` | The job does not exist for this key, or it expired (jobs last a day). |
| 404 | `chart_not_found` | No chart with that id for your organization: it was deleted or it expired. See [Charts](/docs/guides/charts). |
| 422 | `insufficient_data` | A chart from a query found no articles about the topic in the period (with `index` or `questions`: not enough data for the chart). Not billed. Try more `days` or a broader query. |
| 429 | `rate_limited` | Retry after `Retry-After` seconds. |
| 429 | `quota_exceeded` | Daily token quota used. It resets at 00:00 UTC. |
| 499 | `cancelled` | The search stopped because the client closed the connection or aborted the stream. Nothing to fix; send it again if you still need it. |
| 500 | `internal_error` | Something failed on our side. Retry; if it keeps happening, contact support with the `request_id`. |
| 502 | `site_unreachable`, `invalid_redirect`, `bot_protection` | The site failed. Try again later or another site. |
| 503 | `upstream_unavailable`, `browser_unavailable`, `browser_busy` | Temporary. Retry with backoff; `browser_busy` says when in `Retry-After`. |
| 503 | `shutting_down` | The server is restarting. Retry after `Retry-After` seconds. |
| 503 | `spend_cap_reached` | The service reached its daily spend cap. `Retry-After` says when it resets (00:00 UTC). |
| 503 | `job_interrupted` | In a job’s `error` ([`GET /v1/jobs/{id}`](/docs/api-reference/jobs)): the server restarted before the job finished. Request it again. |
| 504 | `timeout`, `site_timeout` | Temporary. Retry, or use a lighter mode. |
In [`POST /v1/contents`](/docs/api-reference/contents) each URL has its own status: a page from a source that isn’t
available comes back with `source_unavailable` in `results[].error.code`, and the rest of the request
succeeds.
## Retries [#retries]
The SDKs retry connection errors, timeouts, `429 rate_limited` and `5xx` twice, with exponential backoff
and jitter, honouring `Retry-After`. They never retry `quota_exceeded`. Calling the API directly, do the
same.
## Warnings are not errors [#warnings-are-not-errors]
Things that don’t stop a request — a domain that is not in the index, a budget reached — come back in
the response’s `warnings` array, each with a `code` and a `message` (in English, or in Spanish with
`Accept-Language: es`).
# Introduction (/docs)
typesearch is a search API built for agents that can’t afford to guess. It searches **our own index** of
news and editorial content — refreshed every few minutes — and returns results your agent can act on:
each one with a **calibrated probability** of being relevant, the top ones **read and verified**, and
**typed answers** to your own questions when you ask for them.
It returns evidence, not prose. No generated summaries, nothing to hallucinate: ranked articles, verbatim
excerpts, and numbers you can threshold on.
## Endpoints [#endpoints]
| Endpoint | What it does |
| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| [`POST /v1/search`](/docs/api-reference/search) | Search the index. One query, or up to five judged together. JSON or a stream of events. |
| [`POST /v1/search/site`](/docs/api-reference/site-search) | Search a live site — its homepage, sections and its own search box. Returns a job. |
| [`GET /v1/jobs/{id}`](/docs/api-reference/jobs) | The status and result of a live site search. |
| [`POST /v1/similar`](/docs/api-reference/similar) | Articles about the same story as a URL. |
| [`POST /v1/contents`](/docs/api-reference/contents) | Metadata and short verbatim excerpts of up to 10 URLs. |
| [`POST /v1/charts`](/docs/api-reference/charts) | A chart card from a question in plain words or from your own data, with an embed, a PNG and an SVG. See [Charts](/docs/guides/charts). |
| [`GET /v1/usage`](/docs/api-reference/usage) | Usage and limits of your key. |
## How a search works [#how-a-search-works]
### Retrieve [#retrieve]
Candidates come from the index, filtered by the domains, sections and dates you set before anything is
judged — what you filter out costs nothing.
### Judge [#judge]
A relevance model scores every candidate headline against your query and returns a calibrated
probability. Results start streaming at this point, about a second in.
### Verify [#verify]
In `normal` and `deep` modes the best candidates are opened and read. A headline that promised more than
the article delivers drops out and is reported in `rejected`.
### Enrich [#enrich]
On request: tone relative to your query, same-story grouping, who published first, verbatim essentials
and answers to your typed questions — all in the same call.
## Coming from Exa, Tako or Perplexity? [#coming-from-exa-tako-or-perplexity]
The API speaks their SDKs. Change the base URL and the key, keep the rest of your code:
see [Migrate from Exa](/docs/migration/exa), [Tako](/docs/migration/tako) or
[Perplexity](/docs/migration/perplexity).
# Modes (/docs/modes)
Every search endpoint takes a `mode`. It sets how many candidates are judged and whether the best ones are
opened and read before they are ranked.
| | `ultra` | `fast` | `normal` | `deep` |
| ------------------------------------ | ------------- | ----------------------- | ----------------------- | ---------------------------------------------------------- |
| What each headline is judged on | headline | headline and standfirst | headline and standfirst | headline and standfirst |
| New headlines judged per search | up to 160 | up to 160 | up to 160 | up to 240, and up to 240 more for the topic in other words |
| Articles opened and read | — | — | up to 4 | up to 8 |
| `highlights` | not available | not available | optional | on by default |
| Excerpts per article, up to 25 words | — | — | 1 | 2 |
| `essential` | optional | optional | optional | on by default |
| Typical latency | about 1 s | about 1 s | 2–5 s | 10–15 s |
| Price per 1,000 searches | $1.00 | $1.40 | $2.20 | $5.60 |
`normal` is the default. Headlines already judged for your query don’t count against the limits above, and
aren’t paid again.
`ultra` is the cheapest mode. Against Exa’s list prices, checked September 25, 2026: `ultra` costs 7× less than
an Exa search ($7, which includes the text of up to 10 results) and `fast` 5× less, `normal`
3.1× less than that same search, and `deep` 2.1× less than an Exa deep search ($12–15). For
headline search alone, Perplexity’s Fast Search ($1 per 1,000 requests of up to 5 queries) and
Parallel’s fast search ($1) cost the same as `ultra` and less than `fast`. Every comparison, mode by
mode and with its source, is on the [pricing page](https://typesearch.ai/pricing).
## Choosing a mode [#choosing-a-mode]
* **`ultra`** judges the headline alone and is the cheapest. Use it for high-volume classification, alerts,
and anything where the headline says it all.
* **`fast`** is the best default for agents in a loop: quick, cheap, and good recall when the headline
names the topic.
* **`normal`** reads the top results before ranking them. Use it when a wrong result is expensive — a
headline that promises more than its article is caught and moved to `rejected`.
* **`deep`** widens the topic with synonyms and related terms, judges more headlines and reads more
articles, with highlights and essentials included. Use it for research, reports and questions where you
need everything.
## When the index falls short [#when-the-index-falls-short]
If the index returns fewer than 3 good results, typesearch looks for sources beyond it, within a time
budget: at most 0.8 s more in `ultra` and `fast` (the rest continues in the background, so the next search
benefits), 3 s in `normal` and 8 s in `deep`, which can also use the search box of the sites it proposes.
Sources are proposed by a model that understands the topic of your query and by an open index of
worldwide headlines built with data from the [GDELT Project](https://www.gdeltproject.org/). Articles are
always read on the original site, following its robots.txt, and judged by the same relevance model. No
third-party search engine is involved.
Results found this way come with `found_in: "discovery"`, and the response says what happened in
`discovery` (`used`, `background`, `skipped`, `budget` or `timeout`). If time ran out, `incomplete` is
`true`: repeating the search a little later usually brings more.
## Results arrive in two stages [#results-arrive-in-two-stages]
In every mode, results judged from the headline arrive first — about a second in — and each one is
confirmed in place as its article is read. With [streaming](/docs/guides/streaming) you get those
partial results as `partial` events; without it, you get the final result.
## What doesn’t cost twice [#what-doesnt-cost-twice]
* **Judgment memory.** What the model already said about a headline for a query is reused for a week, so a
repeated or similar search only pays for what is new.
* **Result cache.** An identical request within 10 minutes is served from cache: `usage.tokens` is `0`
and it is not billed.
See [Costs and caching](/docs/guides/cost-control) for hard caps per request.
# Quickstart (/docs/quickstart)
## Your first search [#your-first-search]
### Get an API key [#get-an-api-key]
Create a key in the [dashboard](https://app.typesearch.ai/login): new accounts get $5 of free
credit to try it, no card needed. Keys start with `ts_live_` and are shown once, so store yours
as an environment variable:
```bash
export TYPESEARCH_API_KEY="ts_live_…"
```
### Run your first search [#run-your-first-search]
It’s plain JSON over HTTPS, from any language: no SDK to install.
cURL
Python
JavaScript
```bash
curl https://api.typesearch.ai/v1/search \
-H "Authorization: Bearer $TYPESEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "query": "inflación en Argentina", "mode": "fast", "max_results": 5 }'
```
```python
import os, requests
res = requests.post(
"https://api.typesearch.ai/v1/search",
headers={"Authorization": f"Bearer {os.environ['TYPESEARCH_API_KEY']}"},
json={"query": "inflación en Argentina", "mode": "fast", "max_results": 5},
).json()
for r in res["results"]:
print(f"{r['score']:.2f}", r["title"], f"({r['source']})")
```
```ts
const res = await fetch('https://api.typesearch.ai/v1/search', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.TYPESEARCH_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: 'inflación en Argentina', mode: 'fast', max_results: 5 }),
}).then((r) => r.json());
for (const r of res.results) {
console.log(r.score.toFixed(2), r.title, `(${r.source})`);
}
```
### Read the response [#read-the-response]
Every result carries a `score`: the calibrated probability that it is about your query. Threshold on it
directly — `0.9` means relevant, and anything between `0.35` and `0.65` means the model is undecided.
```json title="Response (abridged)"
{
"id": "req_8fKq2mZr1xYt",
"object": "search",
"mode": "fast",
"found": true,
"total": 23,
"results": [
{
"url": "https://diarioejemplo.example/economia/…",
"title": "Inflación: qué esperan los analistas para los próximos meses",
"source": "Diario Ejemplo",
"published_at": "2026-09-21T12:40:00.000Z",
"score": 0.95,
"read": null,
"highlights": []
}
],
"usage": { "tokens": 1840, "calls": 2, "cost_usd": 0.0014, "duration_ms": 910 },
"warnings": []
}
```
In `fast` mode results are judged from the headline and standfirst. Switch to `normal` to have the top
results read and confirmed: they come back with `read` and, if you ask for `highlights`, a short verbatim
excerpt.
## Already use the Exa SDK? [#already-use-the-exa-sdk]
Keep it. Point it at our compatibility layer and use a typesearch key: the rest of your code stays.
Python
JavaScript
```python
from exa_py import Exa
exa = Exa(api_key=TYPESEARCH_API_KEY, base_url="https://api.typesearch.ai/compat/exa")
results = exa.search("inflación en Argentina", num_results=5, type="fast")
```
```ts
import Exa from 'exa-js';
const exa = new Exa(process.env.TYPESEARCH_API_KEY, 'https://api.typesearch.ai/compat/exa');
const { results } = await exa.search('inflación en Argentina', { numResults: 5, type: 'fast' });
```
The same works with the Tako and Perplexity SDKs, and with LangChain’s Exa tools: see
[Integrations](/docs/integrations).
## Next steps [#next-steps]
# Rate limits and quotas (/docs/rate-limits)
Every key has two limits:
* **Requests per minute**, over a sliding window.
* **Model tokens per day**, reset at 00:00 UTC. The quota is checked before each request: a request that
starts within quota finishes, even if it goes over.
The limits of your key depend on your plan. See [pricing](/pricing), or talk to
sales for higher ones.
## Headers [#headers]
Every response carries:
| Header | |
| ----------------------- | --------------------------------------- |
| `X-RateLimit-Limit` | Requests allowed per minute. |
| `X-RateLimit-Remaining` | Requests left in the current window. |
| `X-Request-Id` | The request id. |
| `Retry-After` | On `429 rate_limited`: seconds to wait. |
## When you hit a limit [#when-you-hit-a-limit]
| `code` | What happened | What to do |
| ---------------- | ------------------------------ | ------------------------------------------------------- |
| `rate_limited` | Too many requests this minute. | Wait `Retry-After` seconds. The SDKs do it for you. |
| `quota_exceeded` | Daily token quota used. | Wait until 00:00 UTC, or raise your quota. Not retried. |
## Checking your usage [#checking-your-usage]
TypeScript
Python
```ts
const usage = await ts.usage();
console.log(usage.today.remaining_tokens, usage.limits.requests_per_minute);
```
```python
usage = ts.usage()
print(usage.today.remaining_tokens, usage.limits.requests_per_minute)
```
See [`GET /v1/usage`](/docs/api-reference/usage).
# Embed, image and SVG (/docs/api-reference/chart-embed)
What `embed_url`, `image_url` and `svg_url` point to. They are public by id, so anyone with the link can see
the chart, and they need no key. Every view is free.
* **`chart_…`**: a page made for an iframe. It adapts to its width (under 480 px it switches to a compact
layout), shows tooltips, weighs a few KB and loads nothing from third parties. It can be embedded on any
site.
* **`chart_….png`**: the card at twice the resolution, for chat, email or slides.
* **`chart_….svg`**: the vector card.
The page posts its height to the parent page on load and on every resize, with
`{ type: "typesearch:chart:resize", id, height }`, so you can size the iframe to fit. See
[Embed it](/docs/guides/charts#embed-it).
### Path [#path]
| Parameter | Type | |
| --------- | ------ | ------------------------------------------------------------------------------- |
| `file` | string | `chart_…` for the page, `chart_….png` for the image, `chart_….svg` for the SVG. |
### Query [#query]
| Parameter | Type | |
| --------- | ------ | ------------------------------------------------------------------------------------------------------- |
| `theme` | string | Shows it in another theme: `light`, `dark`, `editorial` or `electric`. Default: the chart's `theme`. |
| `size` | string | `compact`: the narrow layout, for phones and stories. PNG and SVG only; the page picks it by its width. |
### Responses [#responses]
| Status | |
| ------ | --------------------------------------------------------------------------------------------------- |
| `200` | The page (`text/html`), the image (`image/png`) or the SVG (`image/svg+xml`). Cached for 5 minutes. |
| `404` | No such chart: the id is wrong, or the chart was deleted or expired. |
| `429` | More than 600 views per minute from the same IP. Retry after `Retry-After` seconds. |
```html title="Embed"
```
```html title="Fit the height"
```
```md title="Image in Markdown"

```
```text title="Compact, electric theme"
https://api.typesearch.ai/embed/chart_8k2m9q4x7w1p3n5z.png?size=compact&theme=electric
```
# Create a chart (/docs/api-reference/charts)
Works out what you expect to see (`interpretation`), gets that data, checked against the sources, and draws
a card with the key figures, a title that states the finding, annotations and the source of every value: race
standings, poll shares, wins in a season, what an outlet published, coverage, tone, outlets, a timeline or the
figures the articles publish. If the exact data isn't published or takes too long, it shows the closest thing
and says how close it is (`rung`); a stored chart keeps `improving` until the exact data arrives. Or send
`data` (series, events, or columns and rows) and get the same card from your own numbers. Send `query` or
`data`, not both. The [Charts guide](/docs/guides/charts) explains every recipe, type and rule.
It answers `201 Created` with a new chart, and `200 OK` with `plan_only`, with `store: false`, or when the
same request within 10 minutes returns the same chart (`cached: true`, not billed). With `stream: true` it
answers with [Server-Sent Events](/docs/guides/charts#stream-the-progress): `step`, `result`, `improved` and
`error`. When there are no articles about the topic at all it answers `422 insufficient_data`, not billed.
From a query it costs its search (the mode it needed, per query) plus $0.50 per 1,000 charts; from your
data, $0.30 per 1,000. See [pricing](/docs/guides/charts#pricing).
## Body [#body]
## Response [#response]
cURL
```bash
curl https://api.typesearch.ai/v1/charts \
-H "Authorization: Bearer $TYPESEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "which outlets cover lithium",
"theme": "editorial"
}'
```
```json title="201 Created (abridged)"
{
"id": "chart_3v7n1q8m2k5x9w4p",
"object": "chart",
"type": "bar_horizontal",
"theme": "editorial",
"locale": "en",
"chart": {
"type": "bar_horizontal",
"title": "Diario Ejemplo published the most about lithium",
"subtitle": "Articles mentioning lithium, per outlet · Aug 28–Sep 26",
"x": { "kind": "category" },
"y": { "format": "number", "label": "articles" },
"part_of_whole": true,
"series": [
{
"name": "Articles",
"points": [
{ "x": "Diario Ejemplo", "y": 42 },
{ "x": "Noticias Ejemplo", "y": 31 },
{ "x": "La Crónica Demo", "y": 27 }
]
}
]
},
"compatible_types": ["bar", "bar_horizontal", "donut", "pie", "kpi", "table"],
"plan": {
"recipe": "outlets",
"measure": "outlets",
"x_axis": "outlet",
"type": "bar_horizontal",
"part_of_whole": true,
"confidence": 1,
"alternatives": [
{ "type": "bar", "probability": 0 },
{ "type": "donut", "probability": 0 },
{ "type": "pie", "probability": 0 }
],
"topic": "lithium",
"entities": [],
"needs": { "queries": ["lithium"], "mode": "ultra", "days": 30, "min_points": 1, "max_results": 10 }
},
"interpretation": { "label": "Outlets that publish the most about lithium", "confidence": 1, "alternatives": [] },
"rung": "exact",
"improving": false,
"sources": [
{
"url": "https://diario.example/economia/litio-exportaciones",
"title": "Lithium exports hit a record in August",
"source": "Diario Ejemplo",
"published_at": "2026-09-25T11:20:00Z"
}
],
"embed_url": "https://api.typesearch.ai/embed/chart_3v7n1q8m2k5x9w4p",
"image_url": "https://api.typesearch.ai/embed/chart_3v7n1q8m2k5x9w4p.png",
"svg_url": "https://api.typesearch.ai/embed/chart_3v7n1q8m2k5x9w4p.svg",
"svg": null,
"created_at": "2026-09-26T14:02:11.482Z",
"expires_at": "2026-12-25T14:02:11.482Z",
"cached": false,
"usage": { "cost_usd": 0.0015, "mode": "ultra", "queries": 1, "duration_ms": 910 },
"warnings": []
}
```
```json title="Body: your own data"
{
"data": {
"series": [
{
"name": "Share",
"points": [
{ "x": "Search", "y": 46 },
{ "x": "Social", "y": 31 },
{ "x": "Direct", "y": 23 }
]
}
],
"y": { "format": "percent" }
},
"title": "Where our readers come from",
"source": "Diario Ejemplo analytics"
}
```
# Get contents (/docs/api-reference/contents)
Returns, for each URL, its title, the description its publisher gives it, its publication date, its source
and a very short verbatim excerpt (up to 25 words, never from the first paragraph) — never the full text.
With a `query`, the model picks the excerpt about it and scores each page’s `relevance`. Articles that can’t
be quoted (short, paid, or whose publisher asked for no snippets) come back without an excerpt.
Each URL has its own `status`: one that fails never fails the request.
## Body [#body]
## Response [#response]
cURL
TypeScript
Python
```bash
curl https://api.typesearch.ai/v1/contents \
-H "Authorization: Bearer $TYPESEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"urls": ["https://reddiaria.example/economia/…", "https://diarioejemplo.example/politica/…"],
"query": "el Presupuesto 2027"
}'
```
```ts
const res = await ts.contents(
['https://reddiaria.example/economia/…', 'https://diarioejemplo.example/politica/…'],
{ query: 'el Presupuesto 2027' },
);
```
```python
res = ts.contents(
["https://reddiaria.example/economia/…", "https://diarioejemplo.example/politica/…"],
query="el Presupuesto 2027",
)
```
```json title="200 OK"
{
"id": "req_Hc4tB8mZw1Ke",
"object": "contents",
"results": [
{
"url": "https://reddiaria.example/economia/…",
"status": "ok",
"error": null,
"title": "Presupuesto 2027: las claves del proyecto",
"description": "El Gobierno envió el proyecto al Congreso.",
"published_at": "2026-09-16T01:12:00.000Z",
"source": "Red Diaria",
"excerpt": "El proyecto prevé un superávit primario…",
"highlights": ["El proyecto prevé un superávit primario…"],
"relevance": 0.97
},
{
"url": "https://diarioejemplo.example/politica/…",
"status": "error",
"error": { "code": "site_unreachable", "message": "…" },
"title": null,
"description": null,
"published_at": null,
"source": null,
"excerpt": null,
"highlights": [],
"relevance": null
}
],
"usage": { "tokens": 1320, "calls": 1, "cost_usd": 0.0004, "duration_ms": 1840 }
}
```
# Overview (/docs/api-reference)
## Base URL [#base-url]
```text
https://api.typesearch.ai
```
Every endpoint lives under `/v1`. Requests and responses are JSON (`Content-Type: application/json`);
field names are `snake_case`.
## Authentication [#authentication]
Send your key as `Authorization: Bearer ts_live_…` or `x-api-key: ts_live_…`. See
[Authentication](/docs/authentication).
## Endpoints [#endpoints]
| | Endpoint | |
| ---------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------ |
| POST | [`/v1/search`](/docs/api-reference/search) | Search the index. |
| POST | [`/v1/search/site`](/docs/api-reference/site-search) | Search a live site. Returns a job. |
| GET | [`/v1/jobs/{id}`](/docs/api-reference/jobs) | A job’s status and result. |
| POST | [`/v1/similar`](/docs/api-reference/similar) | Articles about the same story as a URL. |
| POST | [`/v1/contents`](/docs/api-reference/contents) | Metadata and excerpts of up to 10 URLs. |
| POST | [`/v1/charts`](/docs/api-reference/charts) | Create a chart card. |
| GET | [`/v1/charts`](/docs/api-reference/manage-charts#list-charts) | List your charts. |
| GET | [`/v1/charts/{id}`](/docs/api-reference/manage-charts#get-a-chart) | Get a chart. |
| PATCH | [`/v1/charts/{id}`](/docs/api-reference/manage-charts#change-a-chart) | Change its type, theme, title or language. Free. |
| DELETE | [`/v1/charts/{id}`](/docs/api-reference/manage-charts#delete-a-chart) | Delete a chart. |
| GET | [`/embed/{file}`](/docs/api-reference/chart-embed) | A chart’s public embed, PNG or SVG. No key. |
| GET | [`/v1/usage`](/docs/api-reference/usage) | Usage and limits of your key. |
## Errors [#errors]
Errors are [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details with a stable `code`. See
[Errors](/docs/errors) for every code, and [Rate limits](/docs/rate-limits) for the limit headers.
## Versioning [#versioning]
`/v1` only changes by adding: new response fields, and new options with a default. Anything breaking will
ship as `/v2`, with notice. Ignore fields you don’t know, and don’t send fields that aren’t documented —
unknown request fields are rejected.
## OpenAPI [#openapi]
The whole contract is an [OpenAPI 3.1 document](/openapi.json), generated from the same schemas that
validate every request. Use it to generate a client in any language, or to import the API into your
tools.
# Get a job (/docs/api-reference/jobs)
Returns a job created by [`POST /v1/search/site`](/docs/api-reference/site-search). Poll it every couple of
seconds until `status` is `succeeded` or `failed` — or let the SDK do it with `jobs.wait()`.
Jobs last one day and are visible only to the key that created them; after that, or with another key,
this returns `404 job_not_found`.
## Path [#path]
| Parameter | Type | |
| --------- | ------ | ------------------------------------------------------------- |
| `id` | string | The job id, from the `202` response or its `Location` header. |
## Response [#response]
When the job succeeded, `result` is a search response with the same fields as
[`POST /v1/search`](/docs/api-reference/search#response).
cURL
TypeScript
Python
```bash
curl https://api.typesearch.ai/v1/jobs/job_Qm3sV9aLp2Rt \
-H "Authorization: Bearer $TYPESEARCH_API_KEY"
```
```ts
const job = await ts.jobs.get('job_Qm3sV9aLp2Rt');
// or poll until it finishes
const res = await ts.jobs.wait('job_Qm3sV9aLp2Rt');
```
```python
job = ts.jobs.get("job_Qm3sV9aLp2Rt")
# or poll until it finishes
res = ts.jobs.wait("job_Qm3sV9aLp2Rt")
```
```json title="200 OK"
{
"id": "job_Qm3sV9aLp2Rt",
"object": "job",
"status": "succeeded",
"created_at": "2026-09-22T14:03:11.000Z",
"finished_at": "2026-09-22T14:03:39.000Z",
"result": {
"id": "job_Qm3sV9aLp2Rt",
"object": "site_search",
"mode": "normal",
"site": "diarioejemplo.example",
"found": true,
"total": 6,
"results": [
{
"url": "https://diarioejemplo.example/economia/…",
"title": "Dólar hoy: a cuánto cotiza este lunes",
"score": 0.97,
"found_in": "section"
}
]
},
"error": null
}
```
# Manage charts (/docs/api-reference/manage-charts)
Charts made with any key of your organization, from [`POST /v1/charts`](/docs/api-reference/charts). None of
these requests is billed. Stored charts expire after 90 days on pay as you go and never on the monthly credit
plan (`expires_at`); an expired or deleted chart answers `404 chart_not_found`.
## List charts [#list-charts]
Your charts, newest first, a page at a time.
### Query [#query]
| Parameter | Type | |
| --------- | ------- | --------------------------------------- |
| `limit` | integer | How many, 1 to 100. Default: `20`. |
| `before` | string | The `next_before` of the previous page. |
### Response [#response]
cURL
```bash
curl "https://api.typesearch.ai/v1/charts?limit=2" \
-H "Authorization: Bearer $TYPESEARCH_API_KEY"
```
```json title="200 OK"
{
"object": "list",
"data": [
{
"id": "chart_8k2m9q4x7w1p3n5z",
"type": "line",
"title": "The blue dollar rose 4.1% this week",
"theme": "light",
"locale": "en",
"created_at": "2026-09-26T14:02:11.482Z",
"expires_at": "2026-12-25T14:02:11.482Z",
"embed_url": "https://api.typesearch.ai/embed/chart_8k2m9q4x7w1p3n5z",
"image_url": "https://api.typesearch.ai/embed/chart_8k2m9q4x7w1p3n5z.png",
"svg_url": "https://api.typesearch.ai/embed/chart_8k2m9q4x7w1p3n5z.svg"
},
{
"id": "chart_3v7n1q8m2k5x9w4p",
"type": "bar_horizontal",
"title": "Diario Ejemplo published the most about lithium",
"theme": "editorial",
"locale": "en",
"created_at": "2026-09-26T13:47:30.105Z",
"expires_at": "2026-12-25T13:47:30.105Z",
"embed_url": "https://api.typesearch.ai/embed/chart_3v7n1q8m2k5x9w4p",
"image_url": "https://api.typesearch.ai/embed/chart_3v7n1q8m2k5x9w4p.png",
"svg_url": "https://api.typesearch.ai/embed/chart_3v7n1q8m2k5x9w4p.svg"
}
],
"has_more": true,
"next_before": "2026-09-26T13:47:30.105Z"
}
```
## Get a chart [#get-a-chart]
One chart, with what is drawn, its plan and its sources: the same fields as the
[`POST /v1/charts` response](/docs/api-reference/charts#response), with `usage.cost_usd` at `0`.
A chart created with `improving: true` shows `improving: true` here until the exact data arrives or we stop
looking. Poll it to get the better chart as soon as it replaces the first one; the `id` and the URLs stay the
same.
### Path [#path]
| Parameter | Type | |
| --------- | ------ | ------------------------ |
| `id` | string | The chart id, `chart_…`. |
cURL
```bash
curl https://api.typesearch.ai/v1/charts/chart_8k2m9q4x7w1p3n5z \
-H "Authorization: Bearer $TYPESEARCH_API_KEY"
```
```json title="404 Not Found"
{
"type": "urn:typesearch:error:chart_not_found",
"title": "Not found",
"status": 404,
"detail": "No chart with that id for this key (GET /v1/charts).",
"code": "chart_not_found",
"request_id": "req_Hn2qW7xL4mZa"
}
```
## Change a chart [#change-a-chart]
Switches the type, the theme, the title, the subtitle or the language, and redraws from the same data: no new
search, free. Send at least one field. `type` should be one of the chart's `compatible_types`; if the data
doesn't fit it, the chart is drawn with the closest type that does and `warnings` says so
(`chart_type_adjusted`). Without a title of your own, the title is rewritten for the new type and language.
The `embed_url`, `image_url` and `svg_url` stay the same. Embeds and images are cached for 5 minutes, so the
change shows there within that time. A change made while the chart is `improving` is kept, and the
improvement is dropped.
### Body [#body]
### Response [#response-1]
The updated chart, with the same fields as the [`POST /v1/charts` response](/docs/api-reference/charts#response).
cURL
```bash
curl -X PATCH https://api.typesearch.ai/v1/charts/chart_8k2m9q4x7w1p3n5z \
-H "Authorization: Bearer $TYPESEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "type": "area", "theme": "dark" }'
```
```json title="200 OK (abridged)"
{
"id": "chart_8k2m9q4x7w1p3n5z",
"object": "chart",
"type": "area",
"theme": "dark",
"locale": "en",
"compatible_types": ["line", "area", "bar", "kpi", "table"],
"embed_url": "https://api.typesearch.ai/embed/chart_8k2m9q4x7w1p3n5z",
"cached": false,
"usage": { "cost_usd": 0, "mode": null, "queries": 0, "duration_ms": 0 },
"warnings": []
}
```
## Delete a chart [#delete-a-chart]
Deletes the chart. Its embed, image and SVG stop working; copies already cached can take a few minutes to
go.
### Path [#path-1]
| Parameter | Type | |
| --------- | ------ | ------------------------ |
| `id` | string | The chart id, `chart_…`. |
cURL
```bash
curl -X DELETE https://api.typesearch.ai/v1/charts/chart_8k2m9q4x7w1p3n5z \
-H "Authorization: Bearer $TYPESEARCH_API_KEY"
```
```json title="200 OK"
{ "id": "chart_8k2m9q4x7w1p3n5z", "object": "chart", "deleted": true }
```
# Search (/docs/api-reference/search)
Searches the index and returns the relevant articles, most relevant first, each with a calibrated
`score`. With several queries, results come merged and each query also gets its own entry in `groups`.
Set `stream: true` to receive [Server-Sent Events](/docs/guides/streaming) while it searches.
## Body [#body]
## Response [#response]
cURL
TypeScript
Python
```bash
curl https://api.typesearch.ai/v1/search \
-H "Authorization: Bearer $TYPESEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "el dólar",
"mode": "normal",
"max_results": 10,
"include_domains": ["reddiaria.example", "diarioejemplo.example"],
"published_after": "2026-09-20",
"tone": true
}'
```
```ts
import Typesearch from 'typesearch-js';
const ts = new Typesearch();
const res = await ts.search('el dólar', {
mode: 'normal',
max_results: 10,
include_domains: ['reddiaria.example', 'diarioejemplo.example'],
published_after: '2026-09-20',
tone: true,
});
```
```python
from typesearch import Typesearch
ts = Typesearch()
res = ts.search(
"el dólar",
mode="normal",
max_results=10,
include_domains=["reddiaria.example", "diarioejemplo.example"],
published_after="2026-09-20",
tone=True,
)
```
```json title="200 OK"
{
"id": "req_8fKq2mZr1xYt",
"object": "search",
"mode": "normal",
"queries": ["el dólar"],
"found": true,
"total": 14,
"results": [
{
"url": "https://reddiaria.example/economia/…",
"title": "El dólar oficial cerró estable en una rueda con poco volumen",
"source": "Red Diaria",
"published_at": "2026-09-21T18:05:00.000Z",
"section": "economia",
"snippet": "La divisa se mantuvo sin cambios frente al cierre anterior.",
"score": 0.96,
"headline_relevance": 0.91,
"read": { "probability": 0.96, "centrality": 3 },
"highlights": ["El dólar mayorista terminó la jornada sin variaciones…"],
"tone": {
"label": "neutral",
"probabilities": { "positive": 0.08, "neutral": 0.86, "negative": 0.06 },
"basis": "article"
},
"answers": null,
"duplicates": [],
"found_in": "index"
}
],
"groups": null,
"near_misses": [],
"rejected": [],
"tone": {
"articles": 14,
"overall": { "positive": 2, "neutral": 10, "negative": 2 },
"by_source": []
},
"usage": { "tokens": 4210, "calls": 4, "cost_usd": 0.0022, "duration_ms": 2380 },
"incomplete": false,
"cached_at": null,
"warnings": []
}
```
# Find similar (/docs/api-reference/similar)
Reads the article at `url` and searches the index for others about the same story. The response has the
same shape as [`POST /v1/search`](/docs/api-reference/search), with the reference article in `reference`.
## Body [#body]
## Response [#response]
The same fields as [`POST /v1/search`](/docs/api-reference/search#response), with `object: "similar"` and
`reference` set.
cURL
TypeScript
Python
```bash
curl https://api.typesearch.ai/v1/similar \
-H "Authorization: Bearer $TYPESEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://diarioejemplo.example/economia/…",
"mode": "fast",
"exclude_domains": ["diarioejemplo.example"]
}'
```
```ts
const res = await ts.similar('https://diarioejemplo.example/economia/…', {
mode: 'fast',
exclude_domains: ['diarioejemplo.example'],
});
```
```python
res = ts.similar(
"https://diarioejemplo.example/economia/…",
mode="fast",
exclude_domains=["diarioejemplo.example"],
)
```
```json title="200 OK"
{
"id": "req_Wn7cK2pXq9Ld",
"object": "similar",
"mode": "fast",
"reference": {
"url": "https://diarioejemplo.example/economia/…",
"title": "Inflación: qué esperan los analistas para los próximos meses"
},
"found": true,
"total": 9,
"results": [
{
"url": "https://plaza.example/economia/…",
"title": "Relevamiento de expectativas: las proyecciones de precios",
"source": "Plaza Financiera",
"score": 0.91,
"found_in": "index"
}
]
}
```
# Search a live site (/docs/api-reference/site-search)
Reads the site live: its homepage first and, when that is not enough, the sections most likely to cover the
topic and the site’s own search box, in parallel. It can take up to a minute, so it responds
`202 Accepted` with a [job](/docs/api-reference/jobs) and a `Location` header.
With `stream: true` it responds with [Server-Sent Events](/docs/guides/streaming) instead.
## Body [#body]
## Response [#response]
`202 Accepted` with a job. See [`GET /v1/jobs/{id}`](/docs/api-reference/jobs) for its fields.
cURL
TypeScript
Python
```bash
curl https://api.typesearch.ai/v1/search/site \
-H "Authorization: Bearer $TYPESEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "site": "diarioejemplo.example", "query": "el dólar", "mode": "normal" }'
```
```ts
import Typesearch from 'typesearch-js';
const ts = new Typesearch();
// Create the job and wait for it
const res = await ts.siteSearchAndWait('diarioejemplo.example', 'el dólar', {
mode: 'normal',
max_results: 10,
});
```
```python
from typesearch import Typesearch
ts = Typesearch()
# Create the job and wait for it
res = ts.site_search_and_wait("diarioejemplo.example", "el dólar", mode="normal", max_results=10)
```
```json title="202 Accepted"
{
"id": "job_Qm3sV9aLp2Rt",
"object": "job",
"status": "queued",
"created_at": "2026-09-22T14:03:11.000Z",
"finished_at": null,
"result": null,
"error": null
}
```
# Get usage (/docs/api-reference/usage)
Returns what the calling key has used today (since 00:00 UTC) and over the last 30 days, its limits, the
credit of its organization and the price list: `pricing`, in USD per 1,000 requests for each search mode,
similar and live site search, and per 1,000 pages for contents. It spends no tokens.
## Response [#response]
cURL
TypeScript
Python
```bash
curl https://api.typesearch.ai/v1/usage \
-H "Authorization: Bearer $TYPESEARCH_API_KEY"
```
```ts
const usage = await ts.usage();
console.log(usage.today.remaining_tokens);
```
```python
usage = ts.usage()
print(usage.today.remaining_tokens)
```
```json title="200 OK"
{
"object": "usage",
"key": { "id": "ts_live_Xk3P9qaB", "name": "Production" },
"limits": { "tokens_per_day": 50000000, "requests_per_minute": 600, "requests_per_second": 10 },
"today": { "requests": 412, "tokens": 183920, "cost_usd": 0.41, "remaining_tokens": 816080 },
"last_30_days": { "requests": 9120, "tokens": 4102330, "cost_usd": 9.12 },
"credit": { "balance_usd": 42.6, "plan": "payg", "spent_this_month_usd": 9.12, "monthly_limit_usd": null },
"pricing": {
"currency": "USD",
"per_1000_requests": {
"ultra": 1, "fast": 1.4, "normal": 2.2, "deep": 5.6,
"similar": 2.2, "similar_deep": 4.6, "site_search": 2.4
},
"per_1000_pages": { "contents": 0.2, "contents_with_query": 0.4 }
}
}
```
# Exa-compatible SDKs (/docs/integrations/exa-compatible)
typesearch has a compatibility layer that speaks Exa’s API: the same endpoints (`/search`,
`/findSimilar`, `/contents`), parameters, response shapes and errors. Anything built on Exa’s SDKs that
lets you set the base URL works with a typesearch key.
| Tool | How |
| ----------------------------- | ---------------------------------------------------------------------------------------------------- |
| `exa-py` | `Exa(api_key=…, base_url="https://api.typesearch.ai/compat/exa")` |
| `exa-py`, async | `AsyncExa(api_key=…, api_base="https://api.typesearch.ai/compat/exa")` |
| `exa-js` | `new Exa(apiKey, "https://api.typesearch.ai/compat/exa")` |
| `langchain-exa` (Python) | `exa_base_url="https://api.typesearch.ai/compat/exa"`: see [LangChain](/docs/integrations/langchain) |
| `@langchain/exa` (JavaScript) | Pass it an `exa-js` client with the base URL: see [LangChain](/docs/integrations/langchain) |
Python
JavaScript
```python
from exa_py import Exa
exa = Exa(api_key=TYPESEARCH_API_KEY, base_url="https://api.typesearch.ai/compat/exa")
results = exa.search("EU AI Act enforcement", num_results=10, type="fast")
```
```ts
import Exa from 'exa-js';
const exa = new Exa(process.env.TYPESEARCH_API_KEY, 'https://api.typesearch.ai/compat/exa');
const { results } = await exa.search('EU AI Act enforcement', { numResults: 10, type: 'fast' });
```
The key goes in the `x-api-key` header, as the SDKs already send it. Every parameter, and what it becomes on
typesearch, is in [Migrate from Exa](/docs/migration/exa).
## What doesn’t work [#what-doesnt-work]
Tools that hard-code Exa’s address can’t be pointed elsewhere: Exa’s own MCP server and its AI SDK package
are two of them.
For MCP clients, use [our MCP server](/docs/integrations/mcp); for the Vercel AI SDK, see
[Vercel AI SDK](/docs/integrations/vercel-ai-sdk).
## Tako and Perplexity [#tako-and-perplexity]
Their SDKs work the same way, against their own compatibility layers: see
[Migrate from Tako](/docs/migration/tako) and [Migrate from Perplexity](/docs/migration/perplexity).
# Integrations (/docs/integrations)
Each page below shows the shortest setup that works today, with the same key and the same prices as the
API.
## Your key [#your-key]
Every integration uses the same API key. Create one in the [dashboard](https://app.typesearch.ai/login) —
new accounts get $5 of free credit, valid for 30 days — and keep it in an environment
variable:
```bash
export TYPESEARCH_API_KEY="ts_live_…"
```
## What it costs [#what-it-costs]
Each call is billed like the API request it makes: a search by its [mode](/docs/modes) ($1.00 per 1,000
in `ultra`, $1.40 in `fast`, $2.20 in `normal`, $5.60 in `deep`), contents per page and similar per
request. Identical
requests within 10 minutes come from the cache and cost nothing.
## Other frameworks [#other-frameworks]
Any agent framework that speaks MCP can use the [MCP server](/docs/integrations/mcp): connect to
`https://api.typesearch.ai/mcp` with your key in `Authorization: Bearer`, and the agent gets `search_news`,
`get_contents` and `find_similar`. These examples read the key from `TYPESEARCH_API_KEY`;
they also need the key of the model they use.
Mastra
Agno
Pydantic AI
OpenAI Agents SDK
CrewAI
Google ADK
```ts
// npm install @mastra/core @mastra/mcp zod
import { Agent } from '@mastra/core/agent';
import { MCPClient } from '@mastra/mcp';
const mcp = new MCPClient({
id: 'typesearch',
servers: {
typesearch: {
url: new URL('https://api.typesearch.ai/mcp'),
requestInit: { headers: { Authorization: `Bearer ${process.env.TYPESEARCH_API_KEY}` } },
timeout: 90_000, // a "deep" search can take about a minute
},
},
});
const agent = new Agent({
id: 'news-researcher',
name: 'News researcher',
instructions: 'Answer briefly and cite every article you use by its link.',
model: 'openai/gpt-5-mini',
tools: await mcp.listTools(), // typesearch_search_news, typesearch_get_contents, …
});
const result = await agent.generate('What did news outlets report this week about central bank rate decisions?');
console.log(result.text);
await mcp.disconnect();
```
```python
# pip install "agno[mcp,openai]"
import asyncio
import os
from agno.agent import Agent
from agno.models.openai import OpenAIResponses
from agno.tools.mcp import MCPTools
async def main() -> None:
async with MCPTools(
url="https://api.typesearch.ai/mcp",
transport="streamable-http",
headers={"Authorization": f"Bearer {os.environ['TYPESEARCH_API_KEY']}"},
timeout_seconds=90, # a "deep" search can take about a minute
) as typesearch:
agent = Agent(
model=OpenAIResponses(id="gpt-5.4-mini"),
tools=[typesearch],
instructions=["Answer briefly and cite every article you use by its link."],
)
await agent.aprint_response("What did news outlets report this week about central bank rate decisions?")
asyncio.run(main())
```
```python
# pip install "pydantic-ai-slim[mcp,openai]"
import asyncio
import os
from pydantic_ai import Agent
from pydantic_ai.mcp import MCPToolset
typesearch = MCPToolset(
"https://api.typesearch.ai/mcp",
headers={"Authorization": f"Bearer {os.environ['TYPESEARCH_API_KEY']}"},
)
agent = Agent(
"openai:gpt-5.2",
toolsets=[typesearch],
instructions="Answer briefly and cite every article you use by its link.",
)
async def main() -> None:
async with agent: # keeps one MCP connection open for the whole run
result = await agent.run("What did news outlets report this week about central bank rate decisions?")
print(result.output)
asyncio.run(main())
```
```python
# pip install openai-agents
import asyncio
import os
from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHttp
async def main() -> None:
async with MCPServerStreamableHttp(
name="typesearch",
params={
"url": "https://api.typesearch.ai/mcp",
"headers": {"Authorization": f"Bearer {os.environ['TYPESEARCH_API_KEY']}"},
},
cache_tools_list=True,
client_session_timeout_seconds=90, # a "deep" search can take about a minute
) as typesearch:
agent = Agent(
name="News researcher",
instructions="Answer briefly and cite every article you use by its link.",
mcp_servers=[typesearch],
)
result = await Runner.run(agent, "What did news outlets report this week about central bank rate decisions?")
print(result.final_output)
asyncio.run(main())
```
```python
# pip install crewai
import os
from crewai import Agent, Crew, Task
from crewai.mcp import MCPServerHTTP
researcher = Agent(
role="News researcher",
goal="Answer questions about current events from recent news coverage, citing every source",
backstory="You use search_news to find recent coverage and get_contents to read an article's key passage.",
mcps=[
MCPServerHTTP(
url="https://api.typesearch.ai/mcp",
headers={"Authorization": f"Bearer {os.environ['TYPESEARCH_API_KEY']}"},
cache_tools_list=True,
)
],
llm="gpt-5.4-mini",
)
task = Task(
description="What did news outlets report this week about central bank rate decisions?",
expected_output="A short answer, with the link of every article it uses.",
agent=researcher,
)
print(Crew(agents=[researcher], tasks=[task]).kickoff())
```
```python
# pip install "google-adk[mcp]"
import os
from google.adk.agents import Agent
from google.adk.tools.mcp_tool import McpToolset
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams
# ADK loads root_agent from your agent's module: run it with `adk run` or `adk web`.
root_agent = Agent(
model="gemini-flash-latest",
name="typesearch_agent",
instruction="Help users find recent news, and cite the link of every article you use",
tools=[
McpToolset(
connection_params=StreamableHTTPConnectionParams(
url="https://api.typesearch.ai/mcp",
headers={"Authorization": f"Bearer {os.environ['TYPESEARCH_API_KEY']}"},
timeout=30,
),
)
],
)
```
CrewAI gives each MCP tool call 30 seconds, which a `deep` search can exceed.
## Anything else [#anything-else]
* **Any language:** the API is plain JSON over HTTPS, described by an [OpenAPI 3.1 document](/openapi.json)
you can feed to any client generator. See the [API reference](/docs/api-reference).
* **The Tako and Perplexity SDKs** work too: see [Migrate from Tako](/docs/migration/tako) and
[Migrate from Perplexity](/docs/migration/perplexity).
# LangChain (/docs/integrations/langchain)
## With langchain-exa [#with-langchain-exa]
typesearch speaks Exa’s API, and LangChain’s Exa integration lets you set the base URL. Point it at
typesearch, use a typesearch key, and every Exa tool and retriever searches our news index:
Python
JavaScript
```bash
pip install langchain-exa
```
```bash
npm install @langchain/exa exa-js
```
### A search tool [#a-search-tool]
Python
JavaScript
```python
import os
from langchain_exa import ExaSearchResults
search = ExaSearchResults(
exa_api_key=os.environ["TYPESEARCH_API_KEY"],
exa_base_url="https://api.typesearch.ai/compat/exa",
)
search.invoke({"query": "EU AI Act enforcement", "num_results": 5})
```
```ts
import Exa from 'exa-js';
import { ExaSearchResults } from '@langchain/exa';
const search = new ExaSearchResults({
client: new Exa(process.env.TYPESEARCH_API_KEY, 'https://api.typesearch.ai/compat/exa'),
searchArgs: { numResults: 5 },
});
await search.invoke('EU AI Act enforcement');
```
### In an agent [#in-an-agent]
```python
from langchain.agents import create_agent
agent = create_agent(
model="anthropic:claude-sonnet-4-5",
tools=[search],
system_prompt="Answer with recent news. Cite the source and link of every fact.",
)
agent.invoke({"messages": [{"role": "user", "content": "What changed in EU AI Act enforcement this week?"}]})
```
### A retriever [#a-retriever]
```python
from langchain_exa import ExaSearchRetriever
retriever = ExaSearchRetriever(
exa_api_key=os.environ["TYPESEARCH_API_KEY"],
exa_base_url="https://api.typesearch.ai/compat/exa",
k=5,
type="fast",
)
docs = retriever.invoke("EU AI Act enforcement")
```
`ExaFindSimilarResults` works the same way, with our [similar](/docs/guides/similar) endpoint behind it.
### What to know [#what-to-know]
* **Modes.** `type` maps to our [modes](/docs/modes): `fast`, `auto`, `neural` and `keyword` run as `fast`,
`deep` as `deep`.
* **Page text costs extra.** The Exa tools ask for the text of each result by default. On typesearch that’s a
short excerpt, never the full page, billed per page ($0.20 per 1,000 pages) on top of the search
($1.40 per 1,000 in `fast`).
* **What isn’t supported** is accepted and reported in the `X-Compat-Warnings` header instead of failing:
see [Migrate from Exa](/docs/migration/exa) for every parameter.
* **Tone, essentials and structured answers** are in the native API: see the [API reference](/docs/api-reference).
# LlamaIndex (/docs/integrations/llamaindex)
## A tool over the HTTP API [#a-tool-over-the-http-api]
A news search tool is one request to our API. Write it as a function with a docstring — the agent reads it
to decide when to call it — and wrap it in a `FunctionTool`:
```bash
pip install llama-index httpx
```
```python
import os
import httpx
from llama_index.core.agent.workflow import FunctionAgent
from llama_index.core.tools import FunctionTool
from llama_index.llms.openai import OpenAI
def search_news(query: str, days: int = 7) -> list[dict]:
"""Search recent news articles. Returns the title, URL, source, publication time
and a relevance score (0-1) of each one."""
res = httpx.post(
"https://api.typesearch.ai/v1/search",
headers={"Authorization": f"Bearer {os.environ['TYPESEARCH_API_KEY']}"},
json={"query": query, "days": days, "mode": "fast", "max_results": 8},
timeout=60,
)
res.raise_for_status()
fields = ("title", "url", "source", "published_at", "score", "snippet")
return [{k: r.get(k) for k in fields} for r in res.json()["results"]]
agent = FunctionAgent(
tools=[FunctionTool.from_defaults(fn=search_news)],
llm=OpenAI(model="gpt-4.1"),
system_prompt="Answer with recent news. Cite the source and link of every fact.",
)
response = await agent.run("What changed in EU AI Act enforcement this week?")
```
The search runs in `fast` mode, $1.40 per 1,000 searches. Change it to `"normal"` to have the top results
read before they’re ranked ($2.20); every parameter is in the [API reference](/docs/api-reference/search).
## As a retriever [#as-a-retriever]
To feed articles into a query engine instead of an agent, turn each result into a `Document` with its
excerpt as the text and the rest as metadata:
```python
from llama_index.core import Document
docs = [
Document(text=r["snippet"] or r["title"], metadata={k: r[k] for k in ("title", "url", "source", "published_at")})
for r in search_news("EU AI Act enforcement")
]
```
We return short excerpts, never full pages. For more text about your question, add `"highlights": true`
and use `normal` or `deep` mode: the top results come back with verbatim excerpts chosen for your query.
# MCP server (/docs/integrations/mcp)
The typesearch MCP server gives any [Model Context Protocol](https://modelcontextprotocol.io) client four
tools: three read-only tools for news search, and one that makes chart cards. It's remote (nothing to
install), and it uses your API key and the API's prices.
| | |
| -------------- | ------------------------------------------------------------------------------------------------------------ |
| URL | `https://api.typesearch.ai/mcp` |
| Transport | Streamable HTTP, stateless |
| Authentication | `Authorization: Bearer ` or `x-api-key: `. Clients that only take a URL: `?typesearchApiKey=` |
| Server name | `typesearch` |
The key is never logged: we strip it from our logs and error messages, also when it comes in the URL.
## Install [#install]
The examples read your key from `TYPESEARCH_API_KEY`, or ask for it. Replace `YOUR_API_KEY` where a client
needs it written in its configuration.
### Cursor [#cursor]
[Add to Cursor](https://cursor.com/en/install-mcp?name=typesearch\&config=eyJ1cmwiOiJodHRwczovL2FwaS50eXBlc2VhcmNoLmFpL21jcCIsImhlYWRlcnMiOnsiQXV0aG9yaXphdGlvbiI6IkJlYXJlciAke2VudjpUWVBFU0VBUkNIX0FQSV9LRVl9In19).
Cursor reads the key from the `TYPESEARCH_API_KEY` environment variable. To add it by hand, put this in
`~/.cursor/mcp.json` (or in your project's `.cursor/mcp.json`):
```json title="mcp.json"
{
"mcpServers": {
"typesearch": {
"url": "https://api.typesearch.ai/mcp",
"headers": {
"Authorization": "Bearer ${env:TYPESEARCH_API_KEY}"
}
}
}
}
```
### VS Code [#vs-code]
[Add to VS Code](https://vscode.dev/redirect/mcp/install?name=typesearch\&inputs=%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22typesearch-api-key%22%2C%22description%22%3A%22typesearch%20API%20key%22%2C%22password%22%3Atrue%7D%5D\&config=%7B%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fapi.typesearch.ai%2Fmcp%22%2C%22headers%22%3A%7B%22Authorization%22%3A%22Bearer%20%24%7Binput%3Atypesearch-api-key%7D%22%7D%7D).
VS Code asks for your key once and stores it as a secret. By hand, in `.vscode/mcp.json`:
```json title=".vscode/mcp.json"
{
"servers": {
"typesearch": {
"type": "http",
"url": "https://api.typesearch.ai/mcp",
"headers": {
"Authorization": "Bearer ${input:typesearch-api-key}"
}
}
},
"inputs": [
{
"type": "promptString",
"id": "typesearch-api-key",
"description": "typesearch API key",
"password": true
}
]
}
```
### Claude Code [#claude-code]
```bash
claude mcp add --transport http typesearch https://api.typesearch.ai/mcp --header "Authorization: Bearer $TYPESEARCH_API_KEY"
```
Add `--scope user` to have it in every project.
### Codex [#codex]
```bash
codex mcp add typesearch --url https://api.typesearch.ai/mcp --bearer-token-env-var TYPESEARCH_API_KEY
```
### Claude Desktop [#claude-desktop]
Open **Settings → Developer → Edit config**, add the server to `claude_desktop_config.json` with your key
and restart Claude.
Claude Desktop starts local servers from this file, so it reaches the remote one through
[mcp-remote](https://www.npmjs.com/package/mcp-remote):
```json title="claude_desktop_config.json"
{
"mcpServers": {
"typesearch": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://api.typesearch.ai/mcp",
"--header",
"Authorization:${TYPESEARCH_AUTH}"
],
"env": {
"TYPESEARCH_AUTH": "Bearer YOUR_API_KEY"
}
}
}
}
```
### Windsurf [#windsurf]
Add it to `mcp_config.json`. Windsurf reads the key from `TYPESEARCH_API_KEY`:
```json title="mcp_config.json"
{
"mcpServers": {
"typesearch": {
"serverUrl": "https://api.typesearch.ai/mcp",
"headers": {
"Authorization": "Bearer ${env:TYPESEARCH_API_KEY}"
}
}
}
}
```
### Other clients [#other-clients]
Any client that speaks Streamable HTTP works with the URL and one of the headers above. If it only takes a
URL, put the key in it:
```text
https://api.typesearch.ai/mcp?typesearchApiKey=YOUR_API_KEY
```
## Tools [#tools]
`search_news`, `get_contents` and `find_similar` are read-only. `create_chart` stores the card it makes, so
its image and embed can be opened later. Results are compact (the fields an agent cites, and nothing empty),
so they leave room in the context window for the answer.
### search\_news [#search_news]
Search the news index or, with `index`, your own [custom index](/docs/guides/custom-index). Returns ranked
articles with title, URL, source, publication time, a short excerpt and a calibrated relevance score.
| Parameter | | |
| ------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `query` | required | What to search for, in any language. |
| `index` | | Search your own custom index (`idx_…`) instead of the news index. With `index`, `days` has no default. |
| `mode` | `fast` | `ultra`, `fast`, `normal` or `deep`: how much is read before ranking. See [Modes](/docs/modes). |
| `max_results` | `10` | From 1 to 25. |
| `days` | `7` | Only the last N days, 1 to 365. The default of 7 doesn’t apply when you give `published_after` or `published_before`. |
| `published_after`, `published_before` | | ISO dates (`2026-09-25`) or date-times with their offset (`2026-09-25T14:00:00Z`). |
| `include_domains`, `exclude_domains` | | Lists of domains. |
| `countries`, `languages` | | Only outlets from these countries (ISO 3166-1 alpha-2, e.g. `AR`) or that publish in these languages (ISO 639-1, e.g. `es`). |
### get\_contents [#get_contents]
Metadata and short verbatim excerpts of up to 10 URLs. With `query`, the model picks the excerpts about it.
| Parameter | | |
| --------- | -------- | ------------------------------- |
| `urls` | required | 1 to 10 URLs. |
| `query` | | The excerpts are chosen for it. |
### find\_similar [#find_similar]
Everything in the index about the same story as a URL, ranked, and who published it first.
| Parameter | | |
| ------------- | -------- | ------------------------------- |
| `url` | required | The article to start from. |
| `max_results` | `10` | From 1 to 25. |
| `days` | `7` | Only the last N days, 1 to 365. |
### create\_chart [#create_chart]
A chart card from a question in plain words: the standings of the latest race, poll or election results by
candidate, wins in a season, what an outlet published, coverage over time, share of voice between names, tone,
the outlets that cover a topic most, a timeline of a story, the figures articles publish (a price, a rate), or
data from your [custom index](/docs/guides/custom-index). It works out what the user expects to see, checks
every value against its source, picks the chart, the key figures and a title that states the finding, and
returns an image for the agent to show and an embed. If the exact data isn't published, it shows the closest
thing and says so. See [Charts](/docs/guides/charts).
| Parameter | | |
| ------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `query` | required | What to chart, in plain words and any language: "blue dollar this week", "Milei vs Bullrich coverage", "which outlets cover lithium". |
| `type` | `auto` | Picks the chart that tells the data best. Or force one: `line`, `area`, `bar`, `bar_horizontal`, `stacked_bar`, `pie`, `donut`, `scatter`, `funnel`, `timeline`, `kpi` or `table`. |
| `theme` | `light` | `light`, `dark`, `editorial` (warm paper) or `electric` (deep blue). |
| `days` | | Only the last N days, 1 to 365. By default it depends on the chart: 30 for coverage, 7 for figures. |
| `compare` | | What to compare, 2 to 5 names, when the query doesn't say it with "vs". |
| `countries`, `languages` | | Only outlets from these countries (ISO 3166-1 alpha-2, e.g. `AR`) or that publish in these languages (ISO 639-1, e.g. `es`). |
| `index` | | Chart your custom index (`idx_…`) instead of the news: prices, ratings, availability, brands. |
| `interpretation` | | One of the `alternatives` of a previous call with the same query: chart that reading instead. |
It returns the `title` (it states the finding), the `subtitle`, `image_url` (a PNG to show the user),
`embed_url` (an iframe for a page or an app), `key_figures` (each with its `change`), the values drawn in
`data` (up to 40 per series), the rows of a table in `table` (up to 10), `events` for a timeline, `sources`
and `cost_usd`. From a query in words it also returns what it `understood`, the other readings in
`alternatives`, and `rung`: `exact` for the data asked for, or `partial`, `related`, `coverage` or `stories`
for the closest thing found. `improving: true` means the image and the embed will update themselves when the
exact data arrives. The same call within 10 minutes comes back with `cached: true` and costs nothing.
Every result shows its source and links to the original; we don’t publish the list of sources. Missing an outlet? Suggest it from the dashboard (Suggest a source) or write to [support@typesearch.ai](mailto:support@typesearch.ai).
## Pricing [#pricing]
Each tool call is billed like the API request it makes:
| Tool | Price |
| -------------- | ------------------------------------------------------------------------------------------------------------ |
| `search_news` | $1.00 per 1,000 in `ultra` · $1.40 in `fast` · $2.20 in `normal` · $5.60 in `deep`, also over a custom index |
| `get_contents` | $0.20 per 1,000 pages · $0.40 with `query` |
| `find_similar` | $2.20 per 1,000 requests |
| `create_chart` | Its search (the mode it needs, per query) plus $0.50 per 1,000 charts: from $1.50 per 1,000 |
Identical calls within 10 minutes come from the cache and cost nothing. Every call counts toward your key’s
[rate limits](/docs/rate-limits).
## Errors [#errors]
A problem comes back as a tool error with a message the model can read and act on: a missing or invalid
key, no credit left, a rate limit to wait out, or no articles on a chart's topic (not billed). The same
conditions as the API’s
[errors](/docs/errors).
# n8n (/docs/integrations/n8n)
## With the HTTP Request node [#with-the-http-request-node]
### 1. Save your key as a credential [#1-save-your-key-as-a-credential]
In **Credentials → Create credential**, choose **Header Auth**:
| Field | Value |
| ----- | ----------------------------- |
| Name | `Authorization` |
| Value | `Bearer ts_live_…` (your key) |
### 2. Add the request [#2-add-the-request]
Add an **HTTP Request** node. The quickest way to fill it is **Import cURL** — paste this and press
**Import**:
```bash
curl https://api.typesearch.ai/v1/search \
-H "Content-Type: application/json" \
-d '{ "query": "lithium royalties in Chile", "days": 1, "mode": "fast", "max_results": 10 }'
```
Then set **Authentication** to **Generic Credential Type → Header Auth** and choose the credential from
step 1. Or fill it by hand:
| Setting | Value |
| -------------- | ------------------------------------- |
| Method | `POST` |
| URL | `https://api.typesearch.ai/v1/search` |
| Authentication | Generic Credential Type → Header Auth |
| Send Body | On, JSON |
To search for what an earlier node produced, use an expression in the body:
```json
{ "query": "{{ $json.topic }}", "days": 1, "mode": "fast", "max_results": 10 }
```
The articles come back in `results`. Add a **Split Out** node on `results` to get one item per article,
with `title`, `url`, `source`, `published_at`, `score` and `snippet`.
### 3. As a tool for the AI Agent node [#3-as-a-tool-for-the-ai-agent-node]
Connect an **HTTP Request Tool** to the agent’s **Tool** input, with the same URL, method and credential,
and let the model write the query with `$fromAI`:
```json
{
"query": "{{ $fromAI('query', 'What to search for in the news, in any language', 'string') }}",
"days": 7,
"mode": "fast",
"max_results": 8
}
```
Describe the tool so the agent knows when to call it: *Searches recent news articles and returns title,
URL, source, publication time and a relevance score for each.*
## Ideas [#ideas]
* **A morning briefing:** a Schedule Trigger, one search per topic with `"days": 1`, and an email or Slack
message with the titles and links.
* **Alerts:** keep only results with `score` above `0.8`, and remember the URLs you’ve sent so each article
goes out once.
Each search costs $1.40 per 1,000 in `fast` mode, as in these examples; without `mode`, the API uses
`normal` ($2.20), which reads the top results before ranking them.
# Vercel AI SDK (/docs/integrations/vercel-ai-sdk)
## A tool over the HTTP API [#a-tool-over-the-http-api]
A news search tool is one request to our API. Define it with `tool()` and give it to any model:
```bash
npm install ai zod
```
```ts title="news-search.ts"
import { tool } from 'ai';
import { z } from 'zod';
type Result = { title: string; url: string; source: string | null; published_at: string | null; score: number; snippet: string | null };
export const newsSearch = tool({
description:
'Search recent news articles. Returns the title, URL, source, publication time and a relevance score (0–1) of each one.',
inputSchema: z.object({
query: z.string().describe('What to search for, in any language'),
days: z.number().int().min(1).max(365).optional().describe('Only articles from the last N days'),
}),
execute: async ({ query, days }) => {
const res = await fetch('https://api.typesearch.ai/v1/search', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.TYPESEARCH_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ query, days, mode: 'fast', max_results: 8 }),
});
if (!res.ok) throw new Error(`typesearch ${res.status}: ${await res.text()}`);
const { results } = (await res.json()) as { results: Result[] };
// Only what the model needs to answer and cite: fewer tokens in its context.
return results.map(({ title, url, source, published_at, score, snippet }) => ({ title, url, source, published_at, score, snippet }));
},
});
```
```ts
import { generateText, stepCountIs } from 'ai';
import { newsSearch } from './news-search';
const { text } = await generateText({
model: 'anthropic/claude-sonnet-4.5',
tools: { newsSearch },
stopWhen: stepCountIs(5),
prompt: 'What changed in EU AI Act enforcement this week? Cite your sources.',
});
```
`streamText` and agents take the same tool. The search runs in `fast` mode, $1.40 per 1,000 searches;
change it to `'normal'` to have the top results read before they’re ranked ($2.20). Every parameter is
in the [API reference](/docs/api-reference/search).
## Tips [#tips]
* **Keep the result small.** Return the fields the model cites, as above: a full response has more than it
needs.
* **Let the model choose the time window.** `days` in the schema lets it ask for today’s news or the last
month’s, depending on the question.
* **Threshold on `score`.** It’s a calibrated probability: filter out results under `0.5` before the model
sees them if your prompts need only what’s clearly on topic.
# SDKs (/docs/sdks)
`typesearch-js` and `typesearch` aren’t on npm and PyPI yet: these pages describe them ahead of their
release. Until then, call the [HTTP API](/docs/quickstart) directly, or use the
[Exa SDK pointed at typesearch](/docs/integrations/exa-compatible).
Both SDKs cover the whole API, with the same names as the HTTP API, typed responses, streaming, jobs,
automatic retries and typed errors. They read your key from `TYPESEARCH_API_KEY`.
| | TypeScript | Python |
| ---------------- | ------------------------------------------------------------- | ------------------------------------------------------------------- |
| Search | `search(query, options)` | `search(query, **options)` |
| Stream a search | `searchStream(query, options)` | `search_stream(query, **options)` |
| Similar | `similar(url, options)` | `similar(url, **options)` |
| Contents | `contents(urls, options)` | `contents(urls, query=…)` |
| Live site search | `siteSearch()` · `siteSearchAndWait()` · `siteSearchStream()` | `site_search()` · `site_search_and_wait()` · `site_search_stream()` |
| Jobs | `jobs.get(id)` · `jobs.wait(id)` | `jobs.get(id)` · `jobs.wait(id)` |
| Usage | `usage()` | `usage()` |
## Already on Exa, Tako or Perplexity? [#already-on-exa-tako-or-perplexity]
Their official SDKs work against typesearch as they are: point them at our compatibility layer and use a
typesearch key. See [Migrate from Exa](/docs/migration/exa), [Tako](/docs/migration/tako) or
[Perplexity](/docs/migration/perplexity).
## Any other language [#any-other-language]
The API is plain JSON over HTTPS, described by an [OpenAPI 3.1 document](/openapi.json) you can feed to
any client generator.
# Python SDK (/docs/sdks/python)
`typesearch` isn’t published yet: this page describes it ahead of its release. Until then, call the
[HTTP API](/docs/quickstart) directly, or use the [Exa SDK pointed at typesearch](/docs/integrations/exa-compatible).
```bash
pip install typesearch
```
Python 3.9+. Built on `httpx` and `pydantic`: every response is a typed model, and unknown fields from a
newer API are kept instead of breaking your code.
## Create a client [#create-a-client]
```python
from typesearch import Typesearch
ts = Typesearch() # reads TYPESEARCH_API_KEY
```
| Argument | Default | |
| ----------------- | --------------------------- | --------------------------------------------------------------------------------- |
| `api_key` | `TYPESEARCH_API_KEY` | Your key. |
| `base_url` | `https://api.typesearch.ai` | Or `TYPESEARCH_BASE_URL`. |
| `timeout` | `70.0` | Seconds before a request is aborted. A `deep` search can take about a minute. |
| `max_retries` | `2` | Retries on connection errors, timeouts, `429 rate_limited` and `5xx`. |
| `default_headers` | — | Headers sent with every request. |
| `http_client` | — | Your own `httpx.Client` (or `httpx.AsyncClient` for async), for proxies or tests. |
Use it as a context manager to close connections when you are done: `with Typesearch() as ts: …`.
## Search [#search]
```python
res = ts.search(
"el dólar",
mode="normal",
max_results=10,
include_domains=["reddiaria.example", "diarioejemplo.example"],
published_after="2026-09-20",
highlights=True,
)
for r in res.results:
print(f"{r.score:.2f}", r.title, r.highlights[:1])
```
Keyword arguments have the same names as the [HTTP API](/docs/api-reference/search). Dates accept strings
or `datetime.date` / `datetime.datetime`. `days=None` searches the whole index; leaving `days` out keeps
the default of 7.
Several queries at once:
```python
res = ts.search(["el dólar", "el FMI"], mode="fast")
for group in res.groups or []:
print(group.query, group.total)
```
## Stream [#stream]
```python
with ts.search_stream("el dólar", mode="deep") as stream:
for event in stream:
if event.type == "step":
print("·", event.step.text)
elif event.type == "partial":
render(event.response.results)
elif event.type == "result":
render(event.response.results)
# Or only the final result
final = ts.search_stream("el dólar").final_response()
```
An `error` event is raised as an [`APIError`](#errors).
## Similar and contents [#similar-and-contents]
```python
similar = ts.similar("https://diarioejemplo.example/economia/…", exclude_domains=["diarioejemplo.example"])
pages = ts.contents(["https://reddiaria.example/economia/…"], query="el dólar")
```
## Live site search [#live-site-search]
```python
# Create the job and wait for it
res = ts.site_search_and_wait("diarioejemplo.example", "el dólar", mode="normal")
# Or handle the job yourself
job = ts.site_search("diarioejemplo.example", "el dólar")
done = ts.jobs.wait(job.id, poll_interval=2, timeout=120)
# Or stream it, like a search, instead of a job
with ts.site_search_stream("diarioejemplo.example", "el dólar") as stream:
for event in stream:
...
```
`jobs.wait()` raises `JobFailedError` if the job fails. `site_search_and_wait()` waits up to
`wait_timeout` seconds (120 by default) and then raises `APITimeoutError`.
## Usage [#usage]
```python
# Your key’s usage and limits
usage = ts.usage()
```
Every result shows its source and links to the original. Missing an outlet? Suggest it from the dashboard (Suggest a source) or write to [support@typesearch.ai](mailto:support@typesearch.ai).
## Async [#async]
`AsyncTypesearch` has the same methods, awaitable:
```python
import asyncio
from typesearch import AsyncTypesearch
async def main():
async with AsyncTypesearch() as ts:
res = await ts.search("el dólar", mode="fast")
async for event in ts.search_stream("el FMI"):
...
asyncio.run(main())
```
## Errors [#errors]
Every error subclasses `TypesearchError`. API errors are `APIError` subclasses with `status`, `code`,
`request_id` and, for invalid requests, `errors` per field.
| Class | When |
| ---------------------------------------- | ------------------------------------------------------------------------- |
| `BadRequestError` | 400 |
| `AuthenticationError` | 401 |
| `BudgetError` | 402 — `budget_too_small`, `insufficient_credits` or `spend_limit_reached` |
| `PermissionDeniedError` | 403 |
| `NotFoundError` | 404 |
| `RateLimitError` | 429 — with `retry_after` |
| `InternalServerError` | 5xx |
| `APIConnectionError` · `APITimeoutError` | No response, or too slow |
| `JobFailedError` | A live site search job failed |
```python
from typesearch import APIError, BadRequestError, RateLimitError
try:
ts.search("x")
except BadRequestError as e:
print(e.code, e.errors)
except RateLimitError as e:
print(e.code, e.retry_after)
except APIError as e:
print(e.status, e.code, e.request_id)
```
## Retries and timeouts [#retries-and-timeouts]
Connection errors, timeouts, `429 rate_limited` and `5xx` are retried twice with exponential backoff and
jitter, honouring `Retry-After`; `quota_exceeded` never is. Set `max_retries=0` on the client to handle
them yourself, or override the client’s settings for one call:
```python
res = ts.search("el dólar", mode="deep", timeout=90, max_retries=0, extra_headers={"X-Trace-Id": "abc"})
```
`search()`, `search_stream()`, `similar()`, `contents()` and the site search methods take `timeout`,
`max_retries` and `extra_headers`; `usage()` takes `timeout` and `extra_headers`.
# TypeScript SDK (/docs/sdks/typescript)
`typesearch-js` isn’t published yet: this page describes it ahead of its release. Until then, call the
[HTTP API](/docs/quickstart) directly, or use the [Exa SDK pointed at typesearch](/docs/integrations/exa-compatible).
npm
pnpm
yarn
bun
```bash
npm install typesearch-js
```
```bash
pnpm add typesearch-js
```
```bash
yarn add typesearch-js
```
```bash
bun add typesearch-js
```
Works in Node 18+, Bun, Deno, Cloudflare Workers and other edge runtimes. No dependencies. ESM and
CommonJS, fully typed.
## Create a client [#create-a-client]
```ts
import Typesearch from 'typesearch-js';
const ts = new Typesearch(); // reads TYPESEARCH_API_KEY
```
| Option | Default | |
| ---------------- | --------------------------- | ---------------------------------------------------------------------------------- |
| `apiKey` | `TYPESEARCH_API_KEY` | Your key. `new Typesearch('ts_live_…')` works too. |
| `baseURL` | `https://api.typesearch.ai` | Or `TYPESEARCH_BASE_URL`. |
| `timeout` | `70000` | Milliseconds before a request is aborted. A `deep` search can take about a minute. |
| `maxRetries` | `2` | Retries on connection errors, timeouts, `429 rate_limited` and `5xx`. |
| `defaultHeaders` | — | Headers sent with every request. |
| `fetch` | global `fetch` | A custom implementation, for proxies or tests. |
## Search [#search]
```ts
const res = await ts.search('el dólar', {
mode: 'normal',
max_results: 10,
include_domains: ['reddiaria.example', 'diarioejemplo.example'],
published_after: '2026-09-20',
highlights: true,
});
for (const r of res.results) {
console.log(r.score.toFixed(2), r.title, r.highlights[0]);
}
```
Options and response fields have the same names as the [HTTP API](/docs/api-reference/search) and are
typed: `SearchOptions`, `SearchResponse`, `Result` and the rest are exported.
Several queries at once:
```ts
const res = await ts.search(['el dólar', 'el FMI'], { mode: 'fast' });
res.groups?.forEach((g) => console.log(g.query, g.total));
```
## Stream [#stream]
```ts
for await (const event of ts.searchStream('el dólar', { mode: 'deep' })) {
switch (event.type) {
case 'step':
console.log('·', event.step.text);
break;
case 'partial':
render(event.response.results);
break;
case 'result':
render(event.response.results);
}
}
// Or only the final result
const final = await ts.searchStream('el dólar').finalResponse();
```
An `error` event is thrown as an [`APIError`](#errors) from the loop.
## Similar and contents [#similar-and-contents]
```ts
const similar = await ts.similar('https://diarioejemplo.example/economia/…', { exclude_domains: ['diarioejemplo.example'] });
const pages = await ts.contents(['https://reddiaria.example/economia/…'], { query: 'el dólar' });
```
## Live site search [#live-site-search]
```ts
// Create the job and wait for it
const res = await ts.siteSearchAndWait('diarioejemplo.example', 'el dólar', { mode: 'normal' });
// Or handle the job yourself
const job = await ts.siteSearch('diarioejemplo.example', 'el dólar');
const done = await ts.jobs.wait(job.id, { pollInterval: 2000, waitTimeout: 120_000 });
// Or stream it
for await (const event of ts.siteSearchStream('diarioejemplo.example', 'el dólar')) {
// …
}
```
`jobs.wait()` throws `JobFailedError` if the job fails.
## Usage [#usage]
```ts
// Your key’s usage and limits
const usage = await ts.usage();
```
Every result shows its source and links to the original. Missing an outlet? Suggest it from the dashboard (Suggest a source) or write to [support@typesearch.ai](mailto:support@typesearch.ai).
## Errors [#errors]
Every error extends `TypesearchError`. API errors are `APIError` subclasses with `status`, `code`,
`requestId` and, for invalid requests, `errors` per field.
| Class | When |
| ---------------------------------------- | ------------------------------------------------------------------------- |
| `BadRequestError` | 400 |
| `AuthenticationError` | 401 |
| `BudgetError` | 402 — `budget_too_small`, `insufficient_credits` or `spend_limit_reached` |
| `PermissionDeniedError` | 403 |
| `NotFoundError` | 404 |
| `RateLimitError` | 429 — with `retryAfter` |
| `InternalServerError` | 5xx |
| `APIConnectionError` · `APITimeoutError` | No response, or too slow |
| `JobFailedError` | A live site search job failed |
```ts
import { BadRequestError, RateLimitError, APIError } from 'typesearch-js';
try {
await ts.search('x');
} catch (e) {
if (e instanceof BadRequestError) console.log(e.code, e.errors);
else if (e instanceof RateLimitError) console.log(e.code, e.retryAfter);
else if (e instanceof APIError) console.log(e.status, e.code, e.requestId);
else throw e;
}
```
## Retries, timeouts and cancelling [#retries-timeouts-and-cancelling]
Connection errors, timeouts, `429 rate_limited` and `5xx` are retried twice with exponential backoff and
jitter, honouring `Retry-After`; `quota_exceeded` never is. Every method takes a last argument to override
the client’s settings per request — `{ timeout, maxRetries, signal, headers }`:
```ts
const controller = new AbortController();
const res = await ts.search('el dólar', { mode: 'deep' }, {
timeout: 90_000,
maxRetries: 0,
signal: controller.signal,
headers: { 'X-Trace-Id': 'abc' },
});
```
For a stream, `timeout` covers the wait until the response starts; aborting the signal also stops reading it.
# Charts (/docs/guides/charts)
`POST /v1/charts` turns a question in plain words into a chart card that already made the
data-visualization decisions: which chart, what to measure, how to aggregate it (totals, shares, weekly
grouping, a moving average, a base-100 index), the key figures on top, a title that states the finding
("The blue dollar rose 4.1% this week"), annotations, and the source of every value. You can also send your
own data and get the same card.
From a query, it first works out what you expect to see and then gets that data: "standings last Formula 1
race" finds the latest race and returns its finishing positions, "F1 winners" counts wins per driver this
season, "elections in Brazil" shows poll shares per candidate, and "news in Diario Ejemplo this week" shows
what that outlet published, by section.
* **The chart that tells it.** One of 12 types, picked for the data. Force another and it's respected while
the data allows it.
* **Numbers you can check.** A figure, a position or a poll share is found in what was published, checked
against its source, and comes with the articles it came from. It is never invented.
* **Always something to show.** While there are articles on the topic, you get a chart. If the exact
data isn't published or takes too long, you get the closest thing, and the response says so.
* **Ready to publish.** An iframe that adapts to its width, a PNG at 2× and an SVG, in four themes, public
by id. From the API, the [MCP server](/docs/integrations/mcp) or the dashboard's
[playground](https://app.typesearch.ai/playground).
* **Fast.** A card is drawn in under a millisecond on our side. Coverage charts come back in about a second;
charts with figures, in 2 to 5 seconds. Exact data that has to be read from the articles can take longer,
so a chart from a query answers within about 10 seconds with the best it has, and a stored chart keeps
improving after that.
## From a query [#from-a-query]
cURL
```bash
curl https://api.typesearch.ai/v1/charts \
-H "Authorization: Bearer $TYPESEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "query": "blue dollar this week" }'
```
It answers `201` with the card:
```json title="201 Created (abridged)"
{
"id": "chart_8k2m9q4x7w1p3n5z",
"object": "chart",
"type": "line",
"theme": "light",
"locale": "en",
"chart": {
"type": "line",
"title": "The blue dollar rose 4.1% this week",
"subtitle": "Selling rate, in pesos · Sep 19–25",
"x": { "kind": "time", "granularity": "day" },
"y": { "format": "currency", "currency": "ARS", "label": "Selling" },
"series": [
{
"name": "Selling",
"points": [
{ "x": "2026-09-19", "y": 1215, "sources": [2] },
{ "x": "2026-09-24", "y": 1250, "sources": [1] },
{ "x": "2026-09-25", "y": 1265, "sources": [0, 1] }
]
}
],
"kpis": [
{ "label": "Latest", "value": 1265, "change": { "value": 4.1, "percent": true } },
{ "label": "High", "value": 1265 },
{ "label": "Low", "value": 1215 }
]
},
"compatible_types": ["line", "area", "bar", "kpi", "table"],
"plan": {
"recipe": "figure_trend",
"type": "line",
"confidence": 0.92,
"alternatives": [
{ "type": "area", "probability": 0.3 },
{ "type": "bar", "probability": 0.22 },
{ "type": "kpi", "probability": 0.14 }
],
"topic": "the blue dollar",
"needs": { "queries": ["blue dollar this week"], "mode": "fast", "days": 7, "min_points": 3, "max_results": 50 }
},
"interpretation": { "label": "The blue dollar over time", "confidence": 1, "alternatives": [] },
"rung": "exact",
"improving": false,
"sources": [
{
"url": "https://diario.example/economia/dolar-blue-hoy",
"title": "El dólar blue cerró a 1.265 pesos",
"source": "Diario Ejemplo",
"published_at": "2026-09-25T18:10:00Z"
},
{
"url": "https://noticias.example/mercados/cotizacion",
"title": "Cotización del dólar: el blue tocó su máximo del mes",
"source": "Noticias Ejemplo",
"published_at": "2026-09-24T17:02:00Z"
},
{
"url": "https://cronica.example/economia/blue-arranca-la-semana",
"title": "El blue arranca la semana en 1.215 pesos",
"source": "La Crónica Demo",
"published_at": "2026-09-19T15:40:00Z"
}
],
"embed_url": "https://api.typesearch.ai/embed/chart_8k2m9q4x7w1p3n5z",
"image_url": "https://api.typesearch.ai/embed/chart_8k2m9q4x7w1p3n5z.png",
"svg_url": "https://api.typesearch.ai/embed/chart_8k2m9q4x7w1p3n5z.svg",
"svg": null,
"created_at": "2026-09-26T14:02:11.482Z",
"expires_at": "2026-12-25T14:02:11.482Z",
"cached": false,
"usage": { "cost_usd": 0.0019, "mode": "fast", "queries": 1, "duration_ms": 2840 },
"warnings": []
}
```
* **`chart`**: what is drawn. The series, the key figures on top (`kpis`), the annotations, how each axis is
formatted, and a `title` that states the finding (unless you send your own `title`).
* **`plan`**: what was measured (`recipe`), the chart it chose, and `alternatives`: other types that fit,
which you can switch to for free with [`PATCH`](#change-a-chart).
* **`compatible_types`**: every type this data supports.
* **`interpretation`**: what we understood you expect to see, and other readings you can ask for. See
[What we understood](#what-we-understood).
* **`rung`** and **`improving`**: how close the chart is to what you asked (`exact` here), and whether a
better one is still on its way. See [The closest thing](#the-closest-thing).
* **`sources`** and each point's **`sources`**: the articles behind the chart, and the indexes in `sources`
where each value was published.
* **`embed_url`**, **`image_url`** and **`svg_url`**: public by id, no key needed. See
[Embed it](#embed-it) and [PNG and SVG](#png-and-svg).
* **`usage.cost_usd`**: what the request was billed. See [Pricing](#pricing).
Everything you know from search narrows a chart too: `days`, `published_after`, `published_before`,
`countries`, `languages`, `include_domains`, `exclude_domains` and `timezone`. When the query doesn't name the
things to compare with "vs", pass them in `compare` (2 to 5).
### What you can ask [#what-you-can-ask]
We read your query, work out what you expect to see and pick a recipe: what to measure, the chart that
tells it, the days to look at and the cheapest mode that gets the data (`mode: "auto"`, the default).
| `recipe` | Ask for | What it measures | Chart | Days | Mode |
| -------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| `data` | "standings last Formula 1 race", "F1 winners", "elections in Brazil" | The data itself: the finishing positions of the latest race, wins per driver this season, poll or result shares per candidate. Each value is checked against the articles that published it. | `table` for positions, `bar_horizontal` or `bar` for counts and shares | Set by the question: the latest event, the season | `fast`, or `normal` when it reads the articles |
| `series` | "euro vs Argentine peso this month", "blue dollar in September 2026", "country risk from the 1st to the 15th" | A figure over time: the rate day by day, read in the articles that publish it (one per day), each value checked against its source. `vs` between currencies is one exchange rate; "official vs blue" draws two lines. | `line`, or `kpi` for today's value | The period you name ("September 2026", "this month", "from the 1st to the 15th"), or 30 days | `fast`, or `normal` when it reads the articles |
| `outlet` | "news in Diario Ejemplo this week" | What that outlet published, by section, in percentages. | `bar_horizontal` | 7, or since Monday for "this week" | `ultra` |
| `coverage` | "coverage of the Budget" | Articles that mention the topic, per day, over the whole period. A clear peak is marked with that day's headline. | `area` | 30 | `ultra` |
| `coverage_compare` | "Milei vs Bullrich coverage over time" | The same count for 2 to 5 names, one line each. | `line` | 21 | `ultra` |
| `share_of_voice` | "Milei vs Bullrich vs Kicillof coverage" | Each name's share of the articles. | `donut` | 30 | `ultra` |
| `tone` | "tone of the coverage of the IMF" | Positive, neutral and negative articles, relative to your query. | `donut` | 14 | `fast` |
| `tone_compare` | "tone of Milei vs Bullrich coverage" | The same split, for each name. | `stacked_bar` | 30 | `fast` |
| `outlets` | "which outlets cover lithium" | Articles per outlet. | `bar_horizontal` | 30 | `ultra` |
| `timeline` | "timeline of the dam case" | The milestones of a story: the busiest day of each week, and the story most outlets told that day. | `timeline` | 60 | `fast` |
| `figure_trend` | "blue dollar this week" | A figure the articles publish (a price, a rate, a poll), day by day. | `line`, or `kpi` for "today" | 7 | `fast` |
| `figure_compare` | "inflation" with `compare: ["Chile", "Peru"]` | The latest published value of a figure, for each name. | `bar` | 14 | `fast` |
| `answers` | Any query, plus one typed question in `questions` | How the answers split across the articles. | `donut` | 7 | `fast` |
| `index_prices` | "prices" with `index` | Price per product, cheapest first (up to 12). | `bar_horizontal` | whole index | `fast` |
| `index_scatter` | "price and rating" with `index` | Price against rating. Each dot's size is its number of reviews. | `scatter` | whole index | `fast` |
| `index_availability` | "availability" with `index` | Products in stock, out of stock and on pre-order. | `donut` | whole index | `fast` |
| `index_brands` | "brands" with `index` | Products per brand. | `donut` | whole index | `fast` |
When the exact data can't be shown, `recipe` can also be `people` (the names mentioned most on the topic) or
`stories` (the most covered stories, in a table). See [The closest thing](#the-closest-thing).
* **Days.** The defaults above apply unless a date in the query ("this week", "in August"), `days`,
`published_after` or `published_before` sets the period.
* **Mode.** Set `mode` to fix it. In `auto`, a figure chart that doesn't find enough figures in headlines and
standfirsts reads the best articles (`normal`), and so does exact data that isn't in the headlines; the
chart is billed in that mode. With `mode` set to `ultra` or `fast`, it never reads the articles.
* **Words first.** When the query says it ("coverage", "tone", "which outlets", "timeline", "news in" an
outlet, "vs"), the words decide (`interpretation.confidence: 1`). Otherwise we pick the most likely reading,
return the others in `interpretation.alternatives`, and remember the reading for 7 days.
* **An outlet in the query.** Name an outlet ("Milei in Diario Ejemplo") and the chart only counts what it
published.
* **Custom index.** With `index` (`idx_…`), the chart is built from the structured data each page of your
[custom index](/docs/guides/custom-index) declares: price, rating, reviews, availability, brand.
A typed question makes an `answers` chart: how the articles answer it. One question per chart, with the
same format as in [structured output](/docs/guides/structured-output):
```json title="Request body"
{
"query": "the 2027 Budget",
"questions": {
"stance": {
"type": "choice",
"instructions": "What is the article’s stance on the budget?",
"criteria": { "supportive": null, "critical": null, "neutral": "Reports without taking a side." }
}
}
}
```
## What we understood [#what-we-understood]
A query in words can mean more than one thing. `interpretation` says how we read it:
* **`label`**: what we understood you expect to see, in words.
* **`confidence`**: how sure we are of that reading, from 0 to 1.
* **`alternatives`**: other readings of the same query, each with its `probability`.
cURL
```bash
curl https://api.typesearch.ai/v1/charts \
-H "Authorization: Bearer $TYPESEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "query": "Formula 1" }'
```
```json title="201 Created (abridged)"
{
"id": "chart_5r2k8w1m9q3x7v4n",
"type": "table",
"interpretation": {
"label": "Finishing positions of the last Formula 1 race",
"confidence": 0.64,
"alternatives": [{ "label": "Wins per driver in the 2026 Formula 1 season", "probability": 0.36 }]
},
"rung": "exact",
"improving": false
}
```
To chart another reading, send the same query with that alternative's `label` as `interpretation`:
cURL
```bash
curl https://api.typesearch.ai/v1/charts \
-H "Authorization: Bearer $TYPESEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "Formula 1",
"interpretation": "Wins per driver in the 2026 Formula 1 season"
}'
```
A label that isn't one of the readings of that query is ignored, and the query is read as usual. To see the
reading and its alternatives before charting, send `plan_only: true`: it's free. `interpretation` is `null`
for your own data, a custom index and a typed question, where there is nothing to interpret.
## The closest thing [#the-closest-thing]
While there are articles on the topic, a chart from a query doesn't fail. If the exact data isn't published,
can't be checked or takes too long, you get the closest thing we can show, and `rung` says how close it is:
| `rung` | What the chart shows | Warning |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------------- |
| `exact` | What you asked for: the data itself, complete and checked against its sources, or the measure you named (coverage, when you asked for coverage). | |
| `partial` | A checked part of it, like the podium when the full classification isn't published. | `partial_data` |
| `related` | Another view of the same question, like the names mentioned most on the topic. | `fallback` |
| `coverage` | The coverage of the topic over time, or its timeline. | `fallback` |
| `stories` | The most covered stories on the topic, in a table. | `fallback` |
The warning's `message` says what was asked and what is shown instead. `rung` is `null` for your own data, a
custom index and a typed question.
### Still improving [#still-improving]
A chart from a query answers within about 10 seconds with the best it has. If the exact data is still on its
way, a stored chart comes back with `improving: true` and the `improving` warning, and we keep looking. When
the data arrives, it replaces the chart in place: same `id`, and the embed and the image update themselves
(within the 5 minutes they are cached). If it doesn't arrive, the chart stays as it is.
* **To know when it's done**, poll `GET /v1/charts/{id}`: it shows `improving: true` until then. Or send
`stream: true` and get the better chart as an `improved` event. See [Stream the progress](#stream-the-progress).
* **It isn't billed.** You pay for the chart you got first, and the improvement is free.
* **Your changes win.** If you change the chart with `PATCH` before the improvement arrives, your change stays
and the improvement is dropped.
* **Without storing it** (`store: false`) there is nothing to update later, so the request waits longer for
the exact data before it answers.
Data checked recently comes back at once, with the `verified_earlier` warning saying the day it was checked.
## How figures are found [#how-figures-are-found]
A chart with figures (`figure_trend`, `figure_compare`) never makes a number up:
* **Verbatim.** Numbers are found as written in the headlines, standfirsts and excerpts of the articles,
with their currency or unit. Each one is then only confirmed, or discarded, as the value you asked for.
* **One value per day.** Each point is the median of what was published that day, so two outlets with
slightly different figures don't draw a zigzag. In a comparison, each name gets its latest published value.
* **A source for every point.** Each point's `sources` lists indexes in the response's `sources`: the articles
where that value was published. Show them in a tooltip or a footnote.
```json title="A point, and where it was published"
{ "x": "2026-09-25", "y": 1265, "sources": [0, 1] }
```
If a publisher later asks not to be shown, its sources leave the card when it is served, and a point left
without a source is hidden.
### Standings, results and polls [#standings-results-and-polls]
A chart of the data itself (`data`) shows only the values it could check:
* **The event, resolved.** "The last race" becomes the latest race that was actually run, and only articles
from its days count, so the previous race doesn't leak in.
* **Read where it's published.** It reads the articles that carry the data (and, for a season, its reference
page), not just the headlines. Wins in a season are counted from the headlines that report each win, one
per event.
* **Checked row by row.** The name and the value have to be written in the article, and the value has to make
sense (a position in the race, a share under 100%). When sources disagree, most of them win.
* **Nothing filled in.** A row that can't be checked is left out. If some are missing, the chart is `partial`
and says so.
In a table, each row's value is also in `series`, with the `sources` where it was published.
## From your own data [#from-your-own-data]
Send `data` instead of `query` and you get the same card: we pick the chart (with `type: "auto"`), sort,
summarize, add the key figures and write a title (unless you send `title`). It costs $0.30 per 1,000
charts. `source` sets the line in the footer.
### Series [#series]
cURL
```bash
curl https://api.typesearch.ai/v1/charts \
-H "Authorization: Bearer $TYPESEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"data": {
"series": [
{
"name": "Subscribers",
"points": [
{ "x": "2026-06-01", "y": 18200 },
{ "x": "2026-07-01", "y": 19650 },
{ "x": "2026-08-01", "y": 21400 },
{ "x": "2026-09-01", "y": 23900 }
]
}
],
"y": { "label": "Subscribers", "format": "number" }
},
"source": "Diario Ejemplo, internal data"
}'
```
* **`x`** is a date (`2026-09-01`) for a time axis, a number for a numeric one (a scatter), or text for
categories. To say it yourself, set `data.x.kind` to `time`, `category` or `number`.
* **`data.y`** formats the values: `format` (`number`, `percent` or `currency`, with `currency: "USD"`),
`unit`, `decimals`, `label`, and `higher_is_better: false` when lower is better (a price): rankings
then put the lowest first.
* **For a scatter**, `data.x_measure` formats the x axis, each point's `r` sizes its dot and `label` names it.
* **`role`** on a series (`positive`, `neutral`, `negative`, `other`) colors it as tone. **`highlight`** names
the category to stand out.
### Events [#events]
A list of dated events makes a timeline:
```json title="Request body"
{
"data": {
"events": [
{ "date": "2026-07-30", "label": "Technical report on the dam leaks", "detail": "Diario Ejemplo" },
{ "date": "2026-08-06", "label": "The province halts construction" },
{ "date": "2026-08-19", "label": "The builder files an injunction", "end": "2026-08-22" }
]
},
"title": "The dam case, in three milestones"
}
```
### Columns and rows [#columns-and-rows]
```json title="Request body"
{
"data": {
"columns": [
{ "key": "model", "label": "Model" },
{ "key": "price", "label": "Price", "format": { "format": "currency", "currency": "USD" } },
{ "key": "rating", "label": "Rating", "format": { "decimals": 1 } }
],
"rows": [
{ "model": "Ridgeline Trail 4", "price": 109, "rating": 4.6 },
{ "model": "Canyon Grip GTX", "price": 119, "rating": 4.4 }
]
}
}
```
| Limit | |
| ------- | -------------------------------- |
| Series | 12, with up to 1,000 points each |
| Events | 50 |
| Columns | 8 |
| Rows | 100 |
With `type: "auto"`, events make a `timeline`; columns and rows, a `table`; a numeric `x`, a `scatter`; dates,
a `line` (a `kpi` for one or two points); several series of categories, a `stacked_bar`; percentages that
add up to 100 in up to 6 parts, a `donut`; more than 7 categories, a `bar_horizontal`; fewer, a `bar`.
## Chart types [#chart-types]
`type: "auto"` (the default) picks the one that tells the data best. The other 12:
| `type` | When auto picks it | What the data needs |
| ---------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `line` | A figure over time, or several names over time. | 3 points in time or more. |
| `area` | Coverage over time: a volume. | 3 points in time or more. |
| `bar` | A few categories, or a figure compared across names. | Up to 12 categories. |
| `bar_horizontal` | Rankings: outlets, prices, long names or more than 12 categories. | One series of categories. |
| `stacked_bar` | Each category split into parts: tone by name. | 2 series or more, positive values, up to 12 categories. |
| `pie` | Parts of a whole. | Positive values, 2 to 30 parts. |
| `donut` | Parts of a whole, with the total in the center: share of voice, tone, brands, availability. | Positive values, 2 to 30 parts. |
| `scatter` | Two figures against each other: price and rating. | A numeric `x`, 5 points or more. |
| `funnel` | Stages that shrink. | One series of 3 to 7 categories that never grows. |
| `timeline` | The milestones of a story. | Dated events. |
| `kpi` | One figure with its change: "today", "now". | One point or more. |
| `table` | Rows and columns. | Always possible. |
Forcing a type (`"type": "donut"`) works while the data allows it. When it doesn't, the card is drawn with
the closest type that fits and says so in `warnings` with `chart_type_adjusted`. `compatible_types` lists
the types this data supports, best first.
## The rules we apply [#the-rules-we-apply]
What someone who makes charts for a living would do, applied to every card. The same data always gives the
same card.
* **Long daily series become weeks (past 45 days) or months (past 8 months)**, adding up counts and keeping
the last value of a figure (`grouped`).
* **No dual axes.** Lines on very different scales (more than 25 times apart) are compared as an index, base
100 (`indexed`).
* **Noisy daily series of three weeks or more get a 7-point moving average**, with the raw series faint
underneath (`chart.smoothing`).
* **Pies and donuts show at most 6 slices**, with the rest in "Others", always last. Rankings show at most 12
bars; lines, areas and stacked bars, up to 6 series.
* **Rankings sort biggest first**, or lowest first when lower is better: a price, inflation, country risk.
* **Long category names turn vertical bars into a horizontal ranking**, so every label fits.
* **A log scale only on lines, and only when the values span more than 200 times.** Never on bars.
* **Key figures on top**: total, peak and daily average for coverage; latest (with its change), high and low
for a figure.
* **The title states the finding**: "Budget coverage peaked on Sep 10", "Milei gets 42% of the coverage",
"The priciest models are not the best rated".
## Themes [#themes]
| `theme` | Looks | For |
| ----------- | ------------------------------------- | ------------------------- |
| `light` | White and warm grays. The default. | Apps, dashboards, docs. |
| `dark` | Near black. | Dark interfaces. |
| `editorial` | Warm paper, print ink, serif figures. | Media and blogs. |
| `electric` | Deep blue gradient. | Slides and landing pages. |
The first series is always the typesearch electric blue, and the rest are told apart with color blindness
too. Set `theme` when you create the chart, switch it for free with `PATCH`, or show one view in another theme
with `?theme=` on the embed or the image.
## Embed it [#embed-it]
`embed_url` is a page made to go in an iframe. It adapts to its width (under 480 px it switches to a compact
layout), shows tooltips on hover and touch, weighs a few KB and loads nothing from third parties: the fonts
are ours.
```html title="HTML"
```
The page tells its parent the height it needs, on load and on every resize, with
`{ type: "typesearch:chart:resize", id, height }`. Listen for it once and every chart on the page fits
without scrollbars:
```html title="HTML"
```
`data.id` is the chart id, if you'd rather match by it. Add `?theme=dark` (or `editorial`, `electric`) to show
it in another theme without making a new chart.
Embeds, images and SVGs are cached for 5 minutes, so a change made with `PATCH` shows up within that time.
Each IP can load up to 600 per minute.
## PNG and SVG [#png-and-svg]
* **`image_url`** (`….png`): the card at twice the resolution, for chat, email or slides. In Markdown:
``.
* **`svg_url`** (`….svg`): the vector card.
* **`?size=compact`** gives either one in the narrow layout, for phones and stories, and **`?theme=`** in
another theme: `https://api.typesearch.ai/embed/chart_8k2m9q4x7w1p3n5z.png?size=compact&theme=electric`.
## Change a chart [#change-a-chart]
`PATCH /v1/charts/{id}` switches the `type`, the `theme`, the `title`, the `subtitle` or the `locale` (`en`
or `es`). It redraws from the same data, without searching again, and it's free. Pick the type from
`compatible_types`. Without a title of your own, the title is rewritten for the new type and language.
cURL
```bash
curl -X PATCH https://api.typesearch.ai/v1/charts/chart_8k2m9q4x7w1p3n5z \
-H "Authorization: Bearer $TYPESEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "type": "area", "theme": "editorial" }'
```
It answers with the updated chart. The `embed_url`, `image_url` and `svg_url` stay the same.
## List, get and delete [#list-get-and-delete]
* **`GET /v1/charts`**: your charts, newest first, `limit` at a time (up to 100). When `has_more` is `true`,
pass `next_before` as `before` for the next page.
* **`GET /v1/charts/{id}`**: one chart, with its data, plan and sources, and `improving: true` while it is
still [improving](#still-improving).
* **`DELETE /v1/charts/{id}`**: deletes it (`{ "id": "chart_…", "object": "chart", "deleted": true }`). Its
embed, image and SVG stop working.
Every key of your organization sees the same charts. Stored charts expire after 90 days on pay as you go and
never on the monthly credit plan: `expires_at` says when, or `null`. See the
[reference](/docs/api-reference/manage-charts).
## See the plan first [#see-the-plan-first]
`plan_only: true` returns what we understood (`interpretation`, with its alternatives) and the plan: the
recipe, the chart type, its alternatives and what it would search (`plan.needs`: queries, mode, days, minimum
points). It doesn't search or draw, and it's free. `chart` and `id` come back `null`.
## Without storing it [#without-storing-it]
`store: false` stores nothing: there is no `id` and no URLs, and the SVG comes in the response, in `svg`. It
costs the same. To get the SVG with a stored chart too, add `include_svg: true`.
## Stream the progress [#stream-the-progress]
Send `stream: true` and the response becomes a stream of
[Server-Sent Events](https://html.spec.whatwg.org/multipage/server-sent-events.html): what it's doing while it
works, the chart as soon as it's ready and, if the exact data arrives later, the better chart.
cURL
```bash
curl -N https://api.typesearch.ai/v1/charts \
-H "Authorization: Bearer $TYPESEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "query": "standings last Formula 1 race", "stream": true }'
```
| Event | `data` | When |
| ---------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `step` | `{ id, text, status, detail }` | A step starts (`running`: understanding the request, finding the event, reading the articles…) or ends (`done`, or `failed` before an `error`), with the same `id`. |
| `result` | A chart, like the `201` response | The chart. If it isn't `improving`, the stream ends after it. |
| `improved` | A chart, like the `201` response | The better chart, when the exact data arrives after `result`. The stream ends after it. |
| `error` | Problem details | Something failed, like `422 insufficient_data`. The stream ends after it. |
Here the finishing positions took longer than the deadline: the chart came back first with the names
mentioned most (`related`), and then with the standings (`exact`), under the same `id`.
```text title="The raw stream"
event: step
data: {"id":"chart-1","text":"Understanding the request","status":"running","detail":null}
event: step
data: {"id":"chart-1","text":"Understanding the request","status":"done","detail":null}
event: step
data: {"id":"chart-2","text":"Looking for: Finishing positions of the last Formula 1 race","status":"running","detail":null}
event: step
data: {"id":"chart-2","text":"Looking for: Finishing positions of the last Formula 1 race","status":"done","detail":null}
event: step
data: {"id":"chart-3","text":"Showing what is ready; still working on the rest","status":"running","detail":null}
event: step
data: {"id":"chart-3","text":"Showing what is ready; still working on the rest","status":"done","detail":null}
event: result
data: {"id":"chart_5r2k8w1m9q3x7v4n","object":"chart","type":"bar_horizontal","rung":"related","improving":true,…}
event: improved
data: {"id":"chart_5r2k8w1m9q3x7v4n","object":"chart","type":"table","rung":"exact","improving":false,…}
```
After a `result` with `improving: true`, the stream stays open for up to a minute waiting for `improved`. If
it closes without one, the chart may still improve: `GET /v1/charts/{id}` tells you. Step `text` is meant to
be shown to people and can change; use the event names in code. A request that isn't valid answers `400` as
usual, without a stream. See [Streaming](/docs/guides/streaming) for how search streams.
## Errors and warnings [#errors-and-warnings]
| Status | `code` | When |
| ------ | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| 400 | `invalid_request` | Neither `query` nor `data`, or both; a field out of range; a `PATCH` with nothing to change. `errors` lists each field. |
| 402 | `insufficient_credits`, `spend_limit_reached` | No credit left, or the key reached its monthly limit. |
| 404 | `chart_not_found` | No chart with that id for your organization: it never existed, it was deleted or it expired. |
| 422 | `insufficient_data` | From a query: no articles about the topic in the period. With `index` or `questions`: not enough data for the chart. Not billed. |
From a query in words, a chart only fails for lack of data when there isn't a single article about the topic
in the period. Anything less gets you [the closest thing](#the-closest-thing) instead. Try more days or a
broader query:
```json title="422"
{
"type": "urn:typesearch:error:insufficient_data",
"status": 422,
"code": "insufficient_data",
"detail": "No articles about «the dam case» in this period. Try more days or a broader query. Not billed.",
"request_id": "req_Vt4mQ8zK1pXa"
}
```
With a custom index or a typed question, `detail` says how many points the chart needed and how many it found.
Warnings don't stop the chart. They come in `warnings`, each with a `code` and a `message`:
| `code` | What happened |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `chart_type_adjusted` | The data doesn't fit the type you asked for: it was drawn with the closest one that does. |
| `grouped` | A long daily series was grouped by week, month or year. |
| `indexed` | Series on very different scales were compared as an index, base 100. |
| `partial_data` | Only part of the data you asked for could be checked: the chart shows that part (`rung: "partial"`). |
| `fallback` | The exact data isn't published, or couldn't be checked in time: the chart shows the closest thing (`rung` says which). |
| `improving` | We're still looking for the exact data: the stored chart updates itself if it arrives. |
| `verified_earlier` | The data was checked recently and comes from then: the message says the day. |
## Pricing [#pricing]
A chart from a query costs its search, in the mode it needed and per query, plus $0.50 per 1,000
charts. With one query:
| Mode | Per 1,000 charts | Used by default for |
| -------- | ---------------- | -------------------------------------------------------------------------------------------------------- |
| `ultra` | $1.50 | Coverage, share of voice, outlets. |
| `fast` | $1.90 | Tone, timelines, figures, answers, custom index, and data found in the headlines, like wins in a season. |
| `normal` | $2.70 | Figures, standings, results and polls, when it has to read the articles. |
| `deep` | $6.10 | Only when you set it. |
A chart that compares names searches one query per name: share of voice between three names in `ultra`
costs three searches at $1.00 per 1,000, plus one add-on. Finding exact data can search in several
languages or look up the event first: that is billed as one search. You pay for the chart you get, in the
mode it ended in, even when it is the closest thing. From your own data, a chart costs $0.30 per 1,000.
`usage.cost_usd` says what each one cost.
Free:
* Every view of an embed, image or SVG.
* `GET`, list, `PATCH` and `DELETE`.
* The same request within 10 minutes: the same chart comes back with `cached: true`. If only the search came
from the cache, you pay just the add-on.
* Failures, including `422 insufficient_data`.
* `plan_only`.
* The improvement of a chart that came back `improving`.
The list is in the [OpenAPI document](/openapi.json), in `x-pricing.per_1000_charts`:
`{ "query_addon": 0.5, "from_data": 0.3 }`. For reference, Tako's Search, which
returns chart cards, lists $7 per 1,000. See [Costs and caching](/docs/guides/cost-control).
## From the MCP server [#from-the-mcp-server]
The [MCP server](/docs/integrations/mcp)'s `create_chart` tool makes the same card from a query, so an agent
can answer with a chart:
```json title="create_chart"
{ "query": "Milei vs Bullrich vs Kicillof coverage", "theme": "dark" }
```
It takes `query`, `type`, `theme`, `days`, `compare`, `countries`, `languages`, `index` and
`interpretation`, and returns the `title`, `subtitle`, `image_url` (to show), `embed_url`, `key_figures`, the
values drawn in `data`, the rows of a table in `table`, `events`, what it `understood` and the other readings
in `alternatives` (pass one back as `interpretation`), `rung`, `improving` when the image will still update,
`sources` and `cost_usd`. Same key, same prices.
# Get contents (/docs/guides/contents)
`POST /v1/contents` takes up to 10 URLs and returns, for each one, its title, the description its
publisher gives it, its publication date and its source. With a `query`, the model also picks a short
verbatim excerpt about it and scores each page’s `relevance`.
TypeScript
Python
```ts
const res = await ts.contents(
['https://reddiaria.example/economia/…', 'https://diarioejemplo.example/politica/…'],
{ query: 'el Presupuesto 2027' },
);
for (const page of res.results) {
if (page.status === 'ok') console.log(page.relevance, page.title, page.highlights);
else console.log(page.url, page.error?.code);
}
```
```python
res = ts.contents(
["https://reddiaria.example/economia/…", "https://diarioejemplo.example/politica/…"],
query="el Presupuesto 2027",
)
for page in res.results:
if page.status == "ok":
print(page.relevance, page.title, page.highlights)
else:
print(page.url, page.error.code)
```
## Per-URL status [#per-url-status]
Each URL has its own `status`. A page that fails — unreachable, not an article, blocked — comes back with
`status: "error"` and a reason, and never fails the rest of the request. A page from a source that isn’t
available right now comes back with `source_unavailable`: we don’t say why, and it may be temporary.
## Price [#price]
Contents is billed per page read: $0.20 per 1,000 pages, or $0.40 with a `query`, when the
model reads each page to pick the excerpts and score it. A page that fails is not billed.
## Never the full text [#never-the-full-text]
Like a search engine, we return metadata, a link and at most one short verbatim excerpt per article: up
to 25 words, never from the first paragraph, and none for short articles. Never the full article. It
keeps publishers’ content where it belongs, and your context window lean.
# Costs and caching (/docs/guides/cost-control)
## What each request costs [#what-each-request-costs]
Credit is prepaid and spent per request at list price, in USD per 1,000:
| Request | Price | Billed |
| ----------------------------------------- | ------------------------------------------------------------------------------------ | --------------------- |
| Search, `ultra` | $1.00 | per query |
| Search, `fast` | $1.40 | per query |
| Search, `normal` | $2.20 | per query |
| Search, `deep` | $5.60 | per query |
| Similar | $2.20 ($4.60 in `deep`) | per request |
| Live site search | $2.40 | per request, any mode |
| Contents | $0.20 per 1,000 pages, $0.40 with a `query` | per page read |
| [Custom index](/docs/guides/custom-index) | $2.00 per 1,000 URLs a month; searches over it, as the mode | per day, prorated |
| [Charts](/docs/guides/charts) | From a query, its search in the mode it needed plus $0.50; from your own data, $0.30 | per chart |
Each query in a [multi-query request](/docs/guides/multiple-queries) is one search of its mode. Contents
bills only the pages that were read: a URL that fails is free. `usage.cost_usd` in every response says what
that request was billed, and [`GET /v1/usage`](/docs/api-reference/usage) returns the list in `pricing`.
A chart costs its search per query plus the add-on (from $1.50 per 1,000 in `ultra`); viewing its embed,
image or SVG is free, and so are `GET`, `PATCH` and `DELETE` on charts.
## Hard caps [#hard-caps]
Set `max_tokens` on a request (minimum 2000) and it will never spend more. The budget is reserved before
each model call; when the next call doesn’t fit, the search stops and returns everything judged so far
with `incomplete: true`.
```ts
const res = await ts.search('el dólar', { mode: 'deep', max_tokens: 15_000 });
if (res.incomplete) {
console.log(`Stopped at ${res.budget?.used} of ${res.budget?.max_tokens} tokens`);
}
```
If the cap is too small to judge even the headlines, the request fails with `402 budget_too_small`
before spending anything.
## What you don’t pay twice [#what-you-dont-pay-twice]
* **Result cache.** An identical request within 10 minutes is served from cache: `cached_at` says when
it was computed, `usage.tokens` is `0`, and it is not billed. Set `fresh: true` to skip it. The same chart
request within 10 minutes returns the same chart with `cached: true`, also free.
* **Failed requests.** A request that fails on our side is not billed, and neither is a chart that found no
articles on its topic (`422 insufficient_data`). The improvement of a chart that came back `improving` is
free too.
* **Judgment memory.** What the model said about a headline for a query is remembered for a week, and the
tone of an article too. A repeated search only pays for new headlines.
* **Filters first.** Domains, sections and dates are applied before anything is judged.
* **Several queries together.** Up to five [queries in one request](/docs/guides/multiple-queries) share
the same model calls.
## Seeing what you spend [#seeing-what-you-spend]
Every search response has a `usage` object with the model tokens, calls, time and `cost_usd` of that
request. [`GET /v1/usage`](/docs/api-reference/usage) returns your totals for today and the last 30 days,
your limits and the price list.
# Custom index (/docs/guides/custom-index)
A custom index is a list of URLs you choose, like product pages, investor-relations pages, agency bulletins
or reviews. We read them, keep them fresh and search them the way we search the news: a calibrated probability
on every page, the best candidates read before they are ranked, and typed answers in the same call. You search it with
the same `POST /v1/search`, passing `index`.
## Create an index [#create-an-index]
cURL
```bash
curl https://api.typesearch.ai/v1/indexes \
-H "Authorization: Bearer $TYPESEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Catalog" }'
```
It answers `201` with the index:
```json title="201 Created"
{
"id": "idx_8f3k2m9q",
"object": "index",
"name": "Catalog",
"status": "active",
"urls": { "total": 0, "ready": 0, "pending": 0, "failed": 0, "blocked": 0 },
"sources": [],
"last_read_at": null,
"created_at": "2026-09-26T14:02:11Z",
"updated_at": "2026-09-26T14:02:11Z"
}
```
`GET /v1/indexes` lists your indexes, with your plan’s `limits`; `GET /v1/indexes/{id}` returns one, and
`DELETE /v1/indexes/{id}` deletes it with everything in it (`{ "id": "idx_…", "deleted": true }`). You can
also create and manage indexes from the dashboard, in **Indexes**.
## Add URLs, a sitemap or a feed [#add-urls-a-sitemap-or-a-feed]
`POST /v1/indexes/{id}/urls` takes any of the three, in the same request:
| Field | What it adds |
| --------- | -------------------------------------------------------------------------- |
| `urls` | Up to 1,000 URLs per request. |
| `sitemap` | A sitemap or sitemap index: we add its URLs and check it again every hour. |
| `feed` | An RSS or Atom feed: we add its URLs and check it again every hour. |
cURL
```bash
curl https://api.typesearch.ai/v1/indexes/idx_8f3k2m9q/urls \
-H "Authorization: Bearer $TYPESEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"urls": ["https://shop.example/p/ridgeline-trail-4", "https://shop.example/p/canyon-grip-gtx"],
"sitemap": "https://shop.example/sitemap.xml"
}'
```
It answers `202 Accepted`: the URLs are queued and read right away.
```json title="202 Accepted"
{
"index": "idx_8f3k2m9q",
"added": 2,
"existing": 0,
"invalid": [],
"over_limit": 0,
"total": 2,
"sources": [
{
"url": "https://shop.example/sitemap.xml",
"type": "sitemap",
"status": "pending",
"read_at": null,
"urls": 0,
"error": null
}
],
"limits": { "urls": 100, "urls_used": 2, "indexes": 3, "indexes_used": 1 },
"warnings": []
}
```
* **`added`** and **`existing`**: the URLs that are new, and the ones that were already in the index.
* **`invalid`**: each URL that was rejected, with its `reason`: not a public `http` or `https` address, for
example.
* **`over_limit`**: URLs that didn’t fit in your plan’s limit and were not added. See
[limits and pricing](#limits-and-pricing).
* **`total`**: the URLs in the index now. **`sources`**: the sitemaps and feeds it follows.
* **`warnings`**: notes that don’t stop the request, each with a `code` and a `message`.
To remove URLs, send `DELETE /v1/indexes/{id}/urls` with the `urls` to remove, or with `sources` to remove
sitemaps or feeds by their URL. It answers with how many were `removed`.
## Statuses [#statuses]
Every URL has a `status`:
| `status` | What it means |
| --------- | ------------------------------------------------------------------------------------------------------------------------- |
| `pending` | Queued, or being read. |
| `ready` | Read: it can come up in searches. |
| `failed` | We couldn’t read it: it didn’t answer, or answered with an error. `error` says why, and `next_read_at` when we try again. |
| `blocked` | Its robots.txt, an opt-out signal or a publisher exclusion says no. We don’t read it. |
`GET /v1/indexes/{id}/urls` lists them, filtered by `status` if you want, a page at a time (`limit`, and
`cursor` with the `next_cursor` of the previous page; `null` on the last one):
cURL
```bash
curl "https://api.typesearch.ai/v1/indexes/idx_8f3k2m9q/urls?status=failed&limit=50" \
-H "Authorization: Bearer $TYPESEARCH_API_KEY"
```
```json title="urls[0]"
{
"url": "https://shop.example/p/ridgeline-trail-4",
"status": "ready",
"title": "Ridgeline Trail 4 · trail running shoe",
"description": "Grippy, light and made for long days on rough ground.",
"data": { "price": 109, "currency": "USD", "availability": "InStock", "brand": "Ridgeline", "sku": "RT4-42" },
"published_at": null,
"read_at": "2026-09-26T09:14:02Z",
"changed_at": "2026-09-24T09:10:40Z",
"next_read_at": "2026-09-27T09:14:02Z",
"error": null
}
```
`data` is the structured data the page itself declares (`price`, `currency`, `availability`, `brand`,
`sku`…), or `null` when it declares none.
The index has its own `status`: `active`, or `paused` when your credit runs out. A paused index keeps its
URLs, but they aren’t re-read until there is credit again.
## How fresh it stays [#how-fresh-it-stays]
* Every URL is re-read once a day. `read_at` says when it was last read, `changed_at` when its content last
changed, and `next_read_at` when it will be read again.
* Sitemaps and feeds are checked every hour, and new or updated URLs in them are read right away.
* robots.txt is honored on every read.
Re-reading is included in the price.
## Search it [#search-it]
Pass `index` to `POST /v1/search`. Everything else works as in any search ([modes](/docs/modes),
[typed questions](/docs/guides/structured-output), `tone`, `highlights`, `max_results`, `max_tokens`), with two
differences: a request with `index` takes one query, and there is no date filter unless you set one (`days`,
`published_after` or `published_before`).
cURL
TypeScript
Python
```bash
curl https://api.typesearch.ai/v1/search \
-H "Authorization: Bearer $TYPESEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "query": "trail running shoes under $120", "index": "idx_8f3k2m9q", "mode": "fast" }'
```
```ts
const res = await ts.search('trail running shoes under $120', { index: 'idx_8f3k2m9q', mode: 'fast' });
for (const r of res.results) console.log(r.score, r.title, r.url, r.data?.price);
```
```python
res = ts.search("trail running shoes under $120", index="idx_8f3k2m9q", mode="fast")
for r in res.results:
print(r.score, r.title, r.url, r.data and r.data.get("price"))
```
The response is a regular search response. It adds `custom_index`, and every result says
`found_in: "custom_index"` and carries the page’s `data`:
```json title="Response (abridged)"
{
"object": "search",
"mode": "fast",
"custom_index": { "id": "idx_8f3k2m9q", "name": "Catalog" },
"found": true,
"results": [
{
"url": "https://shop.example/p/ridgeline-trail-4",
"title": "Ridgeline Trail 4 · trail running shoe",
"score": 0.96,
"found_in": "custom_index",
"data": { "price": 109, "currency": "USD", "availability": "InStock", "brand": "Ridgeline", "sku": "RT4-42" }
}
]
}
```
To put your pages next to the press coverage, send the same query twice: once with `index`, once without.
## Typed questions: price, stock, “did it change?” [#typed-questions-price-stock-did-it-change]
`data` gives you what the page declares. A [typed question](/docs/guides/structured-output) answers what it
doesn’t: is size M available, is it under your budget, did the return policy change, did the company revise
its guidance. Use `normal` or `deep` when the answer is in the page and not in its title: they read the best
candidates before ranking them.
cURL
```bash
curl https://api.typesearch.ai/v1/search \
-H "Authorization: Bearer $TYPESEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "Alpine rain jacket in size M",
"index": "idx_8f3k2m9q",
"mode": "normal",
"questions": {
"in_stock": { "type": "boolean", "instructions": "Is size M in stock?" },
"on_sale": { "type": "boolean", "instructions": "Is it discounted from its regular price?" }
}
}'
```
```json title="results[0].answers"
{
"basis": "article",
"values": {
"in_stock": { "type": "boolean", "probability": 0.97 },
"on_sale": { "type": "boolean", "probability": 0.08 }
}
}
```
As in any search, a probability between 0.35 and 0.65 means undecided, and `basis` says whether the answer
came from the page or only from its title.
## Limits and pricing [#limits-and-pricing]
| Plan | URLs | Indexes |
| ------------------- | ------------ | --------- |
| Pay as you go | Up to 100 | 3 |
| Monthly credit plan | Up to 10,000 | 50 |
| Enterprise | Unlimited | Unlimited |
URLs past your limit come back in `over_limit` and are not added. `limits` in `GET /v1/indexes` and in every
`POST /v1/indexes/{id}/urls` says how many you use; `null` means unlimited. More URLs come with the
[monthly credit plan](/pricing).
An index costs $2.00 per 1,000 URLs a month, billed daily and prorated from the same prepaid credit:
a full pay-as-you-go index of 100 URLs costs $0.20 a month, and 10,000 URLs
cost $20. Re-reading is included. Searches over it cost the same as any search in that mode:
$1.00 per 1,000 in `ultra`, $1.40 in `fast`, $2.20 in `normal` and $5.60 in `deep`. See
[Costs and caching](/docs/guides/cost-control).
## What we read, and what we don’t [#what-we-read-and-what-we-dont]
* **Only public URLs.** Pages anyone can open over `http` or `https`: no logins, cookies or paywalls. Private
and internal network addresses are rejected, and come back in `invalid`.
* **The same compliance gate as the news index.** robots.txt, AI opt-out signals and
[publisher exclusions](/publishers) apply to every URL you add. A page that says no comes back as
`blocked`, and we don’t read it.
* **Yours only.** An index and what we read for it belong to your organization: no other customer can search
it.
* **Never used for training.** Your URLs, the pages we read for them and your queries are never used to train
models.
## From the MCP server [#from-the-mcp-server]
The [MCP server](/docs/integrations/mcp)’s `search_news` takes an optional `index`: pass your `idx_…` and it
searches your index instead of the news index, with the same key and prices. Your agent can then check your
catalog and the news with the same tool:
```json title="search_news"
{ "query": "Alpine rain jacket in size M", "index": "idx_8f3k2m9q", "mode": "fast" }
```
# Enrichments (/docs/guides/enrichments)
Tone, dedupe and essentials are options you turn on; diffusion comes with every search that finds
something. They work with `search`, `similar` and `site_search`, and can be combined.
```ts
const res = await ts.search('el dólar', {
mode: 'deep',
tone: true,
dedupe: true,
essential: true,
});
```
## Tone [#tone]
`tone: true` classifies each result as `positive`, `neutral` or `negative` **relative to your query** —
not the general mood of the article — and adds a summary by source.
```json title="results[0].tone"
{ "label": "negative", "probabilities": { "positive": 0.04, "neutral": 0.21, "negative": 0.75 }, "basis": "article" }
```
```json title="tone"
{
"articles": 42,
"overall": { "positive": 6, "neutral": 25, "negative": 11 },
"by_source": [{ "source": "Red Diaria", "positive": 2, "neutral": 9, "negative": 4 }]
}
```
The scale is the same for news, blogs or social posts, so summaries are comparable across sources.
`basis` says whether the article text or only its headline was judged.
## Dedupe [#dedupe]
`dedupe: true` collapses the same story told by several outlets into one result and lists the others in
`duplicates`. It is off by default because sometimes the repetition is the signal.
## Essentials [#essentials]
`essential: true` returns up to three **verbatim** excerpts from different sources that capture the story —
chosen by the model from the highlights and standfirsts of the top results, with the same limits as
[highlights](/docs/guides/search#highlights): up to 25 words each, never from an article’s first paragraph.
It is not a generated summary: every sentence was published by someone, with a link to it.
```json title="essential"
{
"excerpts": [
{ "text": "El dólar mayorista terminó la jornada sin variaciones…", "url": "https://reddiaria.example/…", "source": "Red Diaria", "title": "…" }
]
}
```
On by default in `deep`.
## Diffusion [#diffusion]
Every response to a search with results includes `diffusion`: articles per day, per source, and who
published first. It is computed from the index, so it costs nothing.
```json title="diffusion"
{
"by_day": [{ "day": "2026-09-21", "count": 18 }],
"by_source": [{ "source": "Diario Ejemplo", "count": 5, "first_published_at": "2026-09-21T11:02:00.000Z" }],
"first": { "source": "Diario Ejemplo", "url": "https://diarioejemplo.example/…", "title": "…", "published_at": "2026-09-21T11:02:00.000Z" },
"undated": 0
}
```
# Search a live site (/docs/guides/live-site-search)
`POST /v1/search/site` searches a site live, not the index. It reads the homepage first; if that is not
enough, the model picks the sections most likely to cover the topic and tries the site’s own search box,
in parallel. The best candidates are read before they are ranked, as in `normal` mode.
Because it can take up to a minute, it returns a **job**. Poll it, wait for it with the SDK, or stream it.
## Wait for the result [#wait-for-the-result]
TypeScript
Python
```ts
const res = await ts.siteSearchAndWait('diarioejemplo.example', 'el dólar', { mode: 'normal' });
console.log(res.results.length, res.site);
```
```python
res = ts.site_search_and_wait("diarioejemplo.example", "el dólar", mode="normal")
print(len(res.results), res.site)
```
## Or handle the job yourself [#or-handle-the-job-yourself]
cURL
TypeScript
Python
```bash
curl https://api.typesearch.ai/v1/search/site \
-H "Authorization: Bearer $TYPESEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "site": "diarioejemplo.example", "query": "el dólar" }'
# 202 Accepted · Location: /v1/jobs/job_…
curl https://api.typesearch.ai/v1/jobs/job_… \
-H "Authorization: Bearer $TYPESEARCH_API_KEY"
```
```ts
const job = await ts.siteSearch('diarioejemplo.example', 'el dólar');
const done = await ts.jobs.wait(job.id, { pollInterval: 2000 });
```
```python
job = ts.site_search("diarioejemplo.example", "el dólar")
done = ts.jobs.wait(job.id, poll_interval=2)
```
A job moves from `queued` to `running` to `succeeded` (with `result`) or `failed` (with `error`). Jobs
last one day and are visible only to the key that created them.
## Or stream it [#or-stream-it]
With `stream: true` the request returns [Server-Sent Events](/docs/guides/streaming) instead of a job:
steps, partial results and the final result.
```ts
for await (const event of ts.siteSearchStream('diarioejemplo.example', 'el dólar')) {
if (event.type === 'step') console.log(event.step.text);
}
```
## Limits and errors [#limits-and-errors]
We only read the site you ask for and its subdomains, with `GET` requests, honoring its robots.txt and
opt-out signals, and we never try to get past bot protection.
| Status | `code` | What it means |
| ------ | ------------------------------------ | --------------------------------------------------------------------------------------- |
| 400 | `invalid_site`, `site_not_found` | The site is not a valid public address. |
| 403 | `robots_disallowed` | The site’s robots.txt disallows it, or it couldn’t be read (we retry after 10 minutes). |
| 403 | `source_unavailable` | The site isn’t available right now. We don’t say why, and it may be temporary. |
| 502 | `site_unreachable`, `bot_protection` | The site did not answer, or answered with a bot challenge. |
| 504 | `site_timeout` | The site took too long. |
# Several queries at once (/docs/guides/multiple-queries)
Pass a list as `query` — up to five. Headlines are judged against every query in the same calls, which
uses fewer tokens and fewer round-trips than searching each query separately.
TypeScript
Python
```ts
const res = await ts.search(['el dólar', 'el Presupuesto 2027', 'el FMI'], { mode: 'fast' });
for (const group of res.groups ?? []) {
console.log(group.query, group.total);
}
```
```python
res = ts.search(["el dólar", "el Presupuesto 2027", "el FMI"], mode="fast")
for group in res.groups or []:
print(group.query, group.total)
```
## What comes back [#what-comes-back]
* **`results`** is one merged list without repeated URLs. Each result has a `queries` field with the
queries it answers.
* **`groups`** has one entry per query, with its own `results`, `near_misses`, `rejected` and, when
requested, `tone`, `essential` and `diffusion`.
Use `results` when the queries are facets of one topic; use `groups` when they are separate topics that
happen to travel together.
## Billing [#billing]
Each query counts as one search of the request’s mode: five `fast` queries are five searches at $1.40 per
1,000. The savings are in speed and in what the model does once instead of five times.
# Search the index (/docs/guides/search)
`POST /v1/search` searches the index and returns the relevant articles, most relevant first.
cURL
TypeScript
Python
```bash
curl https://api.typesearch.ai/v1/search \
-H "Authorization: Bearer $TYPESEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "Boca Juniors",
"mode": "fast",
"max_results": 10,
"include_domains": ["reddiaria.example/deportes", "diarioejemplo.example"],
"days": 3
}'
```
```ts
const res = await ts.search('Boca Juniors', {
mode: 'fast',
max_results: 10,
include_domains: ['reddiaria.example/deportes', 'diarioejemplo.example'],
days: 3,
});
```
```python
res = ts.search(
"Boca Juniors",
mode="fast",
max_results=10,
include_domains=["reddiaria.example/deportes", "diarioejemplo.example"],
days=3,
)
```
## Writing a query [#writing-a-query]
A query is a topic, not a question: `el dólar`, `Boca Juniors`, `la visita del papa`. Between 2 and 200
characters.
For the best recall, write it the way the coverage you are after would phrase it — in Spanish for the
Argentine press, with the name the headlines use. Acronyms and paraphrases are found less often than the
name itself.
## Filters [#filters]
Filters are applied before anything is judged, so what you filter out costs nothing.
| Filter | Example | Notes |
| ------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------ |
| `include_domains` | `["diarioejemplo.example", "reddiaria.example/economia"]` | A domain includes its subdomains; a path, everything under it. Up to 20. |
| `exclude_domains` | `["mirador.example"]` | Same rules. |
| `sections` | `["economia", "politica"]` | The section the source declares, or the first segment of the path. |
| `days` | `3` | The last N days. `null` searches the whole index. Defaults to 7. |
| `published_after`, `published_before` | `"2026-09-20"` | A bare date covers the whole day; a date-time needs an offset. |
| `sources` | `["diarioejemplo.example", "reddiaria.example"]` | Source domains in the index. Up to 100. |
A domain that is not in the index, in `sources` or `include_domains`, comes back as a `domain_not_indexed`
entry in `warnings`, not as an error. Missing an outlet? Suggest it from the dashboard (Suggest a source) or write to [support@typesearch.ai](mailto:support@typesearch.ai).
## Reading the results [#reading-the-results]
Each result has a `score`: the **calibrated probability** that the article is about your query. It
comes from the article when it was read (`read` is set), and from the headline otherwise. Results are
the articles scored `0.5` or more:
| `score` | What it means |
| ---------- | ------------------------------------------------------------------------------------ |
| 0.65 – 1 | Relevant. |
| 0.5 – 0.65 | **Undecided.** Returned, but the model is not sure — don’t treat 0.56 as a weak yes. |
Calibrated means you can threshold on it: across many results scored `0.9`, about nine in ten are
relevant. When your agent needs certainty, keep results above `0.8`, or use `normal` mode so the top ones
are read before they are ranked.
Besides `results`, the response tells you what happened on the edges:
* **`rejected`** — articles whose headline looked relevant but scored below `0.6` once read.
* **`near_misses`** — when nothing reaches `0.5`, what came closest (from `0.15`), so your agent can
decide whether to broaden the query.
* **`total`** — how many relevant articles exist, which can be more than `max_results`.
## Highlights [#highlights]
In `normal` and `deep`, set `highlights: true` to get the passage of each read article that is about your
query, quoted verbatim, never rewritten. Excerpts are short on purpose, because the article belongs to its
publisher:
* at most one excerpt per article (two in `deep`), of up to 25 words;
* never taken from the article’s first paragraph, and none for short articles (under 1,200 characters of
text);
* each customer gets at most three different excerpts of the same article per day.
For the rest, the result links to the article.
```ts
const res = await ts.search('el Presupuesto 2027', { mode: 'normal', highlights: true });
console.log(res.results[0]?.highlights);
```
## Next [#next]
* [Several queries in one request](/docs/guides/multiple-queries)
* [Structured output](/docs/guides/structured-output)
* [Enrichments: tone, dedupe, essentials and diffusion](/docs/guides/enrichments)
# Find similar coverage (/docs/guides/similar)
`POST /v1/similar` reads an article and searches the index for others about the same story. Use it to
see how a story was covered elsewhere, to find the original source, or to widen a single link into full
coverage.
TypeScript
Python
```ts
const res = await ts.similar('https://diarioejemplo.example/economia/…', {
mode: 'fast',
exclude_domains: ['diarioejemplo.example'],
});
console.log(res.reference?.title);
for (const r of res.results) console.log(r.source, r.title);
```
```python
res = ts.similar(
"https://diarioejemplo.example/economia/…",
mode="fast",
exclude_domains=["diarioejemplo.example"],
)
print(res.reference.title)
for r in res.results:
print(r.source, r.title)
```
The response has the same shape as a search, plus `reference`: the URL and title of the article you
passed. Every search option and filter works here too — excluding the reference’s own domain is the
usual one.
## Modes and price [#modes-and-price]
The reference article is always read. Its title and standfirst are the query, and the model judges the
index headlines against them: up to 160 in `ultra` and `fast`, 120 in `normal` and `deep`. `deep` also opens
and reads the top 8, with highlights; the other modes don’t read the candidates. A request costs $2.20
per 1,000 in `ultra`, `fast` and `normal`, and $4.60 in `deep`. Exa has deprecated findSimilar and no
longer lists a price for it; the search it points to instead costs $7 per 1,000.
Combine it with `dedupe: true` to get one result per story, or with [`diffusion`](/docs/guides/enrichments#diffusion)
to see who published first.
# Streaming (/docs/guides/streaming)
Set `stream: true` on `POST /v1/search` or `POST /v1/search/site` and the response becomes a stream of
[Server-Sent Events](https://html.spec.whatwg.org/multipage/server-sent-events.html). The first results
arrive about a second in, in every mode.
## With the SDKs [#with-the-sdks]
TypeScript
Python
```ts
const stream = ts.searchStream('el dólar', { mode: 'deep' });
for await (const event of stream) {
if (event.type === 'step') console.log('·', event.step.text);
if (event.type === 'partial') render(event.response.results);
if (event.type === 'result') render(event.response.results, { final: true });
}
```
```python
with ts.search_stream("el dólar", mode="deep") as stream:
for event in stream:
if event.type == "step":
print("·", event.step.text)
elif event.type == "partial":
render(event.response.results)
elif event.type == "result":
render(event.response.results, final=True)
```
Only need the end? `finalResponse()` in TypeScript and `final_response()` in Python consume the stream
and return the result.
## Events [#events]
| Event | `data` | When |
| --------- | ------------------------------ | --------------------------------------------------------------------------------- |
| `step` | `{ id, text, status, detail }` | A step starts or finishes: judging headlines, reading articles, classifying tone… |
| `partial` | A search response | Results judged so far. Each one is confirmed in place as its article is read. |
| `result` | A search response | The final result. The stream ends after it. |
| `error` | Problem details | Something failed. The stream ends after it. The SDKs throw it as an error. |
Results in a `partial` keep their position while articles are read, so you can render them straight away
without the list jumping. The final order comes with `result`.
## The raw stream [#the-raw-stream]
```text
event: step
data: {"id":"juicio-indice","text":"The model reads 160 headlines from the index","status":"running","detail":null}
event: partial
data: {"id":"req_…","object":"search","results":[…]}
event: result
data: {"id":"req_…","object":"search","results":[…]}
```
Step `text` is meant to be shown to people and can change; use `id` and `status` in code. It comes in
English; send `Accept-Language: es` to get it in Spanish.
## Charts [#charts]
`POST /v1/charts` streams too, with `stream: true`: `step` while it works, `result` with the chart and
`improved` if the exact data arrives later. See [Stream the progress](/docs/guides/charts#stream-the-progress).
# Structured output (/docs/guides/structured-output)
Add `questions` to any search and every result comes back with typed answers. No second LLM pass, no
parsing prose: each answer is a probability, a choice or a score.
TypeScript
Python
```ts
const res = await ts.search('el Presupuesto 2027', {
mode: 'normal',
questions: {
mentions_figure: {
type: 'boolean',
instructions: 'Does the article mention a specific amount of money?',
},
stance: {
type: 'choice',
instructions: 'What is the article’s stance on the budget?',
criteria: { supportive: null, critical: null, neutral: 'Reports without taking a side.' },
},
impact: {
type: 'score',
instructions: 'How much does it affect households?',
criteria: ['None', 'Low', 'Moderate', 'High', 'Very high'],
},
},
});
const answers = res.results[0]?.answers?.values;
```
```python
res = ts.search(
"el Presupuesto 2027",
mode="normal",
questions={
"mentions_figure": {
"type": "boolean",
"instructions": "Does the article mention a specific amount of money?",
},
"stance": {
"type": "choice",
"instructions": "What is the article's stance on the budget?",
"criteria": {"supportive": None, "critical": None, "neutral": "Reports without taking a side."},
},
"impact": {
"type": "score",
"instructions": "How much does it affect households?",
"criteria": ["None", "Low", "Moderate", "High", "Very high"],
},
},
)
answers = res.results[0].answers.values
```
## Question types [#question-types]
| `type` | `criteria` | Answer |
| --------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `boolean` | Optional: what counts as `true` and as `false`. | `probability` that the statement is true. |
| `choice` | Required: the options as keys, each with an optional description. At least two. | The `choice`, the `probabilities` of every option and a `confidence`. |
| `score` | Required: the steps of an ordered scale, lowest first. At least two. | An interpolated `score` on the scale, the `probabilities` of each step and a `confidence`. |
Up to 8 questions per request. Keys are letters, digits, `-` or `_`, up to 40 characters.
## Reading the answers [#reading-the-answers]
```json title="results[0].answers"
{
"basis": "article",
"values": {
"mentions_figure": { "type": "boolean", "probability": 0.93 },
"stance": {
"type": "choice",
"choice": "critical",
"probabilities": { "supportive": 0.07, "critical": 0.81, "neutral": 0.12 },
"confidence": 0.81
},
"impact": { "type": "score", "score": 2.6, "confidence": 0.74 }
}
}
```
* **`basis`** says what the answer was based on: `article` when the article was read, `headline` when only
the headline and standfirst were available. Use `normal` or `deep` if you need answers on the text.
* A `boolean` probability between 0.35 and 0.65 means **undecided**, not a weak yes.
Answers are part of the response, so they are cached with it for 10 minutes. They are not reused across
different questions.
# Migrate from Exa (/docs/migration/exa)
typesearch has a compatibility layer that speaks Exa’s API — same parameters, same response shape, same
errors. With the official SDKs, migrating is two lines.
Python
JavaScript
```python
from exa_py import Exa
# before: exa = Exa(api_key=EXA_API_KEY)
exa = Exa(api_key=TYPESEARCH_API_KEY, base_url="https://api.typesearch.ai/compat/exa")
results = exa.search("el dólar", num_results=10, type="fast", include_domains=["reddiaria.example"])
similar = exa.find_similar(results.results[0].url, exclude_source_domain=True)
pages = exa.get_contents([r.url for r in results.results], highlights=True)
```
```ts
import Exa from 'exa-js';
// before: const exa = new Exa(process.env.EXA_API_KEY);
const exa = new Exa(process.env.TYPESEARCH_API_KEY, 'https://api.typesearch.ai/compat/exa');
const { results } = await exa.search('el dólar', { numResults: 10, type: 'fast' });
```
Supported: `/search`, `/findSimilar` and `/contents`. The key goes in the header the SDK already uses.
## Parameters [#parameters]
| Exa | On typesearch |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `query`, `numResults` | Same. Up to 50 results (`limited:numResults` above that). |
| `type` | `instant` → `ultra` · `fast`, `auto`, `neural`, `keyword`, `hybrid` → `fast` · `deep*` → `deep`. `fast` doesn’t read the pages: contents are billed per page (below). |
| `includeDomains`, `excludeDomains` | Same, up to 20. `*.domain.com` works too. |
| `startPublishedDate`, `endPublishedDate` | Same. |
| `includeText`, `excludeText` | Applied to title, standfirst and highlights (`approximated:includeText`). |
| `category` | `news` or nothing. Any other category returns empty without searching or billing (`unsupported:category`). |
| `contents.highlights` | Verbatim excerpts the model picks for the query (or `highlights.query`). In `ultra` and `fast`, each result page is read and billed per page: $0.40 per 1,000. In `deep`, from the articles it read, included in its price. |
| `contents.text` | A short excerpt, never the full page (`limited:text`). In `ultra` and `fast`, billed per page: $0.20 per 1,000. |
| `contents.summary` | Not generated: we don’t write summaries (`unsupported:summary`). |
| `/findSimilar` | Articles in the index about the same story. `excludeSourceDomain` works. |
| `/contents` | Metadata and short excerpts. With `highlights.query`, the model picks them. |
## What it costs [#what-it-costs]
Exa’s search ($7 per 1,000, list price checked September 25, 2026) includes the text and highlights of up to 10
results. Here each part has its own price: a `fast` search is $1.40 per 1,000 and returns headlines and
standfirsts (`type: "instant"` is `ultra`, $1.00 per 1,000, headlines only), and each page you ask contents
for is billed on top. With the text of 10 results a fast search
comes to $3.40 per 1,000; with highlights of 10, $5.40. `normal` ($2.20) reads the
top 4 before ranking them, and `deep` ($5.60, against $12–15 for Exa’s deep search) reads 8 with highlights.
If you need the full text of every result, Exa gives you more for its price: see [what stays with
Exa](#what-stays-with-exa).
## Nothing breaks silently [#nothing-breaks-silently]
What we don’t support is accepted and reported in the `X-Compat-Warnings` response header —
`ignored:…`, `limited:…`, `approximated:…` or `unsupported:…` — so migrated code keeps running. Only what
would change the meaning of a request is an error, in Exa’s format: `{ requestId, error, tag }`.
## Our extras [#our-extras]
Add a `typesearch` field to the request body for what Exa doesn’t have — tone, essentials, a mode or a
cost cap. It comes back in a `typesearch` field of the response. Any client that lets you add fields to
the JSON body works:
```bash
curl https://api.typesearch.ai/compat/exa/search \
-H "x-api-key: $TYPESEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "query": "el dólar", "numResults": 5, "typesearch": { "tone": true, "mode": "normal" } }'
```
For everything else, the [native API](/docs/api-reference) and our [SDKs](/docs/sdks) expose all of it.
## What stays with Exa [#what-stays-with-exa]
People, company and research-paper indexes, full page text, and generated answers or summaries are not
part of typesearch. If your product depends on them, keep those calls on Exa and move the rest.
# Migrate from Perplexity (/docs/migration/perplexity)
typesearch speaks Perplexity’s Search API (`POST /search`) — the ranked results, not Sonar’s generated
answers. With the official SDK, migrating is one line.
Python
JavaScript
```python
from perplexity import Perplexity
# before: client = Perplexity()
client = Perplexity(api_key=TYPESEARCH_API_KEY, base_url="https://api.typesearch.ai/compat/perplexity")
r = client.search.create(query=["el dólar", "el FMI"], max_results=5, search_recency_filter="week")
```
```ts
import Perplexity from '@perplexity-ai/perplexity_ai';
const client = new Perplexity({
apiKey: process.env.TYPESEARCH_API_KEY,
baseURL: 'https://api.typesearch.ai/compat/perplexity',
});
```
You can also set `PERPLEXITY_BASE_URL` instead of changing code.
## Parameters [#parameters]
| Perplexity | On typesearch |
| ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `query` (one, or up to 5) | Same. Several queries are judged together; results come in one list. |
| `max_results` (1–20) | Same. |
| `search_domain_filter` | Same: without a prefix it includes, with `-` it excludes, paths included. Mixing both is an error, as on Perplexity. |
| `search_recency_filter` | `hour`, `day`, `week`, `month`, `year`. |
| `search_after_date_filter`, `search_before_date_filter` | Same (`MM/DD/YYYY`). `last_updated_*` is read as the publication date (`approximated:`). |
| `country` | Only outlets from that country (`approximated:country`: Perplexity treats it as a preferred region; here it filters). |
| `search_language_filter` | Same: only articles in those languages. |
| `max_tokens`, `max_tokens_per_page`, `search_context_size` | Ignored: excerpts are always short. |
| `search_type: "web"`, `"fast"` | Both run in our `fast` mode. |
| `search_type: "people"` | An error: the index is news. |
Without a date filter it searches the whole index, as Perplexity does, in `fast` mode.
## What it costs [#what-it-costs]
Perplexity bills its Search API at $5 per 1,000 requests, or $1 with Fast Search
(`search_type: "fast"`), and a request can carry up to five queries (list prices checked September 25, 2026). Here each
query is one search of its mode: $1.40 per 1,000 in `fast`, the mode the compatibility layer uses,
$1.00 in `ultra`, which judges headlines only, and $2.20 in `normal`, which reads the top 4 before ranking
them. For single headline searches, Perplexity’s Fast Search costs the same as our `ultra` (pick it with
`"mode": "ultra"` in the extras below) and less than our `fast`; with several queries per request it costs
less, because it bills the request and we bill each query. What you get for the difference is a news index
with publication time and outlet on every result, a calibrated relevance probability and, in `normal` and
`deep`, results read and verified before they are ranked.
## Our extras [#our-extras]
With the Perplexity SDK, send them through `extra_body`:
```python
r = client.search.create(query="el dólar", extra_body={"typesearch": {"tone": True, "mode": "normal"}})
```
They come back in a `typesearch` field of the response. Errors keep Perplexity’s format: `422` with
`detail[]` for validation, `error` for the rest.
# Migrate from Tako (/docs/migration/tako)
typesearch speaks Tako Search v3 for web results, in the same format. With the official SDK, migrating is
two lines.
Python
JavaScript
```python
from tako import Configuration, SearchRequest
from tako.lib import Tako
config = Configuration(host="https://api.typesearch.ai/compat/tako/api") # before: Configuration()
config.api_key["apiKey"] = TYPESEARCH_API_KEY # before: TAKO_API_KEY
r = Tako(config).search(SearchRequest.from_dict({
"query": "Boca Juniors",
"sources": {"web": {"count": 10, "category": "sports"}},
}))
```
```ts
import { Tako } from 'tako-sdk';
const tako = new Tako({
apiKey: process.env.TYPESEARCH_API_KEY,
basePath: 'https://api.typesearch.ai/compat/tako/api',
});
```
## Partial, on purpose [#partial-on-purpose]
Tako returns two things: data cards (charts built from licensed datasets) and web results. Through this
compatible endpoint, web results come back complete and in the same format. Data cards come back empty
(`cards: []`), with `unsupported:data_cards` in `X-Compat-Warnings`; a request that only asks for cards
returns empty without searching or billing.
## Chart cards [#chart-cards]
typesearch makes chart cards of its own with [`POST /v1/charts`](/docs/guides/charts), and with the
[MCP server](/docs/integrations/mcp)'s `create_chart` tool. Ask in plain words and you get the chart, the key
figures, a title that states the finding, an embed, a PNG and an SVG. They are built from the news (coverage,
share of voice, tone, outlets, timelines, and the figures articles publish, found verbatim) or from your own
data, with the source of every value. To replace Tako's cards, call it next to the compatible search:
cURL
```bash
curl https://api.typesearch.ai/v1/charts \
-H "Authorization: Bearer $TYPESEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "query": "Boca Juniors coverage" }'
```
The response carries the card's `embed_url`, `image_url` and `svg_url`, the data drawn in `chart`, and
`sources`. From a query it costs its search plus $0.50 per 1,000 charts, from $1.50 per 1,000 in
all; for reference, Tako's Search, which returns chart cards, lists $7 per 1,000.
## Parameters [#parameters]
| Tako | On typesearch |
| ----------------------------------------------- | -------------------------------------------------------------------------- |
| `effort` | `instant` → `ultra` · `fast` → `fast` · `deep` → `deep`. |
| `sources.web.count` | Same, 1–20. |
| `sources.web.category` | `sports` → the sports section · `finance` → economy · `news` → everything. |
| `include_domains`, `exclude_domains` | Same. |
| `published_after`, `published_before` | Same. |
| `highlights` | Verbatim excerpts from the articles that were read. |
| `include_contents` | `content` comes back `null` (`limited:content`): excerpts only. |
| `output_settings.flat_results`, `force_refresh` | Same. |
Errors keep Tako’s format: `{ error_message, error_type }`.
## Our extras [#our-extras]
Add a `typesearch` field to the JSON body for tone, essentials, a mode or a cost cap; it comes back in a
`typesearch` field of the response. For everything else, the [native API](/docs/api-reference) and our
[SDKs](/docs/sdks) expose all of it.
## What stays with Tako [#what-stays-with-tako]
Tako's cards draw on licensed datasets (markets, economics, sports statistics) that we don't have: our charts
come from what the news publishes and from your data. If your product depends on those datasets, or on
answers synthesized by the API itself, keep those on Tako and move your web search here.