Analytics
Referral and share‑link analytics powered by Dub. These endpoints expose click / geo / device / conversion rollups for the links Swayzio creates when you share packs and tracks. They are owner‑scoped and native‑only — they are served by the Vercel runtime and never proxy to the Fastify Core API (see Conventions).
All routes live under https://app.swayzio.com/api/swayzio/v1/analytics and
require a Clerk actor. The server forces
tenantId = ownerId on every Dub query, so a caller can only ever see their own
links — you cannot pass an owner/tenant id to read another account’s analytics.
Endpoints
| Method | Path | Purpose | Auth | Request | Response |
|---|---|---|---|---|---|
GET | /v1/analytics | Aggregate link analytics (counts, timeseries, dimensions) | Clerk actor | (query, below) | dubAnalyticsResponseSchema |
GET | /v1/analytics/events | Raw, PII‑free event stream | Clerk actor | (query, below) | dubAnalyticsEventsResponseSchema |
POST | /v1/analytics/conversion | Record a share‑link signup (lead) from the click cookie | Clerk actor | (none) | { tracked, reason? } |
Revenue is redacted for non‑staff. sales and saleAmount are
referral‑attribution figures that expose Swayzio’s per‑subscription pricing.
The server zeroes them for any caller that is not verified internal staff
(a confirmed @swayzio.com email) and sets revenueVisible: false on the
response. This is enforced server‑side, not merely hidden in the UI — non‑staff
clients never receive the real numbers.
When Dub isn’t connected or the workspace plan doesn’t include analytics, the
response is not an error: it returns available: false with a reason
(one of dub_disabled, plan_gated, rate_limited, provider_error). Only a
hard Dub rate‑limit surfaces as 429 { "error": "analytics_rate_limited" },
and an unexpected provider failure on the aggregate route is 502 { "error": "analytics_provider_error" }.
Related but different: per‑pack analytics
GET /v1/packs/:id/analytics (see Packs) is a separate,
native‑event rollup — the view / play / download funnel recorded on the public
share page. That surface is Swayzio’s own product telemetry; this Analytics
surface is Dub’s click / geo / conversion data. They do not share a schema.
GET /v1/analytics
Aggregate analytics. Pick what to count with event, how to slice it with
groupBy, and the window with either interval or an explicit start+end
(+ optional timezone).
| Query param | Values | Default |
|---|---|---|
event | clicks · leads · sales · composite | composite |
groupBy | count · timeseries · continents · countries · regions · cities · devices · browsers · os · triggers · referers · referer_urls · top_links · top_urls | count |
interval | 24h · 7d · 30d · 90d · mtd · qtd · ytd · 1y · all | 30d |
start / end | ISO dates (use instead of interval) | — |
timezone | IANA‑style zone, max 64 chars | — |
Filters (any combination): browser, city, continent, country,
linkId, os, referer, region, trigger. Plus externalId — your
provider external id; the server auto‑prefixes it with ext_ for Dub if not
already present.
curl -s "https://app.swayzio.com/api/swayzio/v1/analytics?event=clicks&groupBy=countries&interval=30d" \
-H "Authorization: Bearer $SWAYZIO_TOKEN"{
"available": true,
"event": "clicks",
"groupBy": "countries",
"range": { "interval": "30d" },
"revenueVisible": false,
"items": [
{ "label": "United States", "country": "US", "clicks": 412, "leads": 18, "sales": 0, "saleAmount": 0 },
{ "label": "United Kingdom", "country": "GB", "clicks": 96, "leads": 4, "sales": 0, "saleAmount": 0 }
],
"retrievedAt": "2026-06-10T00:00:00.000Z"
}Response shape by groupBy:
count→count: { clicks, leads, sales, saleAmount }timeseries→timeseries: [{ start, clicks, leads, sales, saleAmount }]- any dimension →
items: [{ label, country?, region?, city?, linkId?, shortLink?, url?, entity?, clicks, leads, sales, saleAmount }]
When groupBy=top_links, items are enriched with the local entity they map to,
so you can resolve which pack or track a Dub link belongs to:
{ "entityType": "pack | track | split_sheet", "entityId": "…", "entityName": "…", "localLinkType": "…", "localLinkId": "…" }GET /v1/analytics/events
A raw, ordered event stream (most recent first). Intended for an activity feed, not aggregation.
| Query param | Values | Default |
|---|---|---|
event | clicks · leads · sales | clicks |
interval / start+end / timezone | as above | 30d |
limit | 1–100 | 50 |
| filters | same filter set as the aggregate route | — |
Events are deliberately PII‑free: no IP, no user agent, no customer
identity, and only the referer domain (never the full URL). On a hard Dub
limit the route returns 429 analytics_rate_limited.
curl -s "https://app.swayzio.com/api/swayzio/v1/analytics/events?event=leads&limit=20" \
-H "Authorization: Bearer $SWAYZIO_TOKEN"{
"available": true,
"revenueVisible": false,
"events": [
{
"event": "lead",
"timestamp": "2026-06-09T18:02:11.000Z",
"country": "US",
"region": "California",
"city": "Los Angeles",
"device": "mobile",
"browser": "Mobile Safari",
"os": "iOS",
"refererDomain": "instagram.com",
"linkId": "link_…",
"shortLink": "https://swyz.io/abc",
"conversionEventName": "Share link signup"
}
],
"retrievedAt": "2026-06-10T00:00:00.000Z"
}For non‑staff callers, saleAmount is stripped from every event (and
revenueVisible is false).
POST /v1/analytics/conversion
Records a Dub lead (“Share link signup”) attributed to the signed‑in owner.
The client calls this once after sign‑up when the dub_id click cookie is
present — i.e. the user arrived via a Swayzio share link.
This is best‑effort and never errors: if there is no click cookie or Dub
rejects the call, it returns { tracked: false, reason } so the client can stop
retrying. Dub dedupes on (customer, eventName), so repeat calls are harmless.
The body is empty; the click id is read from the request cookie, and the
customer is the authenticated owner.
curl -s -X POST https://app.swayzio.com/api/swayzio/v1/analytics/conversion \
-H "Authorization: Bearer $SWAYZIO_TOKEN" \
-H "Cookie: dub_id=<click-id-from-share-link>"{ "tracked": true }// no click cookie present
{ "tracked": false, "reason": "no_click_id" }See Data Contracts for the full dubAnalyticsResponseSchema and
dubAnalyticsEventsResponseSchema field detail.