Skip to content
Docs menu

Browser API

Extract

Turn a page into structured data: what it already embeds, a ready-made template, CSS selectors, or a JSON Schema.

POST https://api.104-236-84-15.sslip.io/v1/extract

Body parameters

Extract parameters
NameTypeDescription
urlrequiredstring (≤ 2048 chars)Absolute http:// or https:// URL. No embedded credentials, no private or internal hosts.
structuredtrueReturn the data the page embeds (JSON-LD, microdata, RDFa, OpenGraph, app state such as __NEXT_DATA__) and normalized listing, business and product records. No LLM, render cost.
selectorsobject (1–100 fields)Map of field name → CSS selector, or → { selector, attr?, all?, html? }. Deterministic; see With selectors.
template"lead" | "product" | "article" | "job"A ready-made extraction. Fields come from the page's markup first; the LLM runs only when a required field is missing (+40 credits, only when it ran). See Templates.
llm_fallbackbooleanWith template: false never calls the LLM (markup only, never charged for it). Without LLM extraction in your plan the template runs markup-only anyway. Default: true.
schemaJSON SchemaDescribe the data you want and an LLM fills it from the page (+40 credits). Pay-as-you-go and invoiced modes only; may be turned off while in preview.
mode"auto" | "http" | "browser"As for render. http is cheapest and is usually enough for structured and selectors. Default: "auto".
unblock"none" | "stealth" | "residential" | "auto"Unblocking tier. residential goes through your team's own proxy. auto escalates none → stealth → residential (when your team has a proxy) and charges only for the tier that succeeded. See Unblocking tiers. Default: "none".
wait_forstring (≤ 500 chars)CSS selector to wait for before capturing. Implies a browser render.
markdown_mode"main" | "full"With schema or template: the page text the LLM (and the article template's body) reads. full keeps listing grids that main can drop. Default: "main".
timeout_msinteger (1000–120000)Time budget for the whole request, shared by every unblock tier tried. Running out returns 504 timeout (not charged). Default: 30000.
max_ageinteger (seconds, 0–86400)Opt-in cache. If your account made the same request (same URL and options) within max_age seconds, you get that response back without us contacting the site, for 0 credits. An older copy is revalidated with a conditional request when the site supports it. 0 always fetches but stores the result. Omit it and the cache is not used. See Render → Caching.
respect_robotsbooleanCheck the site’s robots.txt (user agent PageteerBot) first. A disallowed URL fails with 403 robots_disallowed and is not charged; a Crawl-delay spaces our requests to that site. Default: false.
actionsobject[] (≤ 20)Read-only page actions run in the browser after the page loads: wait, wait_for, scroll, click ("Load more", tabs, accordions), press and screenshot. Forces a browser render. No typing, form submission or logins. See Page actions.

Send exactly one of structured, template, selectors or schema (template and schema together are a 400). For leads, products, articles and job postings start with a template: it reads the page's schema.org markup, which is exact and costs no more than the fetch, and pays for the LLM only when that markup is missing.

Embedded structured data

curl
curl -X POST https://api.104-236-84-15.sslip.io/v1/extract \
  -H "Authorization: Bearer $PAGETEER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://shop.example/p/trail-runner-3","structured":true,"mode":"http"}'
JavaScript (fetch)
const res = await fetch('https://api.104-236-84-15.sslip.io/v1/extract', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.PAGETEER_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "url": "https://shop.example/p/trail-runner-3",
    "structured": true,
    "mode": "http"
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error.message);
console.log(data, res.headers.get('x-credits-remaining'));
Python (requests)
import os
import requests

headers = {"Authorization": f"Bearer {os.environ['PAGETEER_API_KEY']}"}
res = requests.post(
    "https://api.104-236-84-15.sslip.io/v1/extract",
    headers=headers,
    json={
        "url": "https://shop.example/p/trail-runner-3",
        "structured": True,
        "mode": "http",
    },
    timeout=120,
)
res.raise_for_status()
print(res.json(), res.headers["x-credits-remaining"])
200 OK (abridged)
{
  "data": {
    "records": [
      { "kind": "product", "type": "Product", "source": "json_ld", "name": "Trail Runner 3",
        "price": 129.99, "currency": "USD", "availability": "InStock", "low_price": 129.99,
        "high_price": 139.99, "offer_count": 2, "sku": "TR3-BLK-42", "gtin": "0123456789012",
        "brand": "Outdoor Co.", "rating": { "value": 4.6, "count": 212 }, … }
    ],
    "json_ld": [ … ], "microdata": [ … ], "rdfa": [], "opengraph": { … }, "twitter": {},
    "app_state": [ … ], "truncated": false, "errors": []
  },
  "url": "https://shop.example/p/trail-runner-3",
  "final_url": "https://shop.example/p/trail-runner-3",
  "status": 200,
  "title": "Trail Runner 3 – Outdoor Co.",
  "tier_used": "none",
  "mode_used": "http",
  "attempts": [{ "tier": "none", "status": 200, "ms": 310 }],
  "cached": false,
  "credits": 1
}

