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-usedis always0, nothing counts toward your monthly credits or spend limit, and they keep working when your credits run out (never a402). - 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
- Open API keys in the dashboard and choose Create key.
- Set Mode to Test. Give it a name (and a project, if you group keys).
- Copy the key. Like live keys it is shown once; it starts with
pk_test_instead ofpk_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.
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.
| Name | Type | Description |
|---|---|---|
render | POST /v1/render | A fixture page built around the URL you sent: a title plus the formats you asked for (markdown, html, text, links, metadata…). status 200. |
screenshot | POST /v1/screenshot | A small valid PNG or JPEG (whichever type you asked for), as bytes or base64 JSON. |
pdf | POST /v1/pdf | A small valid PDF, as bytes or base64 JSON. |
extract | POST /v1/extract | selectors: 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. |
search | GET /v1/search | n fake results for your query, ranked and shaped like live ones. |
map | POST /v1/map | A 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 / batch | POST /v1/crawl, /v1/batch | Jobs complete quickly with fixture pages. Batch webhooks are delivered to your endpoint for real, so you can test your receiver. |
monitors | POST /v1/monitors | Checks 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 test | POST /v1/proxy/test | ok: 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.
| Name | Type | Description |
|---|---|---|
/status/<code> | target status | The 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/timeout | 504 timeout | The site did not answer in time. |
/error/blocked | 422 target_blocked | The site blocks every tier tried (with unblock: "auto" you see the escalation in attempts). |
/error/captcha | 422 captcha_required | The site showed a CAPTCHA. |
/error/proxy_failed | 502 proxy_failed | The proxy could not be reached (reason: "unreachable", which clients retry). |
selector: "#missing" | 422 selector_not_found | On 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 10000 | A 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 -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"]}'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'));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"]){
"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:
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.