Clicktag docs

Sites API

The Clicktag Sites API registers, verifies, lists and updates the hostnames you track, with a Firebase ID token or a management key. Each account holds up to 50 sites, each a website or an API, and every site stays private until you make it public.

Base URL: https://clicktag.io. Download the OpenAPI document.

TypeScript examples run server-side in Node 20+: save a snippet as example.mts, set any referenced environment variables, then run npx tsx example.mts.

Access

Site management uses a Firebase ID token in Authorization: Bearer <token>. Copy a token from Authentication, then set TT_TOKEN in your terminal. Site API keys (tt_...) only authenticate API analytics ingest and cannot manage sites. Management keys (tt_mk_...) can; see Management keys.

Method Route Access
GET /api/sites Signed-in account; returns its own sites
POST /api/sites Signed-in account
GET /api/sites/{hostname} Public site, or its signed-in owner
PATCH /api/sites/{hostname} Site owner
DELETE /api/sites/{hostname} Site owner
POST /api/sites/{hostname}/verify Site owner
GET /api/sites/{hostname}/setup-check Site owner

Responses are JSON with Cache-Control: no-store. Call the product API from a server or the Clicktag origin: /api/* responses do not provide cross-origin CORS headers. Use the normalized hostname returned at registration in subsequent paths; path lookups do not lowercase it for you.

List your sites

GET /api/sites

Returns every site owned by the authenticated account: pinned sites first, most recently pinned first, then the rest by creation time. There are no pagination parameters; an account can register at most 50 sites.

curl
curl --fail-with-body --silent --show-error --max-time 20 \
  -H "Authorization: Bearer ${TT_TOKEN:?Copy a token from Authentication}" \
  https://clicktag.io/api/sites
typescript
const token = process.env.TT_TOKEN;
if (!token) throw new Error("Set TT_TOKEN from the Authentication page");

const response = await fetch("https://clicktag.io/api/sites", {
  headers: { Authorization: `Bearer ${token}` },
  signal: AbortSignal.timeout(20_000),
});
if (!response.ok) {
  throw new Error(`Sites ${response.status}: ${await response.text()}`);
}
console.log(await response.json());

The response is { "sites": [...] }, with an empty array when the account has no sites. Each entry contains:

Field Type Meaning
hostname string Registered hostname
owner_uid string Owning account's Firebase user ID
display_name string Display name; initially an empty string
is_public boolean Whether anonymous metadata and stats reads are allowed
verified_at string or null ISO 8601 verification timestamp; null until verified
created_at string ISO 8601 creation timestamp
kind string web or api, fixed at registration; see API-only sites
pinned_at string or null ISO 8601 time you pinned the site; null when it is not pinned

For daily traffic and live counts across your sites, use All-sites summary.

Register a hostname

POST /api/sites

Send a JSON object with hostname as a string. The server trims whitespace, lowercases it, converts international names to punycode and drops a trailing dot. The normalized hostname must contain a dot, use letters, digits and hyphens in labels of 1-63 characters, and be at most 253 characters overall. No label can begin or end with a hyphen, and names made only of digits and dots, such as IP addresses, are rejected. A full https:// URL is reduced to its hostname; credentials, a non-default port or a scheme other than http or https fail with 400.

Registration creates a private, unverified site with an empty display name. Hostnames are unique across accounts. The account limit is 50 sites.

Set kind to api to register an API instead of a website. It defaults to web when absent or null, any other value fails with 400, and it cannot be changed later.

A new website starts unverified and does not collect data until ownership is verified (see Verify ownership). In the normal case that happens automatically on the first visit once the site-specific tag from settings is installed. An API site can create keys and receive API analytics right away; see API-only sites.

Replace your-domain.example with your hostname. The write examples on this page are templates; the public demo is for reads only.

curl
curl --fail-with-body --silent --show-error --max-time 20 \
  -X POST https://clicktag.io/api/sites \
  -H "Authorization: Bearer ${TT_TOKEN:?Copy a token from Authentication}" \
  -H 'Content-Type: application/json' \
  --data '{"hostname":"your-domain.example"}'
typescript
const token = process.env.TT_TOKEN;
const hostname = process.env.EA_HOSTNAME;
if (!token || !hostname) throw new Error("Set TT_TOKEN and EA_HOSTNAME");

const response = await fetch("https://clicktag.io/api/sites", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ hostname }),
  signal: AbortSignal.timeout(20_000),
});
if (!response.ok) {
  throw new Error(`Register ${response.status}: ${await response.text()}`);
}
console.log(await response.json());

Success is 201 with { "hostname": "your-domain.example", "verified_at": null, "verify_token": "tt-verify-...", "kind": "web" }. Keep the verify_token; you need it to prove ownership next. Only hostname and kind are read. Registering a hostname your account already owns returns 200 with the stored registration, including its original kind.

Verify ownership

POST /api/sites/{hostname}/verify

The website collector only stores data for verified sites, and only a verified site can be made public. API sites do not need verification for API analytics; see API-only sites.

Automatic (default). Copy the Tracking script from site settings. Its data-verify must equal the current site's verify_token, and data-hostname must match the registered hostname. When the first beacon arrives, Clicktag fetches https://{hostname}/ and checks that actual script tag in the served HTML. Successful verification keeps that first visit. Up to five HTTPS redirects within the registered hostname and its www. twin (the same name with www. added or removed) are followed; other hosts, ports and credentials are rejected. Failed probes pause for 30 seconds.

A generic tag, a noscript pixel alone, commented examples, and another registration’s token do not prove ownership. Already-verified sites continue collecting without snippet changes. The verification challenge is public by design; never put your Firebase access token in the tag.

Manual fallback. Use this when the site-specific tag is absent from the served HTML (generic tag, client-side injection, self-hosted script) or the origin blocks our fetcher. Publish the returned verify_token with either method; one passing is enough.

Verification record and file names retain the legacy totallytics identifier for compatibility.

Method A: DNS TXT record. Publish a TXT record at _totallytics-verify.{hostname} with the value totallytics-verify={verify_token}. DNS changes can take a few minutes to propagate.

Method B: well-known file. Serve the verify_token as the entire body of https://{hostname}/.well-known/totallytics-verify.txt over HTTPS with a 200 status. Redirects are not followed, surrounding whitespace is ignored and the file can be at most 8 KB.

curl
curl --fail-with-body --silent --show-error --max-time 30 \
  -X POST https://clicktag.io/api/sites/your-domain.example/verify \
  -H "Authorization: Bearer ${TT_TOKEN:?Copy a token from Authentication}"
typescript
const token = process.env.TT_TOKEN;
const hostname = process.env.EA_HOSTNAME;
if (!token || !hostname) throw new Error("Set TT_TOKEN and EA_HOSTNAME");

const response = await fetch(
  `https://clicktag.io/api/sites/${encodeURIComponent(hostname)}/verify`,
  {
    method: "POST",
    headers: { Authorization: `Bearer ${token}` },
    signal: AbortSignal.timeout(30_000),
  },
);
console.log(response.status, await response.json());

On success the response is 200 with verified_at set. While no method is detected, the response is 422 with per-method tag, dns and file details; publish the proof and retry. An already-verified site returns 200 with already: true. Each hostname gets one manual check about every 10 seconds; calling sooner returns 429 with Retry-After.

API-only sites

Register with "kind": "api" when you send API request metrics from a server and have no website or tracking tag.

curl
curl --fail-with-body --silent --show-error --max-time 20 \
  -X POST https://clicktag.io/api/sites \
  -H "Authorization: Bearer ${TT_TOKEN:?Copy a token from Authentication}" \
  -H 'Content-Type: application/json' \
  --data '{"hostname":"api.your-domain.example","kind":"api"}'
typescript
const token = process.env.TT_TOKEN;
const hostname = process.env.EA_HOSTNAME;
if (!token || !hostname) throw new Error("Set TT_TOKEN and EA_HOSTNAME");

const response = await fetch("https://clicktag.io/api/sites", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ hostname, kind: "api" }),
  signal: AbortSignal.timeout(20_000),
});
if (!response.ok) {
  throw new Error(`Register ${response.status}: ${await response.text()}`);
}
console.log(await response.json());
Capability API site (api) Website (web)
Create API keys Right after registration After verification
Website collector, reports and imports After verification After verification
Public sharing, email reports, Search Console After verification After verification

Create keys in site settings under API, or with POST /api/sites/{hostname}/keys and the owner's Firebase ID token. API analytics covers the middleware setup. A key only writes to its own site's API analytics, which is why API sites skip verification. On an unverified website the same call fails with 403 and Install your tracking script to verify this site first. API analytics reports only count requests timestamped after the site's created_at, for every kind.

Verification stays optional for an API site and unlocks everything in the table. Without a tracking tag, publish the verify_token from registration as the DNS TXT record or the well-known file described in Verify ownership, then call POST /api/sites/{hostname}/verify. The API view stays first after verification.

Check your setup

GET /api/sites/{hostname}/setup-check (owner only) fetches your homepage exactly like the auto-verifier does and reports:

  • reachable / error: whether the page could be fetched over HTTPS at all
  • url / status: the final URL (after allowed redirects within the hostname and its www. twin) and HTTP status
  • verified_at: the site's verification timestamp, or null while unverified
  • tag_host: which Clicktag host serves a script tag that reports to this site, or null when there is none
  • tag_reports_to: when the HTML has Clicktag tags but none report to this site, where the first one reports to (its data-hostname, or the page's own hostname without one); otherwise null
  • tag_verifies: while the site is unverified, whether a tag that reports to it carries this site's data-verify token, which auto-verification needs; null once the site is verified or when no tag reports to it
  • csp: whether your Content-Security-Policy allows the script, the pageview pixel (img-src), and engagement beacons (connect-src), each allowed / blocked / unknown with the effective directive and the fix to apply

Use it when the dashboard stays empty after installing the tag. Tag presence is diagnostic, not proof of ownership; the ownership check also requires the site’s verification challenge. The CSP result checks response headers, not a full browser execution or HTML meta policy evaluation.

Results are shared for 60 seconds; add ?refresh=true to fetch the page again. Each hostname's page is fetched at most about once every 10 seconds, and a call that needs a fetch sooner gets 429 with Retry-After.

Read a site

GET /api/sites/{hostname}

Public metadata needs no token. For a private site, send the owner's bearer token.

curl
curl --fail-with-body --silent --show-error --max-time 20 \
  https://clicktag.io/api/sites/demo.bitgate.dev
typescript
const response = await fetch(
  "https://clicktag.io/api/sites/demo.bitgate.dev",
  { signal: AbortSignal.timeout(20_000) },
);
if (!response.ok) {
  throw new Error(`Site ${response.status}: ${await response.text()}`);
}
console.log(await response.json());
json
{
  "hostname": "demo.bitgate.dev",
  "display_name": "Demo site",
  "is_public": true,
  "created_at": "2026-09-17T17:28:42.089Z",
  "kind": "web"
}

Unlike the account-wide list, this response does not include owner_uid. The owner also receives verified_at, verify_token, reports_mode and pinned_at.

Update a site

PATCH /api/sites/{hostname}

Body field Type Behavior
display_name string Optional. Truncated to 128 characters; an empty string clears it. Omitting the field retains the current value; null and non-strings return 400 invalid display name.
is_public boolean Optional. true enables anonymous metadata and stats reads. Omitting the field retains the current setting; null and non-booleans return 400 invalid sharing setting.
reports_mode string Optional. inherit, custom or off; see Per-site settings. Omitting the field retains the current mode; other values return 400 invalid reports mode.
pinned boolean Optional. true pins the site to the top of your site list; an already pinned site keeps its pin time. false unpins it. Omitting the field retains the current pin; null and non-booleans return 400 invalid pin setting.

There is no rename or ownership-transfer field. Extra fields are ignored. An empty object keeps the current settings. Invalid JSON, null, arrays and other non-object bodies return 400 invalid settings.

curl
curl --fail-with-body --silent --show-error --max-time 20 \
  -X PATCH https://clicktag.io/api/sites/your-domain.example \
  -H "Authorization: Bearer ${TT_TOKEN:?Copy a token from Authentication}" \
  -H 'Content-Type: application/json' \
  --data '{"display_name":"My website","is_public":false}'
typescript
const token = process.env.TT_TOKEN;
const hostname = process.env.EA_HOSTNAME;
if (!token || !hostname) throw new Error("Set TT_TOKEN and EA_HOSTNAME");

const response = await fetch(
  `https://clicktag.io/api/sites/${encodeURIComponent(hostname)}`,
  {
    method: "PATCH",
    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ display_name: "My website", is_public: false }),
    signal: AbortSignal.timeout(20_000),
  },
);
if (!response.ok) {
  throw new Error(`Update ${response.status}: ${await response.text()}`);
}
console.log(await response.json());

Success is 200 with hostname, display_name, is_public, reports_mode and pinned_at. Setting is_public to true on an unverified site fails with 400; verify ownership first. Making a verified site public exposes its metadata and web overview, breakdown and retention endpoints, not only its share-page link. Goals, API request reports, Search Console, AI crawlers, imports, keys, email reports and alerts remain owner-only.

Delete a site

DELETE /api/sites/{hostname}

Deletes the registration together with the site's import jobs, email report subscriptions, goals, API alert rules, site API keys, Search Console link, Cloudflare crawler link and stored icon, not historical analytics. It needs the owner's ID token; a management key gets 403. This is not a data-erasure endpoint. After cached registrations expire, new collection for the unregistered hostname is normally discarded and its reports return 404.

curl
curl --fail-with-body --silent --show-error --max-time 20 \
  -X DELETE https://clicktag.io/api/sites/your-domain.example \
  -H "Authorization: Bearer ${TT_TOKEN:?Copy a token from Authentication}"
typescript
const token = process.env.TT_TOKEN;
const hostname = process.env.EA_HOSTNAME;
if (!token || !hostname) throw new Error("Set TT_TOKEN and EA_HOSTNAME");

const response = await fetch(
  `https://clicktag.io/api/sites/${encodeURIComponent(hostname)}`,
  {
    method: "DELETE",
    headers: { Authorization: `Bearer ${token}` },
    signal: AbortSignal.timeout(20_000),
  },
);
if (!response.ok) {
  throw new Error(`Delete ${response.status}: ${await response.text()}`);
}
console.log(await response.json());

Success is 200 with { "deleted": "your-domain.example" }. A later request for an absent registration returns 404, not a second successful deletion.

Site icons

GET /favicons/{hostname} returns a cached site icon without authentication, including for private sites. This is an image endpoint, not a metadata or statistics read. A successful response is 200 with the image's content type, an ETag and Cache-Control: public, max-age=21600 (six hours). Unregistered or invalid hostnames and unavailable icons return an empty 404, cached for 60 or 300 seconds depending on the failure.

Errors and caching

Errors use { "error": "message" }. Private metadata and web-statistics reads return 404 unknown site for missing or invalid ID tokens as well as non-owner tokens. A management key is checked first, on public sites too: an unknown or revoked key returns 401 and a key without read returns 403. Hostname-scoped management routes first require a valid token (401 otherwise), then return the same 404 for unknown and non-owned sites.

Status Message Meaning
400 Enter a valid website address, such as example.com Registration hostname is missing or fails validation
400 Enter a valid API hostname, such as api.example.com The same, when registering with kind set to api
400 kind must be web or api Registration kind is present but not web or api
400 site limit reached Account already has 50 registered sites
400 invalid settings PATCH body is invalid JSON or is not an object
400 invalid display name PATCH display_name is present but is not a string
400 invalid sharing setting PATCH is_public is present but is not a boolean
400 invalid reports mode PATCH reports_mode is present but is not inherit, custom or off
400 invalid pin setting PATCH pinned is present but is not a boolean
400 verify domain ownership before making the site public Attempted to make an unverified site public
401 sign in required Required token is missing, invalid or expired (management routes only)
401 invalid or revoked management key The management key is unknown or revoked, on every route including public reads
403 scope read required or scope manage required The management key lacks the scope; see Errors and limits
403 management keys cannot call this endpoint Deleting a site needs the owner's ID token
404 unknown site The registration does not exist, or you are not allowed to see it
404 not found No matching product API route
409 This site is already registered to another account Hostname belongs to another account; registering your own hostname again returns 200
409 Site registration changed. Reload and try again. Registration changed while verification was running; reload before retrying
422 verification not found yet No verification method was detected; see the tag/dns/file details
429 rate limited, retry in 1s Too many changes from one management key; wait for Retry-After
429 rate limited, retry in <n>s This hostname was verified or setup-checked less than 10 seconds ago; wait for Retry-After
500 verification token missing The registration has no ownership challenge for verification
500 internal error An unexpected server-side error prevented completion

Site lookups are cached at the edge: a verified registration for up to two minutes, a missing or unverified one for up to 15 seconds. A mutation clears the cache in the handling location, but other locations refresh within those bounds — allow for propagation when registering, verifying, changing visibility or deleting a site. Cache-Control: no-store applies to HTTP responses, not this internal lookup cache. Reads of a public site's overview, overview summary, breakdown and retention by anyone other than the owner are additionally edge-cached for one minute, after checking site access; cache hits carry Cache-Control: public, max-age=14400, so browsers can keep them for up to four hours. Only the owner's requests bypass this response cache.