Vai al contenuto

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_YOURKEY

Va bene anche un'intestazione bearer Authorization, se si adatta meglio al tuo stack:

Authorization: Bearer pb_live_YOURKEY

POST/v1/extract

La richiesta canonica. Invia un url e una mappa fields e ricevi un oggetto data con una chiave per campo.

Parametri del corpo
urlstringobbligatorio

La pagina da scaricare: un URL assoluto http o https.

fieldsobject

Nome del campo → selettore: un selettore CSS, selector@attr, XPath o un oggetto. Vedi Come i campi corrispondono ai selettori. Invia questo oppure prompt.

promptstring

Descrivi i campi a parole invece di scrivere selettori. Piano Scale e superiori. Vedi Estrazione con AI.

schemaobject

Con prompt: un oggetto JSON chiave → tipo che fissa la forma di data.

extract_typestring

Auto-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 true

Con prompt: false restituisce una risposta in linguaggio naturale in answer invece dell'oggetto data.

groupbooleanpredefinito false

Unisce i campi lista in un oggetto per riga sotto items.

Più tutte le opzioni della richiesta.

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"];
Risposta
{
  "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:

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:

CampoSelettore / oggettoValore 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.

Quale CSS supportano i nostri selettori. Il matching dei selettori usa Cascadia di Go, che implementa CSS3. Non è un motore di browser e differisce in entrambe le direzioni: utile saperlo prima di incollare un selettore preso dai devtools.
  • 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 come svg|circle. Tutto ciò che viene da CSS3 funziona: combinatori discendente/figlio/fratello, :nth-child(), :not(), selettori di attributo inclusi ^= $= *= e il flag i.
  • 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.
    SelettoreCosa corrispondeEsempio
    :contains(…)elemento il cui testo contiene una sottostringah1:contains("Example")
    :matches(…)elemento il cui testo corrisponde a un'espressione regolarep:matches(^Price:)
    :matchesOwn(…)uguale, ma solo il testo proprio dell'elemento, ignorando i discendentih1:matchesOwn(^Example)
    :haschild(…)elemento con un figlio diretto che corrisponde a un selettorep: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 letterale h1,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.

Opzioni
countrystring

Paese di uscita, codice ISO, ad es. us. La richiesta parte da quel paese.

sessionstring

Nome di una sessione sticky. Riusa lo stesso IP di uscita tra le chiamate (login, carrelli).

protocolstring

Protocollo di uscita: http, https o socks5, cioè il protocollo che parla il proxy di uscita. Omettilo per accettarli tutti.

max_latencyinteger

Usa solo uscite che hanno risposto in meno di questi millisecondi.

renderbooleanpredefinito false

true per eseguire la pagina in un browser vero prima di leggerla.

wait_forstring

In 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 false

In modalità render, true salta immagini/font/media per un rendering più veloce e leggero (il DOM non cambia, l'estrazione funziona lo stesso).

fingerprintbooleanpredefinito false

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.

actionsarray

In modalità render, passi scriptati prima della cattura: scroll, click, wait, wait_for, fill. Ottimo per scroll infinito e contenuti dietro un clic.

headersbooleanpredefinito false

true per includere le intestazioni di risposta nel risultato.

send_headersobject

Un oggetto con intestazioni di richiesta extra da inoltrare al target (User-Agent personalizzato, Authorization, …).

cookiesstring

Il 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 false

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).

linksbooleanpredefinito false

true restituisce anche un array links: ogni a href risolto in URL assoluto, deduplicato, solo http(s). Comodo per crawling e discovery.

screenshotbooleanpredefinito false

Per /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"}
  }'
Quanto ti costa un rendering. Un fetch semplice scarica un solo documento HTML. Un rendering scarica tutto ciò che la pagina chiede (JavaScript, CSS, immagini, font, tracking) quindi è più lento e tira giù molti più dati: misurato a 1–300× i byte della stessa pagina presa in modo semplice, mediana intorno a 17×, con l'e-commerce pieno di immagini in cima a quell'intervallo. È prezzato di conseguenza: un rendering consegnato costa 5 unità di richiesta invece dell'1 di un fetch semplice, 10 se hai chiesto anche uno 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)

Requisito di piano: l'estrazione con AI (un 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.

CSS contro AI: l'AI legge il testo visibile della pagina più i suoi metadati strutturati. JSON-LD, OpenGraph, microdati e <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.

Parametri del corpo
urlstringobbligatorio

