Sari la conținut
Documentație pentru dezvoltatori

Transformă orice URL în JSON curat.

O singură cheie API. Dă-ne un URL și un set de câmpuri — aducem pagina, rotim IP-ul, pornim browserul dacă e nevoie și îți returnăm date structurate. Niciun proxy de gestionat, niciun parser de întreținut.

API de extragere a datelor

Dă-ne un URL și un set de câmpuri. Aducem pagina, aplicăm selectorii tăi CSS și returnăm JSON curat. Niciun browser de rulat, niciun cod de parsare, nicio rotație de IP de gestionat. URL-ul de bază pentru fiecare endpoint este https://scrape.land.

Există o singură cerere pe care o vei folosi cel mai des: POST /v1/extract. Restul acestei secțiuni arată exact acea cerere, apoi o transpune în fiecare limbaj ca să o poți integra rapid.

Autentificare

Trimite cheia ta API în antetul X-Api-Key la fiecare cerere:

X-Api-Key: pb_live_YOURKEY

Merge și un antet bearer Authorization, dacă se potrivește mai bine cu stack-ul tău:

Authorization: Bearer pb_live_YOURKEY

POST/v1/extract

Cererea canonică. Trimite un url și o hartă fields și primești înapoi un obiect data cu o cheie pentru fiecare câmp. Este aceeași cerere folosită în fiecare exemplu de limbaj de mai jos.

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}
    }
  }'

Răspuns. Fiecare cheie din data corespunde câmpului pe care l-ai cerut:

{
  "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"]
  }
}

Cum se mapează câmpurile la selectori

Fiecare intrare din fields ia una dintre aceste forme:

Deci această hartă de câmpuri:

{
  "title": "h1",
  "price": ".price",
  "image": "img.gallery@src",
  "tags":  {"css": ".tag", "all": true}
}

produce acest obiect de date, câmp cu câmp:

CâmpSelector / obiectValoare returnată
title"h1"textul primului h1
price".price"textul primului .price
image"img.gallery@src"atributul src, ca șir
tags{"css":".tag","all":true}o listă cu textul fiecărui .tag

Când un câmp revine null

Un câmp este null fie pentru că pagina chiar nu are un astfel de element, fie pentru că selectorul în sine nu a putut fi folosit. Pentru tine sunt lucruri foarte diferite, așa că le deosebim: dacă un selector nu a putut fi compilat, răspunsul poartă un obiect field_errors care numește câmpul și motivul. Câmpul rămâne prezent în data ca null, cererea rămâne un 200, iar celelalte câmpuri nu sunt afectate — un selector inutilizabil nu scufundă niciodată restul extragerii.

{
  "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"
  }
}

Lipsa cheii field_errors înseamnă că fiecare selector s-a compilat, deci un null acolo este un „nu se află pe pagină” real. XPath este validat din start și returnează 400.

Ce CSS acceptă selectorii noștri. Potrivirea selectorilor folosește Cascadia din Go, care implementează CSS3. Nu este un motor de browser și diferă în ambele sensuri — bine de știut înainte să lipești un selector din devtools.
  • Selectorii mai noi nu sunt încă acceptați și vor ajunge în field_errors: :is(), :where(), :has(> x) în forma relativă, :focus-within, selectori cu spațiu de nume precum svg|circle. Tot ce ține de CSS3 funcționează — combinatori de descendent/copil/frate, :nth-child(), :not(), selectori de atribut inclusiv ^= $= *= și indicatorul i.
  • Patru selectori funcționează aici și nu sunt acceptați de niciun browser. Sunt extensii Cascadia pentru potrivire pe text, lucru pe care CSS-ul în sine nu îl poate face — chiar utile, dar nu sunt CSS standard. Un selector care le folosește nu va funcționa într-un browser, în devtools sau în orice alt instrument de scraping, așa că tratează-le ca pe o comoditate a acestui API, nu ca pe ceva de standardizat.
    SelectorCe potriveșteExemplu
    :contains(…)element al cărui text conține un subșirh1:contains("Example")
    :matches(…)element al cărui text se potrivește cu o expresie regulatăp:matches(^Price:)
    :matchesOwn(…)la fel, dar doar textul propriu al elementului, ignorând descendențiih1:matchesOwn(^Example)
    :haschild(…)element cu un copil direct care se potrivește cu un selectorp:haschild(a)

    Atenție la tipul argumentelor, aici e greșeala ușoară: :matches() și :matchesOwn() primesc o expresie regulată peste text — nu sunt o altă scriere a lui :is(). :matches(h1,h2) se compilează și apoi nu potrivește nimic, pentru că el caută textul literal h1,h2. Pune între ghilimele orice argument care conține spații sau punctuație: :contains("Learn more"), nu :contains(Learn more).