data has the same shape as the render structured format; see Structured data for every field.

Templates

A template returns the same fields for every page of its kind. Each field is read, in order, from JSON-LD, then microdata, then OpenGraph and meta tags, then the page itself (mailto: and tel: links, social and contact links, headings, <time>, the page Markdown). Only if a required field is still empty does the LLM run, once, for the empty fields; it never replaces a value the markup gave. Fields nothing supplied are null (or []).

Extraction templates
templateRequired (LLM if missing)Fields
lead
v1
namename, description, website, logo, emails, phones, address, social_profiles, contact_page_url, people
product
v1
name, pricename, brand, description, url, price, currency, availability, sku, gtin, mpn, images, rating, review_count
article
v1
title, body_markdowntitle, description, author, publisher, published_at, updated_at, body_markdown, images, canonical_url, keywords
job
v1
title, companytitle, company, company_url, location, remote, employment_types, salary, posted_at, valid_through, apply_url, description

GET /v1/extract/templates returns each template's fields and its JSON Schema (draft 2020-12). A template's version changes only when its output changes incompatibly; new fields can be added within a version.

curl
curl -X POST https://api.104-236-84-15.sslip.io/v1/extract \
  -H "Authorization: Bearer $PAGETEER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://shop.example/p/trail-runner-3","template":"product","mode":"http"}'
JavaScript (fetch)
const res = await fetch('https://api.104-236-84-15.sslip.io/v1/extract', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.PAGETEER_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "url": "https://shop.example/p/trail-runner-3",
    "template": "product",
    "mode": "http"
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error.message);
console.log(data, res.headers.get('x-credits-remaining'));
Python (requests)
import os
import requests

headers = {"Authorization": f"Bearer {os.environ['PAGETEER_API_KEY']}"}
res = requests.post(
    "https://api.104-236-84-15.sslip.io/v1/extract",
    headers=headers,
    json={
        "url": "https://shop.example/p/trail-runner-3",
        "template": "product",
        "mode": "http",
    },
    timeout=120,
)
res.raise_for_status()
print(res.json(), res.headers["x-credits-remaining"])
200 OK (abridged)
{
  "data": {
    "name": "Trail Runner 3", "brand": "Outdoor Co.", "price": 129.99, "currency": "USD",
    "availability": "in_stock", "sku": "TR3-BLK-42", "gtin": "0123456789012", "mpn": null,
    "images": ["https://shop.example/img/tr3.jpg"], "rating": 4.6, "review_count": 212, …
  },
  "template": { "id": "product", "version": 1 },
  "method": "deterministic",
  "sources": { "name": "json_ld", "price": "json_ld", "images": "opengraph", "url": "meta", … },
  "missing": [],
  "llm_used": false,
  "status": 200,
  "mode_used": "http",
  "credits": 1,
  …
}
  • sources says where each field came from: json_ld, microdata, opengraph, meta, html, markdown, page (the URL itself) or llm.
  • method is deterministic (no LLM value), hybrid (markup plus LLM) or llm. llm_used is true when the LLM ran, and only then are its credits in credits.
  • missing lists required fields that are still empty. When the LLM could have filled them but did not run, llm_skipped says why: disabled (llm_fallback: false), not_in_plan, unavailable or target_status (the site answered with a 4xx, so the LLM is not paid for).

With selectors

curl
curl -X POST https://api.104-236-84-15.sslip.io/v1/extract \
  -H "Authorization: Bearer $PAGETEER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://www.iana.org/domains/reserved","mode":"http","selectors":{"title":"h1","sections":{"selector":"h2","all":true},"rfc_links":{"selector":"a[href*=rfc]","attr":"href","all":true}}}'
