#JS and PHP Tracker by Token
Status: Available.
Purpose: the minimal way to count visits on any site without a module: one <script> tag, or a server-side send from PHP. The path, source, and UTM are sent; only the server variant sees bots.
#Contents
- When to use it
- Access
- JS tag
- Server variant (PHP)
- The
POST /api/v1/track/visitrequest - Responses
- Limits
- What is recorded
- What the tracker does not do
- Verification
- Open questions
#When to use it
| You need | Path |
|---|---|
| A visit counter on an arbitrary site | JS tag |
| To see visits by bots and AI crawlers on a PHP site | PHP variant |
| Chat, behavior tracking, consent | Loader or a module |
| Events, forms, an SPA, a mobile app | Events API, Planned |
#Access
| Token | 32 hex characters; in the token table there are two kinds: tracker (regular) and connector (created by a module binding) |
| Issuance | the dashboard, seo.manage permission |
| Paid | no |
| Personal data | IP and User-Agent hashes; the values are not stored (see Overview) |
| Secrecy | the token is public: it sits in the tag on the page. Limits and a daily quota protect against abuse |
#JS tag
<script async src="https://formula-cream.pro/api/v1/track/js?t=0123456789abcdef0123456789abcdef"></script>
Replace the domain with your cell's address: RU https://formula-cream.pro, EN https://wfacademy.org, ID https://wfacademy.id. The token is issued by the dashboard.
#Parameters of GET /api/v1/track/js
| Parameter | Type | Required | Constraints | Example |
|---|---|---|---|---|
t | string | yes | 32 hex characters | 0123456789abcdef0123456789abcdef |
#Response
| Condition | Code | Body |
|---|---|---|
| The token has an invalid form | 400 | /* bad token */ |
| Otherwise | 200 | the script, Cache-Control: public, max-age=3600, Access-Control-Allow-Origin: * |
Whether the token exists in the database is not checked when the script is served: the hit receiver checks it.
#What the script sends
One request per page load through navigator.sendBeacon (a text/plain Blob), with a fallback of fetch in no-cors mode with keepalive.
{
"t": "0123456789abcdef0123456789abcdef",
"kind": "js",
"path": "/blog/emulsions",
"host": "example.ru",
"ref": "https://www.google.com/",
"utm": { "source": "newsletter", "medium": "email", "campaign": "october" }
}
#Server variant (PHP)
It is inserted into Bitrix init.php (or the start of any PHP application). The source is served by GET /api/v1/track/php?t=<token>.
What the snippet does: after the request is processed, it sends a hit with the parameters without delaying the page, using a non-blocking curl_multi with a 150 ms connect timeout and a 200 ms total timeout.
The hit is not sent if:
- the script is run from the CLI;
- the path starts with
/bitrix,upload, orlocal/templates; - the request is for a static file (by extension);
- the request is ajax (
X-Requested-With); - the method is not GET;
Acceptcontains neithertext/htmlnor*/*.
To disable it temporarily, add define('SAPPORT_TRACKER_DISABLED', true); above the inserted block.
The visitor's IP is taken from the first element of X-Forwarded-For (if the header is present), otherwise from REMOTE_ADDR. If the site is reachable directly, a visitor can forge the header: put the server behind a trusted proxy.
#The POST /api/v1/track/visit request
The receiver is shared by the JS tag, the PHP code, and the modules' signed beacon. The signature for modules is described in the connector protocol; no signature is needed for a token.
Headers: Content-Type: application/json (text/plain is also accepted, as sent by sendBeacon). CORS: *.
#Request body
| Field | Type | Required | Constraints | Example |
|---|---|---|---|---|
t | string | yes | tracker token, 32 hex | 0123456789abcdef0123456789abcdef |
kind | string | no | js (default) or server. server is trusted only if host is among the tenant's domains; otherwise the hit is treated as a JS hit | server |
path | string | yes | page path; the query is dropped, the result is ≤ 512 characters (PHP sends up to 600, with the query: UTM is taken from it) | /blog/emulsions |
host | string | yes | page host | example.ru |
ref | string | no | referrer, ≤ 600 | https://www.google.com/ |
utm | object | no | source, medium, campaign, each ≤ 100 characters | {"source":"newsletter"} |
ua | string | server only | User-Agent, ≤ 1000 | Mozilla/5.0 … |
ip | string | server only | visitor IP | 203.0.113.7 |
country | string | no | two-letter code; for JS the country is taken from the cf-ipcountry header | RU |
status | integer | no | 100–599, the HTTP code of the page response | 200 |
The body is at most 8192 bytes; anything larger gets a 204 response without recording.
curl -i -X POST 'https://formula-cream.pro/api/v1/track/visit' \
-H 'Content-Type: application/json' \
-d '{"t":"0123456789abcdef0123456789abcdef","kind":"js","path":"/blog/emulsions","host":"example.ru","ref":"","utm":{"source":"newsletter"}}'
#Responses
Successful receipt and most rejections look the same, 204 with no body, so that tokens cannot be guessed.
| Code | When |
|---|---|
204 | accepted; or the body is not JSON; or the token is invalid, unknown, or revoked; or the daily quota is exceeded; or the body is larger than 8192 bytes |
403 | the loader key (connector) was used for kind: server; or the site is not bound, the counter is off, or the tenant has no access |
429 | the rate limit per address or per token was exceeded |
500 | database error (nothing was recorded) |
Errors do not return application/problem+json: this is a separate counter receiver, not part of the public API (see API conventions for the server API).
#Limits
| Limit | Value |
|---|---|
| Per sender address (unsigned) | 1 request per second, burst up to 60 |
| Per token | 10 requests per second, burst up to 100 |
| Daily token quota | 200,000 hits per day (UTC); beyond the quota, 204 without recording |
| Body size | 8192 bytes |
| Module's signed beacon | 1,200 requests per minute per address |
The quota and limits are counted in process memory: with several processes, the effective limit is softer than the nominal one.
Hits are retained for 180 days.
#What is recorded
| Hit field | Source |
|---|---|
kind | bot or human, by User-Agent |
bot_family | the bot family from a closed list |
path, host | from the request |
referrer_host | the referrer's host |
ai_referrer | the AI assistant label by referrer host |
utm_source, utm_medium, utm_campaign | from utm or from the path's query |
ua_hash, ip_hash | sha256(salt | value) truncated to 32 hex |
country | from cf-ipcountry or from the body of a trusted server hit |
status | HTTP code |
sample_ua | a User-Agent sample, bots only, up to 300 characters |
beacon | js or server |
#Classification
- A bot is identified by User-Agent against a closed list of families. An unknown bot is treated as a human.
- An AI assistant is identified by the suffix of the referrer host. For bots, the AI referrer is not taken into account.
| Label | Referrer hosts |
|---|---|
chatgpt | chatgpt.com, chat.openai.com |
perplexity | perplexity.ai |
gemini | gemini.google.com, bard.google.com |
copilot | copilot.microsoft.com, bing.com/chat |
you | you.com |
claude | claude.ai |
alice | ya.ru/alice, alice.yandex.ru, yandex.ru/alice |
duckassist | duckduckgo.com/aichat |
mistral | mistral.ai |
deepseek | chat.deepseek.com |
Plain ya.ru is not Alice: a visit from it is not labeled as AI.
#What the tracker does not do
- SPA: the page load is counted, not an address change through the History API.
- Events: there is no
track('purchase'), no buttons or forms. The Events API (Planned) is intended for that. - Sessions and visitors: there is no session identifier, cookies, or localStorage. "Unique visitors" are estimated by the cloud from IP and User-Agent hashes over a day.
- UTM: only
source,medium,campaign. Theutm_contentandutm_termfields are not sent. - Bots via the JS tag: bots do not execute JS; only the server variant sees them.
- Consent: the JS tag does not wait for consent and is not controlled by
window.sapport.consent. If your jurisdiction requires consent, load the tag after it, or use the loader.
#Verification
- Open a page; in the "Network" tab find the
track/js?t=…request (200) and thentrack/visit. - A
204response does not mean the hit was recorded (see the response table); check the counter in the dashboard under "Promotion → Overview". - For PHP:
curl -A "Googlebot" https://your-site/makes a bot visit appear under "AI assistants and bots".
#Open questions
- Technical. The receiver's code comments say "the response is always 204" and give limits of "~60/min per address and ~600/min per token"; the actual values differ (token buckets,
429/403/500codes). This page describes the actual behavior; the code comments should be corrected. - Technical. The quota and limits live in process memory: is a shared (Redis) quota needed for public guarantees?
- Technical. The "visitor" unit is impossible in the server beacon's
anonymousmode: in the public API,visitorsisnullin this mode (Reading analytics).