Campaigns
The Campaigns tab answers one question: how is this campaign doing, counting everything it touched — the clicks on your short links and the sessions on your site.
Find it under Analytics → Campaigns.
The Two Surfaces
This is the thing to understand before reading any number on the page.
A campaign leaves traces in two different places, and they are not the same trace:
| Surface | Where it comes from | What it counts |
|---|---|---|
| Clicks | Short links carrying UTM parameters | Someone clicked your LinkForty link |
| Sessions | The web pixel on your site | Someone browsed your site with those UTMs on the URL |
Older campaign reporting showed only the first. That misses an entire ordinary case: a campaign whose UTMs are appended to the landing page URL and never touch a short link at all. Those campaigns had zero rows — not low numbers, no presence whatsoever.
The Campaigns tab joins both, so a campaign appears if either surface saw it. A row with clicks and no sessions means the pixel never saw those visitors. A row with sessions and no clicks means the campaign ran entirely through direct links.
- "How many people did my link reach?" → Clicks
- "How many people actually landed on my site?" → Sessions
They are different populations, not two measurements of one. A click that never becomes a session is a drop-off worth investigating.
Grouping
Switch between Campaign, Source, and Medium with the toggle. This changes which UTM parameter the rows are grouped by — utm_campaign, utm_source, or utm_medium.
utm_term and utm_content are not grouping options here. They reach your destination URL but are not recorded on the click row.
Reading the Table
| Column | Meaning |
|---|---|
| Clicks | Short-link clicks carrying this UTM value, bots excluded |
| Installs | App installs attributed to those clicks |
| Sessions | Distinct site sessions the pixel recorded with this UTM value |
| Conversions | Site events matching your workspace's conversion goals |
| Conv. rate | See below — the denominator changes |
| Revenue | Combined in-app and site revenue, per currency |
The Installs column is hidden when no campaign in range has any. A web-only workspace would otherwise be reading a permanently empty column.
Conversion rate has two possible denominators. It uses conversions over sessions when the campaign has sessions, and installs over clicks when it does not. When there is nothing to divide by, it shows an em dash rather than 0% — "nothing to measure" is not the same as a measured zero.
Revenue is reported per currency, never summed across them. A campaign earning ¥40,000 and $300 shows both; adding them would be meaningless.
One Value in Detail
Click any row to open that value's detail. The tiles at the top are the row's own numbers. Below them:
- Trend — clicks, sessions and conversions per day across the selected range (installs appear when there are any). Two units share the chart, so the caption above it applies here too.
- Top links — the links whose clicks carried the value, with their sessions, conversions and revenue. Click one to make it the page's selected link. Hidden when a link is already selected.
- By source / By medium / By campaign — how the value splits across the two UTM dimensions you did not group by. Not shown for the Parameter grouping, which has no other dimensions.
Hovering a value shows its UTM Library card; copying or editing from there does not open the detail. All campaigns (or sources, or mediums) returns to the table.
Where Campaign Values Come From
A click row records a UTM from either of two places:
- The short URL itself —
https://go.example.com/sale?utm_campaign=spring - The link's own UTM settings — configured on the link or its template
Values on the URL win per key. A link configured with utm_source=newsletter whose URL carries ?utm_source=partner records partner, because the value a sharer put on the URL is more specific than the link's stored configuration.
Each key resolves independently, so a link that configures only utm_source still records a utm_campaign that arrives on the URL.
Empty and whitespace-only UTMs are treated as absent on both surfaces. They are excluded before the two surfaces are joined, so the table never shows a blank-labelled row.
What Is Excluded
Some traffic is deliberately absent, and its absence is not a bug:
- Bot clicks. Classified at ingestion and excluded from every campaign figure.
- SDK-created shared links. Links your app users generate through the SDK are not campaign traffic; see Shared Links for their own reporting.
- Untagged traffic. A link with no UTMs anywhere cannot be grouped and does not appear.
The table shows the top 100 values, ordered by combined revenue, then sessions, then clicks.
One Link or All of Them
The link selector at the top of the Analytics page applies to the Campaigns tab like every other tab. With All Links the table covers the workspace; with one link selected it shows only the UTM values that link's clicks carried, and only site sessions attributed to it by its click — so a link's row is smaller than the workspace row for the same value (see Scoping to One Link below). Changing the link clears any row filter you had applied.
Time Range
The range selector applies to both surfaces at once. How far back you can look depends on your plan's analytics retention — see the plan comparison for the current windows.
Nothing Showing Up?
Campaigns appear once your links carry UTM parameters, or once the site pixel records visits with UTMs on the URL. If the table is empty:
- Confirm the links you are running actually carry UTMs — see UTM Parameters.
- If you expect sessions but see none, check that the web pixel is installed and that its allowed-domains setting includes the site.
- If you expect clicks but see none, check that the traffic is not being classified as bot traffic, and that the time range covers when the campaign ran.
- A
403on this tab means your role lacks the analytics permissions it requires. Both install and event analytics permissions are needed.
API
The same data is available at GET /api/analytics/campaigns, which accepts an API key as well as a session:
| Parameter | Values | Default |
|---|---|---|
days | 1–365 | 30 |
groupBy | campaign, source, medium, param | campaign |
paramKey | the parameter name to group by — required when groupBy=param | — |
value | filter to one value | — |
projectId | scope to one project | — |
linkId | scope to one link; wins over projectId. An id outside your workspace is a 404 | — |
Scoping to One Link
With linkId, the click side counts only that link's clicks and the web side counts only sessions that were attributed to that link by its click — never sessions matched to it by UTM guesswork. That makes a link's row smaller than the workspace row for the same value, and that is correct: the workspace view groups on the UTM each session carries, so it has nothing to guard against; a link-keyed view would otherwise count a session against the very UTM that guessed the link.
The Detail Endpoint
GET /api/analytics/campaigns/detail takes the same parameters plus a required value, and returns that value's daily trend, the links that carried it, and its split across the other UTM dimensions:
{
"groupBy": "campaign",
"value": "spring",
"byDate": [{ "date": "2026-09-11", "clicks": 0, "installs": 0, "sessions": 0, "conversions": 0 }, "…one entry per day in the window"],
"links": [{ "id": "…", "shortCode": "spring24", "title": "Spring launch", "clicks": 2, "installs": 1, "sessions": 2, "conversions": 1,
"totalRevenueByCurrency": [{ "currency": "USD", "amount": 40 }] }],
"breakdowns": { "source": [{ "value": "ig", "clicks": 2, "sessions": 2, "conversions": 1 }], "medium": [ "…" ] }
}
byDateis zero-filled and has exactlydaysentries, bucketed by calendar day (UTC). The totals in the table use a rolling window of hours, so the oldest day may hold slightly less than its share of the row.linkslists up to 25 links, ordered by revenue, then sessions, then clicks. A site session with no attributed link is counted in the table row but cannot be placed under a link here.breakdownsholds the two UTM dimensions you did not group by, up to 10 values each. It is{}forgroupBy=param, which has no other dimensions.linkIdandprojectIdscope it exactly as they scope the table, so a row and its detail cover the same traffic.
Grouping by a Passthrough Parameter
groupBy=param groups by one of the passthrough parameters your links carry, rather than by a UTM value. Name the parameter with paramKey:
curl -H "Authorization: Bearer $LINKFORTY_API_KEY" \
"https://api.linkforty.com/api/analytics/campaigns?groupBy=param¶mKey=slug&days=30"
That answers "which slug performed best" across every link that carried one.
Two differences from the UTM groupings:
- Clicks only. The web pixel records UTM values but not passthrough parameters, so
sessions,pageViews,conversionsandwebRevenueare0on every row. The dashboard hides those columns in this mode. paramKeyis required. Omitting it returns400, rather than grouping by everything.
A paramKey no click carried returns an empty row set, not an error.
The response carries revenue, webRevenue, and totalRevenue for backwards compatibility, but they add amounts across currencies as though they shared a unit. Use revenueByCurrency, webRevenueByCurrency, and totalRevenueByCurrency instead.
Related
- UTM Parameters — tagging links so they appear here
- UTM Library — keeping values consistent across a team
- Web Analytics — the session surface in detail
- Dashboard — workspace-level overview