Documentation

vaaya / docs / reference

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

EndpointWhat it doesPriceMode
POST /api/run/vaaya/onescrapeRead 1 to 5 URLs through the cheap ladder.2 cents per URLsync
POST /api/run/vaaya/onescrape-deepRead up to 50 URLs, or crawl a site, through the full ladder.per page read, capped by a budgetasync

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

FieldTypeRequiredDescription
urlsstring[]yes1 to 5 URLs. Duplicates are collapsed.
formatstringnomarkdown (default) or html. With markdown, a rung that only has the raw page returns it stripped to text and says so in format.
main_contentbooleannoStrip 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

FieldTypeRequiredDescription
urlsstring[]one of urls / site1 to 50 URLs. Each is read through the full ladder.
siteobjectone 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.
formatstringnomarkdown (default) or html.
main_contentbooleannoDefault true.
budgetCentsintegernoMost 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.

FieldTypeDescription
id, urlstringThe URL as normalized from your input.
final_urlstring or nullWhere the page resolved, when the vendor reported it.
titlestring or nullThe page title.
contentstring or nullThe page. null means no rung returned readable content; read hard and error.
formatstringWhat actually came back: markdown, html, or text (exa extraction, or a raw page stripped to text).
charsintegerLength of content.
providerstring or nullThe rung that read the page: exa, crw, firecrawl, scrapedo, brightdata, scrapingant, crw-stealth.
yieldnumberThe yield check score for the winning content, 0 to 1.
quality_flagsstring[]blocked or thin_content from the last attempt, when the yield check flagged.
hopsarrayVendor calls made for this row, in order. Each has provider, action, ok, charged_cents, latency_ms, and skipped when the call was not made.
hardbooleanWhether the unblock rungs were attempted.
charged_centsintegerWhat this row cost on the deep tier.
errorstring"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: null with hard: false. The cheap rungs bounced. Call onescrape-deep to try the unblock rungs.
  • content: null with hard: 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". Raise budgetCents or 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.

RungCallPriceTier
1exa/contents (batch, no JS)0.1 cents per URL, 1 cent minimumboth
2crw/scrape or firecrawl/scrape (JS render), whichever the scraping autorouter ranks higher by measured yield1 centboth
3scrapedo/scrape (cheap anti-bot rung)1 centboth
4brightdata/unblock (Cloudflare, DataDome, PerimeterX, geo walls)2 centsdeep
5scrapingant/scrape_residential (second residential pool)4 centsdeep
6crw/scrape with stealth1 centdeep

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

StatusErrorMeaning
400bad_requestInvalid body. onescrape-deep needs exactly one of urls or site.
400bad_urlOne of the inputs is not a URL. Not charged.
400onescrape_socialA URL on a social platform. Use the platform gateway. Not charged.
400onescrape_documentA PDF or Office file. Use a document parser. Not charged.
402credits_requiredBalance is below the budget the job would hold. Add credit, send fewer URLs, or set a smaller budgetCents.
429too_many_inflightThree onescrape-deep jobs are already running on this account.
502all_blockedNo rung could read any of the pages. Not charged.
  • OneSearch answers questions. OneScrape returns pages. OneSearch with fidelityRequired does 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.