Pagini JavaScript: render și wait_for

Dacă pagina își construiește conținutul cu JavaScript, adaugă "render": true. Pornim un browser real, lăsăm pagina să ruleze, apoi citim DOM-ul și aplicăm selectorii tăi. Combină-l cu "wait_for" setat pe un selector CSS, ca să așteptăm până apare acel element înainte de citire — modul sigur de a aștepta conținut care se încarcă târziu:

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"}
  }'

Alți parametri opționali de nivel superior pe care îi poți adăuga la orice cerere de extragere:

ParametruCe face
countryȚara de ieșire, cod ISO, de ex. us. Cererea pleacă din acea țară.
sessionNumele unei sesiuni persistente. Reutilizează același IP de ieșire între apeluri (autentificări, coșuri).
rendertrue pentru a rula pagina într-un browser real înainte de a o citi.
wait_forÎn modul render, un selector CSS de așteptat înainte de citirea DOM-ului.
headerstrue pentru a include anteturile de răspuns în rezultat.
formatPentru /v1/fetch: html (implicit), text, markdown (Markdown gata pentru LLM — vezi mai jos) sau raw — octeții exacți codificați base64 sub base64, împreună cu content_type, ca să poți trage un PDF, o imagine sau orice fișier binar prin proxy intact (plafonat la 7 MiB).
screenshotPentru /v1/fetch: true returnează un PNG base64 al paginii randate (implică render). Adaugă full_page: true pentru toată înălțimea derulabilă.
send_headersUn obiect cu anteturi de cerere suplimentare, trimise mai departe către țintă (User-Agent propriu, Authorization, …).
cookiesValoarea unui antet Cookie de trimis, de ex. pentru a extrage în spatele unei autentificări.
method / bodyMetoda HTTP (implicit GET) și corpul cererii. Folosește POST/PUT/… pentru API-uri JSON sau pentru trimiterea de formulare. Ce nu e GET nu se reîncearcă niciodată și nu este compatibil cu render.
block_resourcesÎn modul render, true sare peste imagini/fonturi/media pentru o randare mai rapidă și mai ușoară (DOM-ul nu este afectat, extragerea funcționează în continuare).
fingerprintÎn modul render, true prezintă o identitate de browser coerentă cu country — fus orar, locale, geolocație, navigator.languages, WebGL/canvas generice — astfel încât o randare arată ca un client real din acea regiune și declanșează mai puține blocaje anti-bot. O session o menține stabilă.
device"mobile" randează ca pe un telefon (viewport tactil 390×844, 3×, UA de Safari mobil) pentru layoutul mobil; implicit este desktop 1280×800. Implică render.
actionsÎn modul render, pași scriptați înainte de captură: scroll, click, wait, wait_for, fill. Excelent pentru scroll infinit și conținut ascuns în spatele unui clic.
extract_typeAuto-extrage un obiect normalizat pentru un tip de pagină cunoscut, fără prompt și fără schemă. Vezi Auto-extragere.
metadatatrue returnează în plus un obiect metadata: title, description, canonical, lang, etichete OpenGraph/Twitter și jsonld schema.org parsat. Excelent pentru previzualizări de linkuri și SEO — fără niciun selector.
linkstrue returnează în plus o listă links: fiecare a href rezolvat la URL absolut, deduplicat, doar http(s). Util pentru parcurgere și descoperire.
Cât te costă o randare. Un fetch simplu descarcă un singur document HTML. O randare descarcă tot ce cere pagina — JavaScript, CSS, imagini, fonturi, tracking — deci este mai lentă și trage mult mai multe date: măsurat la 1–300× octeții aceleiași pagini aduse simplu, mediana în jur de 17×, cu magazine online pline de imagini la capătul de sus. Este prețuită pe măsură: o randare livrată costă 5 unități de cerere în loc de 1 cât costă un fetch simplu, 10 dacă ai cerut și un screenshot, și 1 — mai ieftin decât o randare simplă și la fel ca un fetch simplu — când trimiți block_resources. O randare care eșuează nu se facturează deloc. Dacă nu ai nevoie de un screenshot sau chiar de imagini, trimite "block_resources": true: imaginile, fonturile și media sunt sărite, DOM-ul pe care îl citesc selectorii tăi este identic, pe o pagină plină de imagini elimină o mare parte din transfer (măsurat pe 14 pagini: mediană 42%, cel mai bun 77% și în jur de 23% chiar și pe CDN-uri de imagini care servesc URL-uri fără extensie de fișier) și scade costul de la 5 unități la 1. Se aplică inclusiv împreună cu screenshot — resursele chiar sunt blocate, deci captura pe care o primești va fi fără imagini. Capacitatea de randare este și ea limitată — dacă toate sloturile de browser sunt ocupate primești un 503 cu Retry-After: 1, deci reîncearcă în loc să tratezi asta ca pe un eșec.

