Scrape a website
Give it a URL and a map of CSS selectors, and it hands back exactly the fields you asked for as structured JSON — no headless browser to babysit, no HTML soup to parse on your end. This is the workhorse endpoint for teams who already know precisely which div, span or table cell holds the data they need.
Run it online
Run this on our servers with your account. Free tools run in your browser; this one bills your KIT balance per the price above.
Built for people who already know their selectors
If you can open dev tools, right-click an element and copy its selector, this endpoint is for you. It suits price monitors, inventory trackers, real-estate listing pullers, job-board watchers and any recurring job where the target page's layout is known and reasonably stable. You define a selector map once — field name to CSS selector — and every request against that page shape reuses it.
What happens after you submit a request
The call to POST /web/scrape-selector returns a task_id immediately; the actual fetch and extraction run asynchronously against our global edge, including JavaScript-rendered pages when the selector targets content injected client-side. When the task finishes you get the result either pushed to your signed webhook or waiting behind a signed link that stays valid for 24 hours, after which the data is deleted and never touches any training pipeline.
CSS selectors: an old idea that still wins
Selectors have described document structure since CSS1 in 1996, and browsers standardized querySelector matching decades ago — which is exactly why they remain the fastest, cheapest way to pull a known field from a known layout. Unlike vision or language models, a selector either matches or it doesn't: zero ambiguity, zero hallucinated fields, and a result you can unit-test.
What you get back, and what happens when a selector misses
The response mirrors your selector map, field by field, with arrays for repeated elements like list items or table rows and null for a selector that matched nothing on that particular page. Because failed extraction attempts are retried automatically up to three times before returning a clear error, a transient timeout or a slow-loading widget won't silently cost you a charge — you are only billed for a task that actually completes.
Where it sits in a bigger pipeline
Most teams chain this endpoint behind a scheduler: cron-trigger a batch of URLs, let each task land on a webhook, and feed the JSON straight into a database, spreadsheet or alerting rule without a human ever opening the source page. Because pricing is a flat per-request base plus a small per-URL fee, it scales predictably whether you are watching five competitor prices or five thousand SKUs.
What you can do with it
Competitor price monitoring
Track the .price and .stock-status selectors on a set of competitor product pages daily and feed the JSON into a pricing dashboard.
Real-estate listing aggregation
Pull address, price, square footage and photo count from a listings site using a single selector map reused across thousands of pages.
Job board watching
Extract title, company and posted-date selectors from a careers page each morning to detect new openings before a public feed exists.
Table-based data collection
Target a table row selector to turn an HTML table of exchange rates or specs into a JSON array your app can index directly.
FAQ
Is the web scraping API free to try?
There's no free tier — free tiers get abused and slow everyone down. Access runs on a prepaid ForHosting KIT balance: top up from $10.00 (it never expires) and each request is charged at its published price, so a call with no balance returns HTTP 402. No subscription, no tokens, no invented credits, and a failed task is never charged.
How much does web.scrape_selector cost?
$0.040 per request plus $0.001 per URL processed, billed only when the task actually completes.
Does it handle JavaScript-rendered pages?
Yes, the fetch layer renders pages before applying your CSS selectors, so content injected by client-side scripts is matchable just like static HTML.
What happens if my selector doesn't match anything?
That field comes back as null in the response instead of failing the whole task, so one broken selector in a map doesn't block the rest of your data.
Can I scrape multiple URLs with one selector map?
Yes, submit a list of URLs alongside a single selector map when the pages share the same layout, which is the most cost-efficient way to use this endpoint.
How do I get the results back?
Either as a push to your signed webhook when the task finishes, or via a signed link valid for 24 hours if you prefer to poll.
What happens if a scrape fails?
Failed attempts are retried automatically up to three times; if it still fails you get a clear error and are never charged for that task.
How is this different from the AI or schema scraping endpoints?
Selector scraping is the fastest and cheapest option when you already know the exact CSS path to each field; the AI and schema endpoints exist for when layouts vary or you need guaranteed data types.
For developers — API access
Everything on this page is available programmatically. This section is for teams who want to wire it into their own systems; everyone else can just use the tool above.
API endpoint
Prefer to automate it? One authenticated POST creates the task; the result comes back by webhook or a signed link. The same capability also runs here on the web, and soon from our app, email and Telegram.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/web/scrape-selector \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://ejemplo.com","selectors":{"titulo":"h1","precio":".price"}}'const res = await fetch("https://api.kit.forhosting.com/web/scrape-selector", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"url": "https://ejemplo.com",
"selectors": {
"titulo": "h1",
"precio": ".price"
}
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/web/scrape-selector",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"url": "https://ejemplo.com",
"selectors": {
"titulo": "h1",
"precio": ".price"
}
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/web/scrape-selector", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"url":"https://ejemplo.com","selectors":{"titulo":"h1","precio":".price"}}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"url":"https://ejemplo.com","selectors":{"titulo":"h1","precio":".price"}}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/web/scrape-selector", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"url": "https://ejemplo.com",
"selectors": {
"titulo": "h1",
"precio": ".price"
}
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "web.scrape_selector",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}The API is asynchronous: the call returns a task_id immediately and the result arrives by webhook. Polling is capped at 1 req/s per task.
Pricing
Published price — no tokens, no invented credits. A failed task is never charged.
Limits
timeout_sec | 30 |
max_crawl_pages | 25 |
Errors
| HTTP | Code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid API key. |
402 | insufficient_balance | Your balance doesn't cover the task price. |
404 | unknown_type | That task type doesn't exist. |
429 | rate_limited | Too many requests. Use the webhook instead of polling. |
422 | task_failed | The task failed after 3 retries. You are never charged for it. |