Skip to content
Docs menu

Getting started

Test keys

A test key (pk_test_…) calls the same API with the same request shapes, but never fetches a real site and never costs credits. Responses are realistic, deterministic fakes, so you can build an integration, wire up an agent or run CI without spending anything or hitting anyone's website.

What a test key does

  • Free. Test requests are never charged: x-credits-used is always 0, nothing counts toward your monthly credits or spend limit, and they keep working when your credits run out (never a 402).
  • No real sites. The request never reaches our browser engine or the target site; the answer is generated from the request itself. Search never queries a search engine. Test results have their own cache, never shared with live requests, and test traffic does not count toward per-site throttling.
  • Same rules. Everything before the fetch runs as with a live key: request validation (unknown fields, limits, 400 invalid_request), blocked and private targets (forbidden_target), plan features, and your team's rate limit and concurrency. A request that a live key would get refused is refused with a test key too, with the same status and error code.
  • Counted separately. Test requests appear in your usage and request log marked as test, with 0 credits, so you can filter them out.

Create one

  1. Open API keys in the dashboard and choose Create key.
  2. Set Mode to Test. Give it a name (and a project, if you group keys).
  3. Copy the key. Like live keys it is shown once; it starts with pk_test_ instead of pk_live_.

Use it exactly like a live key (Authorization: Bearer pk_test_…). To go live, swap the key: nothing else in your code changes.

How to tell a test response

Every response to a test key carries the x-pageteer-test: 1 header, and JSON bodies include "test": true. Binary screenshots and PDFs have the header only.

200 OK (test key)
HTTP/1.1 200 OK
x-pageteer-test: 1
x-credits-used: 0
content-type: application/json; charset=utf-8

{
  "url": "https://example.com/",
  "status": 200,
  "title": "…",
  "markdown": "…",
  "tier_used": "none",
  "credits": 0,
  "test": true,
  …
}

What each endpoint returns

Fake results are deterministic: the same request returns the same content every time, and they always match the live response schema, so your parsing code is exercised for real.

Test responses per endpoint
NameTypeDescription
renderPOST /v1/renderA fixture page built around the URL you sent: a title plus the formats you asked for (markdown, html, text, links, metadata…). status 200.
screenshotPOST /v1/screenshotA small valid PNG or JPEG (whichever type you asked for), as bytes or base64 JSON.
pdfPOST /v1/pdfA small valid PDF, as bytes or base64 JSON.
extractPOST /v1/extractselectors: sample strings for each selector. schema: sample values shaped like your schema, with no LLM call. structured: true: sample structured data. template: the template reads the fixture page's markup for real; required fields it lacks get sample values marked llm (llm_used: true), with no LLM call.
searchGET /v1/searchn fake results for your query, ranked and shaped like live ones.
mapPOST /v1/mapA fixed site map: seven fixture URLs on your host (from a fake sitemap, with lastmod), plus fixture links. On a magic URL (/error/…, /status/…) the fake site has no sitemap, so you get that URL’s error.
crawl / batchPOST /v1/crawl, /v1/batchJobs complete quickly with fixture pages. Batch webhooks are delivered to your endpoint for real, so you can test your receiver.
monitorsPOST /v1/monitorsChecks fetch fixture pages for free, so nothing changes, except https://test.pageteer.com/monitor/changing, which has a new version on every check (data monitors get a _test_version field). Webhooks are delivered for real.
proxy testPOST /v1/proxy/testok: true with an IP from a documentation range (TEST-NET).

tier_used is the tier you asked for (auto reports none), and unblock: "residential" reports proxy_source: "customer". GET /v1/usage returns your real numbers.

Magic URLs

Point a request at a URL under https://test.pageteer.com to get a specific outcome. Errors use the same status, code and body as live ones (see Errors), so you can test your error handling and retries without waiting for a real site to misbehave. Any host under test.pageteer.com works (subdomains too), so crawls and batches can mix outcomes. Other paths on these hosts, and every other URL, get a normal successful fake. Only test keys get these outcomes; magic hosts skip the DNS check, but every other target check still runs.

Magic test URLs
NameTypeDescription
/status/<code>target statusThe site answers with that HTTP status (100–599), e.g. https://test.pageteer.com/status/404 returns a normal response with status: 404. /status/429 is a 429 target_rate_limited with Retry-After, as live.
/error/timeout504 timeoutThe site did not answer in time.
/error/blocked422 target_blockedThe site blocks every tier tried (with unblock: "auto" you see the escalation in attempts).
/error/captcha422 captcha_requiredThe site showed a CAPTCHA.
/error/proxy_failed502 proxy_failedThe proxy could not be reached (reason: "unreachable", which clients retry).
selector: "#missing"422 selector_not_foundOn any URL, a screenshot selector containing missing finds no element. Other selectors return a 160×100 image; full_page a 320×600 one, cut (with clipped: true) at a smaller max_height.
/slow/<ms>delay, max 10000A successful response after that many milliseconds (at most 10,000), e.g. https://test.pageteer.com/slow/3000. Longer than the request's timeout_ms, it is a 504 timeout, as live.
curl
curl -X POST https://api.104-236-84-15.sslip.io/v1/render \
  -H "Authorization: Bearer $PAGETEER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://test.pageteer.com/error/captcha","formats":["markdown"]}'
JavaScript (fetch)
const res = await fetch('https://api.104-236-84-15.sslip.io/v1/render', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.PAGETEER_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "url": "https://test.pageteer.com/error/captcha",
    "formats": [
      "markdown"
    ]
  }),
});
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/render",
    headers=headers,
    json={
        "url": "https://test.pageteer.com/error/captcha",
        "formats": ["markdown"],
    },
    timeout=120,
)
res.raise_for_status()
print(res.json(), res.headers["x-credits-remaining"])
422 Unprocessable Content (test key)
{
  "error": {
    "code": "captcha_required",
    "message": "…",
    …
  },
  "test": true
}

Using test keys in CI

Store a test key as a CI secret and run your integration tests against the real API. Calls are free, results are stable across runs, and the magic URLs cover the failure paths:

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

const pageteer = new Pageteer({ apiKey: process.env.PAGETEER_TEST_KEY }); // pk_test_…
if (!pageteer.testMode) throw new Error('CI must use a test key');

it('renders markdown', async () => {
  const page = await pageteer.render('https://example.com', { formats: ['markdown'] });
  expect(page.test).toBe(true);
  expect(page.markdown).toBeTruthy();
});

it('surfaces a CAPTCHA as captcha_required', async () => {
  await expect(pageteer.render('https://test.pageteer.com/error/captcha')).rejects.toMatchObject({ code: 'captcha_required' });
});

The JavaScript client exposes isTestKey(key), a testMode getter, test on JSON results and meta.test (from the header) on every response. The MCP server works with a test key too, which is a cheap way to check your agent's tool wiring.

Limits

  • Test requests share your team's rate limit and concurrency with live keys. A busy CI run can slow your production traffic, so keep test suites modest or give CI its own team.
  • Results are fixtures, not the real page: never use a test key to check what a site actually contains, how it is blocked, or whether a selector matches.
  • Batch webhooks are real HTTP requests to your endpoint, under the same rules as live webhooks.
  • unblock: "residential" is simulated: test requests never go through your saved team proxy, so they cannot tell you whether it works. Use Test proxy in Settings → Proxy for that.
  • The dashboard playground always uses a live key.

Test keys are free on every plan, including the free one. See Pricing.