Skip to content
Docs menu

Clients

JavaScript client

@pageteer/client is a small TypeScript client for the whole API with no runtime dependencies. It runs on Node 18+, Bun, Deno, edge runtimes and in browsers (server-side keys only), using the global fetch. It ships as ESM with type definitions.

Install

The package is not on npm yet. Install it from the Pageteer repository (clients/js); installing builds it:

shell
npm install /path/to/pageful/clients/js
# or in package.json: "@pageteer/client": "file:../pageful/clients/js"

Create a client

TypeScript
import { Pageteer, PageteerError } from '@pageteer/client';

const pageteer = new Pageteer({
  apiKey: process.env.PAGETEER_API_KEY, // default: PAGETEER_API_KEY
  baseUrl: 'https://api.104-236-84-15.sslip.io', // default: PAGETEER_BASE_URL, then https://api.pageteer.com ("/v1" is added)
  timeoutMs: 150_000, // per attempt
  maxRetries: 2, // 429 (honoring Retry-After), 5xx and network errors
});
Client options
NameTypeDescription
apiKeystringpk_live_… key, or a free pk_test_… test key. Without one (and no env var) the constructor throws missing_api_key.
baseUrlstringAPI origin, with or without /v1.
timeoutMsnumberPer-attempt timeout (above the API’s 120 s maximum). Default: 150000.
maxRetriesnumberRetries on 429, 5xx and network errors. Default: 2.
maxRetryAfterMsnumberLongest Retry-After the client sleeps through; a longer one is thrown at once. Default: 60000.
retryBaseDelayMs / retryMaxDelayMsnumberJittered exponential backoff. Default: 500 / 8000.
fetchtypeof fetchCustom fetch implementation.
headersRecord<string, string>Headers sent with every request.

Every method also takes request options as its last argument: { signal, timeoutMs, maxRetries, headers }.

TypeScript
const r = await pageteer.search('best pizza minneapolis', { n: 5, country: 'us' });
for (const hit of r.results) console.log(hit.position, hit.title, hit.url);
console.log(r.meta.creditsUsed, r.meta.creditsRemaining, r.meta.cache); // 2, 964, "MISS"

Options: n, engines (an array or a comma list), country, language, location, page, max_age. See Search.

Render, screenshot, PDF

TypeScript
const page = await pageteer.render('https://example.com', {
  formats: ['markdown', 'links', 'metadata'],
  mode: 'auto', // http | browser | auto
  unblock: 'auto', // none | stealth | residential | auto
  max_age: 3600, // accept a cached copy up to 1 h old (0 credits)
});
console.log(page.status, page.title, page.markdown, page.tier_used, page.credits);

// Binary by default: { data: Uint8Array, contentType, tierUsed, targetStatus, meta }
const shot = await pageteer.screenshot('https://example.com', { full_page: true });
await fs.promises.writeFile('shot.png', shot.data);

// Or JSON with base64: { screenshot_base64, content_type, clipped, credits, … }
const json = await pageteer.screenshot('https://example.com', { as: 'json' });
const bytes = Pageteer.decodeBase64(json.screenshot_base64!);

// A phone, one element, WebP, cookie banner hidden, dark theme (DEVICE_NAMES lists the presets)
const card = await pageteer.screenshot('https://example.com/pricing', {
  device: 'iphone-15', selector: '#plans', format: 'webp',
  hide_selectors: ['#onetrust-banner-sdk'], dark_mode: true,
});
// Full page, cut at 8,000 CSS px: shot.clipped says whether the page was taller
const tall = await pageteer.screenshot('https://example.com', { full_page: true, max_height: 8000 });

const pdf = await pageteer.pdf('https://example.com', { format: 'Letter', print_background: true });

Extract

TypeScript
const { data, status } = await pageteer.extract<{ title: string; links: string[] }>({
  url: 'https://example.com',
  selectors: { title: 'h1', links: { selector: 'a', attr: 'href', all: true } },
});

await pageteer.extract({ url, structured: true }); // JSON-LD, microdata, OpenGraph, records; no LLM
await pageteer.extract({ url, schema: { type: 'object', properties: { … } } }); // LLM, +40 credits

// Templates: markup first, the LLM only for missing required fields; data is typed (ProductData, …)
const product = await pageteer.extract({ url, template: 'product' });
console.log(product.data.price, product.sources, product.llm_used);
const { templates } = await pageteer.extractTemplates(); // fields + JSON Schemas

Map

TypeScript
// A site's URLs from its sitemaps and the start page's links, without fetching them (1 credit)
const { urls, truncated } = await pageteer.map('https://example.com', { search: 'blog', limit: 500 });
urls.forEach(u => console.log(u.url, u.source, u.lastmod));

Crawl

TypeScript
// Start, then wait (polls with backoff until done, failed or canceled)
const { job_id } = await pageteer.crawl.start({ url: 'https://example.com', max_pages: 20, include: ['/blog/**'] });
const job = await pageteer.crawl.wait(job_id, {
  intervalMs: 1000, maxIntervalMs: 10_000, timeoutMs: 600_000,
  onPoll: s => console.log(s.status, s.progress),
});

// Or both in one call
const done = await pageteer.crawl.run({ url: 'https://example.com', max_pages: 5 });
done.pages.forEach(p => console.log(p.url, p.title, p.markdown?.length));

await pageteer.crawl.get(job_id);
await pageteer.crawl.cancel(job_id);

// Export a finished crawl (free): markdown | llms.txt | jsonl as text, zip as bytes
const { text: llms } = await pageteer.crawl.export(job_id, 'llms.txt');
const zip = await pageteer.crawl.export(job_id, 'zip');
await fs.promises.writeFile(zip.filename ?? 'site.zip', zip.data);