Extragere cu AI (descrie câmpurile în limbaj natural)

Cerință de plan: extragerea cu AI — un prompt, o schema sau orice preset extract_type — este disponibilă pe Scale și mai sus (Scale, Business, Business XL/XXL/Ultra). Pe Free, Starter, Growth și pay-as-you-go acestea returnează 403 cu planul de care ai avea nevoie. Extragerea cu fields pe bază de selectori (CSS și XPath, mai jos) funcționează pe orice plan, inclusiv Free. Verifică GET /v1/capabilities cu cheia ta ca să vezi ce ai.

Nu vrei să scrii și să întreții selectori? Trimite un prompt în loc de fields: aducem pagina (toți parametrii de mai sus se aplică în continuare), o convertim în Markdown, iar un model de limbaj returnează exact câmpurile pe care le-ai descris, ca JSON. Selectorii se strică atunci când se schimbă markupul unui site; un prompt se adaptează. Adaugă o schema opțională (un obiect JSON de key→tip) ca să fixezi forma exactă a răspunsului. Trimite model ca să alegi un nivel de putere: "fast" (implicit — mediană 1,5 s și alegerea potrivită pentru aproape orice extragere, fiindcă a scoate dintr-o pagină câmpuri care sunt scrise acolo este o sarcină de citire) sau "smart" (mediană 3,4 s și mai bun când răspunsul trebuie dedus din pagină, nu doar citit — de pildă combinând o oră de început, o durată și un fus orar — facturat la o rată mai mare de unități de cerere). Un nume necunoscut este un 400 care le enumeră pe cele valide. Dacă nivelul cerut nu este disponibil, răspundem pe celălalt în loc să eșuăm, facturăm nivelul care a servit efectiv și îți spunem returnând requested_model alături de model.

Pui o întrebare în loc să enumeri câmpuri? Un prompt precum "what's this business about?" este o întrebare, nu o listă de câmpuri — dar modelul tot trebuie să inventeze nume de chei pentru ea (business, category, competitors_mentioned…), iar tu trebuie să ghicești ce a ales. Trimite "structured": false și primești în schimb un răspuns în limbaj natural sub answer, fără nicio cheie 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 și answer nu apar niciodată împreună, deci un client tipizat ramifică după cea pe care a primit-o, în loc să facă type-switch pe un singur câmp. structured este implicit true, deci nimic din ce trimiți deja nu se schimbă. Costă exact aceleași unități de cerere ca extragerea structurată — este aceeași pagină și același apel de model. O schema sau un extract_type fixează o formă JSON și deci implică ieșire structurată; trimițându-le împreună cu "structured": false primești un 400 care numește ambele câmpuri, în loc să ghicim în tăcere ce ai vrut. Nu are niciun efect asupra extragerii cu fields pe bază de selectori, care nu ajunge niciodată la un model.

