Link stats API
GET /v1/urls/{slug}/stats returns pre-aggregated analytics for one of your links. Owner-scoped, cookie-less, aggregate-only (no per-click rows, no visitor identifiers; see How we count). The /x/urls/{slug}/stats web-app route returns the identical body.
Request
GET /v1/urls/{slug}/stats?from=YYYY-MM-DD&to=YYYY-MM-DD
Authorization: Bearer YOUR_TOKEN_HERE
Any API token works here; see Authentication for workspace and personal tokens.
| Param | Where | Required | Default | Notes |
|---|---|---|---|---|
slug | path | yes | none | A link you own (else 404). |
from | query | no | to − 29 days | Window start, UTC date YYYY-MM-DD. |
to | query | no | today (UTC) | Window end, UTC date; never the future. |
Window semantics (clamp, don't reject). from is clamped to your plan's analytics retention (30 days on Free, 730 days on Pro and Team). A window longer than your retention is not an error: it is clamped and the effective start is echoed in range.clampedTo (with retentionDays and storedDays). A malformed window (from after to, or an unparseable date) is a 400.
Rate limit. One allowance covers every /v1 endpoint together: 300 requests/minute on Pro, 600 on Team, shared by all tokens of a workspace. Over the limit returns 429 with a Retry-After header. See Rate limits.
Response 200
{
"stats": {
"slug": "spring-sale",
"zoneDomain": "snipzr.li",
"range": { "from": "2026-06-14", "to": "2026-07-13", "clampedTo": "2026-06-14", "retentionDays": 30, "storedDays": 122 },
"allTime": { "total": 5120, "preTracking": 4900, "trackingSince": "2026-07-13" },
"totals": { "human": 82, "bots": 61, "requests": 143, "qr": 12, "click": 70, "botsAndPreviews": 44, "aiAssistants": 2, "suspected": 15 },
"series": [ { "date": "2026-07-12", "n": 90, "b": 40, "bs": 9 }, { "date": "2026-07-13", "n": 53, "b": 21, "ba": 2, "bs": 6 } ],
"hourly": { "fromUtc": "2026-07-12T16:00:00Z", "buckets": [0,0,1,2,3,4,6,8,7,9,5,6,4,3,2,2,1,3,4,2,1,0,0,0] },
"geo": [ { "k": "US", "v": 40 }, { "k": "DE", "v": 22 }, { "k": "unknown", "v": 3 } ],
"referrers": [ { "k": "~direct", "v": 31 }, { "k": "reddit.com", "v": 18 }, { "k": "~other", "v": 4 } ],
"devices": [ { "k": "desktop", "v": 51 }, { "k": "mobile", "v": 28 } ],
"browsers": [ { "k": "chrome", "v": 40 } ],
"os": [ { "k": "windows", "v": 25 } ],
"languages": [ { "k": "en", "v": 60 } ],
"source": { "click": 70, "qr": 12 },
"notHuman": {
"botsAndPreviews": [ { "k": "preview", "v": 30 }, { "k": "crawler", "v": 10 }, { "k": "head", "v": 4 } ],
"aiAssistants": [ { "k": "chatgpt", "v": 2 } ],
"suspected": [ { "k": "no_language", "v": 11 }, { "k": "sweep", "v": 4 } ]
},
"freshness": "todayLive"
}
}
Fields
| Field | Meaning |
|---|---|
range | Effective window + retention/clamp metadata (clampedTo = effective start; storedDays may exceed retentionDays). |
allTime | Lifetime hits decomposed into pre-tracking vs tracked era. On a link imported from another shortener that reported a click total, imported {count, source, importedAt} holds that total: source is bitly, rebrandly, tinyurl or csv (an import file), importedAt the UTC day of the import. It is never part of total or totals, and absent on every other link. |
totals | Window sums. human = requests − bots is the headline. bots is every request not counted as human, split into botsAndPreviews + aiAssistants + suspected. requests (raw, incl. bots/monitors): don't bill or alert on it. |
series | Per-UTC-day {date, n, b} (human = n − b), ascending, zero-days omitted; ba (AI assistants) and bs (suspected automated), parts of b, appear on days with any. |
hourly | Rolling last 24 hours of human clicks by UTC hour, independent of the window (empty for a purely-historical window). |
geo,referrers,devices,browsers,os,languages | [{k, v}], sorted by v desc. Referrers are host-only; sentinels ~direct (no referrer) and ~other (folded tail). Country key unknown when undeterminable. |
source | {click, qr} split of human clicks. |
notHuman | {botsAndPreviews, aiAssistants, suspected}: each a [{k, v}] list of reasons that sums to its total. Bots & previews: preview, crawler, tool, monitor, prefetch, head, no_user_agent, link_check, declared, and unrecorded for days before reasons were kept. AI assistants: chatgpt, claude, perplexity, mistral, meta, other. Suspected: no_language, not_navigation, inconsistent, data_center, burst, sweep, clockwork. What each means: How we count. |
freshness | todayLive | final | degraded. |
Counts are monotonic non-decreasing per UTC day except a brief dip during an hourly consolidation window; they settle within the hour. Don't treat a transient same-day decrease as data loss.
Errors
| Status | When |
|---|---|
400 | malformed window (from > to, bad date) or invalid slug |
404 | slug not found for this owner |
429 | over the plan's shared /v1 rate limit (Retry-After header) |
503 | analytics store unreachable and no live data covers the window |
A degraded 200 (store down but recent live data available) is distinct from a 503 (nothing serveable): surface freshness: "degraded" as "recent data only".
Example
curl -H "Authorization: Bearer $SNIPZR_TOKEN" \
"https://api.snipzr.com/v1/urls/spring-sale/stats?from=2026-06-14&to=2026-07-13"
JSON Schema
The response is described by a published JSON Schema (2020-12): https://www.snipzr.com/schemas/v1/link-stats.schema.json. Point your validator or code generator at that URL.
Versioning
Additive, backward-compatible fields may appear without a version bump: tolerate unknown fields, and expect the JSON Schema to be updated with them. Removals/renames are a new major version. The web app can export this same data as CSV (long format date,metric,key,value); the API is JSON-only.