wait throws PageteerError with code wait_timeout after timeoutMs (default: no limit). It returns as soon as the job is final, so check job.status and job.error.

Batch

TypeScript
const b = await pageteer.batch.create(
  { urls: ['https://a.example', 'https://b.example'], formats: ['markdown'] },
  { idempotencyKey: 'nightly-2026-10-02' }, // optional: a random key is sent by default
);
if (b.replayed) console.log('this batch already existed');

await pageteer.batch.wait(b.job_id);

// Every result, following the cursor for you
for await (const item of pageteer.batch.iterResults(b.job_id, { status: 'succeeded' })) {
  console.log(item.url, item.result?.markdown);
}

const page = await pageteer.batch.results(b.job_id, { limit: 100, after: -1 }); // { items, next }
const csv = await pageteer.batch.export(b.job_id, 'csv'); // { text, meta }; or 'jsonl'
await pageteer.batch.list({ limit: 20 });
await pageteer.batch.cancel(b.job_id);

Monitors

TypeScript
const m = await pageteer.monitors.create({
  url: 'https://example.com/pricing',
  interval: 'daily',
  selector: '#plans', // or selectors: { price: '.price' } for a field diff
  ignore: ['Updated * ago'],
  webhook: { url: 'https://your-app.example.com/hooks/pageteer' }, // m.webhook_secret is shown once
});

const now = await pageteer.monitors.check(m.id); // { baseline, changed, change, credits, monitor }
for await (const change of pageteer.monitors.iterChanges(m.id)) console.log(change.at, change.diff);
await pageteer.monitors.pause(m.id); // resume, update, list, get, delete

monitors.create and monitors.check are not retried on a 5xx, so a lost response never creates a second monitor or charges a second check. See Monitors.

Usage

TypeScript
const u = await pageteer.usage();
// { period, plan, granted, used, remaining, overage_cap, billable, spend_limit_cents, estimated_cost_cents, resets_at, meta }

// Broken down per key, project, UTC day or endpoint (up to 400 days; default this month)
const byKey = await pageteer.usage({ group: 'key', from: '2026-09-01', to: '2026-09-30' });
for (const g of byKey.groups ?? []) console.log(g.key_name, g.project, g.totals.requests, g.totals.credits);

group is 'key', 'project', 'day' or 'endpoint'; see Usage for the fields. Request options can go in the same object.

Test keys

With a test key every call is free and returns a deterministic fake. The client tells you which kind of key and response you have:

TypeScript
import { Pageteer, isTestKey } from '@pageteer/client';

isTestKey('pk_test_…'); // true (prefix check only)
const pageteer = new Pageteer({ apiKey: process.env.PAGETEER_TEST_KEY });
pageteer.testMode; // true for a pk_test_ key

const page = await pageteer.render('https://example.com');
page.test; // true in JSON bodies of test responses
page.meta.test; // true from the x-pageteer-test header (binary captures too)

Proxy test

Check your team's own proxy (free). Responses served through it carry proxy_source: "customer".

TypeScript
const t = await pageteer.proxyTest({ country: 'de' }); // POST /v1/proxy/test
// { ok, ip, country, ms, error: { code, reason, message } | null, meta }
// No team proxy: throws PageteerError with code proxy_not_configured (404)

Response metadata

Every JSON result has a meta field built from the response headers (binary captures carry it too):

TypeScript
interface ResponseMeta {
  status: number;
  requestId: string | null; // x-request-id
  creditsUsed: number | null; // x-credits-used
  creditsRemaining: number | null; // x-credits-remaining
  cache: string | null; // x-cache: HIT | MISS | REVALIDATED
  cacheAgeS: number | null; // x-cache-age
  rateLimitRemaining: number | null;
  rateLimitLimit: number | null;
  retries: number; // retries this call needed
  test: boolean; // x-pageteer-test: made with a test key
}

Errors and retries

API errors and client failures are thrown as PageteerError:

TypeScript
try {
  await pageteer.render(url);
} catch (err) {
  if (!(err instanceof PageteerError)) throw err;
  err.status; // HTTP status; 0 for client-side failures
  err.code; // error.code, e.g. captcha_required, or a client code below
  err.message;
  err.requestId; // x-request-id
  err.retryAfter; // seconds, from Retry-After or error.retry_after
  err.details; // the rest of the error body (issues, provider, final_url, …)
  err.proxyFailureReason; // proxy_failed: auth | unreachable | status | blocked; else null
  err.reason; // 402: free_credits_used | payment_failed | subscription_inactive | spend_limit (open-ended)
  err.billingUrl; // 402: the Billing page that fixes it
  err.creditsNeeded; // 402: the request's cost, when known; else null
  err.creditsRemaining; // 402: credits left, when known; else null
}
  • Client codes (status 0): missing_api_key, network_error, client_timeout, aborted, wait_timeout; plus invalid_response for an unreadable body and http_<status> for a non-JSON error.
  • 429 is retried after Retry-After plus jitter (thrown at once if that is longer than maxRetryAfterMs). 5xx and network errors are retried with jittered exponential backoff.
  • Never retried: other 4xx (including 402, whose message ends with the billing URL, and proxy_not_configured), proxy_failed with reason auth or blocked, client timeouts and aborts.
  • crawl.start and batch.create send a random Idempotency-Key (or yours: { idempotencyKey }), so they are retried on 5xx too: a replay returns the original job. With idempotencyKey: null no key is sent and a 5xx is not retried, because it might already have created the job.

The full list of API error codes, and what to do about each, is in the errors reference.