Ai nevoie de o listă? Cere "every product on the page" și primești lista înapoi sub data.items. Dacă pagina nu are text (o pagină doar-JS adusă fără render), primești un 422 care îți spune să reîncerci cu "render": true, în loc de un răspuns plin de valori nule.

CSS față de AI: AI-ul citește textul vizibil al paginii plus metadatele ei structurate — JSON-LD, OpenGraph, microdate și <time datetime> ajung la model, deci un preț exact, un cod de monedă sau o dată ISO de publicare sunt disponibile chiar și când pagina afișează doar "From $49" sau "3 days ago". Datele codificate cu totul altundeva (un rating într-un nume de clasă CSS) se scot totuși mai bine cu fields (CSS/XPath), care văd HTML-ul brut. Combină-le liber.
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-extragere (un cuvânt în loc de schemă)

Auto-extragerea este tot extragere cu AI dedesubt, deci cere același plan Scale sau mai sus.

Pentru tipurile de pagină uzuale nu ai nevoie nici măcar de un prompt. Trimite extract_type și aplicăm noi un prompt și o schemă îngrijite, ca să primești înapoi un obiect normalizat: product, article, job, discussion, event, recipe, real_estate și profile. prompt-ul sau schema ta au întâietate dacă le trimiți și pe ele.

Preseturile rulează pe nivelul implicit fast și răspund de obicei în câteva secunde. Trimite "model": "smart" dacă o pagină este chiar ambiguă — durează cam de două ori mai mult și costă mai multe unități de cerere. Cererile sincrone au un buget de 150 s; peste el primești un 504, deci trimite una lentă ca job asincron (POST /v1/jobs, buget de 5 minute).

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"
  }'

Obținerea anteturilor de răspuns

Uneori ai nevoie de anteturile de răspuns, nu doar de corp: un Location de redirect, un Set-Cookie, un Content-Type. Adaugă "headers": true și includem un obiect headers alături de 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"
  }
}

Mai multe endpointuri

POST/v1/fetch

Când vrei pagina întreagă în loc de câmpuri anume, folosește /v1/fetch. Setează format pe html (implicit), text (textul vizibil), markdown sau raw (octeți base64 + content_type, pentru PDF-uri/imagini/binare, până la 7 MiB). Se aplică aceleași opțiuni country, session, render, wait_for și 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 gata pentru LLM

"format": "markdown" returnează pagina ca Markdown curat sub o cheie markdown — forma pe care pipeline-urile RAG și ferestrele de context ale LLM-urilor chiar o vor, ca să nu trimiți etichete HTML brute unui model și să le plătești ca tokeni. Elementele de șablon ale site-ului (nav, header, footer, aside, scripturi și stiluri) sunt scoase, titlurile, listele, linkurile, blocurile de cod și tabelele sunt păstrate, iar linkurile și imaginile relative sunt rezolvate la URL-uri absolute, ca un fragment să funcționeze și după ce este separat de pagina sursă. Se combină cu orice altă opțiune — country, session, render, wait_for, actions, fingerprint — și funcționează și în /v1/batch. Costă exact o unitate de cerere, la fel ca orice alt fetch — nu există niciun supliment pentru Markdown sau 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

Adu până la 20 de URL-uri într-un singur apel. Trimite o listă urls plus orice parametri comuni; primești o intrare results pentru fiecare URL, în ordinea de intrare — fiecare fie un rezultat obișnuit de fetch, fie un {"url","error"}. Fiecare URL se facturează ca o cerere livrată separată.

Extragere în masă: adaugă fields sau un prompt și fiecare URL este extras exact ca la /v1/extract — fiecare rezultat poartă un obiect data. Excelent pentru a transforma o listă de URL-uri în rânduri, într-un singur apel.

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

