Fetch a PDF, image, office file, ebook, data file, or audio file, and get the same agent-ready Markdown back.
Not every URL an agent follows is a web page. Research trails end in PDFs, data links end in spreadsheets, and a citation chain will happily drop you on a scanned report.
Fetch converts those files on the endpoint you already call and returns the same Markdown. There is nothing to turn on. POST /v1/fetch decides for itself whether a URL is a page to render or a file to convert.
import { ExpandClient } from '@expandai/sdk'
const expand = new ExpandClient()
const markdown = await expand.fetch({ url: 'https://arxiv.org/pdf/1706.03762' })expandai // Arguments for the fetch tool
{
"url": "https://arxiv.org/pdf/1706.03762"
}curl https://api.expand.ai/v1/fetch \
-H "x-expand-api-key: $EXPAND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://arxiv.org/pdf/1706.03762"}'| Family | Formats | How it converts | Extra credits |
|---|---|---|---|
.pdf | From the text layer. Pages with no usable text go to a vision model. | Per page, plus vision-model pages. | |
| Images | .png, .jpg, .jpeg, , , , , , |
Everything else, including HTML and SVG, is rendered in a browser and captured as a page. See Browser-rendered pages. A YouTube video is its own case and comes back as a transcript. See YouTube videos.
The extension is only a hint. Fetch looks at what the origin serves, so https://arxiv.org/pdf/1706.03762 converts as a PDF even though the URL has no extension, and a .pdf URL that serves a web page is captured as a page. A spreadsheet served as application/octet-stream still converts.
text/plain is captured as a page. Give it a .txt or .md extension, or serve text/markdown, to get the text back as is.A file comes back in either of Fetch's two modes, as Markdown text or as a JSON object. See Two modes.
Markdown Mode returns the converted file as Markdown text. It is the document itself, with no frontmatter added. Details such as the content type and the link to the original file are only in the JSON object.
import { ExpandClient } from '@expandai/sdk'
const expand = new ExpandClient()
const markdown = await expand.fetch({ url: 'https://arxiv.org/pdf/1706.03762' })expandai fetch https://arxiv.org/pdf/1706.03762curl https://api.expand.ai/v1/fetch \
-H "x-expand-api-key: $EXPAND_API_KEY" \
-H "Content-Type: application/json"
# Attention Is All You Need
## Abstract
The dominant sequence transduction models are based on complex recurrent or convolutional neural networks ...JSON Mode returns the converted file as a JSON object. It has the standard fields, plus data.document, a link to the original file, and data.usage, what the conversion cost.
import { ExpandClient } from '@expandai/sdk'
const expand = new ExpandClient()
const page = await expand.fetchJson({ url: 'https://arxiv.org/pdf/1706.03762' })expandai fetch https://arxiv.org/pdf/1706.03762 --format json// Arguments for the fetch tool
{
"url": "https://arxiv.org/pdf/1706.03762"
{
"meta": {
"version": 1,
"url": "https://arxiv.org/pdf/1706.03762",
"capturedAt": "2026-09-01T00:00:00.000Z",
"snapshotId": "0f9c3a1e-...",
"contentType":
| Field | Definition |
|---|---|
markdown | The whole converted document. |
meta.contentType | The detected content type. It can differ from what the origin's header said. |
meta.snapshotId | The ID of the stored conversion. There is no page to replay. |
data.document.url |
json comes back as an empty list, and the other fields are left out. The Playground previews the converted document instead.Fetching a file costs the usual 1 credit, plus credits for the work of converting it:
| Unit | Credits | When it applies |
|---|---|---|
| Converted page | 1 per page | Every page a conversion delivers. An image counts as one page. |
| Vision-model page | 5 per page, on top of the page | A page read by the vision model instead of from a text layer. |
| Audio | per minute |
Office files, ebooks, data files, and text convert locally and add nothing. Those Fetches cost the flat 1 credit.
For example, a 12-page PDF with a text layer costs 13 credits. The same document scanned, with no text layer, costs 73 credits.
You are only charged for a delivered result, never when a file can't be converted. data.usage reports the units each Fetch spent, so you can check how it was charged. See Pricing & Usage.
documentConfig caps the work a conversion can do, and so what it can cost. Every field is optional.
| Field | Definition | Default | Range |
|---|---|---|---|
maxPages | Refuse the Fetch rather than convert a PDF with more pages than this. | No limit | Any positive integer |
maxAudioSeconds | Refuse the Fetch rather than transcribe audio longer than this. |
The ocr modes:
auto keeps the readable text from each page and sends pages with missing or unreliable text to the vision model.never keeps the vision model out. It is the cheapest and most predictable setting. It also returns an empty document for a scanned PDF, and nothing at all for an image, since an image has no text layer to fall back on.always reads every page with the vision model, even where a text layer exists.import { ExpandClient } from '@expandai/sdk'
const expand = new ExpandClient()
const markdown = await expand.fetch({
url: 'https://www.irs.gov/pub/irs-pdf/fw4.pdf',
documentConfig: { maxPages: 10, ocr: 'never' },
})curl https://api.expand.ai/v1/fetch \
-H "x-expand-api-key: $EXPAND_API_KEY" \
-H
413 FetchPageTooLarge, not cut short. A half-converted document is worse than a clear error, because nothing in the Markdown says where it stopped. A refused Fetch costs nothing.A conversion that is within the limits can still take a while.
Batched Fetch doesn't convert files yet. It renders every URL in a browser, so a file URL comes back as a failed item rather than as Markdown. The batched endpoint accepts documentConfig but ignores it. Send file URLs to POST /v1/fetch or POST /v1/fetch/json until this lands.
On top of the standard Fetch errors, a file Fetch can return four more errors. Their full payloads are in the API reference.
_tag | Code | Retryable |
|---|---|---|
FetchUnsupportedContentType | 415 | No |
FetchOriginHttpError | 404, 410, or |
FetchUnsupportedContentTypeThe file can't be converted. Its format is unknown, or it is too damaged to read.
{
"_tag": "FetchUnsupportedContentType",
"url": "https://example.com/archive.7z",
"contentType": "application/x-7z-compressed"
}contentType is present when Fetch could detect one.
FetchOriginHttpErrorThe origin refused to serve the file. statusCode is the status it answered with.
{
"_tag": "FetchOriginHttpError",
"url": "https://example.com/report.pdf",
"statusCode": 404
}A missing or removed file comes back as 404 or 410, and any other refusal as 502. Only the 502 is worth retrying, and the SDK retries it for you.
A page Fetch works differently. When a page's origin answers with an error, Fetch renders that error page and reports the status beside it, because an error page is often the answer you needed. A file has no page to render, so the origin's status comes back as the error.
200 FetchOriginError with contentAvailable: false, originStatusCode, and a retryable verdict. Support both shapes during the transition.FetchDocumentProcessingUnavailableThe vision model or the speech model couldn't finish.
{
"_tag": "FetchDocumentProcessingUnavailable",
"url": "https://example.com/scan.pdf",
"operation": "ocr",
"message": "OCR provider unavailable"
}The operation field is set to ocr when the vision model couldn't finish, and transcription when the speech model couldn't finish. The SDK retries this error automatically, with backoff.
FetchPageTooLargeThe file is over one of your documentConfig limits.
{
"_tag": "FetchPageTooLarge",
"url": "https://example.com/book.pdf",
"limitKind": "document_pages",
"limit": 50,
"measured": 312
}limitKind says which limit: document_pages for maxPages, or audio_seconds for maxAudioSeconds. limit and measured are in that unit. To convert the file, raise the limit. See Limiting what gets converted.
.gif.webp.avif.bmp.tif.tiff| A vision model reads the text and describes the image. |
| One vision-model page. |
| Office | .doc, .docx, .docm, .ppt, .pptx, .pptm, .pps, .ppsx, .ppsm, .pot, .xls, .xlsx, .xlsm, .xlsb, .odt, .ods, .odp, .rtf | Locally. Spreadsheets become Markdown tables with their number formats intact. | None |
| Ebooks | .epub | Locally, chapter by chapter. | None |
| Data | .csv, .tsv, .json, .xml, .rdf | Locally. CSV and TSV become tables. JSON and XML keep their structure. | None |
| Text | .txt, .text, .md, .markdown | Markdown comes back as is. Plain text is fenced so its whitespace survives. | None |
| Audio | .mp3, .wav, .m4a, .aac, .ogg, .oga, .opus, .flac | Transcribed with a speech model. | Per minute. |
curl https://api.expand.ai/v1/fetch/json \
-H "x-expand-api-key: $EXPAND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://arxiv.org/pdf/1706.03762"}'| A presigned link to the original file, for sharing or downloading it. |
data.document.sizeBytes | The size of the original, as downloaded. |
data.usage | The pages converted, pages read by the vision model, and seconds of audio transcribed. See Additional credit usage. |
10| Audio files. Rounded up, one-minute minimum. |
36001 to 14400 |
ocr | Whether pages may go to the vision model. | auto | auto, never, always |
502| Sometimes |
FetchDocumentProcessingUnavailable | 503 | Yes |
FetchPageTooLarge | 413 | No |