EaserixDocs
DevelopersAPI referenceLinksAnalytics

Export a link's clicks as CSV

Raw enriched click log for one link over the requested window as a CSV attachment (at, country, device, browser, os, referrerHost, referrer, isBot). Bot rows are included and flagged, unlike the in-app analytics which exclude them. Capped at 50000 rows; a trailing "truncated" row signals the cap.

View as Markdown
GET
/v1/links/{id}/clicks.csv
AuthorizationBearer <token>

In: header

Path Parameters

id*string

Query Parameters

days?integer

Analytics window in days; 0 means all-time (no cutoff); other values fall back to 14. 365 is the dashboard's own window — wide enough for its Month/Week tabs and to always have at least one full prior year for the Year tab, since bucketing a shorter window by month/year can collapse to a single bucket with nothing to draw a line between.

Default14

Value in

  • 0
  • 7
  • 14
  • 30
  • 90
  • 365

Response Body

text/csv

application/json

application/json

application/json

application/json

application/json

curl -X GET "https://example.com/v1/links/string/clicks.csv"
"string"

Public short-link redirect

The product itself: 302 to the destination URL (recording the click) when the link exists, is active, and is unexpired — otherwise 302 to the web app's /unavailable page with a reason (notfound, paused, expired). Unauthenticated; served on esrx.ly. Resolves by short code alone — org, owner, and visibility never narrow it, so a private link still redirects for anyone holding the code. This is the only endpoint that increments a link's `clicks` (EAS-31) — see that field's description on the Link schema for the atomic-update and async-batching detail. When code starts with `@`, this same route renders the public bio page (EAS-37) instead of redirecting — there is no separate /v1/bio/{slug} route — returning 200 HTML and incrementing that page's `views` (see BioPage.views). When the link has a password set (Link.passwordProtected), a request without a valid lt query parameter 302s to the web app's /locked page instead of the destination or /unavailable — see POST /v1/public/links/{code}/unlock for how a visitor obtains that token after submitting the correct password. The check runs before routing rules, screening, and usage-cap consumption, so none of those are ever revealed or spent by a request that hasn't passed it. Retargeting pixels (EAS-78): a link that opted in (Link.retargeting) keeps this exact flow — rate limit, password, usage caps, screening, rule resolution and click recording all run first — and only then, for a visitor we may fire for, answers 200 with a small HTML interstitial instead of the 302. The page carries the already resolved and screened destination, loads the configured providers in parallel, and forwards after at most 500 ms whatever the providers do (a blocked, slow or failing provider never holds the redirect up; with JavaScript disabled it forwards immediately and fires nothing). The address bar is scrubbed of query data before any provider code runs, and the destination and page script are removed from the DOM, so lt/qr tokens and esrx_id are never visible to a pixel. Every other request — links without pixels, known bots, link-preview fetchers (WhatsApp, Skype and similar, or any fetch whose Sec-Fetch-Mode is not navigate), prefetches, visitors sending Sec-GPC or DNT (as configured), and visitors whose signed consent decision is denied — gets the ordinary 302, so a preview can still read the destination's own metadata. Every response for a link with retargeting enabled, redirects included, carries Cache-Control: no-store, because it depends on cookies, region and the visitor's signals. A visitor in a region that requires consent, or of unknown region, with no valid decision is instead shown a first-party consent page (200, no provider scripts, never framed): this happens after every gate above but BEFORE usage-cap consumption and click recording, so the decision round trip counts the visitor once. Allow/Decline post to POST /v1/public/consent, which sets a signed host-only cookie and 303s back here, where the decision is read. If the consent flow is not configured on the deployment, or the request arrived on a host a decision cannot be posted from (anything outside CONSENT_TRUSTED_ORIGINS), such visitors get the 302 instead of a prompt they could not complete. A visitor returning from a decision with a valid cr token but no cookie (cookies blocked) gets the 302 too, never a second prompt. Conversion tracking is unrelated: pixels never receive esrx_id or any conversion data.

Live click feed across the caller's own links (SSE)

EAS-83 follow-up: a workspace-scoped Server-Sent Events feed for surfaces that aren't pinned to one link (the Dashboard, Links List, Campaigns, and Analytics pages) — same underlying ~500ms click-batch pipeline as GET /v1/links/{id}/stream, just a second subscription mode over it. Emits `data: {"linkId": "...", "clicks": N, "campaignId": "...", "committedAt": "..."}` — a per-link count for that flush, not enriched per-click detail — as each affected link's batch is durably persisted, plus `: ping` heartbeat comments roughly every 25s. `campaignId` is omitted when the link has none, so a client can attribute a live click to its campaign (the Campaigns list/detail pages) without a second lookup. Scoped to exactly the links the caller owns in their active org (the same population GET /v1/analytics/summary reports on) — a teammate's org-shared link never contributes here, matching that endpoint's existing scope. Open streams are capped per workspace and per user on each replica; past a cap the response is 429 with a plain-language message and the client retries on its own. `committedAt` is when the batch was recorded, to compare with the `asOf` of a read. The server ends the stream when the caller's access token expires, so a client reconnects (refreshing its token) and refetches — clicks in the gap are not replayed. Every couple of minutes the server also re-checks that the caller still has access (their identity has not changed) and ends the stream if not.