Rulează o căutare web și primești înapoi rezultatele organice — title, url-ul real de destinație și un snippet — fără să parsezi tu un SERP. Trimite {"q": "...", "count": 10} plus orice parametri de proxy comuni. Se facturează ca o singură cerere livrată. count este plafonat la 10 — o singură pagină de rezultate de la motorul de căutare; ceri mai mult și tot 10 primești.

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

Clasează linkurile de pe o pagină după cât de relevant este fiecare pentru un scop, ca să alegi ce sub-pagini să extragi în continuare în loc să parcurgi tot. Dă un url şi un query; noi aducem pagina, îi citim linkurile (cu tot cu textul ancorei), iar un LLM le returnează ordonate cu un score (0–1) şi un reason scurt. Este stateless: clasează linkurile pe care pagina le are deja şi nu le accesează. Adaugă top_k ca să limitezi lista, model ("fast"/"smart") şi orice parametri comuni de fetch. Dacă pasul AI eşuează, tot primeşti linkurile în ordinea din document, cu un ranking_error — codul tău nu se strică. Se taxează ca un fetch plus suprataxa AI; o clasare eşuată taxează doar fetch-ul.

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

Pentru lucrări de durată (un batch mare, o randare lentă) rulează-le asincron: trimiți un job, primești imediat un id, apoi îl interoghezi sau primești rezultatul pe un webhook. Trimite parametrii obișnuiți ai operației plus type (fetch/extract/batch/search) și un webhook_url opțional.

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"

Când un job se termină trimitem un POST cu {"id","status","result"} către webhook_url-ul tău (un URL https public — cele interne/loopback sunt respinse). Joburile rulează până la câteva minute; fiecare este contorizat exact ca apelul sincron.

Verificarea webhookului. Fiecare livrare poartă un antet X-Scrapeland-Signature de forma t=<unix>,v1=<hex>, unde hexul este HMAC-SHA256("<t>.<raw body>") cu cheia secretului tău de webhook. Semnează octeții BRUȚI ai corpului (nu o copie reserializată), compară în timp constant și respinge orice are un t mai vechi de câteva minute — asta oprește o livrare capturată să fie reluată împotriva ta mai târziu. Cere secretul de la suport dacă folosești 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 cacheabil, ca să poți detecta din cod ce acceptă o instalare înainte de a trimite o cerere: formats și features disponibile, dacă render (și subopțiunile) și extragerea cu AI sunt pornite, models și extract_types ale AI-ului, endpoints-urile active și limits. Ambele SDK-uri îl expun ca capabilities().

Funcționează și fără cheie, dar trimite-ți X-Api-Key și răspunsul este adaptat contului tău. Extragerea cu AI depinde de plan (vezi Extragere cu AI), deci un apel fără cheie îți poate spune doar ce acceptă instalarea, în timp ce unul cu cheie îți spune ce poți apela tu: ai.enabled este atunci adevărat doar dacă planul tău îl include, iar extract_type/extract_types dispar odată cu el. ai.configured raportează instalarea, ai.min_plan numește planul de care ai avea nevoie, iar ai.plan_scoped îți spune dacă răspunsul a fost personalizat. Detectează pe ai.enabled și nu vei primi niciodată un da urmat de 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"}
}

Dimensiunea răspunsului. limits.max_response_bytes este cel mai mare răspuns unic pe care îl returnăm și depinde de planul tău (Free și Starter 2 MB, Pay As You Go și Growth 5 MB, Scale 10 MB, Business 25 MB). Paginile reale sunt departe de această valoare — răspunsul median pe care îl servim are cam 40 KB, iar 99% sunt sub 1,2 MB — deci prinde doar un video, o arhivă sau un export de date. Un răspuns peste limită este refuzat în întregime, cu un 413 care numește dimensiunea, limita și planul tău; nu îți dăm niciodată o pagină trunchiată, pentru că o pagină care și-a pierdut coada în tăcere se facturează ca reușită și se parsează ca date corupte. limits.max_response_bytes_scope îți spune al cui număr ai primit: "key" când ai trimis un X-Api-Key (limita planului tău) sau "lowest_plan" când nu ai trimis — pragul minim pe care îl primește orice cont, deci mereu sigur pentru dimensionare, iar prezentarea unei chei nu poate decât să îl ridice.

