Trasforma qualsiasi URL in JSON pulito.
Una sola chiave API. Dacci un URL e un insieme di campi — recuperiamo la pagina, ruotiamo l'IP, avviamo il browser se serve e restituiamo dati strutturati. Nessun proxy da gestire, nessun parser da mantenere.
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.
C'è una sola richiesta che userai quasi sempre: POST /v1/extract. Il resto di questa sezione mostra proprio quella richiesta, poi la trasporta in ogni linguaggio così la integri in fretta.
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. È la stessa richiesta usata in ogni esempio di linguaggio più sotto.
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",
"image": {"css": "img.gallery", "attr": "src"},
"tags": {"css": ".tag", "all": true}
}
}'Risposta. Ogni chiave in data corrisponde al campo che hai chiesto:
{
"url": "https://example-product.com/item/42",
"status": 200,
"data": {
"title": "Vintage desk lamp",
"price": "49.00",
"image": "https://example-product.com/img/a.jpg",
"tags": ["retro", "lighting", "brass"]
}
}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: qualsiasi stringa che inizia con
/è trattata come XPath."title": "//h1/text()","link": "//a/@href". - 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). Si combinano.
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, 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).
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"}
}'Altri parametri opzionali di primo livello che puoi aggiungere a qualsiasi richiesta di estrazione:
| Parametro | Cosa fa |
|---|---|
country | Paese di uscita, codice ISO, ad es. us. La richiesta parte da quel paese. |
session | Nome di una sessione sticky. Riusa lo stesso IP di uscita tra le chiamate (login, carrelli). |
render | true per eseguire la pagina in un browser vero prima di leggerla. |
wait_for | In modalità render, un selettore CSS da attendere prima di leggere il DOM. |
headers | true per includere le intestazioni di risposta nel risultato. |
format | Per /v1/fetch: html (predefinito), text, markdown (Markdown pronto per gli LLM — vedi sotto) o raw — 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). |
screenshot | Per /v1/fetch: true restituisce un PNG in base64 della pagina renderizzata (implica render). Aggiungi full_page: true per tutta l'altezza scorrevole. |
send_headers | Un oggetto con intestazioni di richiesta extra da inoltrare al target (User-Agent personalizzato, Authorization, …). |
cookies | Il valore di un'intestazione Cookie da inviare, ad es. per estrarre dietro un login. |
method / body | 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. |
block_resources | In modalità render, true salta immagini/font/media per un rendering più veloce e leggero (il DOM non cambia, l'estrazione funziona lo stesso). |
fingerprint | In 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. |
device | "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. |
actions | In modalità render, passi scriptati prima della cattura: scroll, click, wait, wait_for, fill. Ottimo per scroll infinito e contenuti dietro un clic. |
extract_type | Auto-estrae un oggetto normalizzato per un tipo di pagina noto, senza prompt né schema. Vedi Auto-estrazione. |
metadata | true 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. |
links | true restituisce anche un array links: ogni a href risolto in URL assoluto, deduplicato, solo http(s). Comodo per crawling e discovery. |
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, Starter, Growth e pay-as-you-go 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) o "smart" (mediana 3,4 s, migliore quando la risposta va dedotta dalla pagina invece che letta — mettendo insieme un orario di inizio, una durata e un fuso, ad esempio — fatturato a una tariffa più alta di unità di richiesta). 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.
{
"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"
}
}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.
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"}'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.
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"}'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.
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}'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.
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}'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.
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 (un URL https pubblico — quelli interni/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)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.
{
"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"], "default": "fast"},
"extract_types": ["product", "article", "job", "discussion",
"event", "recipe", "real_estate", "profile"],
"endpoints": ["/v1/fetch", "/v1/extract", "/v1/batch", "/v1/search",
"/v1/jobs", "/v1/account", "/v1/capabilities"],
"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"}
}Dimensione della risposta. limits.max_response_bytes è la risposta singola più grande che restituiamo e dipende dal tuo piano (Free e Starter 2 MB, Pay As You Go e 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 — la soglia minima che ha ogni account, quindi sempre sicura su cui dimensionare, e presentare una chiave può solo alzarla.
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": "pro",
"included_requests_remaining": 421900,
"prepaid_credit_cents": 0,
"rate_limit_rps": 100,
"key_budget": {"max_requests": 0, "requests_used": 1234, "unlimited": true}
}La stessa richiesta, in ogni linguaggio
Una richiesta, trasposta in modo idiomatico. Ognuna fa la stessa cosa: un POST HTTP verso https://scrape.land/v1/extract con l'intestazione X-Api-Key e il corpo JSON, poi interpreta la risposta JSON.
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"];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.Parametri di controllo
Aggiungi coppie -nome-valore alla chiave nel nome utente, oppure invia intestazioni di controllo X-Proxy-*.
| 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 { HttpsProxyAgent } from "https-proxy-agent";
const KEY = "pb_live_YOURKEY";
const agent = new HttpsProxyAgent(`http://${KEY}-country-gb:@gateway.scrape.land:8080`);
const res = await fetch("https://api.ipify.org?format=json", { agent });
console.log(await res.json());p, _ := 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 tuo piano superato (vedi Limiti del piano). Un'intestazione Retry-After dice quando riprovare. |
403 | Destinazione non consentita (bloccata dal guard SSRF/di destinazione) oppure estrazione con AI richiesta su un piano sotto Scale. Il messaggio dice 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). |
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 — le richieste gratuite restano bloccate finché l’indirizzo email dell’account non è confermato; i piani a pagamento non lo incontrano mai). 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 li elenca.L'intestazione di risposta X-Proxy-Country riporta il paese di uscita usato. Gli stati del sito di destinazione (i suoi 200/403/404) sono trasmessi invariati.
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 |
| 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. | ||||
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. I piani a pagamento 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 pay-as-you-go è $0,1165 / 1.000, la tariffa più bassa che vendiamo.
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.