# POST /v1/batch

> Read, score or extract up to 50 URLs — or a whole site — in one call, at one credit per URL.

Source: https://docs.buildonto.dev/api/batch
Section: Read API

---

POST /v1/batch\[01\]

## POST /v1/batch

Read, score, or extract up to 50 URLs in a single call — an explicit list or a whole site. 1 credit per URL; failed URLs are refunded.

**1 credit per URL.** A batch of 50 URLs costs 50 credits. Every result that comes back `ok: false` is refunded. Every URL runs through the same deterministic engine as [/v1/read](/api/read) (no AI, no third-party).

### Endpoint

\[02\]


```
POST https://api.buildonto.dev/v1/batch
Authorization: Bearer onto_sk_live_YOUR_KEY
Content-Type: application/json
```

### Request body

\[03\]

Supply **either** `urls` (an explicit list) or `site` (a URL whose pages are auto-discovered). Discovery reads `/sitemap.xml` at the site root, and falls back to the links on the page you pass. Only same-host URLs are kept. There is no `fresh` option.

urlsstring\[\]

Explicit list of http(s) URLs to process (max 50). URLs past the 50th, and URLs that can't be parsed, are dropped silently. Use this OR "site".

sitestring

URL of a site whose pages are auto-discovered on the same host. Use this OR "urls".

mode'read' | 'read-and-score' | 'extract'

What to do per URL. Default: 'read-and-score'. An unknown value silently becomes 'read-and-score'. 'read' returns Markdown only; 'extract' returns JSON-LD/OpenGraph/meta plus the score fields where scoring is available.

limitnumber

Site mode only: max pages to discover, 1–50 (default 25).

### Response

\[04\]

**Success (200):** a `results` array, one entry per URL. A page that can't be read (robots, WAF, 404) comes back with `ok: false` and an `error` — it never fails the whole batch. Per-URL fields depend on `mode`: `markdown` for read modes, `structured` + `counts` for extract. Both `read-and-score` and `extract` also carry `aio_score`, `grade` and `hallucination_risk` whenever scoring is available (PDFs carry text but no score). Batch `markdown` is cut at 12,000 characters per URL. A cut page ends with `…[truncated by /v1/batch — call /v1/read on this URL for the full page]`. There is no per-result truncated flag, so check for that marker. Call [/v1/read](/api/read) on that URL for the full body.

A success item has `url`, `ok: true`, `title`, `reduction_percent` and the mode's fields above. A failed item is `{ url, ok: false, error: { code, message } }`. The top level has `status`, `mode`, `source` (`"urls"` or `"site"`), `requested`, `succeeded`, `results` and `cache`.

Onto fetches 8 URLs at a time, in fixed rounds. A slow URL holds up the rest of its round.

**The whole response is cached for 1 hour**, including `ok: false` items. The cache key is the mode plus the sorted set of URLs. A URL that failed is replayed as failed for an hour unless you change the URL set. A cache hit is still billed per URL, with failed items refunded.


```
{
  "status": "success",
  "mode": "read-and-score",
  "source": "urls",
  "requested": 3,
  "succeeded": 2,
  "results": [
    {
      "url": "https://stripe.com/pricing",
      "ok": true,
      "title": "Pricing — Stripe",
      "aio_score": 88,
      "grade": "Good",
      "hallucination_risk": "low",
      "reduction_percent": 96.4,
      "markdown": "# Stripe pricing\n\n…"
    },
    {
      "url": "https://blocked.example.com",
      "ok": false,
      "error": { "code": "ROBOTS_BLOCKED", "message": "Target site's robots.txt disallows AI crawlers." }
    }
  ],
  "cache": { "hit": false, "ttl_seconds": 3600 }
}
```

**Errors:** see [error codes](/api/errors). Whole-request errors: `INVALID_REQUEST` (400, no valid urls/site), `INVALID_URL` (400, `site` could not be parsed or points at a private or internal host), `UNAUTHORIZED` (401), `NO_RESULTS` (404, site discovery found nothing — in practice discovery always returns at least the start URL), `RATE_LIMITED` (429), `CONCURRENT_LIMIT` (429), `PAYMENT_REQUIRED` (402). Per-URL failures are reported inline, not as a top-level error.

### Billing

\[05\]

A batch of N URLs costs N credits (N is at most 50). The credits are taken before any URL is fetched, then every `ok: false` result is refunded.

The quota check is all-or-nothing. If N is more than your monthly plan credits left, all N are billed to bought credits on a paid plan. On Free, the whole batch gets `RATE_LIMITED`. A batch holds one concurrency slot, however many URLs it has.

Response headers: `X-Onto-Cache`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `X-Onto-Billed`, `X-Concurrent-Remaining`, and `X-Credits-Remaining` when the batch was billed to credits. The credit headers already include the refunds.

### Examples

\[06\]

cURL — explicit list:


```
curl -X POST https://api.buildonto.dev/v1/batch \
  -H "Authorization: Bearer $ONTO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "urls": ["https://stripe.com/pricing", "https://stripe.com/docs"],
    "mode": "read-and-score"
  }'
```

cURL — whole site, extract mode:


```
curl -X POST https://api.buildonto.dev/v1/batch \
  -H "Authorization: Bearer $ONTO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "site": "https://stripe.com", "limit": 25, "mode": "extract" }'
```

Node (fetch):


```
const res = await fetch('https://api.buildonto.dev/v1/batch', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.ONTO_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    urls: ['https://stripe.com/pricing', 'https://stripe.com/docs'],
    mode: 'read-and-score',
  }),
});
const data = await res.json();
for (const r of data.results) {
  // r.markdown is capped at 12,000 chars by /v1/batch
  if (r.ok) console.log(r.url, r.aio_score, r.markdown.length);
}
```

Copy as Markdown[](/api/batch.md "Open the raw Markdown")

---
## Structured Data (JSON-LD)
```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "Onto Docs",
  "url": "https://docs.buildonto.dev",
  "description": "How to use Onto: serve AI agents Markdown from your Next.js site, call the Read API, connect over MCP, and read the AIO score.",
  "inLanguage": "en",
  "publisher": {
    "@type": "Organization",
    "name": "Onto",
    "url": "https://buildonto.dev"
  }
}
```