Sites, hostnames and keys
A site is what UsherStats measures: a name, a time zone, the hostnames it is served on, and its keys.
- The public key (
pk_and 26 characters) goes in the snippet. It is public by design; collect accepts it only from the site's hostnames. - A server secret (
sk_live_and 40 characters) authenticates the server SDK and proxy mode. It is shown once and stored hashed, like an API token.
Reading needs sites:read, changing needs sites:write. Another workspace's site, or one outside a token's site
restriction, is 404. The Free plan has 5 sites, Starter 20, Growth and Business unlimited; past the limit,
creating a site answers 402 naming the plan.
| Route | Scope |
|---|---|
GET /v1/sites | sites:read |
POST /v1/sites | sites:write (not for site-restricted tokens) |
GET /v1/sites/{id} | sites:read |
PATCH /v1/sites/{id} | sites:write |
DELETE /v1/sites/{id} | sites:write |
POST /v1/sites/{id}/hostnames | sites:write |
GET /v1/sites/{id}/hostnames/{hostname} | sites:read |
POST /v1/sites/{id}/hostnames/{hostname}/verify | sites:write |
DELETE /v1/sites/{id}/hostnames/{hostname} | sites:write |
GET /v1/sites/{id}/keys | sites:read |
POST /v1/sites/{id}/keys | sites:write |
POST /v1/sites/{id}/keys/{keyId}/rotate | sites:write |
DELETE /v1/sites/{id}/keys/{keyId} | sites:write |
GET /v1/sites/{id}/snippet | sites:read |
GET /v1/sites/{id}/install-status | sites:read |
PATCH /v1/sites/{id}/settings | sites:write |
The examples assume $US_TOKEN holds a token with account:admin.
Create a site
curl https://api.usherstats.com/v1/sites \
-H "Authorization: Bearer $US_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Field notes", "timezone": "Europe/Bucharest", "hostnames": ["fieldnotes.example", "www.fieldnotes.example"]}'The answer holds the site and its first public key.
curl https://api.usherstats.com/v1/sites \
-H "Authorization: Bearer $US_TOKEN"curl https://api.usherstats.com/v1/sites/$SITE_ID \
-H "Authorization: Bearer $US_TOKEN"curl -X PATCH https://api.usherstats.com/v1/sites/$SITE_ID \
-H "Authorization: Bearer $US_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Field Notes", "timezone": "America/New_York"}'Install it
The snippet is a script tag with the public key. Serving it through your own domain (/_us/) keeps it working where
third-party analytics hosts are blocked; the answer includes the steps.
curl https://api.usherstats.com/v1/sites/$SITE_ID/snippet \
-H "Authorization: Bearer $US_TOKEN"<script defer src="https://collect.usherstats.com/t.js" data-site="pk_…"></script>Once a page with the snippet has been viewed, the install check says so (collect stamps the site at most once a minute):
curl https://api.usherstats.com/v1/sites/$SITE_ID/install-status \
-H "Authorization: Bearer $US_TOKEN"Hostnames
Bare hostnames only (no scheme, path or port); they are lower-cased. Up to 50 per site.
A hostname may be on sites of more than one workspace: collect only counts a page for a site whose key the page
carries. Proxy mode is stricter: a proxied hostname belongs to exactly one site, and is switched on only after its
ownership is verified (verifiedAt), through its Cloudflare custom hostname's validation or a DNS TXT record.
curl https://api.usherstats.com/v1/sites/$SITE_ID/hostnames \
-H "Authorization: Bearer $US_TOKEN" \
-H "Content-Type: application/json" \
-d '{"hostname": "blog.fieldnotes.example"}'Each hostname carries verification: the DNS record that proves the hostname is yours, a TXT record
_usherstats.<hostname> with the value usherstats-verify=<token> (the token is this site's own for this hostname).
Publish it at your DNS provider, then ask for the check. A verified hostname (verifiedAt set, verification null) is
one the bot-protection challenge may send visitors back to, and one delegated Search Console access may read; an
unverified one is still counted by collect. The check is limited to 30 an hour per site.
curl -X POST https://api.usherstats.com/v1/sites/$SITE_ID/hostnames/blog.fieldnotes.example/verify -H "Authorization: Bearer $US_TOKEN"The answer's verified says whether the record was found; when it was not, message names the record to publish.
Proxy-mode hostnames need no TXT record of ours: Cloudflare's validation of the custom hostname verifies them.
One hostname: for a proxy-mode hostname, proxy is its live status at Cloudflare and the DNS records it still needs;
for any other, proxy is null.
curl https://api.usherstats.com/v1/sites/$SITE_ID/hostnames/blog.fieldnotes.example \
-H "Authorization: Bearer $US_TOKEN"To serve a hostname through proxy mode, add it with "proxy": true and the origin its
requests are forwarded to. The answer's proxy.records are the DNS records to set (a CNAME to the proxy; the TXT
records let it go live before the DNS moves). Proxy hostnames count against the plan's allowance: paid plans are billed for each one past it, Free is
refused with a 402; and
a hostname another site already proxies is a 409.
{ "hostname": "shop.fieldnotes.example", "proxy": true, "origin": "https://origin.fieldnotes.example" }Removing a proxy-mode hostname removes it at Cloudflare as well.
curl -X DELETE https://api.usherstats.com/v1/sites/$SITE_ID/hostnames/blog.fieldnotes.example \
-H "Authorization: Bearer $US_TOKEN"Keys
A server secret, for the server SDK or proxy mode. value is in this answer only:
curl https://api.usherstats.com/v1/sites/$SITE_ID/keys \
-H "Authorization: Bearer $US_TOKEN" \
-H "Content-Type: application/json" \
-d '{"kind": "secret"}'Listing shows public keys in full and secrets by prefix:
curl https://api.usherstats.com/v1/sites/$SITE_ID/keys \
-H "Authorization: Bearer $US_TOKEN"Rotating makes a new key of the same kind and ends the old one at once, so deploy the new secret right after:
curl -X POST https://api.usherstats.com/v1/sites/$SITE_ID/keys/$SECRET_ID/rotate \
-H "Authorization: Bearer $US_TOKEN"curl -X DELETE https://api.usherstats.com/v1/sites/$SITE_ID/keys/$NEW_SECRET_ID \
-H "Authorization: Bearer $US_TOKEN"Settings
Four lists; a PATCH replaces the ones it names and keeps the rest.
internalNetworks: CIDRs (IPv4 or IPv6) whose traffic is your own, counted apart from readers.botWindows: time windows (from,to, optionalcountry,network,note) whose traffic is filed as a bot fleet after the fact.goals: conversions, by event name ("type": "event") or path ("type": "path").experiments: A/B tests bykey, with at least twovariantsand astatusof draft, running or stopped.
curl -X PATCH https://api.usherstats.com/v1/sites/$SITE_ID/settings \
-H "Authorization: Bearer $US_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"internalNetworks": ["203.0.113.0/24", "2001:db8::/32"],
"botWindows": [{"from": "2026-09-30T03:00:00Z", "to": "2026-09-30T03:10:00Z", "country": "CN", "note": "one fleet"}],
"goals": [{"name": "Newsletter", "type": "event", "match": "subscribe"}],
"experiments": [{"key": "hero-copy", "variants": ["control", "short"], "status": "running"}]
}'A value that does not fit is 400 naming the field, such as internalNetworks[0]:
curl -X PATCH https://api.usherstats.com/v1/sites/$SITE_ID/settings \
-H "Authorization: Bearer $US_TOKEN" \
-H "Content-Type: application/json" \
-d '{"internalNetworks": ["10.0.0.0/33"]}'Delete a site
Its keys stop working at once.
curl -X DELETE https://api.usherstats.com/v1/sites/$SITE_ID \
-H "Authorization: Bearer $US_TOKEN"curl https://api.usherstats.com/v1/sites/$SITE_ID \
-H "Authorization: Bearer $US_TOKEN"