Web fetch

Give it URLs, get back readable markdown. No headless browser to run, no HTML to strip, no per-site parser to maintain.

POST/v1/fetch

Pages 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#

Fields
FieldTypeDescription
modelrequiredstringAlways xkiro/web-fetch. Required even though there is one value — your account limits and spend controls are resolved from it.
urlsrequiredstring[]Up to 10 http or https URLs. url with a single string is accepted as well. Duplicates are removed, order is kept.
max_content_tokensintegerCap 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#

200 OK
{
  "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.

Per-result error values
FieldTypeDescription
nullRead successfully.
not_fetchedThe page could not be opened — offline, blocked, login required, or not a page at all.
empty_contentThe page opened but held no extractable text, e.g. a pure image or video page.
unreadable_contentThe 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.

URLs read per day, and burst limits
FieldTypeDescription
Free20Then pay-as-you-go at $0.01 each, from your wallet.
Pro10010 requests/minute · 3 at a time.
Pro Plus20015 requests/minute · 3 at a time.
Max30020 requests/minute · 4 at a time.
Ultra1,50030 requests/minute · 6 at a time.
Power3,00060 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 429 telling 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.

Status codes
FieldTypeDescription
400invalid_requestMalformed URL, a non-http(s) scheme, or more than 10 URLs. The message says which.
401authentication_errorMissing or invalid API key.
402insufficient_quotaFree allowance used up and wallet balance is zero.
429rate_limitYour 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.
502no_fetch_performedNone of the URLs could be read. You are not billed for this.
503service_unavailableTemporarily 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_tokens to 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.

Was this page helpful?