ADVANCED
DocsAnalytics and rules over the API
Two things sit on top of a link and are worth automating: reading its traffic and steering it. Stats are read-only GETs a read key can call; routing rules are write actions that evaluate inside the redirect at the edge. Both are the same endpoints the dashboard uses.
Read a link’s stats
GET /api/links/:slug/stats returns a summary for a time range. Pick the range with ?range= - one of 1h, 24h, 7d, 30d, 90d, 183d, 365d, mtd, or ytd - or give an explicit window with ?from=<ms>&to=<ms> in epoch milliseconds.
curl "https://qr2r.com/api/links/aA3k9/stats?range=30d" \
-H "Authorization: Bearer $KILO_API_KEY"The response echoes the resolved range, your plan, the headline totals, a timeseries, and per-dimension breakdowns:
{
"range": { "from": 1716408000000, "to": 1719000000000, "clamped": false, "bucket": "day" },
"plan": { "slug": "pro", "limits": { /* … */ } },
"totals": { "opens": 1284, "visitors": 512 },
"timeseries": [ /* one point per bucket */ ],
"meta": { "generated_at": 1719000000000, "query_ms": 37, "cache": "miss" }
}opens counts served redirects; visitors is the distinct count. When your plan’s dimensions allow it, the response also carries breakdown blocks (countries, devices, referrers, traffic quality, and more). Retention and which dimensions you get are plan-gated: if you ask for a window longer than your plan keeps, the response comes back with range.clamped set to true and from pushed forward to the edge of your retention.
Same numbers, no raw IPs
These are the figures the dashboard analytics view draws from - the API is just the other client. As everywhere on Kilo, no raw IP is ever stored to produce them; IP-derived data is hashed with a rotating pepper first.
Companion GETs cover the same data at different shapes: /stats/breakdown for a single dimension, /stats/timeseries for the series alone, and /stats/export for a download. Group-level totals live at GET /api/groups/:id/stats.
List and add routing rules
A link’s rules decide where a scan actually goes. Read them with GET /api/links/:slug/rules:
{ "rules": [ /* rule objects, in priority order */ ] }Add one with POST /api/links/:slug/rules. A rule’s type is geo, device, or ab, and each carries a target_url. A geo rule matches on a list of ISO-2 country codes:
curl https://qr2r.com/api/links/aA3k9/rules \
-H "Authorization: Bearer $KILO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "type": "geo", "condition": { "countries": ["DE", "AT"] }, "target_url": "https://example.com/de" }'A create returns 201 with the stored rule:
{
"id": 44,
"type": "geo",
"condition": { "countries": ["DE", "AT"] },
"target_url": "https://example.com/de",
"weight": 1,
"priority": 0,
"created_at": 1719000000000
}Update a rule’s target, weight, or priority with PUT /api/rules/:id, and remove one with DELETE /api/rules/:id (which returns { "ok": true }). Reorder a link’s rules atomically with POST /api/links/:slug/rules/reorder.
Rules are a paid feature
Enforced at the API, not just the dashboard: a geo or device rule needs the targeting_rules plan limit, and an ab rule needs ab_testing. Without it the create is refused with 402 plan_required, so the free tier can’t be unlocked by calling the endpoint directly.
Rules evaluate at the edge inside the redirect itself, never touching a database on the hot path - the mechanics are in the routing rules guide. That’s the API surface: keys and scopes, links, stats, and rules, all the way down.
Back to all guides