Documentation
OneScrape
OneScrape turns a list of URLs, or a site, into rows: title, content, the provider that read it, and every vendor call made. Two endpoints share one row format. vaaya/onescrape reads up to 5 URLs through the cheap ladder for a flat 2 cents per URL. vaaya/onescrape-deep reads up to 50 URLs, or crawls a site, through the full ladder including the unblock rungs, and charges only for pages that were read.
Endpoints
| Endpoint | What it does | Price | Mode |
|---|---|---|---|
POST /api/run/vaaya/onescrape | Read 1 to 5 URLs through the cheap ladder. | 2 cents per URL | sync |
POST /api/run/vaaya/onescrape-deep | Read up to 50 URLs, or crawl a site, through the full ladder. | per page read, capped by a budget | async |
Both take a JSON body and a bearer API key. Both return the same row format, described below.
OneScrape reads pages. Social platform URLs (LinkedIn, X, Instagram, TikTok, Reddit, YouTube and the CN platforms) are refused: read those through Social data. Documents (PDF, Office files) are refused too. Both refusals are 400s and cost nothing.
Read pages
Send up to 5 URLs. Bare hosts are accepted. Get back a row per URL.
curl -X POST https://vaaya.ai/api/run/vaaya/onescrape \
-H "Authorization: Bearer $VAAYA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"urls": ["https://stripe.com/pricing", "https://www.amazon.com/dp/B09B8V1LZ3"]}'Request
| Field | Type | Required | Description |
|---|---|---|---|
urls | string[] | yes | 1 to 5 URLs. Duplicates are collapsed. |
format | string | no | markdown (default) or html. With markdown, a rung that only has the raw page returns it stripped to text and says so in format. |
main_content | boolean | no | Strip navigation and boilerplate. Default true. |
Response
{
"ok": true,
"data": {
"url_count": 2,
"row_count": 2,
"read_count": 1,
"format": "markdown",
"hard": false,
"rows": [
{
"id": "https://stripe.com/pricing",
"url": "https://stripe.com/pricing",
"final_url": "https://stripe.com/pricing",
"title": "Pricing | Stripe",
"content": "# Pricing\n...",
"format": "markdown",
"chars": 18422,
"provider": "firecrawl",
"yield": 0.91,
"quality_flags": [],
"hops": [
{ "provider": "exa", "action": "contents", "ok": false, "charged_cents": 0, "latency_ms": 140 },
{ "provider": "firecrawl", "action": "scrape", "ok": true, "charged_cents": 1, "latency_ms": 620 }
],
"hard": false,
"charged_cents": 1
},
{
"id": "https://www.amazon.com/dp/B09B8V1LZ3",
"url": "https://www.amazon.com/dp/B09B8V1LZ3",
"final_url": null,
"title": null,
"content": null,
"format": "markdown",
"chars": 0,
"provider": null,
"yield": 0,
"quality_flags": ["blocked"],
"hops": [ ... ],
"hard": false,
"charged_cents": 0,
"error": "blocked"
}
],
"calls": [ ... ],
"next": "1 of 2 URLs came back blocked. Call onescrape-deep with urls: [...] ..."
},
"charged_cents": 4
}The call is charged 2 cents per URL sent, whether or not every row was read. If no row was read at all, the call fails with all_blocked and is not charged. charged_cents on a row is what the rung that read it would cost on the deep tier; it is informational here.
Read through the unblock rungs, or crawl a site
Send urls to read up to 50 pages through the full ladder, or site to map a site and read its pages. The call returns a job_id. Poll it until the job finishes.
curl -X POST https://vaaya.ai/api/run/vaaya/onescrape-deep \
-H "Authorization: Bearer $VAAYA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"urls": ["https://www.amazon.com/dp/B09B8V1LZ3"], "budgetCents": 20}'curl -X POST https://vaaya.ai/api/run/vaaya/onescrape-deep \
-H "Authorization: Bearer $VAAYA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"site": {"url": "https://docs.example.com", "max_pages": 30, "include": ["/guides/*"]}}'Request
| Field | Type | Required | Description |
|---|---|---|---|
urls | string[] | one of urls / site | 1 to 50 URLs. Each is read through the full ladder. |
site | object | one of urls / site | { url, max_pages, include, exclude }. The site is mapped with crw/map (1 cent), pages on the same host are kept, include and exclude are URL globs (* matches anything; a pattern without * matches as a substring), and max_pages (1 to 50, default 20) caps the list. The seed URL is always first. |
format | string | no | markdown (default) or html. |
main_content | boolean | no | Default true. |
budgetCents | integer | no | Most you will pay, 10 to 500. Default is 10 cents per URL (plus 1 for the map in site mode). The job holds this amount until it finishes. |
Response
{
"ok": true,
"data": {
"job_id": "7c1d0f6e-...",
"status": "queued",
"async": true
},
"charged_cents": 0
}Poll the job
Call vaaya/result with the job_id until data.status is succeeded or failed. Jobs usually finish within a minute or two; a batch that hits the residential rung on many pages takes longer.
curl -X POST https://vaaya.ai/api/run/vaaya/result \
-H "Authorization: Bearer $VAAYA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"job_id": "7c1d0f6e-..."}'{
"ok": true,
"data": {
"ok": true,
"job_id": "7c1d0f6e-...",
"status": "succeeded",
"result": {
"site": { "url": "https://docs.example.com/", "mapped_pages": 212 },
"url_count": 30,
"row_count": 30,
"read_count": 29,
"blocked_count": 1,
"skipped_count": 0,
"hard": true,
"rows": [ ... ],
"calls": [ ... ],
"spent_cents": 34,
"budget_cents": 301
},
"charged_cents": 34
},
"charged_cents": 0
}The poll is free and writes nothing to your transaction history. data.status is running while the job works, then succeeded or failed. data.charged_cents is what the job itself cost; the outer charged_cents is the poll (always 0). The same endpoint polls every async Vaaya action.
Row format
Every row has the same fields in both endpoints.
| Field | Type | Description |
|---|---|---|
id, url | string | The URL as normalized from your input. |
final_url | string or null | Where the page resolved, when the vendor reported it. |
title | string or null | The page title. |
content | string or null | The page. null means no rung returned readable content; read hard and error. |
format | string | What actually came back: markdown, html, or text (exa extraction, or a raw page stripped to text). |
chars | integer | Length of content. |
provider | string or null | The rung that read the page: exa, crw, firecrawl, scrapedo, brightdata, scrapingant, crw-stealth. |
yield | number | The yield check score for the winning content, 0 to 1. |
quality_flags | string[] | blocked or thin_content from the last attempt, when the yield check flagged. |
hops | array | Vendor calls made for this row, in order. Each has provider, action, ok, charged_cents, latency_ms, and skipped when the call was not made. |
hard | boolean | Whether the unblock rungs were attempted. |
charged_cents | integer | What this row cost on the deep tier. |
error | string | "blocked" when every attempted rung came back a wall or a shell. "over budget" means the budget ran out before this row started. |
Reading a null
content: nullwithhard: false. The cheap rungs bounced. Callonescrape-deepto try the unblock rungs.content: nullwithhard: true. Every rung bounced. A browser session is the last resort.
Pricing
onescrape: 2 cents per URL, 2 to 10 cents per call. A call where no page was read is failed and charged 0.onescrape-deep: you pay the price of the rung that read each page. exa contents is 1 cent (per batch), crw and firecrawl are 1 cent, scrape.do is 1 cent, Bright Data unblock is 2 cents, ScrapingAnt residential is 4 cents, crw stealth is 1 cent. Rungs that came back blocked are free. The map call in site mode is 1 cent.- The job holds
budgetCents(or 10 cents per URL) when it starts and releases what it did not spend when it finishes. - Rows the budget could not cover are returned with
error: "over budget". RaisebudgetCentsor send fewer URLs. - A job that reads no page is failed and charged 0.
How pages are read
Each URL climbs a fixed ladder until a rung returns content that passes the yield check. A 200 whose body is a Cloudflare wall or an empty shell does not count; the ladder escalates.
| Rung | Call | Price | Tier |
|---|---|---|---|
| 1 | exa/contents (batch, no JS) | 0.1 cents per URL, 1 cent minimum | both |
| 2 | crw/scrape or firecrawl/scrape (JS render), whichever the scraping autorouter ranks higher by measured yield | 1 cent | both |
| 3 | scrapedo/scrape (cheap anti-bot rung) | 1 cent | both |
| 4 | brightdata/unblock (Cloudflare, DataDome, PerimeterX, geo walls) | 2 cents | deep |
| 5 | scrapingant/scrape_residential (second residential pool) | 4 cents | deep |
| 6 | crw/scrape with stealth | 1 cent | deep |
The order comes from the August 2026 scraping bench and the 2026-08-20 probe: crw and firecrawl are coverage-equal on ordinary pages, scrape.do returned the real page on a DataDome target where the renderers bounced, and Bright Data beat the ScrapingAnt residential pool on a live DataDome page at half the price. Domain memory reorders the renderers for hosts where one was seen blocked.
Errors
| Status | Error | Meaning |
|---|---|---|
| 400 | bad_request | Invalid body. onescrape-deep needs exactly one of urls or site. |
| 400 | bad_url | One of the inputs is not a URL. Not charged. |
| 400 | onescrape_social | A URL on a social platform. Use the platform gateway. Not charged. |
| 400 | onescrape_document | A PDF or Office file. Use a document parser. Not charged. |
| 402 | credits_required | Balance is below the budget the job would hold. Add credit, send fewer URLs, or set a smaller budgetCents. |
| 429 | too_many_inflight | Three onescrape-deep jobs are already running on this account. |
| 502 | all_blocked | No rung could read any of the pages. Not charged. |
Related
- OneSearch answers questions. OneScrape returns pages. OneSearch with
fidelityRequireddoes search and read in one call. - Web scraping lists the individual vendors, including the knobs OneScrape does not expose: screenshots, link lists, structured extraction, pollable crawls, geo-targeted egress.
- Browser for pages that need a click or a login.