PureStats documentation
Agent Quickstart
Create an authorized PureStats site, install its tracker and verify an isolated browser test using the live agent API.
Start at llms.txt, then read this guide and the OpenAPI contract. Use the operation IDs, request fields and scopes from that contract. Examples below use example.com as a placeholder, not a target for test traffic.
1. Choose an Identity
For a new standalone account, start with POST /api/v1/agents/identity/challenges. A challenge does not create an account, reserve a domain or prove ownership. This installation workflow needs sites:read and sites:write, plus the separate sites:create permission for the optional additional-site example below. Omit sites:create if you only need to configure an already granted site; sites:write alone does not authorize creating sites. Request additional analytics or feature scopes only for subsequent operations that actually need them. Generate a recovery secret on the client from 32 cryptographically random bytes and encode it as 64 lowercase hexadecimal characters. Keep it in a secret store outside the repository before making any identity request.
$recoverySecret = bin2hex(random_bytes(32));
// Store through your secret manager. Never print this value.
All four public identity endpoints accept JSON, reject unknown fields, require a private Idempotency-Key header of 64 lowercase hexadecimal characters, and return HTTP 201 with a data object and request_id. Generate the header independently from another 32 random bytes. Keep it stable for retries of the same endpoint and body, and keep it secret: it can recover a confidential response. Never reuse the recovery secret as the header. Honor Retry-After on HTTP 429; do not cache identity responses.
The challenge body requires domain, method (dns or https) and recovery_key. It accepts optional purpose (use register, the default), name and contact_email; email is optional contact information, not a human login or recovery factor. Construct the body in memory using secret-manager values, not a literal secret in source:
$challengeRequest = [
'domain' => getenv('PURESTATS_SITE_DOMAIN'),
'method' => 'dns',
'purpose' => 'register',
'name' => 'Website Agent',
'recovery_key' => $recoverySecret,
];
// Send JSON over HTTPS with a separate secret Idempotency-Key. Do not print it.
Use the real public hostname you control. example.com and reserved fixture domains are deliberately rejected by registration; documentation templates must not be executed against them.
For an existing human account, do not register a second account. Bootstrap a pending confidential device client with POST /api/v1/agents/identity/delegations, JSON name and scopes, and the identity idempotency header. Then request browser consent through POST /oauth/device/code. Show the returned verification URI and user code to the owner, then wait for explicit approval of scopes and sites. Read Authentication.
A human with zero sites can consent with no existing sites selected when explicitly approving sites:create, or an account-only capability such as account.capabilities with account:read. Approving creation allows a new authorized site to be created and granted to this client; it does not grant access to foreign sites. Existing-site operations still require both an explicit site grant and the owner's membership. For the installation workflow after creating a new site, also request and obtain consent for sites:read and sites:write.
2. Prove Domain Control
For standalone registration, the challenge's data contains challenge_id, domain, method, purpose, expires_at, dns_name, https_url and value. Publish the exact value in a TXT record at dns_name when using DNS, or as the body served at https_url when using HTTPS. Use the returned names and locations; never guess the filename, TXT prefix or challenge format. Proof expires within 30 minutes; the returned expires_at is authoritative.
Once the proof is published, call POST /api/v1/agents/identity/registrations with JSON challenge_id, scopes (including the additional-site example, ["sites:read", "sites:write", "sites:create"]) and the same recovery_key used for the challenge. Use a new identity idempotency header for this operation. This request verifies the proof and activates the account atomically; there is no separate verification endpoint. Failed proof does not grant access. If a challenge expires, obtain a new one rather than replaying stale proof.
Successful activation returns confidential data.client_id and data.client_secret, plus credential_id, owner_id, account_type, scopes, requested_scopes, site_ids, grant_type, token_endpoint, device_authorization_endpoint and resource. Store the credentials privately without logging the response. Activation provisions the canonical private site and grants it to this client; save its identifier from site_ids. The recovery key is never returned. Public domain proof is not client authentication: activation also requires your original recovery key. Remove proof after successful activation. A proof for one hostname does not authorize arbitrary other domains.
3. Obtain API Access
After activation, exchange the confidential client credentials at POST /oauth/token using a form-encoded body: grant_type=client_credentials, client_id, client_secret, space-separated scope=sites:read sites:write sites:create and the exact resource returned by activation (https://purestats.io/api/v1/management). Omit sites:create from both registration and token scopes when not using the additional-site example. The token response is a top-level OAuth object containing token_type, expires_in and access_token, not an identity data envelope. Machine access tokens expire within 15 minutes and have no refresh token; obtain a new token through client credentials when needed. Send it only in Authorization: Bearer <access token> for protected API calls, never in a query string, browser bundle or tracker tag.
Use environment or secret-manager references such as PURESTATS_CLIENT_ID, PURESTATS_CLIENT_SECRET, PURESTATS_ACCESS_TOKEN and PURESTATS_RECOVERY_SECRET. Disable shell tracing, redact HTTP debug output, and do not echo token responses. See Authentication for delegation and recovery.
4. Create the Site
Use operation sites.create: POST /api/v1/management/actions/sites.create, with the explicit sites:create scope, bearer authorization and a stable Idempotency-Key header. This permission is separate from sites:write, which configures existing granted sites. Submit its schema for an authorized canonical domain:
{
"domain": "example.com",
"name": "Example",
"timezone": "UTC",
"public": false
}
This is a template, not a request to run against production. Standalone activation already provisioned the canonical site: use its site_ids identifier instead of creating that domain twice. Use sites.create only for a new authorized site, or when a delegated owner intentionally wants one. The catalog is authoritative for aliases, timezone, scopes and required fields. Do not substitute browser-only /analytics/* form actions.
Store data.site_id from a successful site-creation result, or the activation's provisioned site identifier, and use the exact canonical domain in the tracker. Management mutations accept their own Idempotency-Key format from OpenAPI; a private 64-character random key also meets it. Add only aliases you control and are authorized to manage. Reuse the same idempotency key only for the same operation and payload; use a new key for a new intended mutation. A key conflict is not a reason to blindly retry under another key. Machine grants and site membership are separate access boundaries; confirm the intended site is actually granted before inspecting it.
5. Install the Tracker
Install one tag in the shared document head or root layout:
<script defer src="https://purestats.io/pf.min.js" data-domain="example.com"></script>
Replace example.com with the real canonical site domain. This URL contains no account credential and no version parameter. The public unminified pf.js examples in older product guides remain valid; pf.min.js is the built minified asset. Do not invent versioned tracker URLs or attach API tokens to either asset.
Read the framework guide or first-party proxy guide before adding routing hooks or a proxy. Tracker configuration is loaded separately from /api/tracker-config; optional modules load only when enabled. Do not add duplicate manual pageviews to ordinary History API navigation.
6. Test Without Adding Production Traffic
Use fixture domains, mocked HTTP and a local tracker harness for automated tests. Inspect the tag, domain, consent configuration, CSP and optional module paths without sending fabricated events to purestats.io. Never call the production ingestion API with an invented visit merely to make installation health turn green.
After deploying the normal tracker, start an isolated installation test using operation sites.installation.tests.start: POST /api/v1/management/actions/sites.installation.tests.start, with sites:write, bearer authorization, Idempotency-Key and JSON {"site_id": SITE_ID}. The returned data contains id, token, test_url, expires_at and isolated: true. Handle token and test_url privately; never print or publish the response. Use the returned URL rather than inventing a token.
The real core tracker supports the returned URL fragment #purestats-test= followed by 64 lowercase hexadecimal characters. The token is in the fragment, not an API credential or query parameter; browser JavaScript must still treat it as sensitive. Core version 1.7.2 supports this flow using the normal unversioned pf.min.js asset. Open the URL only on the intended authorized site, in an isolated browser profile without replaying normal synthetic production traffic. The pageview traverses the real ingestion checks inside a rolled-back transaction. It does not create production visitors, sessions or rollups. Optional replay is skipped; a passing result does not prove replay or every optional feature works.
Poll operation sites.installation.tests.status: GET /api/v1/management/actions/sites.installation.tests.status?site_id=SITE_ID&id=TEST_ID, with sites:read. The test is bound to its site and initiating client. Inspect data.status, data.reason, data.tracker_version, data.received_count and data.test_session_received; distinguish passed, filtered, waiting, expired and stopped. Respect expires_at rather than retrying an expired token. When finished, call sites.installation.tests.stop: POST /api/v1/management/actions/sites.installation.tests.stop, with sites:write, Idempotency-Key and JSON {"site_id": SITE_ID, "id": "TEST_ID"}, then close the test profile.
Inspect operation sites.installation: GET /api/v1/management/actions/sites.installation?site_id=SITE_ID, with sites:read scope and the granted site identifier. Its returned data contains the tracking code, privacy configuration and installation health. Isolated test success is separate evidence from health based on a legitimate normal production visit; it must not turn invented test traffic into production analytics.
Operation sites.verify: POST /api/v1/management/actions/sites.verify, with sites:write, bearer authorization, Idempotency-Key and JSON {"site_id": SITE_ID}, performs an authorized public-page/script check and records health. It is not a synthetic event generator. Use fixture HTTP in automated tests; run a real check only for the intended authorized deployment. Script presence alone does not prove accepted ingestion; an empty report does not prove a broken tracker. See Testing.