# Agent Quickstart

Start at [llms.txt](/llms.txt), then read this guide and the [OpenAPI contract](/openapi.json). 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.

```php
$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:

```php
$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](/docs/agents/authentication.md).

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](/docs/agents/authentication.md) 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:

```json
{
  "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:

```html
<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](/docs/agents/frameworks.md) or [first-party proxy guide](/docs/agents/proxy.md) 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](/docs/agents/testing.md).