JavaScript (fetch)
const res = await fetch('https://api.104-236-84-15.sslip.io/v1/extract', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.PAGETEER_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "url": "https://www.iana.org/domains/reserved",
    "mode": "http",
    "selectors": {
      "title": "h1",
      "sections": {
        "selector": "h2",
        "all": true
      },
      "rfc_links": {
        "selector": "a[href*=rfc]",
        "attr": "href",
        "all": true
      }
    }
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error.message);
console.log(data, res.headers.get('x-credits-remaining'));
Python (requests)
import os
import requests

headers = {"Authorization": f"Bearer {os.environ['PAGETEER_API_KEY']}"}
res = requests.post(
    "https://api.104-236-84-15.sslip.io/v1/extract",
    headers=headers,
    json={
        "url": "https://www.iana.org/domains/reserved",
        "mode": "http",
        "selectors": {
            "title": "h1",
            "sections": {
                "selector": "h2",
                "all": True,
            },
            "rfc_links": {
                "selector": "a[href*=rfc]",
                "attr": "href",
                "all": True,
            },
        },
    },
    timeout=120,
)
res.raise_for_status()
print(res.json(), res.headers["x-credits-remaining"])
200 OK
{
  "data": {
    "title": "IANA-managed Reserved Domains",
    "sections": ["Example domains", "Test IDN top-level domains", "Policy-reserved domains", "Other Special-Use Domains", "Domain Names"],
    "rfc_links": ["https://www.iana.org/go/rfc2606", "https://www.iana.org/go/rfc6761", "https://www.iana.org/go/rfc6761"]
  },
  "url": "https://www.iana.org/domains/reserved",
  "final_url": "https://www.iana.org/domains/reserved",
  "status": 200,
  "title": "IANA-managed Reserved Domains",
  "tier_used": "none",
  "mode_used": "http",
  "attempts": [{ "tier": "none", "status": 200, "ms": 249 }],
  "cached": false,
  "credits": 1
}
  • A string selector returns the trimmed text of the first match (script, style and noscript text inside it is left out).
  • {selector, all: true} returns an array of every match; {selector, attr: "href"} returns an attribute instead of text (href and src are resolved to absolute URLs against final_url); {selector, html: true} returns inner HTML. They combine.
  • A selector that matches nothing gives null (or [] with all). Selecting a script element itself (for example a JSON-LD block) returns its contents. An invalid CSS selector is a 400.

Response fields

Every extract response carries url (what you asked for), final_url (after redirects), the target's status, the page title, tier_used, attempts and credits. Check status and final_url before trusting data: a 404 page or a redirect to a sign-in page still yields data, just not the data you wanted. If the site answers with a 5xx, you get 200 with that status and credits: 0, like render; with schema or template the LLM is not run on the error page and data is null. mode_used says whether the page was fetched over HTTP or in a browser.

With a JSON Schema

curl
curl -X POST https://api.104-236-84-15.sslip.io/v1/extract \
  -H "Authorization: Bearer $PAGETEER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/product/42","schema":{"type":"object","properties":{"name":{"type":"string"},"price":{"type":"number"},"in_stock":{"type":"boolean"}}}}'
JavaScript (fetch)
const res = await fetch('https://api.104-236-84-15.sslip.io/v1/extract', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.PAGETEER_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "url": "https://example.com/product/42",
    "schema": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        },
        "price": {
          "type": "number"
        },
        "in_stock": {
          "type": "boolean"
        }
      }
    }
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error.message);
console.log(data, res.headers.get('x-credits-remaining'));
Python (requests)
import os
import requests

headers = {"Authorization": f"Bearer {os.environ['PAGETEER_API_KEY']}"}
res = requests.post(
    "https://api.104-236-84-15.sslip.io/v1/extract",
    headers=headers,
    json={
        "url": "https://example.com/product/42",
        "schema": {
            "type": "object",
            "properties": {
                "name": {
                    "type": "string",
                },
                "price": {
                    "type": "number",
                },
                "in_stock": {
                    "type": "boolean",
                },
            },
        },
    },
    timeout=120,
)
res.raise_for_status()
print(res.json(), res.headers["x-credits-remaining"])
200 OK (abridged)
{
  "data": { "name": "Example Widget", "price": 19, "in_stock": true },
  "url": "https://example.com/product/42",
  "final_url": "https://example.com/product/42",
  "status": 200,
  "title": "Example Widget",
  "tier_used": "none",
  "mode_used": "browser",
  "attempts": [{ "tier": "none", "status": 200, "ms": 1840 }],
  "cached": false,
  "credits": 45
}

