SDKs and frameworks
JavaScript SDK
Install @telemai/sdk, and your JavaScript or TypeScript code can search the web and
read pages through Telem.
Using a coding agent? Find the one you use on Integrations.
@telemai/sdk is a typed TypeScript client for the Telem search API. It searches the
web across providers and returns the results in the same shape for every provider.
It also reads pages, groups calls into sessions, runs monitors, lists the
providers, and sends query lineage for agents.
The package has no runtime dependencies. It uses the platform fetch, and you can
supply your own. So the package runs in these places:
- on Node
- in an edge worker
- inside another CLI
- in tests
Install
Section titled “Install”npm install @telemai/sdkThe package ships ESM and CommonJS builds with type definitions. Node 20.16 or later
is necessary to read the credentials file. On other runtimes that have fetch, such
as edge workers and browsers, pass the API key in code.
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.
The client resolves apiKey and baseUrl in this order. The first value found wins:
- A constructor argument.
- The
TELEM_API_KEYandTELEM_BASE_URLenvironment variables. ~/.telem/credentials.json.- The defaults: no key, and
https://router.telem.aiforbaseUrl.
See API keys for the full rules.
import Telem from "@telemai/sdk"
const telem = new Telem() // environment, then ~/.telem/credentials.jsonconst withKey = new Telem("tlm_...") // just a keyconst withOptions = new Telem({ apiKey: "tlm_...", baseUrl: "https://router.telem.ai", timeoutMs: 60_000, // per attempt maxRetries: 2, // on 5xx, network errors and 429 tier: "extended", // client-wide search defaults go here})The credentials file is the only file the client opens. The client never reads
.telem/telem.json, the file the other Telem tools use for
search options. A library must not let a
checked-out repository control the spend of a program. Set search defaults as
constructor options (tier, fields, providersInclude, providersExclude,
includeFullContent and autoRouting) or call arguments. The TELEM_* variables
apply below them.
Providers
Section titled “Providers”telem.providers() lists the search providers the deployment runs, as
ProviderInfo objects with name, type, description, activeByDefault,
tiers and autoRouted. autoRouted is true when auto-routing can choose the
provider without a provider list, and null when the router does not say.
Embedding and edge runtimes
Section titled “Embedding and edge runtimes”The client has no HTTP dependency, and it accepts your own fetch. This is how it
runs inside another platform, in an edge worker, and in tests without a network:
const telem = new Telem({ apiKey: env.TELEM_API_KEY, fetch: (input, init) => myFetch(input, init), // a worker, a proxy, a recording stub})Use the client
Section titled “Use the client”Use Telem from JavaScript
const response = await telem.search("climate policy 2026", { tier: "extended", // minimalist | default | extended | max providersInclude: ["exa"], // omit to use the default provider set numResults: 10, // rows per provider (server default 5)})for (const result of response.results) { console.log(result.title, result.url)}response.results is one list across all providers, and response.byProvider
keeps each provider’s run, failed ones included. Every option is on
Search parameters.
Batches, the response, and every option
A batch of queries. Pass an array of queries. They run at the same time, as one request. Each provider run includes the query it served:
const response = await telem.search([ "EU AI Act obligations for general-purpose models 2026", "how the amended EU AI Act timeline changed",])for (const run of response.byProvider) { console.log(run.batchIndex, run.query, run.provider, run.results.length)}response.results is the flat list across every run. An array with one query
behaves the same as a plain string.
The response.
response.results // flat SearchResult[]: providers in run order, rows in provider orderresponse.byProvider // ProviderRun[]: one entry per provider, failed providers includedresponse.sessionId // the session this call belongs toresponse.interactionId // the id of this requestresponse.status // "succeeded" | "partially_succeeded" | "failed"A provider that failed is still in byProvider, with status and error set and
results empty. Each run has provider, status, results, error, latencyMs,
preprocessorRunId, tier, fields, query and batchIndex, plus the
provider-native answer, entities, related, verticals, usage, warnings and
raw. Each result has url, title, summary, excerpt, fullContent,
publishDate, rank, thumbnail, favicon, source, enrichments, fetchMeta,
provider and raw. raw is the row exactly as the server sent it.
Every option, as JavaScript spells it. search() takes tier, fields,
providersInclude, providersExclude, providerOverrides, numResults,
includeRaw, includeFullContent, autoRouting, publishedAfter,
publishedBefore, includeDomains, goal, context, session, metadata and
signal.
- An option you do not set uses the client default, then the server default. An
undefinedvalue is unset;[]andfalseare explicit values. publishedAfterandpublishedBeforetake an ISO 8601 string or aDate. ADateis an exact instant. To include the whole day inpublishedBefore, pass a"YYYY-MM-DD"string.tierandfieldsdo not go together, and the client sends one of them.- When you set both provider lists, the client removes the excluded names from the
allow-list. If that leaves no provider, the call fails with a
TelemError(codeinvalid_request) before anything is sent. - The client does not check other values. An unknown tier, field or provider name comes back as an error from the server. See Errors.
signaltakes anAbortSignal. Aborting stops the request, and no retry follows.numResults,includeRawandproviderOverridesare call arguments only. As client defaults they do nothing.
const response = await telem.fetch(["https://example.com/a", "https://example.com/b"])for (const page of response.results) { console.log(page.url, page.status, (page.content ?? "").slice(0, 200))}Pass one URL or a list of up to five. A URL that failed has its status and
error instead of content. The options are on
Fetch parameters.
const first = await telem.search("onsen towns near kyoto")const second = await telem.search("best ramen in kyoto", { session: first.sessionId })
for (const session of await telem.sessions.list()) { console.log(session.id, session.interactionCount)}const summary = await telem.sessions.results(first.sessionId) // query, goal, context, resultsPass sessionId back as session to group calls. Make the first call alone:
until it returns, there is no id to share.
const monitor = await telem.monitors.create("Nvidia files a new 8-K with the SEC", { frequency: "1h", ttl: "30d" })console.log(monitor.agentState?.status) // what its agent does: compiling, then readyfor await (const change of telem.monitors.iterateChanges(monitor.id)) { // what its agent found console.log(change.seq, change.kind, change.subject.name)}const agent = await telem.monitors.agent(monitor.id) // the agent: status and plan versionawait telem.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() throws a TelemError
(invalid_request). The fields, the agent reads and the errors are on
Monitors.
import { createTelemVercelTools } from "@telemai/sdk/vercel"
const tools = createTelemVercelTools({ telem, harness: "my-app" }) // lineage on every callQuery 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 Vercel AI SDK tools attach it for you. For any other agent loop, build it by hand.
Build the lineage by hand
When lineage is present, the client ignores a session id. You build the lineage
data from four ids you already have. The model never sees
them. The example column uses Codex, which has all four ids:
| Id | What it is | Example |
|---|---|---|
conversationId |
One id for one agent conversation. Make it when the conversation starts, and keep it. A subagent gets its own. | Codex: the thread id. One thread is one conversation. |
windowId |
The id of the current context window. It changes when you trim or summarize the start of the history. | Codex: the window id. Codex makes a new one when it compacts the context. |
messageId |
The id of the model turn that makes the call. All the tool calls in one turn share it. | Codex: the turn id. |
toolCallId |
The id of this tool call. | Codex: the id of the tool call in that turn. |
In your own code the ids come from your agent loop. This example uses the OpenAI Node SDK, where the completion id is the turn and each tool call has its own id:
import OpenAI from "openai"import Telem, { buildMetadata, toolMarker, withTrajectory, windowAnchor } from "@telemai/sdk"
const telem = new Telem()const openai = new OpenAI()const messages = [{ role: "user" as const, content: "Is the Nintendo Museum open over New Year?" }]const tools = [ { type: "function" as const, function: { name: "telem_search", description: "Search the public web.", parameters: { type: "object", properties: { query: { type: "string" }, goal: { type: "string" }, context: { type: "string" } }, required: ["query", "goal", "context"], }, }, },]
const conversationId = crypto.randomUUID() // once, when the conversation startsconst windowId = await windowAnchor(messages) // once, from the first message
const completion = await openai.chat.completions.create({ model: "gpt-4o", messages, tools })for (const call of completion.choices[0].message.tool_calls ?? []) { if (call.type !== "function") continue const { query, goal, context } = JSON.parse(call.function.arguments) const lineage = await buildMetadata({ harness: "my-app", // a stable name for your integration conversationId, windowId, messageId: completion.id, // this model turn toolCallId: call.id, // this tool call history: [ // [{ role, content }]: the conversation so far, ending with this call ...messages, { role: "assistant", content: toolMarker("telem_search", "running", { query, goal, context }) }, ], ancestors: [], // the parent snapshot; [] for the first agent }) const response = await telem.search(query, withTrajectory({ goal, context }, lineage))}history has a rule that the console depends on. The example follows it on its
history line:
- Write each tool call into the history as a marker with
toolMarker(name, status, input). - End the history of every call with the call being made, marked
running. - When a call completes, keep its marker in the history as
completed.
The console reads the conversation’s goals from those markers, so build them with the helper, not by hand.
withTrajectory attaches the lineage data to one call. Use it the same way with
telem.fetch.
When an agent delegates a task, it makes a snapshot of itself with
buildSnapshot. The snapshot is a record of the parent at that moment. The subagent
keeps the snapshot in its own ancestors. Subagents that start in the same turn
share the snapshot, and the console shows them side by side under the parent:
import { buildSnapshot } from "@telemai/sdk"
const parent = await buildSnapshot({ harness: "my-app", conversationId, windowId, messageId: completion.id, // the turn that delegates context: messages, // the conversation at that moment ancestors: [],})// in the subagent: ancestors: [parent]Errors
Section titled “Errors”The client throws exactly two error classes. Branch on status and code. Do not
branch on the message text:
import Telem, { TelemError, TimeoutError } from "@telemai/sdk"
try { await telem.search("climate policy 2026")} catch (error) { if (error instanceof TimeoutError) { // error.code === "timeout": one attempt took longer than timeoutMs } else if (error instanceof TelemError) { error.status // the HTTP status, or undefined when nothing was sent error.code // "invalid_request" | "unsupported_server_version" | "connection_error", or a server code error.details // the parsed response body, or the raw text }}| Situation | Result |
|---|---|
Bad input (empty query, empty URL list, a monitor with an agent and no ttl) |
TelemError, code: "invalid_request", nothing sent |
| Network failure | TelemError, code: "connection_error", after the retries |
| 5xx or 429 | Retried with backoff (Retry-After applies), then TelemError with the status |
| Other 4xx | TelemError with the status and the code from the server, not retried |
Attempt longer than timeoutMs |
TimeoutError, not retried |
monitors.create() retries only a 429. It does not retry a 5xx or a network failure:
the server may already have created the monitor.
See Errors for every status code and what to do.
Next steps
Section titled “Next steps”- Vercel AI SDK: tools with query lineage for AI SDK agents
- Search parameters: what each search option does
- Fetch parameters: options for reading pages
- Errors: every error the API returns