Use @expandai/sdk to call Fetch from TypeScript and read Main Markdown plus State JSON in code.
Use @expandai/sdk to call Fetch from TypeScript: Main Markdown, State JSON, Highlights, Batched Fetch, and citation helpers.
npm install @expandai/sdkpnpm add @expandai/sdkbun add @expandai/sdkimport { ExpandClient, isFetchOriginError } from '@expandai/sdk'
const expand = new ExpandClient({ apiKey: process.env.EXPAND_API_KEY })
Use fetchJson() for application code that needs Main Markdown plus State JSON. Use fetch() when you only want the Markdown string.
Jump to: JSON Mode · Markdown-only Fetch · Highlights · Batched Fetch · Citation Helpers · Errors · Effect
Install the package, then set your API key. The SDK reads EXPAND_API_KEY from the environment by default.
export EXPAND_API_KEY="xpnd_..."import { ExpandClient } from '@expandai/sdk'
const expand = new ExpandClient({
apiKey: process.env.EXPAND_API_KEY,
})The SDK sends your key on every request as the x-expand-api-key header.
| Option | Default | Description |
|---|---|---|
apiKey | process.env.EXPAND_API_KEY | Expand API key. |
baseUrl | https://api.expand.ai | API base URL. |
| Method | Use when | Returns |
|---|---|---|
fetchJson(params, options?) | App code needs Main Markdown, State JSON, snapshot metadata, or inline Highlights. | FetchJsonResponse: captured content or FetchOriginError. |
fetch(params, options?) | You only need the Markdown string. | string |
fetchJsonfetchJson() is the recommended SDK method for applications. It returns either captured content — Main Markdown, State JSON, snapshot metadata, and optional search results — or a typed FetchOriginError when the origin could not be reached. Use isFetchOriginError() before reading content fields; it narrows only this top-level network-error case. An HTTP 4xx or 5xx still has captured content and reports its outcome in page.originError.
const page = await expand.fetchJson({
url: 'https://example.com',
})
if (isFetchOriginError(page)) {
console.error(page.kind, page.url, page.retryable)
} else {
console.log(page.markdown)
console.log(page.json)
console.log(page.meta.snapshotId)
console.log(page.meta.playground)
}The result shape, abbreviated:
type FetchJsonResponse =
| {
meta: {
snapshotId: string
playground: string
url: string
capturedAt: string
// ...
}
markdown: string
json: Array<unknown>
data?: {
search?: {
query: string
snippets: Array<{
source: 'markdown' | 'appendix' | 'statejson'
text: string
json?: unknown
markdown is the Main Markdown.json is State JSON and extracted evidence.meta.snapshotId is the handle for later Highlights with fetchSearch.meta.playground is the human inspection link.data.search appears when inline search is requested.originError appears on captured content when the origin answered with HTTP 400 or above; it is not a top-level union member.This shape is not exhaustive. See Output Model and the API Reference for exact fields.
fetchfetch() is the convenience method for scripts and agents that only need the Markdown string.
const markdown = await expand.fetch({
url: 'https://example.com',
})
console.log(markdown)string./v1/fetch.fetchJson() if the application needs State JSON, snapshot metadata, or structured search results.Both fetch and fetchJson accept query include options through the second argument.
const markdownWithAppendix = await expand.fetch(
{ url: 'https://example.com' },
{ include: 'appendix,statejson' },
)const objectMode = await expand.fetchJson(
{ url: 'https://example.com' },
{ include: 'appendix' },
)include is a query option passed as the second argument.fetchJson also accepts include options in the request body when you need exact structured control.{ include: null } omits the include query param.Exact include semantics belong to Include Options and the API Reference.
Highlights ("search") run two ways: inline with a fresh Fetch, or against a stored snapshot.
fetchJsonconst page = await expand.fetchJson({
url: 'https://docs.example.com',
search: {
query: 'authentication limits',
maxResults: 5,
minScore: 0.6,
},
})
if (isFetchOriginError(page)) {
console.error(page.kind, page.url, page.retryable)
} else {
const snippets = page.data?.search?.snippets ?? []
}data.search.snippets.json.location, not the MCP-only citationUrl field.fetchSearchconst page = await expand.fetchJson({
url: 'https://docs.example.com',
})
if (isFetchOriginError(page)) {
console.error(page.kind, page.url, page.retryable)
} else {
const result = await expand.fetchSearch({
snapshotId: page.meta.snapshotId,
search: {
query: 'authentication limits',
maxResults: 5,
minScore: 0.6,
},
fetchSearch calls /v1/fetch/search.Raw SDK results expose snippet location. Use Playground helpers when you want to turn a snippet into a user-visible citation link.
import { citationUrl, resolvePlaygroundHost } from '@expandai/sdk/Playground'if (isFetchOriginError(page)) {
console.error(page.kind, page.url, page.retryable)
} else {
const host = resolvePlaygroundHost(page.meta.playground, 'https://expand.land')
for (const snippet of page.data?.search?.snippets ?? []) {
const url = citationUrl(page.meta.snapshotId, snippet.location, host)
console.log(snippet.text, url)
}
}| Helper | Purpose |
|---|---|
playgroundBase(snapshotId, host?) | Build the whole-snapshot Playground URL. |
playgroundOrigin(metaPlayground) | Extract the origin from meta.playground. |
resolvePlaygroundHost(metaPlayground, fallback) | Prefer the server-provided Playground host, fall back if missing. |
SDK API responses do not already contain citationUrl. MCP adds that field; SDK users build it with these helpers. See Playground & Replay for URL semantics.
snapshotsfetchJson returns meta.resources when a snapshot was saved. Its snapshotId is the public
snapshot-link ID; the snapshots methods read the catalog, collections, and artifact previews
behind it without an API key and without refetching the page. Catalog links are paths on the API
origin, so resolve them against meta.resources.url before fetching bytes.
const page = await expand.fetchJson({ url: 'https://example.com' })
if (isFetchOriginError(page) || !page.meta.resources) throw new Error('no snapshot to disclose')
const snapshotId = page.meta.resources.snapshotId
const catalog = await expand.snapshots.get({ snapshotId })
const network = await expand.snapshots.network({ snapshotId })
// Archives over the server budget answer 413 here; the content link below still works.
const preview = await expand.snapshots
.preview({ snapshotId, artifactId:
The full walkthrough is in Progressive Disclosure.
const run = await expand.batched({
urls: [
'https://example.com',
'https://example.com/about',
],
})
let status = await expand.getBatched(run.id, { limit: '10', offset: '0' })
while (status.batchedStatus === 'QUEUED' || status.batchedStatus === 'RUNNING') {
await new Promise((resolve) => setTimeout(resolve, 1000))
batched() returns a run ID.getBatched() polls the run.data.markdown and State JSON at data.json by default when extraction succeeds.status alongside data, so handle failed or cancelled URLs independently.idempotencyKey and reuses it for the configured retry policy.idempotencyKey to deduplicate separate calls or calls made after a process restart.409 BatchedIdempotencyConflict with reason: 'payload_mismatch'.getBatched query params use strings for limit, offset, and include.The full lifecycle belongs to Batched Fetch.
Use ExpandService when your application is already Effect-native. It exposes the same operations as ExpandClient, but failures are typed in the Effect error channel.
import { NodeRuntime } from '@effect/platform-node'
import { Effect } from 'effect'
import { ExpandService, isFetchOriginError } from '@expandai/sdk'
const program = Effect.gen(function* () {
const expand = yield* ExpandService
const page = yield* expand.fetchJson({ url: 'https://example.com' })
if (isFetchOriginError(page)) {
yield* Effect.logWarning(page.kind, page.url, page.retryable)
You do not need this section to use the SDK from plain Promise code.
Promise-side errors, thrown by ExpandClient:
| Error | Meaning |
|---|---|
ExpandClientError | Base Promise-side SDK error and invalid client options. |
ExpandClientApiError | Non-2xx API response. Inspect status and body. |
ExpandClientConnectionError | Network failure. |
Effect-side errors, surfaced in the ExpandService error channel:
| Error | Meaning |
|---|---|
ExpandSdkError | Invalid service options or SDK setup error. |
ExpandApiError | Non-2xx API response. |
ExpandConnectionError | Network failure. |
ExpandTimeoutError |
Retry and timeout behavior:
timeoutMs is 60000.maxRetries is 2.408, 409, 429, and 5xx.FetchNavigationFailed (502) and FetchCaptureTimeout (504) are exempt from automatic retries. A 504 means the capture started but no result arrived in time; it does not prove the page caused the timeout. Skipping automatic retries keeps an Expand-side incident from being amplified; a deliberate manual retry can still make sense.FetchCapacityTimeout (529) means the request was still waiting for capacity when its deadline passed. It is retryable, and the SDK honors it through the normal retry policy; the response advertises Retry-After: 5.import { ExpandClientApiError } from '@expandai/sdk'
try {
await expand.fetchJson({ url: 'https://example.com' })
} catch (error) {
if (error instanceof ExpandClientApiError) {
console.error(error.status, error.body)
}
throw error
}timeoutMstimeoutMs is a total wall-clock budget across all SDK attempts, and it is enforced client-side. Synchronous Fetch has one 150-second budget per request that covers queueing, rendering, and internal retries, and it never moves. Expand then takes at most five seconds to decide the outcome, so use 180 seconds to leave 25 seconds for the response to reach you:
const page = await expand.fetchJson(
{ url: 'https://example.com' },
{ timeoutMs: 180_000, maxRetries: 0 },
)180000 covers the budget, the outcome decision, and delivery headroom. A result that arrives while Expand is deciding the outcome is still returned successfully; otherwise the API produces a typed FetchCaptureTimeout/FetchCapacityTimeout by 155 seconds. The remaining 25 seconds are for the HTTP response and network.60000 default is sized for typical pages; it aborts long captures with ExpandClientTimeoutError before Expand answers.timeoutMs per additional attempt you allow, or set maxRetries: 0 when you only want one verdict.FetchCaptureTimeout.timeoutMs is 150000 by default; it is the request ceiling, not the time remaining. The tag means the capture started and did not finish in time; a request that never started is reported separately as FetchCapacityTimeout.| Topic | Where to go |
|---|---|
| First SDK or API setup | Quickstart |
| Choosing SDK vs CLI/MCP/API | Ways to Use Expand |
| Product behavior | Fetch Overview |
| Main Markdown and State JSON |
timeoutMs60000 |
| Total wall-clock budget across attempts. |
maxRetries | 2 | Retry attempts for retryable failures. |
fetchSearch(params, options?) | You already have snapshotId and want Highlights from stored artifacts. | Snapshot Highlights result. |
batched(params, options?) | You want to start many async Fetch jobs. The SDK generates options.idempotencyKey when omitted. | { id } |
getBatched(id, options?) | You want to poll a Batched Fetch run. | Batched status/results. |
snapshots.get({ snapshotId }) | You have meta.resources.snapshotId and want the public catalog of saved artifacts and collections. | SnapshotCatalog |
snapshots.network({ snapshotId }) | You want the captured network requests for a snapshot. | unknown: the collection JSON { items, next? }; narrow before use. |
snapshots.preview({ snapshotId, artifactId, maxBytes? }) | You want a bounded look at an artifact before downloading it. | unknown: { contentType, encoding: "base64", body, bytes, totalBytes, truncated }; narrow before use. |
citationUrl(snapshotId, location, host?)Build a citation link from location.evidenceId. |
ExpandClientTimeoutErrorRequest exceeded timeoutMs. |
Request exceeded timeoutMs. |
| Include semantics | Include Options |
| Highlights behavior | Highlights |
| Citation URLs and replay | Playground & Replay |
| Batched lifecycle | Batched Fetch |
| Snapshot catalog, collections, artifacts | Progressive Disclosure |
| Exact endpoint schemas | API Reference |