Exporting Links (CSV)
Export every link in a project — together with its click and install analytics — to a CSV file. Exports are a paid-plan feature and run as a background job, so a large project never blocks your browser. When the file is ready you get an email and a download link.
Exporting from the dashboard
There are two places to start an export:
From the Links page (filtered export)
- Go to Dashboard → Links.
- (Optional) Apply a search or a date range. The date range on this page is an activity window: the list shows every link with its clicks and installs counted inside that range, and the export uses exactly the same window, so the CSV matches the numbers on screen.
- Click Export CSV, then choose:
- Summary by link for one row per link with aggregate performance.
- Individual clicks for one row per retained click in the original 17-column format.
- Individual clicks + locations for the version 2 file with separate click and app-match geography.
From the Analytics overview (time-range export)
- Go to Dashboard → Links → Analytics.
- Pick a time range (24 Hours, 7 Days, 30 Days or All Time). The headline numbers and the list update to that window.
- Click Export CSV and choose an export type. Summary by link exports
every link in the project with its
Clicks,New Installs,Reinstallsand the top-X / attributed columns counted inside the selected window, and the window is stamped in thePeriod Start/Period Endcolumns. With All Time those two columns are empty and the numbers are lifetime totals. Individual clicks exports each click whose timestamp falls inside the selected window.
The job starts immediately. You can keep working; when it finishes you'll receive an "export ready" email with a download link, and the download also appears in the dashboard.
Summary export columns
One row per link, with these columns:
| Column | Description |
|---|---|
| Name | The link's name |
| Type | dynamic or unified |
| Slug | The link's slug |
| Short URL | The full short link |
| Created At | When the link was created |
| Clicks | Total clicks |
| New Installs | Installs attributed to the link |
| Returning Users | Deprecated compatibility alias for Reinstalls; despite the legacy name, this value contains reinstall rows, not ordinary returning sessions |
| Attributed Clicks | Clicks matched to an installation |
| Top Device | Most common device |
| Top OS | Most common operating system |
| Top Browser | Most common browser |
| Top Country | Most common country |
| Last Clicked At | Timestamp of the most recent click |
| Period Start | Start of the activity window the numbers were counted in (empty = lifetime) |
| Period End | End of the activity window (empty = lifetime) |
| Reinstalls | Reinstall rows attributed to the link; use this canonical column for new integrations |
Individual-click export columns
The click-level export has one row per retained click, ordered chronologically. It is a snapshot through the time it was requested, so clicks arriving while a large file is generated cannot shift its pages or produce duplicate rows.
| Column | Description |
|---|---|
| Click ID | Unique click-event identifier |
| Clicked At UTC | Exact click timestamp in UTC |
| Link Name | The link's display name |
| Slug | The link's slug |
| Short URL | The full short link |
| Referrer | Referring page when available |
| UTM Source / Medium / Campaign | Captured campaign parameters |
| Device / Operating System / Browser | Derived device information |
| Country / Region / City | Derived approximate location |
| Channel | QR or Direct / unknown |
| Attributed Install | Whether the click was matched to an installation |
The Individual clicks + locations option requests click export version 2,
which appends these six columns. The original dashboard option and API clients
that omit clickExportVersion continue to receive the original 17 columns.
API clients can request version 2 with
"clickExportVersion": 2 alongside "kind": "clicks".
| Version 2 column | Description |
|---|---|
| Click Location Source | Unverified request IP (client-reported direct route) when the client reported a direct API resolve; Unverified request IP otherwise. The client-reported route is not trusted proof of the network path. Blank if no click location was resolved. |
| Attributed App Match At UTC | Time the SDK deferred-match request attributed the click. Blank for older matches without a snapshot. |
| App Country / App Region / App City | Approximate location of that SDK match request, kept separate from click location. Blank when unavailable. |
| App Location Source | Attributed SDK match IP when app geography is available. |
The two locations may differ. IP-based geography is approximate; historical click locations can reflect an intermediate proxy and are not retroactively corrected. An attributed install without an app-match snapshot leaves the new app-location cells blank. They also remain blank when attribution used a link-level fallback and could not identify the exact browser click. New deferred matches carry the browser click's ID for this purpose; older redirect pages and unmatched click rows do not produce an app-location snapshot. New redirect pages pass a short-lived click receipt with the click ID. When the receipt matches the resolved click, app-match location can be recorded even if the device changed networks between browser and SDK requests. Older pages without a receipt retain best-effort attribution without an app-location snapshot; during a rolling deployment, new click rows opened by old page scripts require matching browser network/device signals.
Raw IP addresses, full user-agent strings and arbitrary metadata are not included in customer exports.
Limits
- 100,000 links per export. If a project (after filters) exceeds this, the job fails with a message asking you to narrow the filters — split the export by date range to stay under the cap.
- 50,000 clicks per individual-click export. Narrow the selected analytics period or link filters when a click export exceeds this limit.
- Download links are signed and valid for 7 days. Re-open the export from the dashboard to mint a fresh link after that.
How it works (API)
The dashboard drives two endpoints. They authenticate with a dashboard (Supabase) session — these are not API-key endpoints, and an export cannot be triggered with an SDK API key.
Start an export
POST https://api.ulink.ly/api/v1/projects/{projectId}/links/export
Authorization: Bearer <supabase-session-jwt>
Content-Type: application/json
{
"kind": "summary",
"search": "summer",
"createdAfter": "2026-01-01",
"createdBefore": "2026-06-30",
"sortBy": "createdAt",
"sortOrder": "desc"
}
All body fields are optional and mirror the filters on the Links page.
kind accepts summary (the default) or clicks.
To export with an activity window instead (what the dashboard's Links page and
Analytics overview do), send activeAfter / activeBefore ISO timestamps. The
click and install columns are then counted inside that window, and every link
in the project is included whether or not it had activity. createdAfter /
createdBefore still apply on top of the window if you send both, so "links
created in May, with their June clicks" is one request. An open-ended bound is
fine; if the two bounds are reversed they are swapped. Note that the 100,000-row
cap counts links, not clicks, so a narrower window does not reduce the row count;
use a creation-date range or a search to get under the cap:
{
"kind": "clicks",
"activeAfter": "2026-09-03T12:00:00.000Z",
"activeBefore": "2026-09-10T12:00:00.000Z",
"sortBy": "clickCount",
"sortOrder": "desc"
}
The response returns a job to poll:
{ "success": true, "data": { "jobId": "…", "status": "pending" } }
Poll for the result
GET https://api.ulink.ly/api/v1/projects/{projectId}/links/export/{jobId}
Authorization: Bearer <supabase-session-jwt>
{
"success": true,
"data": {
"status": "completed",
"rowCount": 1280,
"error": null,
"downloadUrl": "https://…signed-url…"
}
}
status moves through pending → processing → completed (or failed, with the
reason in error). A fresh signed downloadUrl is minted on each read of a
completed job, so you can always poll again for a new link rather than caching
the URL.
Free plans receive 402 Payment Required from the start endpoint.