SDKs and frameworks
Python SDK
Install telem-sdk, and your Python code can search the web and read pages through
Telem.
Using a coding agent? Find the one you use on Integrations.
telem-sdk is a typed Python client (httpx + pydantic v2) for the Telem search
orchestration API. It searches the web, reads pages, groups calls into sessions,
runs monitors and lists the providers. It reads back the server’s normalized
response, which has one result shape for every provider. The synchronous client (Telem) and the asynchronous client
(AsyncTelem) mirror each other and share every behavior.
Install
Section titled “Install”pip install telem-sdkIt requires Python 3.10+. Two optional extras add integrations. A plain install does not pull in their dependencies:
pip install "telem-sdk[openai]"pip install "telem-sdk[langchain]"[openai]: letsclient.wrap()patch a realopenai.OpenAI/AsyncOpenAIclient in place. The model can then call Telem search as a tool.[langchain]: addstelem.integrations.langchain.create_telem_search_tool(), a native LangChain/LangGraph tool.
Authentication
Section titled “Authentication”Create an API key under API keys in the Telem console.
The guided installer writes it to ~/.telem/credentials.json. You can also set it
as an environment variable, or pass it in code.
api_key and base_url resolve the same way, in this order. The first match wins:
- An explicit constructor argument.
TELEM_API_KEY/TELEM_BASE_URL.~/.telem/credentials.json, the machine-written file that the guided installer produces.- The defaults: no key, and the hosted deployment for
base_url.
See API keys for the full rules.
from telem import Telem
client = Telem() # environment, then ~/.telem/credentials.json# or, explicitly:client = Telem(api_key="tlm_...", base_url="https://your-router.example.com")That credentials file is the only file the client opens on its own. Every other
Telem tool reads .telem/telem.json for search options.
Here, that file is opt-in. An application asks for it with
telem.resolve_search_options(project_root=...), and for fetch options with
telem.resolve_fetch_options(project_root=...). The reason: a library must not let a
checked-out repository control the spend of an arbitrary program.
Per-search defaults resolve in the same way: a constructor argument, then a TELEM_*
variable, then the server’s default. The constructor arguments are default_tier,
default_fields, default_providers_include, default_providers_exclude,
default_include_full_content and default_auto_routing. timeout (in seconds,
default 60) and user_agent set up the HTTP client. A call argument to
search() overrides the default.
client = Telem(default_tier="extended", default_providers_include=["exa", "brave"], timeout=30)Providers
Section titled “Providers”client.providers() lists the search providers the deployment runs, as
ProviderInfo objects with name, type, description, active_by_default and
auto_routed. auto_routed is True when auto-routing can choose the provider
without a provider list, and None when the router does not say.
Sync and async clients
Section titled “Sync and async clients”Telem and AsyncTelem expose exactly the same surface: every method, argument
and default.
from telem import Telem
client = Telem()response = client.search("best python http client")print(len(response.results))client.close()import asynciofrom telem import AsyncTelem
async def main(): async with AsyncTelem() as client: response = await client.search("best python http client") print(len(response.results))
asyncio.run(main())Both support the context-manager form. If you do not use with, Telem also needs
an explicit client.close().
Use the client
Section titled “Use the client”Use Telem from Python
resp = client.search( "climate policy 2026", tier="extended", # minimalist | default | extended | max providers_include=["exa"], # omit to use the default provider set num_results=10, # rows per provider (server default 5))for result in resp.results: print(result.title, result.url)resp.results is one list across all providers, and resp.by_provider keeps each
provider’s run, failed ones included. Every option is on
Search parameters.
Batches, the response, and every option
A batch of queries. query also accepts a sequence of strings. They batch into
one interaction, and the backend runs them concurrently. Each provider run in
by_provider is tagged with the query it served:
resp = client.search(["onsen towns near kyoto", "best ramen in kyoto"])
for run in resp.by_provider: print(run.batch_index, run.query, run.provider, len(run.results))resp.results stays the flattened list across every run. A one-element sequence
behaves exactly like a plain string.
The response.
resp.results # flattened list[SearchResult]: providers in run order, rows in envelope orderresp.by_provider # list[ProviderRun]: one per provider that ran, failed ones includedresp.session_id # continue the conversation by passing session=resp.session_idresp.interaction_id # the interaction this call createdresp.status # "succeeded" | "partially_succeeded" | "failed"A provider that failed still has a ProviderRun, with status and error set and
results empty. Each run has provider, status, results, error, latency_ms,
preprocessor_run_id, tier, fields, query and batch_index. It also has
provider-native extras: answer, entities, related, verticals, usage,
warnings and raw.
Each SearchResult has url, title, summary, excerpt, full_content,
publish_date, rank, thumbnail, favicon, source, enrichments, fetch_meta,
provider and raw. raw is the row exactly as the server sent it.
result.content is a shortcut: summary, else full_content["content"], else "".
Every option. search() takes tier, fields, providers_include,
providers_exclude, provider_overrides, num_results, include_raw,
include_full_content, auto_routing, published_after, published_before,
include_domains, goal, context, session and metadata.
Nonemeans unset. The value then falls through to the client default, and then to the server’s default.published_afterandpublished_beforetake an ISO 8601 string, adateor adatetime. Adatetimewith no time zone is local time.tierandfieldsdo not go together. The one set at the more specific level wins. Only on a tie at the same level doesfieldsreplacetier.- When both provider lists resolve, the excluded names are removed from the
allow-list. If that leaves no provider,
search()raisesValueErrorbefore it sends anything. - Nothing else is checked in the client. An unknown tier, field or provider name comes
back as a
BadRequestError. See Errors. num_resultsandinclude_raware call arguments only. In a config file, they do nothing.
resp = client.fetch(["https://example.com/a", "https://example.com/b"])for page in resp.results: print(page.url, page.status, (page.content or "")[:200])Pass a list of up to five URLs. A URL that failed has its status and error
instead of content. The options are on Fetch parameters.
first = client.search("onsen towns near kyoto")second = client.search("best ramen in kyoto", session=first.session_id)
for session in client.sessions.list(): print(session.id, session.interaction_count)summary = client.sessions.results(first.session_id) # query, goal, context, resultshistory = client.sessions.history(first.session_id, full=True) # each callPass session_id back as session to group calls. Make the first call alone:
until it returns, there is no id to share.
monitor = client.monitors.create("Nvidia files a new 8-K with the SEC", frequency="1h", ttl="30d")print(monitor.agent_state.status if monitor.agent_state else "no agent")for change in client.monitors.iter_changes(monitor.id): # what its agent found print(change.seq, change.kind, change.subject.name)agent = client.monitors.agent(monitor.id) # the agent: status and plan versionclient.monitors.delete(monitor.id)A monitor has an agent unless you pass agent=False, and a monitor with an agent needs
a ttl, at most "30d": without one, create() raises ValueError. The fields, the
agent reads and the errors are on Monitors.
from openai import OpenAI
oai = OpenAI()client.wrap(oai) # every search the model makes now carries lineageQuery lineage tells Telem which conversation and which turn made each call, so the console shows the calls in order, with subagents under their parent. The OpenAI wrap and the LangChain tool attach it for you.
Errors
Section titled “Errors”Every error that the SDK raises derives from TelemError. Each error has .message,
.status_code and .body:
| Status | Exception |
|---|---|
| 400 | BadRequestError |
| 401 / 403 | AuthError |
| 404 | NotFoundError |
| other non-2xx | APIStatusError |
The Python SDK does not retry. On a 429 or a 5xx, wait and send the call again
in your own code: see Rate limits.
Two failures have no status code. A request that got no HTTP response at all raises
TelemConnectionError. A search answered without the normalized response raises
TelemServerVersionError.
from telem import Telem, BadRequestError
try: Telem().search("hi", providers_include=["does-not-exist"])except BadRequestError as exc: print(exc.status_code, exc.message)See Errors for every status code and what to do.
Next steps
Section titled “Next steps”- Search parameters: what each search option does
- Fetch parameters: options for reading pages
- Errors: every error the API returns
- OpenAI: give an OpenAI client a Telem search tool