La 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.

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"}'
Risposta
{
  "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.

Parametri del corpo
urlsarrayobbligatorio

Fino a 20 URL assoluti. I risultati tornano nello stesso ordine.

fields / promptobject / string

Facoltativo. 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.

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"}'
Risposta
{
  "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.

Parametri del corpo
qstringobbligatorio

La query di ricerca.

countintegerpredefinito 10

Numero di risultati, da 1 a 10.

Più le opzioni proxy country, session, protocol e max_latency.

Richiesta
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}'
Risposta
{
  "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.

Parametri del corpo
urlstringobbligatorio

La pagina da scaricare: un URL assoluto http o https.

querystringobbligatorio

A cosa devono essere pertinenti i link.

top_kinteger

Quanti link ordinati restituire, al massimo 50.

modelstringpredefinito "fast"

Livello AI: fast, smart o max.

Più tutte le opzioni della richiesta.

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}'
Risposta
{
  "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.

Parametri del corpo
typestringobbligatorio

L'operazione da eseguire: fetch, extract, batch o search.

webhook_urlstring

Un 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.

Richiesta
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.

Parametri del corpo
categorystringobbligatorio

Un 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.

locationstring

Un 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 200

Numero massimo di righe restituite, fino a 200 (il massimo, e quello che ricevi se lo ometti). Sul piano Free è limitato a 10.

offsetintegerpredefinito 0

Da 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.

Richiesta
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"}'
Risposta
{
  "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.

Risposta
{
  "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.

Risposta
{
  "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.

ToolDoesSame as
fetch_pageFetch a page as markdown (default), text or HTML through our rotating tunneling network. Arguments: url, format, render, country./v1/fetch
extract_dataStructured JSON from a page, by CSS fields or a plain-English prompt (with optional model: fast, smart or max)./v1/extract
web_searchOrganic web results for q (up to 10)./v1/search
find_local_businessesBusinesses 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.

Add to Cursor Add to Kiro

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 scrapeland
from 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"))
Drop-in per Zyte: 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 request
Porta bloccata? Lo stesso proxy risponde anche su gateway.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.
Ogni richiesta ruota verso un nuovo IP di uscita se non imposti una sessione sticky. I browser non possono impostare un proxy per singola richiesta, quindi chiama il tunnel da un backend.
Come si fattura il tunnel. Un'unità di richiesta per ogni tunnel 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.

ObiettivoForma nel nome utenteIntestazione
Paese di uscitaKEY-country-usX-Proxy-Country: us
Sessione sticky (stesso IP)KEY-session-abc123X-Proxy-Session: abc123
ProtocolloKEY-protocol-socks5X-Proxy-Protocol: socks5
Anonimato (premium)KEY-anonymity-eliteX-Proxy-Anonymity: elite
Latenza massima (ms)KEY-maxlatency-3000X-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 IP
import { 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 IP
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

StatoSignificato
401Chiave API mancante o non valida sull'API di estrazione. Imposta X-Api-Key.
407Chiave API mancante o non valida sul tunnel grezzo. Impostala come nome utente del tunnel.
402Quota esaurita. Ricarica il saldo o passa a un piano superiore.
429Rate 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.
403Destinazione 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.
413Il 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.
502Nessun upstream funzionante corrisponde ai tuoi filtri (prova con meno vincoli).
504La 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.
503Siamo 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.
Ogni rifiuto dovuto al tuo piano risponde allo stesso modo, così scrivi un solo ramo invece di confrontare frasi in inglese. Sull'API di estrazione il corpo JSON porta un 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)"
}
I codici sono 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.

Le uscite prioritarie sono una differenza di instradamento, non una coda, non c'è nessuna fila in cui mettersi davanti. Su un piano a pagamento alla tua richiesta vengono offerte prima le uscite ISP/datacenter premium, poi una riserva più ampia di proxy del pool controllati di continuo, e infine un ultimo tentativo di consegna perché una richiesta pagata non fallisca per mancanza di uscite. Sul piano Free ogni richiesta è servita da un pool pubblico ad alta latenza, che è a cosa serve il livello gratuito: prototipare e provare. Stessa rotazione, targeting geografico, sessioni e rendering, su indirizzi più lenti e più spesso bloccati dai target difficili. Misurato sul nostro traffico di produzione, il pool gratuito ha avuto una mediana di 1.015 ms contro i 453 ms del premium, e una coda lunga (p99 32 s contro 4,5 s) che un pool pubblico avrà sempre. Sono osservazioni su un pool volatile di terze parti, non un livello di servizio che garantiamo.
PianoIncluse / meseRate limitRisposta massimaUscite prioritarieEstrazione con AI
Free1.0005 richieste/s2 MBno (pool ad alta latenza)no
Pool ($19)senza conteggio10 richieste/s2 MBno (uscite senza priorità)no
Starter ($99)600.00050 richieste/s2 MBsìno
Growth ($249)1.600.000100 richieste/s5 MBsìno
Scale ($499)3.400.000200 richieste/s10 MBsìsì
Business ($999)7.200.0001.000 richieste/s25 MBsìsì
Business XL ($1,999)15.000.0001.000 richieste/s25 MBsìsì
Business XXL ($3,499)30.000.0001.000 richieste/s25 MBsìsì
Business Ultra ($9,999)100.000.0001.000 richieste/s25 MBsìsì
Pay as you goIn 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

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).

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.