Docs
Quickstart
You ask a page for something in plain words. You get an answer: the data, how complete it is, and what it cost. One call — with wait, the answer comes back in the same response.
Set me up on Plainscrape: read https://www.plainscrape.com/skills/onboard.md (curl -fsSL it, or open it) and follow it. Do the steps yourself, and only ask me what it tells you to ask.
claude mcp add --transport http plainscrape https://www.plainscrape.com/mcp # approve in the browser tab it opens, then in a new chat: ask(url="https://books.toscrape.com/", ask="every book with title, price, rating and availability", budget=1)
curl -X POST https://www.plainscrape.com/ask \
-H "Content-Type: application/json" \
-H "x-api-key: ps_live_YOUR_KEY" \
-d '{"url": "https://books.toscrape.com/", "ask": "every book with title, price, rating and availability", "budget": 1, "wait": 30}'import httpx
a = httpx.post("https://www.plainscrape.com/ask", timeout=70,
headers={"x-api-key": "ps_live_YOUR_KEY"},
json={"url": "https://books.toscrape.com/", "ask": "every book with title, price, rating and availability",
"budget": 1, "wait": 30}).json()
print(a["result"], a["coverage"], a["receipt"]){
"answer_id": "ans_…",
"state": "ready",
"shape": "rows",
"budget": 1,
"result": [
{
"title": "A Light in the Attic",
"price": 51.77,
"rating": 3,
"availability": "In stock",
"currency": "GBP"
},
{
"title": "Tipping the Velvet",
"price": 53.74,
"rating": 1,
"availability": "In stock",
"currency": "GBP"
},
{
"title": "Soumission",
"price": 50.1,
"rating": 1,
"availability": "In stock",
"currency": "GBP"
}
],
"coverage": {
"rows": 20,
"complete": 20,
"missing": {}
},
"receipt": {
"pages": 1,
"billed": true
},
"sources": [
"https://books.toscrape.com/"
],
"notes": [],
"stopped_early": false,
"error": null,
"took_s": 4.6
}
No key yet? The Agent tab sets everything up, or get one on the dashboard — 100 free pages, 14 days, free.
POST /ask
Say what you want in ask. The shape of the answer is read from your words and comes back as shape:
| You write | shape | What you get |
|---|---|---|
| Anything else — “title, price, availability” | record | One record from that page. Always 1 page. |
A list — “every book with …”, “top 10 …”, or budget above 1 | rows | Every row, following next-page links up to your budget. |
| A question — “how many books are in the Travel category?” | reply | Reads the start page and follows the links that answer it (up to 20 pages). |
| Field | Notes | |
|---|---|---|
url | required | The page or site, https. |
ask | required | Plain words, up to 1,600 characters. |
budget | optional | Most pages this ask may spend — your cost cap. Rows default to 10, a reply to 5 (2 to 20). Trial asks are capped at 10. Above your cap it is trimmed, not refused; budget in the response is what applied. |
shape | optional | record, rows or reply — say it when the automatic choice is wrong. |
wait | optional | Seconds to hold the call for the answer, up to 60. Ready in time: 200 and the answer. Otherwise 202, state: "working" and check, the URL to look at. |
callback_url | optional | A public https URL. Once the answer is ready we POST it there, the same JSON GET /answers/{answer_id} returns, exactly once. A 5xx or no reply is retried three times; redirects are not followed. For Zapier, Make and your own webhooks. |
network | optional | "residential" goes out through home-broadband IPs, for sites that turn datacentres away. Same price. |
country | optional | Exit country (ISO code, e.g. de) for rows on the residential network. |
A record
ask(url="https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html", ask="title, price, rating, availability")
curl -X POST https://www.plainscrape.com/ask \
-H "Content-Type: application/json" \
-H "x-api-key: ps_live_YOUR_KEY" \
-d '{"url": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html", "ask": "title, price, rating, availability", "wait": 30}'import httpx, time
H = {"x-api-key": "ps_live_YOUR_KEY"}
a = httpx.post("https://www.plainscrape.com/ask", headers=H, timeout=70, json={"url": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html", "ask": "title, price, rating, availability", "wait": 30}).json()
while a["state"] == "working": # only if it outlasted wait
time.sleep(2)
a = httpx.get("https://www.plainscrape.com" + a["check"], headers=H).json()
print(a["result"], a["coverage"], a["receipt"]){
"answer_id": "ans_…",
"state": "ready",
"shape": "record",
"budget": 1,
"result": {
"title": "A Light in the Attic",
"price": 51.77,
"rating": 3,
"availability": "In stock (22 available)"
},
"coverage": {
"rows": 1,
"complete": 1,
"missing": {}
},
"receipt": {
"pages": 1,
"billed": true
},
"sources": [
"https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"
],
"notes": [],
"stopped_early": false,
"error": null,
"took_s": 2.7
}
Asked one page for a list without saying “every”? notes tells you to say “every …” to get separate rows.
Rows, across pages
ask(url="https://books.toscrape.com/", ask="every book with title, price, rating and availability", budget=1)
curl -X POST https://www.plainscrape.com/ask \
-H "Content-Type: application/json" \
-H "x-api-key: ps_live_YOUR_KEY" \
-d '{"url": "https://books.toscrape.com/", "ask": "every book with title, price, rating and availability", "budget": 1, "wait": 30}'import httpx, time
H = {"x-api-key": "ps_live_YOUR_KEY"}
a = httpx.post("https://www.plainscrape.com/ask", headers=H, timeout=70, json={"url": "https://books.toscrape.com/", "ask": "every book with title, price, rating and availability", "budget": 1, "wait": 30}).json()
while a["state"] == "working": # only if it outlasted wait
time.sleep(2)
a = httpx.get("https://www.plainscrape.com" + a["check"], headers=H).json()
print(a["result"], a["coverage"], a["receipt"]){
"answer_id": "ans_…",
"state": "ready",
"shape": "rows",
"budget": 1,
"result": [
{
"title": "A Light in the Attic",
"price": 51.77,
"rating": 3,
"availability": "In stock",
"currency": "GBP"
},
{
"title": "Tipping the Velvet",
"price": 53.74,
"rating": 1,
"availability": "In stock",
"currency": "GBP"
},
{
"title": "Soumission",
"price": 50.1,
"rating": 1,
"availability": "In stock",
"currency": "GBP"
}
],
"coverage": {
"rows": 20,
"complete": 20,
"missing": {}
},
"receipt": {
"pages": 1,
"billed": true
},
"sources": [
"https://books.toscrape.com/"
],
"notes": [],
"stopped_early": false,
"error": null,
"took_s": 4.6
}
A reply about a site
ask(url="https://books.toscrape.com/", ask="How many books are in the Travel category?", budget=3)
curl -X POST https://www.plainscrape.com/ask \
-H "Content-Type: application/json" \
-H "x-api-key: ps_live_YOUR_KEY" \
-d '{"url": "https://books.toscrape.com/", "ask": "How many books are in the Travel category?", "budget": 3, "wait": 30}'import httpx, time
H = {"x-api-key": "ps_live_YOUR_KEY"}
a = httpx.post("https://www.plainscrape.com/ask", headers=H, timeout=70, json={"url": "https://books.toscrape.com/", "ask": "How many books are in the Travel category?", "budget": 3, "wait": 30}).json()
while a["state"] == "working": # only if it outlasted wait
time.sleep(2)
a = httpx.get("https://www.plainscrape.com" + a["check"], headers=H).json()
print(a["result"], a["coverage"], a["receipt"]){
"answer_id": "ans_…",
"state": "ready",
"shape": "reply",
"budget": 3,
"result": {
"travel_category_book_count": 11
},
"coverage": null,
"receipt": {
"pages": 2,
"billed": true
},
"sources": [
"https://books.toscrape.com/catalogue/category/books/travel_2/index.html"
],
"notes": [],
"stopped_early": false,
"error": null,
"took_s": 16.0
}
GET /answers/{answer_id}
Without wait (or when an answer outlasts it), check GET /answers/{answer_id} every 2 seconds while state is working, or pass callback_url and we post the answer to you when it is ready. Typical times: about 8 seconds for a record and 20–30 for a reply. Rows take 30–60 seconds the first time we see a site, while we work out how to read it, and 5–15 seconds on later asks. Long lists can take a few minutes; an answer is capped at 12 minutes and returns what it has.
| Field | Meaning | |
|---|---|---|
state | working, ready, failed or expired. | |
result | The data: an object for a record or a reply, a list for rows. Prices and ratings are numbers; rows add currency when the page shows one. Rows are read straight from the page by code that runs on it; a value the page doesn't have comes back empty. | |
coverage | How complete the rows are: rows, rows with every field filled (complete), and how many rows lack each field (missing). | |
receipt | What this answer cost: pages billed, billed, and pages_left on the account. One page fetched is one page; failed pages are free. | |
sources | The pages the answer was read from. | |
notes | Plain sentences worth knowing: a filter that couldn't apply, a page that was too long, rows left out to fit the size cap. | |
stopped_early | true when the time cap or a failed page ended it after rows came back; you get those rows and pay only for pages read. | |
error | On failed: what happened, what to do, and whether it cost anything — see errors. | |
took_s | How long it took. Answers are kept 24 hours; over MCP, ready answers also carry download_url, the whole answer as a JSON file at GET /answers/{answer_id}/file (send your key). |
GET /account
Plan, pages left, limits and the answers still working. MCP: account().
{
"email": "you@example.com",
"plan": "trial",
"pages": {
"left": 76,
"used_this_period": 24,
"period_start": "2026-10-01T00:00:00Z",
"plan_pages": 0,
"trial_pages": 100,
"trial_ended": false
},
"subscription": null,
"upgrade_url": "https://www.plainscrape.com/app",
"limits": {
"budget_max": 10,
"reply_budget_max": 20,
"asks_per_minute": 15,
"checks_per_minute": 60,
"working_at_once": 3,
"result_bytes_max": 5000000,
"wait_max_s": 60
},
"working": []
}
Plans, top-ups and cancelling are on the dashboard; see pricing.
Keys, and letting an agent get one
Send x-api-key: ps_live_… on every request. A missing or wrong key is refused before anything starts, so it never costs pages. Get a key on the dashboard, or let an agent do it in two calls:
curl -X POST https://www.plainscrape.com/agent/signup -H "Content-Type: application/json" \
-d '{"email":"you@company.com"}'
# → { "session_id": "…", "message": "A 6-digit code is on its way to you@company.com.", "expires_in_s": 900 }
curl -X POST https://www.plainscrape.com/agent/claim -H "Content-Type: application/json" \
-d '{"code":"123456","session_id":"…"}'
# → { "api_key": "ps_live_…", "key_prefix": "ps_live_…", "mcp_config": {…}, "pages_left": 100, "trial_ends_at": "…" }A code works once, for 15 minutes; five wrong codes end the session. The key is shown once — make a new one on the dashboard if you lose it.
Errors your agent can act on
Every error says what happened, what to do next, whether it cost anything, and whether trying again makes sense. A failed answer has the same error object inside it, with state: "failed".
{
"error": {
"code": "out_of_pages",
"message": "You've used all 2,000 pages on your Starter plan this month. Add pages or upgrade at https://www.plainscrape.com/app — both take effect right after payment — or wait until your plan renews on 2026-11-04.",
"fix": "Add pages or upgrade at upgrade_url.",
"billed": false,
"retry": false,
"pages_left": 0,
"pages_asked": 5,
"upgrade_url": "https://www.plainscrape.com/app",
"docs": "https://www.plainscrape.com/docs#errors-out-of-pages"
}
}
| code | What it means, what to do | |
|---|---|---|
page_unreadable | answer | The page could not be read (blocked, empty or unreachable). Failed pages are free and don't stop the rest of the answer. If the site turns datacentres away, ask again with network: "residential". |
page_not_found | answer | The site showed a "not found" page for that address. Check that the URL opens the page you mean in a browser. Nothing billed. |
nothing_found | answer | Nothing on the page matched your ask. Name the fields you want, or check the URL. |
nothing_usable | answer | The answer came back empty. Name the fields more specifically and ask again. Nothing billed. |
too_slow | answer | The answer took too long and was stopped; nothing billed. A long list that runs out of time after returning rows comes back ready with stopped_early: true instead, billed for the pages read. |
residential_unavailable | answer | You asked for network: "residential" and it is unavailable right now. Ask without it, or in a few minutes. |
not_started / never_finished | answer | The ask couldn't start, or never finished. Nothing charged; ask again. |
answer_expired | answer | Answers are kept 24 hours. After that state is expired and result is null; the receipt keeps what was billed. The saved copy stays downloadable from the dashboard. |
address_not_public | 422 | The URL isn't a public web page (a private or internal address). Nothing was fetched. |
site_not_supported | 403 | The site's terms forbid scraping (LinkedIn, Yelp, Zillow). Use its official API. |
bad_request | 422 | A field is missing or isn't one we take. message says which, in a sentence; fields lists them. |
ask_too_long | 400 | The ask is over 1,600 characters. Nothing reserved or billed. |
out_of_pages | 402 | No pages left. message names your plan and renewal date; upgrade_url is where a person adds pages. A budget above what's left is trimmed to what's left, not refused. |
answer_not_found | 404 | Unknown answer id, or one that isn't yours — both look the same. |
too_many_working | 429 | This account already has the most answers working at once. working lists them; a slot frees as soon as one is ready. |
slow_down | 429 | Too many requests in the window. Wait retry_in_s. Checking answers you already asked for is a separate window, so it keeps working. |
paused | 503 | Asks are paused on our side for a moment; your request is fine. Nothing created. Retry in a few seconds. |
missing_key / unknown_key | 401 | Send x-api-key, or create a new key on the dashboard. |
Limits
Asks and working answers are counted per account — a second key doesn't buy a second window. Checks are per key and separate, so waiting on a slow answer never uses up the next ask.
| Limit | Trial | Starter | What it counts |
|---|---|---|---|
| Asks | 15 / min | 60 / min | POST /ask, across every key on the account. |
| Checks | 60 / min | 600 / min | GET /answers/… and /account. Sized for checking every 2 seconds; wait doesn't use them. |
| Working at once | 3 | 10 | Answers asked but not yet ready, across the account. |
| Budget per ask | 10 | No cap | A reply reads at most 20 pages on every plan. Higher values are trimmed, not refused. |
| Ask length | 1,600 characters | 1,600 | Over it: ask_too_long, nothing billed. |
| Result size | 5 MB | 5 MB | Beyond it, rows are dropped from the end and notes says how many. |
Every response carries the window in the standard RateLimit fields; a 429 adds Retry-After:
RateLimit: limit=15, remaining=14, reset=42 RateLimit-Policy: 15;w=60 Retry-After: 42
Upgrading raises these; GET /account always shows yours. Tell us what you're building.
MCP
Three tools at https://www.plainscrape.com/mcp: ask, answer, account — the same answer object as the REST API. ask waits up to about 45 seconds, so most answers come back in one call. Add the URL with no key and your client opens a browser tab to sign in (OAuth 2.1 with PKCE); approve once. Clients without sign-in send x-api-key. Setup for each client: MCP page, or Connect at the top.
{
"mcpServers": {
"plainscrape": {
"url": "https://www.plainscrape.com/mcp",
"headers": {
"x-api-key": "ps_live_YOUR_KEY"
}
}
}
}
On the roadmap
Recipes — rerun what worked on a site, on any of its pages, faster — and watches: scheduled asks that tell you what changed. Today, ask again or check /answers.