Documentazione
Autenticazione, ogni endpoint con i suoi parametri, errori e limiti dei piani per l'API di scrape.land.
API di estrazione dati
Dacci un URL e un insieme di campi. Recuperiamo la pagina, applichiamo i tuoi selettori CSS e restituiamo JSON pulito. Nessun browser da avviare, nessun codice di parsing, nessuna rotazione di IP da gestire. L'URL di base di ogni endpoint è https://scrape.land.
Autenticazione
Invia la tua chiave API nell'intestazione X-Api-Key a ogni richiesta:
X-Api-Key: pb_live_YOURKEYVa bene anche un'intestazione bearer Authorization, se si adatta meglio al tuo stack:
Authorization: Bearer pb_live_YOURKEYPOST/v1/extract
La richiesta canonica. Invia un url e una mappa fields e ricevi un oggetto data con una chiave per campo.
urlstringobbligatorioLa pagina da scaricare: un URL assoluto http o https.
fieldsobjectNome del campo → selettore: un selettore CSS, selector@attr, XPath o un oggetto. Vedi Come i campi corrispondono ai selettori. Invia questo oppure prompt.
promptstringDescrivi i campi a parole invece di scrivere selettori. Piano Scale e superiori. Vedi Estrazione con AI.
schemaobjectCon prompt: un oggetto JSON chiave → tipo che fissa la forma di data.
extract_typestringAuto-estrae un oggetto normalizzato per un tipo di pagina noto, senza prompt né schema. Vedi Auto-estrazione.
modelstringpredefinito "fast"Livello AI: fast, smart o max.
structuredbooleanpredefinito trueCon prompt: false restituisce una risposta in linguaggio naturale in answer invece dell'oggetto data.
groupbooleanpredefinito falseUnisce i campi lista in un oggetto per riga sotto items.
Più tutte le opzioni della richiesta.
curl https://scrape.land/v1/extract \
-H "X-Api-Key: pb_live_YOURKEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example-product.com/item/42",
"fields":{"title":"h1","price":".price"}}'import requests
r = requests.post("https://scrape.land/v1/extract",
headers={"X-Api-Key": "pb_live_YOURKEY"},
json={"url": "https://example-product.com/item/42",
"fields": {"title": "h1", "price": ".price"}},
timeout=60)
print(r.json()["data"])from scrapeland import ScrapelandClient
client = ScrapelandClient("pb_live_YOURKEY") # or $SCRAPELAND_API_KEY
data = client.extract("https://example-product.com/item/42",
{"title": "h1", "price": ".price"})
print(data)const res = await fetch("https://scrape.land/v1/extract", {
method: "POST",
headers: { "X-Api-Key": "pb_live_YOURKEY", "Content-Type": "application/json" },
body: JSON.stringify({
url: "https://example-product.com/item/42",
fields: { title: "h1", price: ".price" },
}),
});
console.log((await res.json()).data);package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
func main() {
body, _ := json.Marshal(map[string]any{
"url": "https://example-product.com/item/42",
"fields": map[string]string{"title": "h1", "price": ".price"},
})
req, _ := http.NewRequest("POST", "https://scrape.land/v1/extract", bytes.NewReader(body))
req.Header.Set("X-Api-Key", "pb_live_YOURKEY")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var out map[string]any
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(out["data"])
}$ch = curl_init("https://scrape.land/v1/extract");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["X-Api-Key: pb_live_YOURKEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode([
"url" => "https://example-product.com/item/42",
"fields" => ["title" => "h1", "price" => ".price"],
]),
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true)["data"];{
"url": "https://example-product.com/item/42",
"status": 200,
"data": {
"title": "Vintage desk lamp",
"price": "49.00"
}
}Come i campi si mappano ai selettori
Ogni voce in fields ha una di queste forme:
- Una stringa con un selettore CSS. Restituiamo il testo del primo elemento corrispondente.
"title": "h1"restituisce il testo dentro il primoh1. - Una scorciatoia selettore@attr per leggere un attributo.
"image": "img.gallery@src"restituiscesrc; usa@texto nessun parametro per il testo. - Una scorciatoia XPath: una stringa il cui primo carattere è
/o(è trattata come XPath, tutto il resto come CSS."title": "//h1/text()","link": "//a/@href","first": "(//p)[1]". - Un oggetto con
csspiù chiavi opzionali:attr(legge un attributo invece del testo),all(restituisce ogni corrispondenza come array) oxpath(un'espressione XPath invece dicss, per i campi di primo livello). Si combinano. - Un gruppo: un oggetto con
csse i proprifields. Ilcssseleziona il contenitore che si ripete e i campi interni vengono letti dentro ciascuno, quindi ottieni un elenco di oggetti invece di array paralleli che possono disallinearsi. Usa"."per il contenitore stesso."articles": {"css": ".card", "fields": {"title": "h3", "url": {"css": "a", "attr": "href"}}}restituisce[{"title": ..., "url": ...}, ...], e un campo mancante in un contenitore ènullin quella riga senza spostare le righe successive.
Dove funziona XPath. XPath è supportato per i campi di primo livello, come scorciatoia o con la chiave xpath, e un XPath che non si compila dà 400 con il nome del campo. Il contenitore di un gruppo deve essere CSS: {"xpath": ..., "fields": {...}} dà 400. Dentro i fields di un gruppo XPath non è supportato: un sottocampo XPath torna null in ogni riga, quindi lì usa CSS.
Imposta "group": true nella richiesta per unire i campi elenco in un oggetto per riga sotto items, invece di array paralleli da allineare a mano. Le righe sono costruite sull’elenco più lungo e un campo senza valore a quell’indice è null. Se gli elenchi hanno numeri diversi di elementi la risposta lo segnala, perché il riempimento avviene in fondo: se la lacuna reale era nel mezzo, ogni colonna successiva risulta sfalsata. Quando le righe possono avere lacune, preferisci il gruppo annidato sopra, che legge ogni valore dal contenitore della riga stessa e non può disallinearsi.
Quindi questa mappa di campi:
{
"title": "h1",
"price": ".price",
"image": "img.gallery@src",
"tags": {"css": ".tag", "all": true}
}produce questo oggetto di dati, campo per campo:
| Campo | Selettore / oggetto | Valore restituito |
|---|---|---|
title | "h1" | testo del primo h1 |
price | ".price" | testo del primo .price |
image | "img.gallery@src" | l'attributo src, come stringa |
tags | {"css":".tag","all":true} | un array con il testo di ogni .tag |
Quando un campo torna null
Un campo è null o perché la pagina davvero non ha quell'elemento, o perché il selettore stesso non si è potuto usare. Per te sono cose molto diverse, quindi le distinguiamo: se un selettore non si è potuto compilare, la risposta porta un oggetto field_errors che indica il campo e il motivo. Il campo resta presente in data come null (un array vuoto per un campo con "all": true), la richiesta resta un 200 e gli altri campi non ne risentono: un selettore inutilizzabile non affonda mai il resto dell'estrazione.
{
"data": {"title": "Example Domain", "price": null, "tags": null},
"field_errors": {
"tags": "selector did not compile (unknown pseudoclass or pseudoelement :is) — this field is null because the selector could not be used, not because the page lacks the element"
}
}L'assenza della chiave field_errors significa che ogni selettore si è compilato, quindi lì un null è un vero «non è sulla pagina». L'XPath invece è validato a monte e restituisce 400.
- I selettori più recenti non sono ancora supportati e finiranno in
field_errors::is(),:where(),:has(> x)nella forma relativa,:focus-within, selettori con namespace comesvg|circle. Tutto ciò che viene da CSS3 funziona: combinatori discendente/figlio/fratello,:nth-child(),:not(), selettori di attributo inclusi^= $= *=e il flagi. - Qui funzionano quattro selettori che nessun browser supporta. Sono estensioni Cascadia per il matching sul testo, cosa che il CSS in sé non sa fare: davvero utili, ma non sono CSS standard. Un selettore che le usa non funzionerà in un browser, nei devtools o in qualsiasi altro strumento di scraping, quindi trattale come una comodità di questa API e non come qualcosa su cui standardizzarsi.
Selettore Cosa corrisponde Esempio :contains(…)elemento il cui testo contiene una sottostringa h1:contains("Example"):matches(…)elemento il cui testo corrisponde a un'espressione regolare p:matches(^Price:):matchesOwn(…)uguale, ma solo il testo proprio dell'elemento, ignorando i discendenti h1:matchesOwn(^Example):haschild(…)elemento con un figlio diretto che corrisponde a un selettore p:haschild(a)Attenzione al tipo degli argomenti, è l'errore facile:
:matches()e:matchesOwn()prendono una regex sul testo, non sono un altro modo di scrivere:is().:matches(h1,h2)si compila e poi non corrisponde a nulla, perché cerca il testo letteraleh1,h2. Metti tra virgolette ogni argomento con spazi o punteggiatura::contains("Learn more"), non:contains(Learn more).
Opzioni della richiesta
Funzionano su ogni endpoint che scarica una pagina: /v1/extract, /v1/fetch, /v1/batch, /v1/rank e dentro i job. Le opzioni del browser richiedono render.
countrystringPaese di uscita, codice ISO, ad es. us. La richiesta parte da quel paese.
sessionstringNome di una sessione sticky. Riusa lo stesso IP di uscita tra le chiamate (login, carrelli).
protocolstringProtocollo di uscita: http, https o socks5, cioè il protocollo che parla il proxy di uscita. Omettilo per accettarli tutti.
max_latencyintegerUsa solo uscite che hanno risposto in meno di questi millisecondi.
renderbooleanpredefinito falsetrue per eseguire la pagina in un browser vero prima di leggerla.
wait_forstringIn modalità render, un selettore CSS da attendere prima di leggere il DOM.
devicestringpredefinito "desktop""mobile" renderizza come su un telefono (viewport touch 390×844, 3×, UA di Safari mobile) per il layout mobile; il predefinito è desktop 1280×800. Implica render.
block_resourcesbooleanpredefinito falseIn modalità render, true salta immagini/font/media per un rendering più veloce e leggero (il DOM non cambia, l'estrazione funziona lo stesso).
fingerprintbooleanpredefinito falseIn modalità render, true presenta un'identità di browser coerente con country (fuso orario, locale, geolocalizzazione, navigator.languages, WebGL/canvas generici) così un rendering appare come un client reale di quella regione e fa scattare meno blocchi anti-bot. Una session la mantiene stabile.
actionsarrayIn modalità render, passi scriptati prima della cattura: scroll, click, wait, wait_for, fill. Ottimo per scroll infinito e contenuti dietro un clic.
headersbooleanpredefinito falsetrue per includere le intestazioni di risposta nel risultato.
send_headersobjectUn oggetto con intestazioni di richiesta extra da inoltrare al target (User-Agent personalizzato, Authorization, …).
cookiesstringIl valore di un'intestazione Cookie da inviare, ad es. per estrarre dietro un login.
method / bodystringpredefinito "GET"Il metodo HTTP (predefinito GET) e il corpo della richiesta. Usa POST/PUT/… per API JSON o per inviare form. Ciò che non è GET non viene mai ritentato e non è compatibile con render.
metadatabooleanpredefinito falsetrue restituisce anche un oggetto metadata: title, description, canonical, lang, tag OpenGraph/Twitter e jsonld schema.org già interpretato. Ottimo per anteprime dei link e SEO (senza alcun selettore).
linksbooleanpredefinito falsetrue restituisce anche un array links: ogni a href risolto in URL assoluto, deduplicato, solo http(s). Comodo per crawling e discovery.
screenshotbooleanpredefinito falsePer /v1/fetch: true restituisce un PNG in base64 della pagina renderizzata (implica render). Aggiungi full_page: true per tutta l'altezza scorrevole.
Pagine JavaScript: render e wait_for
Se la pagina costruisce i contenuti con JavaScript, aggiungi "render": true. Avviamo un browser vero, lasciamo girare la pagina, poi leggiamo il DOM e applichiamo i tuoi selettori. Abbinalo a "wait_for" impostato su un selettore CSS per aspettare che quell'elemento compaia prima della lettura. È il modo affidabile di attendere contenuti che arrivano tardi:
curl https://scrape.land/v1/extract \
-H "X-Api-Key: pb_live_YOURKEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example-product.com/item/42",
"render": true,
"wait_for": "h1.title",
"fields": {"title": "h1.title", "price": ".price"}
}'screenshot, e 1 (meno di un rendering semplice, e come un fetch semplice) quando invii block_resources. Un rendering che fallisce non viene fatturato affatto. Se non ti servono uno screenshot o le immagini stesse, invia "block_resources": true: immagini, font e media vengono saltati, il DOM che leggono i tuoi selettori è identico, su una pagina piena di immagini toglie buona parte del trasferimento (misurato su 14 pagine: mediana 42%, massimo 77% e circa 23% anche su CDN di immagini che servono URL senza estensione) e fa scendere il costo da 5 unità a 1. Vale anche insieme a screenshot: le risorse sono davvero bloccate, quindi lo screenshot che ricevi sarà senza immagini. Anche la capacità di rendering ha un tetto: se ogni slot di browser è occupato ricevi un 503 con Retry-After: 1, quindi riprova invece di trattarlo come un errore.Estrazione con AI (descrivi i campi a parole)
prompt, uno schema o qualsiasi preset extract_type) è disponibile da Scale in su (Scale, Business, Business XL/XXL/Ultra). Su Free, Pool, Starter e Growth restituiscono 403 con il piano che ti servirebbe. L'estrazione con fields basata su selettori (CSS e XPath, più sotto) funziona su ogni piano, Free incluso. Controlla GET /v1/capabilities con la tua chiave per vedere cosa hai.Non vuoi scrivere e mantenere selettori? Invia un prompt invece di fields: recuperiamo la pagina (tutti i parametri sopra valgono ancora), la convertiamo in Markdown e un modello linguistico restituisce esattamente i campi che hai descritto, in JSON. I selettori si rompono quando cambia il markup di un sito; un prompt si adatta. Aggiungi uno schema opzionale (un oggetto JSON di key→tipo) per fissare la forma esatta dell'output. Passa model per scegliere il livello. "fast" è il predefinito: mediana 1,5 s, ed è la scelta giusta per quasi ogni estrazione, perché tirare fuori campi già scritti in pagina è un compito di lettura. "smart" ha una mediana di circa 10 s ed è migliore quando la risposta va dedotta dalla pagina invece che letta, ad esempio mettendo insieme un orario di inizio, una durata e un fuso orario. "max" usa il nostro modello più forte (mediana di circa 3 s) ed è per le estrazioni in cui ogni campo deve essere giusto. Ogni livello si fattura a partire da un minimo oltre all'unità di recupero (fast 2, smart 4, max 25 unità di richiesta), e una pagina molto grande o una risposta lunga si fattura di più, in proporzione a quanto è costata davvero la chiamata al modello. Un nome non riconosciuto è un 400 che elenca quelli validi. Se il livello che hai chiesto non è disponibile rispondiamo con l'altro invece di fallire, fatturiamo il livello che ha davvero servito e te lo diciamo restituendo requested_model accanto a model.
Stai facendo una domanda invece di elencare campi? Un prompt come "what's this business about?" è una domanda, non un elenco di campi, ma il modello deve comunque inventare i nomi delle chiavi (business, category, competitors_mentioned…) e tu devi indovinare quali ha scelto. Invia "structured": false e ricevi invece una risposta in linguaggio naturale sotto answer, senza alcuna chiave data:
curl https://scrape.land/v1/extract \
-H "X-Api-Key: pb_live_YOURKEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://scrape.land/",
"prompt": "what's this business about?",
"structured": false
}'{
"url": "https://scrape.land/",
"status": 200,
"extracted_by": "ai",
"model": "fast",
"answer": "scrape.land is a web data-extraction API: you send a URL and it returns clean structured data, handling proxy rotation, retries, geo-targeting and JavaScript rendering for you. Pricing is flat per 1,000 delivered requests."
}data e answer non sono mai entrambi presenti, quindi un client tipizzato si ramifica su quale dei due ha ricevuto invece di fare type-switch su un solo campo. structured vale true per impostazione predefinita, quindi nulla di ciò che invii già cambia. Costa esattamente le stesse unità di richiesta dell'estrazione strutturata: è la stessa pagina e la stessa chiamata al modello. Uno schema o un extract_type fissa una forma JSON e quindi implica output strutturato; inviarne uno insieme a "structured": false dà un 400 che nomina entrambi i campi invece di farci indovinare in silenzio. Non ha alcun effetto sull'estrazione con fields basata su selettori, che non arriva mai a un modello.
Ti serve un elenco? Chiedi "every product on the page" e ricevi l'array sotto data.items. Se la pagina non ha testo (una pagina solo-JS presa senza render), ricevi un 422 che ti dice di riprovare con "render": true, invece di una risposta piena di valori nulli.
<time datetime> arrivano al modello, quindi un prezzo esatto, un codice valuta o una data ISO di pubblicazione sono disponibili anche quando la pagina mostra solo "From $49" o "3 days ago". I dati codificati altrove (un voto dentro il nome di una classe CSS) si tirano ancora meglio con fields (CSS/XPath), che vedono l'HTML grezzo. Mescolali liberamente.curl https://scrape.land/v1/extract \
-H "X-Api-Key: pb_live_YOURKEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://books.toscrape.com/",
"prompt": "the page title, and the title and price (as a number) of the first book"
}'{
"url": "https://books.toscrape.com/",
"status": 200,
"extracted_by": "ai",
"model": "fast",
"data": {
"page_title": "Books to Scrape",
"first_book_title": "A Light in the Attic",
"first_book_price": 51.77
}
}Auto-estrazione (una parola invece di uno schema)
L'auto-estrazione è estrazione con AI sotto il cofano, quindi richiede lo stesso piano Scale o superiore.
Per i tipi di pagina più comuni non serve nemmeno un prompt. Passa extract_type e applichiamo noi un prompt e uno schema curati, così ricevi un oggetto normalizzato: product, article, job, discussion, event, recipe, real_estate e profile. Il tuo prompt/schema ha comunque la precedenza se lo invii.
I preset girano sul livello predefinito fast e di solito rispondono in un paio di secondi. Passa "model": "smart" se una pagina è davvero ambigua, ci mette circa il doppio e costa più unità di richiesta. Le richieste sincrone hanno un budget di 150 s; oltre ricevi un 504, quindi manda quelle lente come job asincrono (POST /v1/jobs, budget di 5 minuti).
curl https://scrape.land/v1/extract \
-H "X-Api-Key: pb_live_YOURKEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html",
"extract_type": "product",
"model": "fast"
}'Ottenere le intestazioni di risposta
A volte ti servono le intestazioni di risposta, non solo il corpo: un Location di redirect, un Set-Cookie, un Content-Type. Aggiungi "headers": true e includiamo un oggetto headers accanto a data. I nomi delle intestazioni arrivano in forma canonica (Content-Type, Set-Cookie) e un'intestazione ripetuta è unita con ", ". L'oggetto contiene anche due intestazioni nostre: X-Proxy-Country, il paese di uscita da cui è partita la richiesta, e X-Proxy-Tier, il livello di uscita che l'ha trasportata (premium o pool). Nessuna delle due è un'intestazione della risposta HTTP dell'API stessa.
{
"url": "https://example-product.com/item/42",
"status": 200,
"data": {"title": "Vintage desk lamp"},
"headers": {
"Content-Type": "text/html; charset=utf-8",
"Set-Cookie": "sid=abc123; Path=/; HttpOnly",
"Cache-Control": "max-age=300",
"X-Proxy-Country": "US",
"X-Proxy-Tier": "premium"
}
}Altri endpoint
POST/v1/fetch
Quando vuoi l'intera pagina invece di campi specifici, usa /v1/fetch. Imposta format su html (predefinito), text (testo visibile), markdown o raw (byte in base64 + content_type, per PDF/immagini/binari, fino a 7 MiB). Valgono le stesse opzioni country, session, render, wait_for e headers.
urlstringobbligatorioLa pagina da scaricare: un URL assoluto http o https.
formatstringpredefinito "html"Per /v1/fetch: html (predefinito), text, markdown (Markdown pronto per gli LLM, vedi sotto) o raw, che restituisce i byte esatti codificati in base64 sotto base64 con il content_type, così puoi far passare un PDF, un'immagine o qualsiasi binario dal proxy intatto (limite 7 MiB).
Più tutte le opzioni della richiesta.
curl https://scrape.land/v1/fetch \
-H "X-Api-Key: pb_live_YOURKEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example-product.com/item/42", "format": "html"}'{
"url": "https://example-product.com/item/42",
"status": 200,
"html": "<!doctype html><html>…"
}Quando la pagina restituita è una sfida anti-bot o un rifiuto (per esempio una pagina Cloudflare "Just a moment") invece del contenuto, la risposta contiene anche "blocked": true e l'header X-Scrapeland-Blocked: 1, anche se il target ha risposto 200. Riprova: le risposte bloccate non vengono mai fatturate.
Markdown pronto per gli LLM
"format": "markdown" restituisce la pagina come Markdown pulito sotto una chiave markdown, la forma che le pipeline RAG e le finestre di contesto degli LLM vogliono davvero, così non mandi tag HTML grezzi a un modello pagandoli come token. Gli elementi di contorno del sito (nav, header, footer, aside, script e stili) vengono tolti, titoli, elenchi, link, blocchi di codice e tabelle restano, e link e immagini relativi sono risolti in URL assoluti così un frammento funziona anche staccato dalla pagina di origine. Si combina con ogni altra opzione (country, session, render, wait_for, actions, fingerprint) e funziona anche in /v1/batch. Costa esattamente una unità di richiesta, come qualsiasi altro fetch; non c'è alcun sovrapprezzo per Markdown o AI.
curl https://scrape.land/v1/fetch \
-H "X-Api-Key: pb_live_YOURKEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/blog/post", "format": "markdown", "render": true}'
# -> {"url":"...","status":200,"markdown":"# Post title\n\nBody text..."}POST/v1/batch
Recupera fino a 20 URL in una sola chiamata. Invia un array urls più i parametri condivisi; ricevi una voce results per URL, nell'ordine di ingresso: ognuna o un normale risultato di fetch o un {"url","error"}. Ogni URL è fatturato come una richiesta consegnata a sé.
Estrazione in blocco: aggiungi fields o un prompt e ogni URL viene estratto esattamente come in /v1/extract (ogni risultato porta un oggetto data). Ottimo per trasformare un elenco di URL in righe con una sola chiamata.
urlsarrayobbligatorioFino a 20 URL assoluti. I risultati tornano nello stesso ordine.
fields / promptobject / stringFacoltativo. Ogni URL viene estratto come con /v1/extract e il suo risultato contiene data.
formatstringpredefinito "html"Senza campi né prompt: il formato di ogni pagina, come con /v1/fetch.
Più tutte le opzioni della richiesta.
curl https://scrape.land/v1/batch \
-H "X-Api-Key: pb_live_YOURKEY" \
-H "Content-Type: application/json" \
-d '{"urls": ["https://a.example/", "https://b.example/"], "format": "text"}'{
"results": [
{"url": "https://a.example/", "status": 200, "text": "…"},
{"url": "https://b.example/", "error": "…"}
]
}POST/v1/search
Esegui una ricerca web e ricevi i risultati organici (title, l'url reale di destinazione e uno snippet) senza fare il parsing di una SERP. Invia {"q": "...", "count": 10} più i parametri proxy condivisi. Fatturato come una sola richiesta consegnata. count è limitato a 10: una sola pagina di risultati dal motore di ricerca; se ne chiedi di più ne ricevi comunque 10.
qstringobbligatorioLa query di ricerca.
countintegerpredefinito 10Numero di risultati, da 1 a 10.
Più le opzioni proxy country, session, protocol e max_latency.
curl https://scrape.land/v1/search \
-H "X-Api-Key: pb_live_YOURKEY" \
-H "Content-Type: application/json" \
-d '{"q": "best web scraping api", "count": 5}'{
"query": "best web scraping api",
"count": 2,
"results": [
{"title": "…", "url": "https://…", "snippet": "…"},
{"title": "…", "url": "https://…", "snippet": "…"}
]
}POST/v1/rank
Ordina i link di una pagina in base a quanto ciascuno è pertinente a un obiettivo, così puoi scegliere quali sotto-pagine recuperare invece di scansionare tutto. Fornisci un url e una query; recuperiamo la pagina, leggiamo i suoi link (testo dell’ancora incluso) e un LLM li restituisce ordinati con uno score (0–1) e una breve reason. È stateless: ordina i link che la pagina ha già e non li recupera. Aggiungi top_k per limitare la lista, model ("fast"/"smart") e qualsiasi parametro di fetch condiviso. Se il passaggio AI fallisce ricevi comunque i link in ordine di documento con un ranking_error (il tuo codice non si rompe). Fatturato come un fetch più il sovrapprezzo AI; un ranking fallito fattura solo il fetch.
urlstringobbligatorioLa pagina da scaricare: un URL assoluto http o https.
querystringobbligatorioA cosa devono essere pertinenti i link.
top_kintegerQuanti link ordinati restituire, al massimo 50.
modelstringpredefinito "fast"Livello AI: fast, smart o max.
Più tutte le opzioni della richiesta.
curl https://scrape.land/v1/rank \
-H "X-Api-Key: pb_live_YOURKEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/blog", "query": "in-depth technical tutorials", "top_k": 10}'{
"url": "https://example.com/blog",
"query": "in-depth technical tutorials",
"status": 200,
"total_links": 84,
"ranked": [
{"url": "https://example.com/blog/go-generics",
"text": "A deep dive into Go generics",
"score": 0.93, "reason": "long-form technical tutorial"}
],
"ranked_count": 10,
"extracted_by": "ai",
"model": "fast"
}POST/v1/jobs: async & webhooks
Per lavori lunghi (un batch grosso, un rendering lento) eseguili in modo asincrono: invii un job, ricevi subito un id, poi lo interroghi o ricevi il risultato su un webhook. Invia i normali parametri dell'operazione più type (fetch/extract/batch/search) e un webhook_url opzionale.
typestringobbligatorioL'operazione da eseguire: fetch, extract, batch o search.
webhook_urlstringUn URL https pubblico a cui inviamo il risultato con POST quando il job termina.
Più i parametri di quell'operazione, esattamente come sul suo endpoint.
curl https://scrape.land/v1/jobs \
-H "X-Api-Key: pb_live_YOURKEY" -H "Content-Type: application/json" \
-d '{"type":"batch","urls":["https://a.example","https://b.example"],
"fields":{"title":"h1"},"webhook_url":"https://your.app/hook"}'
# -> {"id":"job_ab12","status":"queued"}
# poll it:
curl https://scrape.land/v1/jobs/job_ab12 -H "X-Api-Key: pb_live_YOURKEY"
# -> {"id":"job_ab12","type":"batch","status":"done","result":{}}
# list recent jobs (newest first, no result blobs):
curl "https://scrape.land/v1/jobs?limit=20" -H "X-Api-Key: pb_live_YOURKEY"Quando un job finisce facciamo un POST di {"id","status","result"} al tuo webhook_url, che deve essere un URL https pubblico (gli indirizzi interni e loopback sono rifiutati). I job girano fino a qualche minuto; ognuno è contabilizzato esattamente come la chiamata sincrona.
Verifica del webhook. Ogni consegna porta un'intestazione X-Scrapeland-Signature della forma t=<unix>,v1=<hex>, dove l'hex è HMAC-SHA256("<t>.<raw body>") con la chiave del tuo segreto webhook. Firma i byte GREZZI del corpo (non una copia riserializzata), confronta a tempo costante e rifiuta tutto ciò il cui t è più vecchio di qualche minuto: è questo a impedire che una consegna intercettata ti venga rigiocata contro più tardi. Chiedi il segreto al supporto se usi webhook_url.
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
ts, sig = parts.get("t", ""), parts.get("v1", "")
if not ts or not sig or abs(time.time() - int(ts)) > tolerance:
return False
expected = hmac.new(secret.encode(),
ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)POST/v1/places
Attività locali di un certo tipo in un certo luogo, sotto forma di righe: nome, indirizzo, sito, telefono, email e orari. Una lista di lead senza scrivere uno scraper. Invia una category e una location (o un bbox), tieni facoltativamente solo le attività che hanno o non hanno sito, telefono o email, e ricevi le corrispondenze ordinate per contatto migliore: una riga con sito e telefono viene prima di una che non ha nessuno dei due, poi per nome.
categorystringobbligatorioUn id di categoria come dentist, restaurant, plumber o it_company. L'elenco completo è in GET /v1/capabilities sotto entitlements.places.categories; un id sconosciuto dà 400.
locationstringUn luogo in cui cercare, come "Austin, TX" o "Lyon". Invia questo o bbox.
bboxarray[south, west, north, east] in gradi, al posto di location. Al massimo circa 1,2 gradi quadrati (una grande area metropolitana); un'area più grande, o una località che corrisponde a una, dà 400.
website / phone / emailstringpredefinito "any"Ognuno accetta any, yes (solo le attività che ne indicano uno) o no (solo quelle che non lo indicano). "website": "no" ti dà una lista di attività senza sito.
limitintegerpredefinito 200Numero massimo di righe restituite, fino a 200 (il massimo, e quello che ricevi se lo ometti). Sul piano Free è limitato a 10.
offsetintegerpredefinito 0Da dove parte la pagina: 0 (predefinito) per la prima pagina, poi il next_offset della risposta precedente. Ignorato sul piano Free, che restituisce solo la prima pagina.
results contiene le righe. Ogni riga ha name, category, lat, lon e osm_url, più quelli tra address, city, website, profile (una pagina social, di prenotazione o di directory indicata al posto di un sito proprio), phone, email e opening_hours che la mappa ha per quell'attività: un campo senza dati viene omesso, non inviato vuoto. count sono le righe restituite, matched quante corrispondevano ai filtri prima del limite, units quanto ha fatturato la chiamata e bbox l'area cercata.
Tutte le attività. Una chiamata restituisce fino a 200 risultati. Per averli tutti, richiama con offset uguale al next_offset della risposta precedente finché è null; le pagine vengono dalla stessa lista ordinata, quindi non si sovrappongono né saltano righe. Un'area restituisce al massimo 3.000 attività di una categoria; quando ne ha di più, la risposta indica "truncated": true e cercare in aree più piccole (un quartiere o un bbox) le restituisce tutte.
Fatturazione e piani. 1 unità di richiesta ogni 10 righe restituite, minimo 1 (una risposta vuota ha comunque eseguito una query), sullo stesso contatore di ogni altra chiamata. Ogni piano può chiamarlo. Su Free una chiamata è un'anteprima: le prime 10 corrispondenze, e quando ne corrispondono di più la risposta lo dice con "preview": true e un preview_note. I piani a pagamento ricevono fino a 200 righe per chiamata. Se il servizio dei dati cartografici è occupato ricevi un 502 e non ti viene fatturato nulla.
Dati e attribuzione. Le righe provengono da OpenStreetMap, © contributori di OpenStreetMap, con licenza ODbL. Ogni risposta riporta questo credito in attribution e ogni riga rimanda alla sua fonte con osm_url. Se pubblichi o condividi i dati, devi citare OpenStreetMap come richiede la licenza. Le risposte restano in cache fino a 24 ore.
curl https://scrape.land/v1/places \
-H "X-Api-Key: pb_live_YOURKEY" \
-H "Content-Type: application/json" \
-d '{"category": "dentist", "location": "Austin, TX", "website": "yes"}'{
"attribution": "© OpenStreetMap contributors, ODbL (openstreetmap.org/copyright)",
"bbox": [30.0985133, -97.9367663, 30.5166255, -97.5605288],
"category": "dentist",
"count": 10,
"matched": 79,
"preview": true,
"preview_note": "Free plan: the first 10 matches. Every paid plan returns up to 200 per call.",
"results": [
{"name": "Aloha Dental", "category": "dentist",
"address": "1614 East Riverside Drive, 78741", "city": "Austin",
"lat": 30.245304, "lon": -97.730394,
"website": "https://www.aloha-dental.com/", "phone": "+1-512-707-7300",
"email": "riverside@aloha-dental.com",
"opening_hours": "Mo 08:00-17:00; Tu 09:00-18:00; We 08:00-17:00; Th 09:00-18:00; Fr 08:00-17:00",
"osm_url": "https://www.openstreetmap.org/node/3856445942"},
{"name": "Austin Dental", "category": "dentist",
"address": "2206 West Parmer Ln, 78727", "city": "Austin",
"lat": 30.417672, "lon": -97.698822,
"website": "https://www.lifetimedental.com/", "phone": "+1-512-982-4006",
"opening_hours": "Mo-Fr 07:30-17:00",
"osm_url": "https://www.openstreetmap.org/way/383188243"}
],
"units": 1
}GET/v1/capabilities
Un endpoint memorizzabile in cache, così il tuo codice può rilevare cosa supporta un'installazione prima di inviare una richiesta: formats e features disponibili, se render (e le sue sotto-opzioni) e l'estrazione con AI sono attivi, i models e gli extract_types dell'AI, gli endpoints attivi e i limits. Entrambi gli SDK lo espongono come capabilities().
Funziona anche senza chiave, ma invia la tua X-Api-Key e la risposta è tarata sul tuo account. L'estrazione con AI dipende dal piano (vedi Estrazione con AI), quindi una chiamata senza chiave può solo dirti cosa supporta l'installazione, mentre una con chiave ti dice cosa puoi chiamare tu: ai.enabled è vero solo se il tuo piano lo include, e extract_type/extract_types spariscono con esso. ai.configured riporta l'installazione, ai.min_plan indica il piano che ti servirebbe e ai.plan_scoped ti dice se la risposta è stata personalizzata. Rileva su ai.enabled e non ti sentirai mai dire sì per poi ricevere un 403.
Dimensione della risposta. limits.max_response_bytes è la risposta singola più grande che restituiamo e dipende dal tuo piano (Free, Pool e Starter 2 MB, Growth 5 MB, Scale 10 MB, Business 25 MB). Le pagine reali non ci si avvicinano (la risposta mediana che serviamo è di circa 40 KB e il 99% sta sotto 1,2 MB), quindi intercetta solo un video, un archivio o un dump di dati. Una risposta oltre il limite è rifiutata per intero con un 413 che indica dimensione, limite e piano; non restituiamo mai una pagina troncata, perché una pagina che ha perso la coda in silenzio si fattura come riuscita e si interpreta come dato corrotto. limits.max_response_bytes_scope ti dice di chi è il numero che hai ricevuto: "key" quando hai inviato una X-Api-Key (il limite del tuo piano) o "lowest_plan" quando non l'hai fatto, cioè la soglia minima che ha ogni account. Per questo è sempre sicura su cui dimensionare, e presentare una chiave può solo alzarla.
{
"formats": ["html", "text", "markdown", "raw"],
"render": {"enabled": true, "screenshot": true, "actions": true,
"device": true, "block_resources": true, "fingerprint": true},
"ai": {"enabled": true, "configured": true, "min_plan": "scale",
"plan_scoped": true, "models": ["fast", "smart", "max"], "default": "fast"},
"extract_types": ["product", "article", "job", "discussion",
"event", "recipe", "real_estate", "profile"],
"endpoints": ["/v1/fetch", "/v1/extract", "/v1/rank", "/v1/batch",
"/v1/search", "/v1/jobs", "/v1/places", "/v1/account",
"/v1/capabilities", "/mcp"],
"limits": {"batch_max_urls": 20, "links_max": 2000,
"search_max_count": 10, "raw_max_bytes": 7340032,
"max_response_bytes": 5242880,
"max_response_bytes_scope": "key"}
}GET/v1/account
Controlla dal codice il piano della tua chiave, la quota rimasta, il credito prepagato, il rate limit e il budget per chiave (ad es. prima di un job grosso) invece di aprire la dashboard. Riporta sempre e solo il tuo account. Entrambi gli SDK lo espongono come account().
plan è il codice del piano, non il nome visualizzato: free, pool, starter, pro (il piano Growth), scale, business, business_xl, business_xxl o business_ultra. Gli stessi codici compaiono come plan e required_plan nei rifiuti e in GET /v1/capabilities.
{
"plan": "scale",
"included_requests_remaining": 2871400,
"prepaid_credit_cents": 0,
"rate_limit_rps": 200,
"key_budget": {"max_requests": 0, "requests_used": 1234, "unlimited": true}
}MCP server (Claude, Cursor and other AI clients)
scrape.land is also a remote Model Context Protocol server, so an AI assistant can fetch pages, extract data, search the web and find local businesses as tools. Endpoint: https://scrape.land/mcp (Streamable HTTP, JSON-RPC over POST). Authenticate with your API key as Authorization: Bearer YOUR_KEY. Each tool call runs the matching endpoint on your account, so it is billed, rate limited and plan gated exactly like the API.
| Tool | Does | Same as |
|---|---|---|
fetch_page | Fetch a page as markdown (default), text or HTML through our rotating tunneling network. Arguments: url, format, render, country. | /v1/fetch |
extract_data | Structured JSON from a page, by CSS fields or a plain-English prompt (with optional model: fast, smart or max). | /v1/extract |
web_search | Organic web results for q (up to 10). | /v1/search |
find_local_businesses | Businesses of a category in a location, with optional website, phone and email filters (any, yes or no), a limit and an offset to page through every result. | /v1/places |
Aggiungilo al tuo strumento AI. Sostituisci YOUR_KEY con una chiave API dalla dashboard; i pulsanti di installazione lo aggiungono con questo segnaposto, quindi apri poi le impostazioni del server e incolla la tua chiave.
Claude (claude.ai, Desktop, mobile): Settings, Connectors, Add custom connector. Chiamalo scrape.land, imposta l'URL su https://scrape.land/mcp e aggiungi l'header di richiesta Authorization con il valore Bearer YOUR_KEY.
Claude Code:
claude mcp add --transport http scrape-land https://scrape.land/mcp --header "Authorization: Bearer YOUR_KEY"Cursor, configurazione di Claude Desktop, Cline e altri client che usano una configurazione JSON mcpServers:
{
"mcpServers": {
"scrape-land": {
"type": "http",
"url": "https://scrape.land/mcp",
"headers": { "Authorization": "Bearer YOUR_KEY" }
}
}
}VS Code (GitHub Copilot), in .vscode/mcp.json; VS Code chiede la chiave una sola volta e la conserva in modo sicuro:
{
"inputs": [
{ "type": "promptString", "id": "scrapeland-key", "description": "scrape.land API key", "password": true }
],
"servers": {
"scrape-land": {
"type": "http",
"url": "https://scrape.land/mcp",
"headers": { "Authorization": "Bearer ${input:scrapeland-key}" }
}
}
}Gemini CLI, in ~/.gemini/settings.json:
{
"mcpServers": {
"scrape-land": {
"httpUrl": "https://scrape.land/mcp",
"headers": { "Authorization": "Bearer YOUR_KEY" }
}
}
}Windsurf, in mcp_config.json:
{
"mcpServers": {
"scrape-land": {
"serverUrl": "https://scrape.land/mcp",
"headers": { "Authorization": "Bearer YOUR_KEY" }
}
}
}Zed, in settings.json:
{
"context_servers": {
"scrape-land": {
"url": "https://scrape.land/mcp",
"headers": { "Authorization": "Bearer YOUR_KEY" }
}
}
}Il tuo agente, OpenAI Agents SDK (Python):
from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHttp
async with MCPServerStreamableHttp(params={
"url": "https://scrape.land/mcp",
"headers": {"Authorization": "Bearer YOUR_KEY"},
}) as scrapeland:
agent = Agent(name="researcher", mcp_servers=[scrapeland])
result = await Runner.run(agent, "Find three dentists in Austin without a website")
print(result.final_output)Il tuo agente, Claude API (MCP connector, header beta anthropic-beta: mcp-client-2025-11-20):
{
"model": "claude-sonnet-5",
"max_tokens": 2048,
"messages": [{ "role": "user", "content": "Summarise https://example.com" }],
"mcp_servers": [{
"type": "url",
"url": "https://scrape.land/mcp",
"name": "scrape-land",
"authorization_token": "YOUR_KEY"
}],
"tools": [{ "type": "mcp_toolset", "mcp_server_name": "scrape-land" }]
}SDK Python e migrazione da Zyte
Il nostro client Python racchiude sia l'API di estrazione sia il tunnel grezzo, e include un adattatore drop-in per il client Zyte API. Sei già su Zyte? Cambia l'import e la chiave: le forme della richiesta e della risposta coincidono.
pip install scrapelandfrom scrapeland import ScrapelandClient
client = ScrapelandClient("pb_live_YOURKEY") # or $SCRAPELAND_API_KEY
# structured extraction:
res = client.extract("https://example-product.com/item/42", {"title": "h1"})
# raw tunnel fetch through a fresh exit IP:
r = client.get("https://api.ipify.org", country="us", session="job42")
# or hand the tunnel mapping to your existing requests/httpx code:
import requests
requests.get("https://api.ipify.org", proxies=client.proxies(country="de"))from scrapeland.zyte import ZyteAPI. url, httpResponseBody (base64), httpResponseHeaders, customHttpRequestHeaders, geolocation→paese di uscita, sessionContext→sessione sticky e iter()/AsyncZyteAPI si mappano tutti. browserHtml/screenshot/auto-estrazione non sono disponibili tramite il tunnel grezzo: usa invece l'API di estrazione con render.Tunnel grezzo (avanzato)
La maggior parte delle persone vuole dati strutturati e dovrebbe usare l'API di estrazione qui sopra. Se invece ti serve l'accesso HTTP grezzo alla pagina, il tunnel grezzo è un forward proxy: punta qualsiasi client HTTP verso di esso con la tua chiave API come nome utente, e ogni richiesta esce da un IP nuovo, con controlli opzionali di paese, sessione e protocollo. Host: gateway.scrape.land:8080.
curl -x http://pb_live_YOURKEY:@gateway.scrape.land:8080 https://api.ipify.org
# -> an exit IP, different on each requestgateway.scrape.land:443, che le reti aziendali, universitarie e alberghiere filtrano di rado. Comportamento e fatturazione identici (cambia solo la porta). Mantieni lo schema http://: la tua chiave viaggia sempre nel nome utente, esattamente come sulla 8080.CONNECT, cioè quello che il tuo client apre per ogni destinazione https://, e una per richiesta per le destinazioni http:// semplici. Un tunnel che non trasmette nulla dalla destinazione non si fattura. Un browser (Playwright, Puppeteer) apre un tunnel separato per ogni host da cui carica la pagina, spesso più di uno per host, quindi un caricamento di pagina attraverso il tunnel vale di solito diverse unità. Per pagine renderizzate intere, il render dell'API di estrazione costa un fisso di 5 unità (1 con block_resources).Parametri di controllo
Aggiungi coppie -nome-valore alla chiave nel nome utente, oppure invia intestazioni di controllo X-Proxy-*. Ti colleghi sempre al gateway come a un proxy HTTP (schema http://); protocol sceglie quale tipo di proxy di uscita porta avanti la richiesta: http, https o socks5.
| Obiettivo | Forma nel nome utente | Intestazione |
|---|---|---|
| Paese di uscita | KEY-country-us | X-Proxy-Country: us |
| Sessione sticky (stesso IP) | KEY-session-abc123 | X-Proxy-Session: abc123 |
| Protocollo | KEY-protocol-socks5 | X-Proxy-Protocol: socks5 |
| Anonimato (premium) | KEY-anonymity-elite | X-Proxy-Anonymity: elite |
| Latenza massima (ms) | KEY-maxlatency-3000 | X-Proxy-Max-Latency: 3000 |
Generatore di URL per il tunnel
Scegli le opzioni e copia il comando pronto da eseguire. Sostituisci YOURKEY con una chiave dalla tua dashboard.
Usare il tunnel dal codice
import requests
KEY = "pb_live_YOURKEY"
proxy = f"http://{KEY}-country-us:@gateway.scrape.land:8080"
r = requests.get("https://api.ipify.org?format=json",
proxies={"http": proxy, "https": proxy}, timeout=30)
print(r.json()) # {'ip': '...'} a US exit IPimport { ProxyAgent, fetch } from "undici"; // npm i undici
const KEY = "pb_live_YOURKEY";
// The key is the proxy username and the password is empty. undici only builds
// Proxy-Authorization from a URL that has both, so pass it as a token.
const dispatcher = new ProxyAgent({
uri: "http://gateway.scrape.land:8080",
token: "Basic " + Buffer.from(`${KEY}-country-gb:`).toString("base64"),
});
// Node's fetch ignores an `agent` option: the tunnel goes in `dispatcher`.
const res = await fetch("https://api.ipify.org?format=json", { dispatcher });
console.log(await res.json()); // { ip: '...' } a GB exit IPp, _ := url.Parse("http://pb_live_YOURKEY-country-us:@gateway.scrape.land:8080")
client := &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(p)}}
resp, _ := client.Get("https://api.ipify.org")import { chromium } from "playwright";
const browser = await chromium.launch({
proxy: {
server: "http://gateway.scrape.land:8080",
username: "pb_live_YOURKEY-country-us",
password: "",
},
});
const page = await browser.newPage();
await page.goto("https://api.ipify.org");# settings.py
DOWNLOADER_MIDDLEWARES = {
"scrapeland.scrapy.ScrapelandMiddleware": 740,
}
SCRAPELAND_API_KEY = "pb_live_YOURKEY"
SCRAPELAND_COUNTRY = "us" # optional default for every request
# per-request override:
yield scrapy.Request(url, meta={"scrapeland": {"country": "de", "session": "job42"}})Errori
| Stato | Significato |
|---|---|
401 | Chiave API mancante o non valida sull'API di estrazione. Imposta X-Api-Key. |
407 | Chiave API mancante o non valida sul tunnel grezzo. Impostala come nome utente del tunnel. |
402 | Quota esaurita. Ricarica il saldo o passa a un piano superiore. |
429 | Rate limit del piano raggiunto: il limite di richieste al secondo del tuo piano (vedi Limiti del piano) è tutto in uso, allo stesso modo per destinazioni http:// e https://. Il code nel JSON è rate-limited e un'intestazione Retry-After dice quando riprovare. Non viene fatturato. |
403 | Destinazione non consentita, codice destination-blocked: gli indirizzi di loopback, privati, link-local (inclusi i metadata cloud) e altri indirizzi interni sono rifiutati su ogni endpoint e sul tunnel, sia per http che per https, e non vengono addebitati. Inoltre: estrazione AI o rendering nel browser richiesti su un piano che non li include. Il campo code indica quale. |
413 | Il corpo della richiesta, o la risposta, supera il limite di dimensione del tuo piano. Il messaggio indica dimensione, limite e piano; vedi limits.max_response_bytes. |
502 | Nessun upstream funzionante corrisponde ai tuoi filtri (prova con meno vincoli). |
504 | La richiesta ha superato il suo budget di tempo (45 s per una richiesta sincrona, 150 s quando include l'estrazione con AI). Riprova, semplificala (meno redirect o actions, "model": "fast") o inviala a POST /v1/jobs, che ha 5 minuti. |
503 | Siamo momentaneamente al limite di capacità (di solito ogni slot di browser è occupato da un render). Riprova dopo l'intestazione Retry-After, non è una richiesta fallita e non viene fatturata. |
code stabile, il piano su cui sei e cosa farne:
{
"error": "AI extraction (prompt, schema, extract_type) is not included on the \"free\" plan (it needs \"scale\" or above). Upgrade on the Plans page of your dashboard, or use CSS/XPath \"fields\" extraction, which every plan includes.",
"code": "plan-upgrade-required",
"plan": "free",
"required_plan": "scale",
"fallback": "use CSS/XPath \"fields\" extraction, which every plan includes",
"capability": "AI extraction (prompt, schema, extract_type)"
}plan-upgrade-required (403), quota-exhausted (402), key-budget-exhausted (402), rate-limited (429), response-too-large (413) e email-unverified (403: nessuna richiesta viene servita finché l’indirizzo email dell’account non è confermato, con qualsiasi piano). required_plan compare solo quando comprare qualcosa alza davvero il limite: il tetto di format=raw, ad esempio, è identico su ogni piano, quindi lì viene omesso invece di sventolare un upgrade che non cambia nulla. fallback è sempre presente.
Il tunnel grezzo è un proxy HTTP e risponde in text/plain (un corpo JSON finirebbe dentro il tuo tunnel), quindi porta lo stesso codice nell'intestazione di risposta X-Scrapeland-Error. Stesso vocabolario su entrambe le superfici: entitlements.refusal_codes in GET /v1/capabilities elenca quelli legati al piano (tutti i codici sopra tranne email-unverified).Gli stati del sito di destinazione (i suoi 200/403/404) sono trasmessi invariati. Il paese di uscita non è un'intestazione della risposta dell'API stessa: invia "headers": true e lo trovi come X-Proxy-Country dentro l'oggetto headers. Sul tunnel grezzo, X-Proxy-Country è sulla risposta al CONNECT per le destinazioni https:// e sulla risposta stessa per le destinazioni http://.
Limiti del piano
Con il piano cambiano quattro cose: quanto in fretta puoi inviare richieste, quanto può essere grande una singola risposta, se l'estrazione con AI è disponibile e se ottieni le uscite prioritarie. Valgono allo stesso modo per l'API di estrazione e per il tunnel grezzo. GET /v1/capabilities con la tua chiave restituisce i tuoi diritti sotto entitlements, e GET /v1/account i tuoi numeri.
| Piano | Incluse / mese | Rate limit | Risposta massima | Uscite prioritarie | Estrazione con AI |
|---|---|---|---|---|---|
| Free | 1.000 | 5 richieste/s | 2 MB | no (pool ad alta latenza) | no |
| Pool ($19) | senza conteggio | 10 richieste/s | 2 MB | no (uscite senza priorità) | no |
| Starter ($99) | 600.000 | 50 richieste/s | 2 MB | sì | no |
| Growth ($249) | 1.600.000 | 100 richieste/s | 5 MB | sì | no |
| Scale ($499) | 3.400.000 | 200 richieste/s | 10 MB | sì | sì |
| Business ($999) | 7.200.000 | 1.000 richieste/s | 25 MB | sì | sì |
| Business XL ($1,999) | 15.000.000 | 1.000 richieste/s | 25 MB | sì | sì |
| Business XXL ($3,499) | 30.000.000 | 1.000 richieste/s | 25 MB | sì | sì |
| Business Ultra ($9,999) | 100.000.000 | 1.000 richieste/s | 25 MB | sì | sì |
| Pay as you go | In arrivo nel 2027. Oggi non è venduto come piano: parti da Free o scegli un abbonamento. Le ricariche prepagate sono già disponibili su ogni piano. | ||||
Pool costa $19/mese ($190/anno) ed è senza conteggio: non ha un numero mensile di richieste, e il rate limit di 10 richieste/s è l'unico tetto. Gira su uscite senza priorità, non include l'estrazione con AI né il rendering nel browser, e include gli strumenti di lead generation (POST /v1/places).
Rate limit. Richieste al secondo sostenute, applicate per account su tutte le chiavi che possiedi (un limite per chiave non sarebbe affatto un limite: le chiavi si creano gratis); una singola chiave può essere limitata più in basso. Brevi picchi oltre il limite sono tollerati. Sopra di esso ricevi un 429 con un'intestazione Retry-After: rallenta e prosegui, non si perde nulla e non si fattura nulla. La velocità di ogni piano è dimensionata perché una corsa a tavoletta sull'intera dotazione mensile duri ore e non minuti.
Risposta massima. La risposta singola più grande che restituiamo, esposta come limits.max_response_bytes in GET /v1/capabilities. Le pagine reali non ci si avvicinano (la risposta mediana che serviamo è di circa 40 KB e il 99% sta sotto 1,2 MB) quindi intercetta solo un video, un archivio o un dump di dati. Tutto ciò che è più grande viene rifiutato per intero con un 413 che indica dimensione, limite e piano. Non restituiamo mai una pagina troncata, e una risposta rifiutata non viene fatturata.
La fatturazione non supera mai quello che hai pagato. Le richieste incluse si consumano per prime. Dopo di esse, le richieste attingono al credito prepagato a $0,20 / 1.000: il saldo scende e si ferma a zero, e a quel punto le richieste restituiscono 402 invece di proseguire. Non c'è alcuna fattura a posteriori né modo di accumulare una spesa che non hai autorizzato in anticipo.
Guide
Sessioni a rotazione vs. sticky
Per impostazione predefinita ogni richiesta esce da un IP nuovo (ottimo per distribuire il carico ed evitare i rate limit). Quando ti serve lo stesso IP tra più richieste (login, carrelli, flussi in più passi) aggiungi un parametro session (o -session-NOME sul tunnel); fissiamo quella sessione su un solo IP di uscita per circa 10 minuti, poi ruota. Usa un nome di sessione diverso per ogni utente logico.
Gestione dei rate limit e degli errori
- Il target restituisce 429/403: riprova. Una nuova richiesta ruota su un IP nuovo. Aggiungi un piccolo backoff.
- 402 da noi: la tua quota o il tuo saldo è esaurito. Passa a un piano superiore o ricarica.
- 401/407 da noi: la chiave API manca o è sbagliata (intestazione per l'API, nome utente per il tunnel).
- 502 da noi: nessun upstream corrisponde ai tuoi filtri. Allenta
country/maxlatencye riprova.
import requests, time
KEY = "pb_live_YOURKEY"
def get(url, tries=4):
for i in range(tries):
proxy = f"http://{KEY}:@gateway.scrape.land:8080" # fresh IP each try
try:
r = requests.get(url, proxies={"http": proxy, "https": proxy}, timeout=30)
if r.status_code < 400:
return r
except requests.RequestException:
pass
time.sleep(2 ** i) # exponential backoff
raise RuntimeError("all retries failed")Domande frequenti
Esiste un piano gratuito?
Sì: 1.000 richieste, senza carta. Il piano a pagamento più economico è Pool a $19/mese (senza conteggio, a 10 richieste/s, senza estrazione con AI né rendering). I piani a consumo partono da $99/mese (600k richieste = $0,165 / 1k) e la tariffa migliora salendo di livello: $0,139 / 1k su Business ($999 / 7,2M) e $0,100 / 1k su Business Ultra ($9.999 / 100M). Vedi Limiti del piano per la scala completa.
Come vengo fatturato?
Per 1.000 richieste consegnate. Se un proxy è bloccato o restituisce 403/429, riproviamo con un altro IP e non lo fatturiamo mai. Paghi solo quando la pagina torna.
Le richieste incluse nel tuo piano si consumano per prime. Dopo di esse, le richieste escono dal credito prepagato a $0,20 / 1.000: ricarichi in anticipo, il saldo scende e si ferma a zero. Non c'è alcuna fattura a posteriori né modo di accumulare un conto che non hai autorizzato. Senza abbonamento, il credito prepagato costa gli stessi $0,20 / 1.000. Il pay-as-you-go come piano a sé arriva nel 2027; oggi non è in vendita.
Posso scegliere un paese specifico?
Sì. Sull'API di estrazione aggiungi "country": "us" al corpo della richiesta. Sul tunnel grezzo aggiungi -country-us (qualsiasi codice ISO) alla tua chiave. Vedi il generatore di URL per il tunnel.
Come estraggo pagine che richiedono JavaScript?
Aggiungi "render": true alla tua richiesta di estrazione e "wait_for" con un selettore CSS se il contenuto arriva tardi. Vedi render e wait_for.
Offrite IP residenziali?
Oggi il pool è fatto di IP a rotazione di tipo datacenter/ISP, autovalidati di continuo. I pool residenziali/mobili sono nella roadmap (piani premium).
È legale?
Sono strumenti standard; sei tu il responsabile di usarli in modo lecito e di rispettare i termini dei siti di destinazione. Vedi i nostri Termini.