POST /v1/batch

Read, score, or extract many URLs in a single call — an explicit list or a whole site, billed as one request.

One call, one credit. Pulling 50 pages of a site costs the same as one request — you don't spend a credit per URL. Every URL runs through the same deterministic engine as /v1/read (no AI, no third-party).

Endpoint

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

Request body

Supply either urls (an explicit list) or site (a base URL whose pages are auto-discovered via its sitemap, falling back to on-page links).

urlsstring[]optionalExplicit list of http(s) URLs to process (max 50). Use this OR "site".
sitestringoptionalBase URL of a site whose pages are auto-discovered. Use this OR "urls".
mode'read' | 'read-and-score' | 'extract'optionalWhat to do per URL. Default: 'read-and-score'. 'read' returns Markdown only; 'extract' returns JSON-LD/OpenGraph/meta.
limitnumberoptionalSite mode only: max pages to discover (default 25, max 50).

Response

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.

json
{
  "status": "success",
  "mode": "read-and-score",
  "source": "urls",
  "requested": 3,
  "succeeded": 3,
  "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. Whole-request errors: INVALID_REQUEST (400, no valid urls/site), UNAUTHORIZED (401), NO_RESULTS (404, site discovery found nothing), RATE_LIMITED (429), PAYMENT_REQUIRED (402). Per-URL failures are reported inline, not as a top-level error.

Billing

A batch counts as one request against your plan quota regardless of how many URLs it processes (capped at 50 per call). Response headers match the other endpoints: X-RateLimit-Remaining, X-Onto-Billed, X-Onto-Cache, X-Concurrent-Remaining.

Examples

cURL — explicit list:

bash
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:

bash
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):

ts
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) {
  if (r.ok) console.log(r.url, r.aio_score, r.markdown.length);
}