POST /v1/read[01]

POST /v1/read

Fetch any public URL and return clean Markdown plus extraction stats. 1 credit per call. No score — use /v1/read-and-score for that.

Do this firstTerminal
curl -X POST https://api.buildonto.dev/v1/read \
  -H "Authorization: Bearer onto_sk_live_YOUR_KEY" \
  -d '{"url":"https://stripe.com/pricing"}'

Returns clean Markdown plus extraction stats.

Endpoint

[02]
POST https://api.buildonto.dev/v1/read
Authorization: Bearer onto_sk_live_YOUR_KEY
Content-Type: application/json

Request body

[03]
urlstringreq

The public URL to fetch and clean. Must be http:// or https://. Private or internal hosts return INVALID_URL.

freshboolean

If true, skip the cache lookup and fetch again. The result is still written to the cache. Default: false.

Response

[04]

Success (200): JSON by default. Add Accept: text/markdown to get raw Markdown back instead. The url in the response is canonicalised — for example, GitHub blob URLs become raw.githubusercontent.com URLs. A warnings array appears only when something is worth flagging, such as a page that looks like a JavaScript shell with little server-rendered content.

{
  "status": "success",
  "url": "https://stripe.com",
  "markdown": "# Stripe — Online payments…\n\n…",
  "metadata": {
    "title": "Stripe | Financial Infrastructure...",
    "description": "Stripe powers online and...",
    "language": "en"
  },
  "stats": {
    "raw_html_size_kb": 605.0,
    "markdown_size_kb": 14.8,
    "reduction_percent": 97.6,
    "extraction_time_ms": 412
  },
  "cache": { "hit": false, "ttl_seconds": 3600 }
}

Errors: see error codes. This endpoint can return INVALID_URL (400, also private or internal hosts and non-http schemes), UNAUTHORIZED (401), PAYMENT_REQUIRED (402), ROBOTS_BLOCKED (403), WAF_BLOCKED (403), URL_NOT_FOUND (404, also DNS failure), TOO_LARGE (413, over 10 MB), UNSUPPORTED_TYPE (415), IMAGE_PDF (422), RATE_LIMITED (429), CONCURRENT_LIMIT (429), EXTRACTION_FAILED (500), TLS_ERROR (502), TIMEOUT (504, 15 s page fetch).

Response headers

[05]
X-Onto-Cache'HIT' | 'MISS'

Whether this came from the one-hour cache. A HIT means no outbound fetch happened. It still costs 1 credit.

X-RateLimit-Remainingint

Monthly plan credits left. Stays 0 while you are billed from bought credits. Not a short rate-limit window.

X-RateLimit-ResetISO date

When monthly plan credits reset (end of the UTC month).

X-Onto-Billed'plan' | 'credit'

Whether this request used monthly plan credits or bought credits.

X-Credits-Remainingint

Bought-credit balance after this request. Sent only when the request was billed to credits.

X-Concurrent-Remainingint

How many more in-flight requests you have headroom for at this moment.

Examples

[06]

cURL — JSON response:

curl -X POST https://api.buildonto.dev/v1/read \
  -H "Authorization: Bearer $ONTO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://stripe.com"}'

cURL — Markdown response (no JSON wrapper):

curl -X POST https://api.buildonto.dev/v1/read \
  -H "Authorization: Bearer $ONTO_API_KEY" \
  -H "Accept: text/markdown" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://stripe.com"}'

Node (fetch):

const res = await fetch('https://api.buildonto.dev/v1/read', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.ONTO_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ url: 'https://stripe.com' }),
});
const data = await res.json();
console.log(data.markdown);

Python (httpx):

import os, httpx

r = httpx.post(
    "https://api.buildonto.dev/v1/read",
    headers={"Authorization": f"Bearer {os.environ['ONTO_API_KEY']}"},
    json={"url": "https://stripe.com"},
)
print(r.json()["markdown"])