GET/v1/account

Vezi din cod planul cheii tale, cota rămasă, creditul preplătit, rate limit-ul și bugetul per cheie (de ex. înainte de un job mare), în loc să deschizi panoul. Raportează întotdeauna doar contul tău. Ambele SDK-uri îl expun ca 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}
}

Aceeași cerere, în fiecare limbaj

O cerere, transpusă idiomatic. Fiecare face același lucru: un POST HTTP către https://scrape.land/v1/extract cu antetul X-Api-Key și corpul JSON, apoi parsează răspunsul 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 și migrarea de la Zyte

Clientul nostru Python înglobează atât API-ul de extragere, cât și tunelul brut, și include un adaptor drop-in pentru clientul Zyte API. Ești deja pe Zyte? Schimbă importul și cheia — formele cererii și ale răspunsului se potrivesc.

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 pentru Zyte: from scrapeland.zyte import ZyteAPIurl, httpResponseBody (base64), httpResponseHeaders, customHttpRequestHeaders, geolocation→țara de ieșire, sessionContext→sesiune persistentă și iter()/AsyncZyteAPI se mapează toate. browserHtml/screenshot/auto-extragerea nu sunt disponibile prin tunelul brut — folosește în schimb API-ul de extragere cu render.

Tunel brut (avansat)

Majoritatea oamenilor vor date structurate și ar trebui să folosească API-ul de extragere de mai sus. Dacă în schimb ai nevoie chiar tu de acces HTTP brut la pagină, tunelul brut este un forward proxy: îndreaptă orice client HTTP spre el cu cheia ta API pe post de nume de utilizator, și fiecare cerere iese dintr-un IP nou, cu controale opționale de țară, sesiune și protocol. Gazdă: 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
Port blocat? Același proxy răspunde și pe gateway.scrape.land:443, pe care rețelele corporative, universitare și de hotel îl filtrează rar. Comportament și facturare identice — diferă doar portul. Păstrează schema http://: cheia ta călătorește tot în numele de utilizator, exact ca pe 8080.
Fiecare cerere se rotește spre un IP de ieșire nou dacă nu setezi o sesiune persistentă. Browserele nu pot seta un proxy per cerere, deci apelează tunelul dintr-un backend.

Parametri de control

Adaugă perechi -nume-valoare la cheia din numele de utilizator, sau trimite anteturi de control X-Proxy-*.

ScopForma din numele de utilizatorAntet
Țara de ieșireKEY-country-usX-Proxy-Country: us
Sesiune persistentă (același IP)KEY-session-abc123X-Proxy-Session: abc123
ProtocolKEY-protocol-socks5X-Proxy-Protocol: socks5
Anonimat (premium)KEY-anonymity-eliteX-Proxy-Anonymity: elite
Latență maximă (ms)KEY-maxlatency-3000X-Proxy-Max-Latency: 3000

Generator de URL pentru tunel

Alege-ți opțiunile și copiază comanda gata de rulat. Înlocuiește YOURKEY cu o cheie din panoul tău.

