Web fetch
Give it URLs, get back readable markdown. No headless browser to run, no HTML to strip, no per-site parser to maintain.
/v1/fetchPages come back as markdown with navigation, ads and boilerplate already removed — the shape you would want to hand to a model or index. Results are returned in the order you sent the URLs, so you can line them up by position.
curl https://api.xkiro.com/v1/fetch \
-H "Authorization: Bearer $XKIRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "xkiro/web-fetch",
"urls": ["https://example.com", "https://www.iana.org/help/example-domains"],
"max_content_tokens": 4000
}'Request body#
| Field | Type | Description |
|---|---|---|
modelrequired | string | Always xkiro/web-fetch. Required even though there is one value — your account limits and spend controls are resolved from it. |
urlsrequired | string[] | Up to 10 http or https URLs. url with a single string is accepted as well. Duplicates are removed, order is kept. |
max_content_tokens | integer | Cap the text returned per page. Default 100,000, maximum 200,000. Truncation happens at a word boundary and is marked with an ellipsis. It does not change the price. |
http and https only
Any other scheme is rejected with 400 before the request leaves us. This endpoint reads a URL on your behalf, so it will not be pointed at internal addresses, files on disk, or anything that is not a web page.
Response#
{
"id": "ftch_5167eff5-7f90-4015-8d63-101091589fa8",
"model": "xkiro/web-fetch",
"results": [
{
"url": "https://example.com",
"title": "Example Domain",
"content": "# Example Domain\n\nThis domain is for use in documentation examples…",
"publishedDate": "2026-09-15T23:41:26",
"error": null
},
{
"url": "https://private.example/report",
"title": null,
"content": "",
"publishedDate": null,
"error": "not_fetched"
}
],
"usage": { "fetches": 2, "remainingToday": 98 }
}Every URL you sent comes back, in order. A page that could not be read still appears, with an error and empty content— it is never dropped silently, and its slot is never filled with another page’s text.
| Field | Type | Description |
|---|---|---|
null | Read successfully. | |
not_fetched | The page could not be opened — offline, blocked, login required, or not a page at all. | |
empty_content | The page opened but held no extractable text, e.g. a pure image or video page. | |
unreadable_content | The page opened but its content could not be parsed into text. |
Plan allowances#
Web page reads are included in every paid plan, with a daily allowance counted per account — not per API key, so extra keys do not buy extra quota. The window rolls continuously over 24 hours rather than resetting at midnight.
| Field | Type | Description |
|---|---|---|
Free | 20 | Then pay-as-you-go at $0.01 each, from your wallet. |
Pro | 100 | 10 requests/minute · 3 at a time. |
Pro Plus | 200 | 15 requests/minute · 3 at a time. |
Max | 300 | 20 requests/minute · 4 at a time. |
Ultra | 1,500 | 30 requests/minute · 6 at a time. |
Power | 3,000 | 60 requests/minute · 8 at a time. |
Search and page reads have separate allowances
A heavy day of searching never eats into your page-read allowance, or the other way round. Each response carries usage.remainingToday so you always know where you stand without having to hit the limit first.
Pricing#
- On a paid plan, reads inside your allowance cost nothing. When the allowance runs out you get a
429telling you how long until it frees up — your wallet is never charged behind your back for something the plan said was included. - Without a plan: 20 free URLs a day, then $0.01 each — $10 per 1,000. A request with five URLs is five.
- Page content is included either way. No token charge for what comes back, however long the page is. A 200-page document costs the same as a one-paragraph page.
- You are billed only for URLs actually read. If three of five come back, you pay for three.
Errors#
These are request-level failures. A single unreadable page is not an error — it comes back as a result with an error field, and the request is still 200.
| Field | Type | Description |
|---|---|---|
400 | invalid_request | Malformed URL, a non-http(s) scheme, or more than 10 URLs. The message says which. |
401 | authentication_error | Missing or invalid API key. |
402 | insufficient_quota | Free allowance used up and wallet balance is zero. |
429 | rate_limit | Your plan's daily allowance is used up, or you exceeded the per-minute or concurrent limit. The message says which, and how long to wait. |
502 | no_fetch_performed | None of the URLs could be read. You are not billed for this. |
503 | service_unavailable | Temporarily unavailable. Retry shortly. |
Practical notes
- Batch URLs into one request rather than firing ten. Same price, one round trip, and the per-minute limit counts requests.
- Set
max_content_tokensto what you will actually use. The default is deliberately generous; a long page you then truncate yourself is context you paid a model to read. - Cache by URL. Most pages do not change between two runs of the same job.
- Looking for pages rather than reading known ones? Start with web search and fetch the URLs worth reading in full.