The LLM reads the page title and Markdown (markdown_mode decides how much). Very long pages are cut to fit, and the response then has input_truncated: true. A failed extraction is 502 internal_error and is not charged.

LLM extraction can be wrong. Validate the output before you rely on it.

Credits

The page fetch costs the same as a render (HTTP 1, browser 5, plus any tier add-on). structured and selectors add nothing. Schema extraction adds 40. The schema example in a browser costs 45 credits. A template adds nothing when the markup has every required field and 40 only when the LLM ran (llm_used); the request needs room for that add-on up front, and what was not used is returned right away.

Errors

Extract errors
StatusCodeWhenChargedWhat to do
400invalid_requestA parameter is missing, invalid or unknown (the message and error.issues name it).NoFix the request; retrying it unchanged fails again.
401invalid_api_keyNo key, an unknown key, or a revoked or expired key.NoCheck the Authorization header and the key on the API keys page.
402insufficient_creditsNot enough credits left this month for this request, and billable usage is off. error.reason says why: free_credits_used, payment_failed or subscription_inactive.NoAct on the reason at error.billing_url: add a card, update the payment method, or restart the subscription. Or wait for the 1st (UTC).
402spend_limit_reachedBillable usage is on and this month’s spend limit is reached (error.reason: spend_limit).NoRaise the spend limit at error.billing_url (the Billing page), or wait for the 1st (UTC).
403forbidden_targetThe URL is denylisted, resolves (or redirects) to a private or internal address, or sends cookies or credentials to a login, account or checkout page.NoDo not retry. See the Acceptable Use Policy.
403robots_disallowedrespect_robots is on (default for crawl and batch) and the site’s robots.txt disallows the URL for PageteerBot.NoSkip the URL. Turn respect_robots off only for sites you own or may crawl.
403org_suspendedThe account is suspended.NoContact support.
403feature_not_in_planSchema extraction on the free mode or while it is not available on this deployment.NoUse selectors or structured, or add a card.
403proxy_not_configuredunblock "residential" without a team proxy saved in Settings → Proxy (404 from POST /v1/proxy/test).NoSave your proxy URL in Settings → Proxy (see Bring your own proxy), or use another tier.
413payload_too_largeThe request body is over 64 KB (8 MB for POST /v1/batch), or a crawl is too large to export in one file.NoSend less, or split a batch; read a large crawl with GET /v1/crawl/:id.
422target_blockedThe site blocked every tier we tried (with unblock "auto", the tiers this deployment offers). block_reason, block_vendor and attempts say how.NoTry a stronger unblock tier (or "auto"); if auto already failed, retry much later.
422captcha_requiredThe site answered with a CAPTCHA or bot challenge. We never solve them and try no further tier.NoDo not retry soon. Use the site’s API or data feed if it has one.
422login_requiredThe site redirected to a login or sign-in page (final_url says where). We never log in.NoThe content is not public: skip it.
429rate_limitedOver your requests per minute, or too many requests to one site across the platform right now.NoWait Retry-After seconds, then back off with jitter.
429concurrency_limitToo many of your requests are in flight at once.NoWait for one to finish (Retry-After: 1) or lower your parallelism.
429target_rate_limitedThe site asked us to slow down (429, or 503 with Retry-After), or per-site pacing would not let your request start in time. We pause that site for everyone.NoWait error.retry_after seconds (also in Retry-After).
502proxy_failedYour proxy failed: reason "auth" (it rejected the credentials), "unreachable", "status" (it refused the connection) or "blocked" (its host resolves to a private address).NoFix the URL or credentials in Settings → Proxy and test it. "unreachable" and "status" may pass on retry.
500 / 502 / 503internal_errorSomething failed on our side (rendering engine, LLM extraction or the API itself), or the rendering engine is at capacity (503, Retry-After: 5).NoRetry with backoff (honor Retry-After); quote x-request-id if it persists.
504timeoutThe site did not answer within timeout_ms.NoRetry, maybe with a larger timeout_ms or mode "http".