Folosirea tunelului din cod

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 { 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"}})

Erori

StatusSemnificație
401Cheie API lipsă sau invalidă pe API-ul de extragere. Setează X-Api-Key.
407Cheie API lipsă sau invalidă pe tunelul brut. Setează-o ca nume de utilizator al tunelului.
402Cotă epuizată. Reîncarcă-ți soldul sau treci pe un plan superior.
429Rate limit-ul planului tău a fost depășit (vezi Limitele planului). Un antet Retry-After spune când să reîncerci.
403Destinație nepermisă (blocată de garda SSRF/de destinație) sau extragere cu AI cerută pe un plan sub Scale. Mesajul spune care dintre ele.
413Corpul cererii, sau răspunsul, depășește limita de dimensiune a planului tău. Mesajul numește dimensiunea, limita și planul; vezi limits.max_response_bytes.
502Niciun upstream funcțional nu s-a potrivit cu filtrele tale (încearcă mai puține constrângeri).
503Suntem la capacitate pentru moment (cel mai des toate sloturile de browser sunt ocupate cu un render). Reîncearcă după antetul Retry-After — nu este o cerere eșuată și nu se facturează.
Orice refuz cauzat de planul tău răspunde la fel, ca să scrii o singură ramură în loc să potrivești propoziții în engleză. Pe API-ul de extragere, corpul JSON poartă un code stabil, planul pe care ești și ce ai de făcut:
{
  "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)"
}
Codurile sunt plan-upgrade-required (403), quota-exhausted (402), key-budget-exhausted (402), rate-limited (429), response-too-large (413) și email-unverified (403 — cererile gratuite sunt reținute până la confirmarea adresei de email a contului; planurile plătite nu întâlnesc niciodată acest cod). required_plan apare doar când o achiziție chiar ridică limita — plafonul de la format=raw, de pildă, este același pe orice plan, deci acolo este omis, în loc să fluture un upgrade care nu schimbă nimic. fallback este mereu prezent. Tunelul brut este un proxy HTTP și răspunde în text/plain (un corp JSON ar ajunge în interiorul tunelului tău), deci poartă același cod în antetul de răspuns X-Scrapeland-Error. Același vocabular pe ambele suprafețe — entitlements.refusal_codes din GET /v1/capabilities le enumeră.

Antetul de răspuns X-Proxy-Country raportează țara de ieșire folosită. Statusurile site-ului țintă (200/403/404 ale site-ului însuși) sunt transmise neschimbate.

Limitele planului

Patru lucruri se schimbă odată cu planul: cât de repede ai voie să trimiți cereri, cât de mare poate fi un singur răspuns, dacă extragerea cu AI este disponibilă și dacă primești ieșiri prioritare. Se aplică identic pe API-ul de extragere și pe tunelul brut. GET /v1/capabilities cu cheia ta returnează drepturile tale sub entitlements, iar GET /v1/account cifrele tale.

Ieșirile prioritare sunt o diferență de rutare, nu o coadă — nu există niciun rând în fața căruia să stai. Pe un plan plătit, cererii tale i se oferă întâi ieșirile ISP/datacenter premium, apoi o rezervă mai adâncă de proxy-uri din pool verificate constant, iar la final o ultimă încercare de livrare, ca o cerere plătită să nu eșueze din lipsă de ieșire. Pe planul Free fiecare cerere este servită dintr-un pool public cu latență mare, exact la ce e bun nivelul gratuit: prototipare și testare. Aceeași rotație, țintire geografică, sesiuni și randare, pe adrese care sunt mai lente și mai des blocate de țintele dificile. Măsurat pe traficul nostru din producție, pool-ul gratuit a avut o mediană de 1.015 ms față de 453 ms la premium — și o coadă lungă (p99 32 s față de 4,5 s) pe care un pool public o va avea mereu. Sunt observații asupra unui pool volatil, al unor terți, nu un nivel de serviciu la care îl obligăm.
PlanIncluse / lunăRate limitRăspuns maximIeșiri prioritareExtragere cu AI
Free1.0005 cereri/s2 MBnu — pool cu latență marenu
Starter — $99600.00050 cereri/s2 MBdanu
Growth — $2491.600.000100 cereri/s5 MBdanu
Scale — $4993.400.000200 cereri/s10 MBdada
Business — $9997.200.0001.000 cereri/s25 MBdada
Business XL — $1,99915.000.0001.000 cereri/s25 MBdada
Business XXL — $3,49930.000.0001.000 cereri/s25 MBdada
Business Ultra — $9,999100.000.0001.000 cereri/s25 MBdada
Pay as you goVine în 2027. Nu se vinde ca plan astăzi — începe pe Free sau alege un abonament. Reîncărcările preplătite sunt deja disponibile pe orice plan.

Rate limit. Cereri pe secundă susținute, aplicate per cont pe toate cheile pe care le deții (o limită per cheie n-ar fi deloc o limită — cheile se creează gratis); o cheie individuală poate fi limitată și mai jos. Rafalele scurte peste limită sunt tolerate. Peste ea primești un 429 cu un antet Retry-After — încetinește și continuă, nu se pierde nimic și nu se facturează nimic. Rata fiecărui plan este dimensionată astfel încât o rulare la maximum pe toată alocația lunară să dureze ore, nu minute.

Răspuns maxim. Cel mai mare răspuns unic pe care îl returnăm, expus ca limits.max_response_bytes în GET /v1/capabilities. Paginile reale sunt departe de această valoare — răspunsul median pe care îl servim are cam 40 KB, iar 99% sunt sub 1,2 MB — deci prinde doar un video, o arhivă sau un export de date. Orice depășește este refuzat în întregime, cu un 413 care numește dimensiunea, limita și planul tău. Nu returnăm niciodată o pagină trunchiată, iar un răspuns refuzat nu se facturează.

Facturarea nu depășește niciodată ce ai plătit. Cererile incluse se consumă primele. După ele, cererile trag din creditul preplătit, la $0,20 / 1.000 — soldul scade și se oprește la zero, moment în care cererile returnează 402 în loc să continue. Nu există factură ulterioară și nicio cale de a acumula o cheltuială pe care nu ai autorizat-o dinainte.

Ghiduri

Sesiuni rotative vs. persistente

Implicit, fiecare cerere iese dintr-un IP nou (excelent pentru a distribui încărcarea și a evita rate limit-urile). Când ai nevoie de același IP între cereri (autentificări, coșuri, fluxuri în mai mulți pași) adaugă un parametru session (sau -session-NUME pe tunel); fixăm acea sesiune pe un singur IP de ieșire pentru vreo 10 minute, apoi se rotește. Folosește un nume de sesiune unic pentru fiecare utilizator logic.

Gestionarea rate limit-urilor și a erorilor

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

Întrebări frecvente

Există un plan gratuit?

Da — 1.000 de cereri, fără card. Planurile plătite încep de la $99/lună (600k cereri = $0,165 / 1k), iar tariful se îmbunătățește pe măsură ce crește nivelul: $0,139 / 1k pe Business ($999 / 7,2M) și $0,100 / 1k pe Business Ultra ($9.999 / 100M). Vezi Limitele planului pentru scara completă.

Cum sunt facturat?

Per 1.000 de cereri livrate. Dacă un proxy este blocat sau returnează 403/429, reîncercăm cu alt IP și nu îl facturăm niciodată. Plătești doar când pagina revine.

Cererile incluse în planul tău se consumă primele. După ele, cererile ies din creditul preplătit, la $0,20 / 1.000 — reîncarci în avans, soldul scade și se oprește la zero. Nu există factură ulterioară și nicio cale de a acumula o notă pe care nu ai autorizat-o. Fără abonament, pay-as-you-go este $0,1165 / 1.000, cel mai ieftin tarif pe care îl vindem.

Pot alege o anumită țară?

Da. Pe API-ul de extragere adaugă "country": "us" în corpul cererii. Pe tunelul brut adaugă -country-us (orice cod ISO) la cheia ta. Vezi generatorul de URL pentru tunel.

Cum extrag pagini care au nevoie de JavaScript?

Adaugă "render": true la cererea ta de extragere și "wait_for" cu un selector CSS dacă tot conținutul se încarcă târziu. Vezi render și wait_for.

Oferiți IP-uri rezidențiale?

Astăzi pool-ul este format din IP-uri rotative de tip datacenter/ISP, autovalidate continuu. Pool-urile rezidențiale/mobile sunt pe foaia de parcurs (planuri premium).

Acestea sunt instrumente standard; tu ești responsabil să le folosești în mod legal și să respecți termenii site-urilor de destinație. Vezi Termenii noștri.