# Reports and stats endpoints

These routes return click reports for one short link and site-wide stats for the dashboard. Each entry lists the parameters, response fields and errors.
Page: https://docs.linkmanagerpro.com/developers/rest-api/reports
Use when someone asks: link manager pro click stats api; short link report api; click statistics rest api; export clicks csv api
Intent: Reference

Use these routes to read click numbers: one short link's report, its chart data, its recent clicks and a CSV download, plus the site-wide stats behind the dashboard. Sign-in, permissions, the response envelope and paging work as in the [REST API overview](https://docs.linkmanagerpro.com/developers/rest-api/).

All paths are relative to the base URL `/wp-json/link-manager-pro/v1`. Examples use `https://example.com` and an Application Password placeholder (`USER:APPLICATION_PASSWORD`). Inside WordPress, send the `X-WP-Nonce` header instead.

Every route here needs the permission `link_manager_pro_access` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions)) and answers `200` on success.

```bash
curl -u USER:APPLICATION_PASSWORD \
  "https://example.com/wp-json/link-manager-pro/v1/stats/clicks?start=2026-10-01&end=2026-10-23"
```

## How the numbers are counted

Read this section once. The entries refer back to it.

### Dates and time zones

- Date parameters (`start`, `end`) are site-local days written `Y-m-d`, in the time zone set in WordPress. A value that is not a string, not that exact shape or not a real date (`2026-02-31`) counts as missing.
- Click times are stored in UTC. A day boundary means midnight in the site time zone, so a day can be 23 or 25 hours long where clocks change.
- Chart rows are bucketed by site-local day, using the offset in force at each click.
- Fields that hold a stored time (`clicked_at`, `first_click_at`, `last_click_at`, `created_at`) are UTC strings written `Y-m-d H:i:s`. The CSV export is the exception: it writes site time.

### Date ranges

Routes that take a range behave in one of two ways.

| Kind | Routes | Missing or invalid dates | Limits |
|---|---|---|---|
| Resolved range | `/stats`, `/stats/clicks`, `/stats/heatmap`, `/stats/countries`, `/links/{id}/clicks` (when you send a date) | A missing `end` is the site's today. A missing `start` is 30 days ending on `end` (the end day and the 29 days before it). | Reversed dates are swapped. `end` never goes past the site's today. The span is at most 7305 days (20 years); an older `start` is moved up silently. |
| Open range | `/links/{id}/report`, `/links/{id}/report/export` | A missing or invalid side has no bound. Neither date means all time. | No cap and no swap. A `start` after `end` matches no clicks. |

`GET /links/{id}/clicks` with neither `start` nor `end` is all time: it starts on the site-local day of the earlier of the link's creation and its first click, and ends on the site's today.

### Which clicks count

- Site-wide routes count recorded clicks on short links. Clicks on the hidden links behind redirects, and internal-link clicks, are left out.
- Per-link routes (`/links/{id}/...`) count every recorded click of that link ID, including the ID of a redirect's hidden link.
- A click is recorded only while click tracking is on. When bot detection is on, requests the tracker filters out (bots and blocked IP addresses) are not recorded, so they never appear here.
- Old clicks are removed by the click retention setting, so totals cover the kept history.

### Caching

| What | Cache |
|---|---|
| `GET /links/{id}/report` (entity, summary, breakdowns, recent) | Cached as a transient for the setting `report_cache_ttl` in the Advanced group (default `600` seconds, `0` to `86400`; `0` turns the cache off). The `link_manager_pro_report_cache_ttl` filter can change it. The key follows the date bounds, the link's `updated_at` and a report generation number that most writes bump. |
| `GET /stats` (`range`, `previous`, `all_time`, and the totals) | Cached for 60 seconds per range. `window`, `site`, `health.last_scan` and the tracking fields are never cached. |
| `GET /stats/top`, `GET /stats/links` | Cached for 60 seconds per `type` and `limit`. |
| Other routes | Read live. |

A cache entry is dropped at once when a link, affiliate, redirect or link-health write happens, or a click purge or backup restore runs.

### Short addresses

Fields named `short_url` hold the public address the plugin builds: the site URL, a prefix and the slug. Which prefix applies is explained under [Affiliate link prefix](https://docs.linkmanagerpro.com/guides/settings/general#affiliate-link-prefix).

## Breakdown and click objects

### The breakdown item

Report breakdowns are arrays of items, most clicks first, ties by label without regard to case.

| Field | Type | What it holds |
|---|---|---|
| `label` | string | The group name. |
| `count` | integer | Clicks in that group. |

### The recent click object

`recent` in the report and each row of the recent-clicks route share one shape.

| Field | Type | What it holds |
|---|---|---|
| `clicked_at` | string | When the click happened, UTC, `Y-m-d H:i:s`. |
| `is_unique` | boolean | Whether the click was the visitor's first. |
| `post_id` | integer | The post the click came from. `0` when none. |
| `device` | string | `Desktop`, `Mobile`, `Tablet` or `Other`, from the user agent. |
| `os` | string | The OS family, for example `Windows`, `macOS`, `iOS`, `Android`, `Linux` or `Other`. |
| `browser` | string | The browser family, for example `Chrome`, `Firefox`, `Edge`, `Opera`, `Samsung Internet` or `Other`. |
| `referrer_domain` | string or null | The referring host, when stored. |
| `referrer_url` | string or null | The referring URL, when stored. |
| `country_code` | string or null | The two-letter country code. `null` when empty. |
| `post_title` | string or null | The plain-text title of the post, or `Post #ID` when it has no title or no longer exists. `null` when `post_id` is `0`. |

## Short link reports

### Get a short link's click series

`GET /links/{id}/clicks`

Returns one row per day with total and unique clicks, for the chart of one short link.

**Permission:** `link_manager_pro_access` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Path parameters:**

| Name | Type | What it is |
|---|---|---|
| `id` | integer | The short link ID. |

**Query parameters:**

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `start` | string | No | all time | First day, `Y-m-d`, site-local. See [Date ranges](#date-ranges). |
| `end` | string | No | the site's today | Last day, `Y-m-d`, site-local. |

**Response:** `200`. `data` is an array with one item per day in the range, oldest first. Days without clicks are included with zeros.

| Field | Type | What it holds |
|---|---|---|
| `date` | string | The site-local day, `Y-m-d`. |
| `total` | integer | Clicks that day. |
| `unique` | integer | Unique clicks that day. |

```json
{
  "success": true,
  "data": [
    { "date": "2026-10-21", "total": 4, "unique": 3 },
    { "date": "2026-10-22", "total": 0, "unique": 0 }
  ]
}
```

**Errors:**

| Status | When |
|---|---|
| `404` | No short link has that ID. |

### Get a short link's report

`GET /links/{id}/report`

Returns the totals, breakdowns and newest clicks of one short link for an optional date range.

**Permission:** `link_manager_pro_access` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Path parameters:**

| Name | Type | What it is |
|---|---|---|
| `id` | integer | The short link ID. |

**Query parameters:**

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `start` | string | No | no bound | First day, `Y-m-d`, site-local. An empty or invalid value counts as missing. |
| `end` | string | No | no bound | Last day, `Y-m-d`, site-local. The whole day is included. |

**Response:** `200`. `data` holds:

| Field | Type | What it holds |
|---|---|---|
| `entity` | object | The link and live extras, fields below. |
| `summary` | object | Totals for the range, fields below. |
| `breakdowns` | object | Click groups for the range, fields below. |
| `recent` | array | Up to 20 newest clicks in the range, newest first by ID. Each is [a recent click object](#the-recent-click-object). |
| `tracking` | object | Tracking settings that affect how to read the numbers, fields below. |

`entity`:

| Field | Type | What it holds |
|---|---|---|
| `type` | string | Always `link`. |
| `id` | integer | The link ID. |
| `name` | string | The slug. |
| `slug` | string | The slug. |
| `url` | string | The destination URL. |
| `forward_params` | boolean | Whether the visitor's query string passes on to the destination. |
| `created_at` | string or null | When the link was created, UTC, `Y-m-d H:i:s`. |
| `short_url` | string | The public short address (live, not cached). |
| `lifetime_clicks` | integer | The link's lifetime click counter. Ignores the date range and can differ from `summary.clicks`. |
| `placed_in` | integer | How many published posts have content that links to this short link. |
| `rank` | object or null | Where the lifetime counter ranks: `position` (1 is most, ties share it), `total` (all links) and `share` (percent of all lifetime clicks, one decimal). |

`summary`:

| Field | Type | What it holds |
|---|---|---|
| `clicks` | integer | Clicks in the range. |
| `unique_clicks` | integer | Unique clicks. |
| `qualified_clicks` | integer | Clicks that came from a post (`post_id` above 0). |
| `conversion_label` | string | Always `Qualified Clicks`. |
| `first_click_at` | string or null | Earliest click in the range, UTC. `null` with no clicks. |
| `last_click_at` | string or null | Latest click in the range, UTC. `null` with no clicks. |

`breakdowns` (each list holds at most 10 [breakdown items](#the-breakdown-item) unless noted):

| Field | Type | What it holds |
|---|---|---|
| `device` | array | Clicks by device type. |
| `os` | array | Clicks by OS family. |
| `browser` | array | Clicks by browser family. |
| `country` | array | Clicks by country code. An empty code is labeled `Unknown`. |
| `referrer` | array | Clicks by referring host. An empty host is labeled `Direct`. |
| `utm` | object | `source`, `medium` and `campaign` arrays of breakdown items (top 10 of the inbound UTM values), and `totals` with the integer count of all tagged clicks for each of the three. |
| `source` | object | `content` (from a post on this site), `site` (from another page of this site, such as a menu, widget, the home page or an archive), `referral` (another site) and `direct` (no referrer). Integers that add up to `clicks`. |
| `posts` | array | Top 10 posts sending clicks. Each item has `post_id` (integer), `label` (the title) and `count` (integer). |
| `sites` | array | Top 10 referring hosts for clicks not sent from a post. This site's own host is left out. |

Device, OS and browser are counted from the 2000 most frequent distinct user agents.

`tracking`:

| Field | Type | What it holds |
|---|---|---|
| `tracking_enabled` | boolean | Whether click tracking is on. |
| `retention_days` | integer | Days of click history kept. `0` keeps it forever. |
| `autolink_site_enabled` | boolean | Whether auto-linking is on for the site. |
| `history_trusted` | boolean | Whether the last 90 days were fully recorded. |
| `history_reason` | string | Why not, when `history_trusted` is `false`: `tracking_off`, `retention_short`, `tracking_resumed`, `retention_changed`, `clicks_restored` or `gap`. Empty string when trusted. |

```json
{
  "success": true,
  "data": {
    "entity": {
      "type": "link",
      "id": 12,
      "name": "spring-sale",
      "slug": "spring-sale",
      "url": "https://example.com/shop/spring",
      "forward_params": false,
      "created_at": "2026-09-30 09:15:00",
      "short_url": "https://example.com/link/spring-sale",
      "lifetime_clicks": 240,
      "placed_in": 3,
      "rank": { "position": 2, "total": 48, "share": 12.5 }
    },
    "summary": {
      "clicks": 31,
      "unique_clicks": 24,
      "qualified_clicks": 18,
      "conversion_label": "Qualified Clicks",
      "first_click_at": "2026-10-01 07:02:11",
      "last_click_at": "2026-10-22 18:40:03"
    },
    "breakdowns": {
      "device": [{ "label": "Desktop", "count": 20 }],
      "os": [{ "label": "Windows", "count": 14 }],
      "browser": [{ "label": "Chrome", "count": 19 }],
      "country": [{ "label": "US", "count": 17 }],
      "referrer": [{ "label": "Direct", "count": 13 }],
      "utm": {
        "source": [{ "label": "newsletter", "count": 5 }],
        "medium": [],
        "campaign": [],
        "totals": { "source": 5, "medium": 0, "campaign": 0 }
      },
      "source": { "content": 18, "site": 3, "referral": 4, "direct": 6 },
      "posts": [{ "post_id": 77, "label": "Spring picks", "count": 12 }],
      "sites": [{ "label": "news.example.org", "count": 4 }]
    },
    "recent": [
      {
        "clicked_at": "2026-10-22 18:40:03",
        "is_unique": true,
        "post_id": 77,
        "device": "Desktop",
        "os": "Windows",
        "referrer_domain": "example.com",
        "referrer_url": "https://example.com/spring-picks/",
        "browser": "Chrome",
        "country_code": "US",
        "post_title": "Spring picks"
      }
    ],
    "tracking": {
      "tracking_enabled": true,
      "retention_days": 0,
      "autolink_site_enabled": true,
      "history_trusted": true,
      "history_reason": ""
    }
  }
}
```

**Errors:**

| Status | When |
|---|---|
| `404` | No short link has that ID. The WordPress error code is `link_not_found` and the message is "Link not found". |

### Export a short link's clicks as CSV

`GET /links/{id}/report/export`

Downloads every recorded click of one short link as a CSV file, newest first.

**Permission:** `link_manager_pro_access` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Path parameters:**

| Name | Type | What it is |
|---|---|---|
| `id` | integer | The short link ID. |

**Query parameters:**

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `start` | string | No | no bound | First day, `Y-m-d`, site-local. An empty, malformed or impossible date counts as missing. |
| `end` | string | No | no bound | Last day, `Y-m-d`, site-local. The whole day is included. |

**Response:** `200`. The body is a CSV file, not the JSON envelope. The response sends these headers:

| Header | Value |
|---|---|
| `Content-Type` | `text/csv; charset=utf-8` |
| `Content-Disposition` | `attachment; filename="link-manager-pro-report-SLUG-YYYY-MM-DD.csv"`. `SLUG` is the link slug made safe for a file name. The date is the site's today. |
| `Cache-Control` | `no-cache, no-store, must-revalidate` |

The first row holds these column names. Each later row is one click, newest first (by click time, then ID). The file is streamed 500 rows at a time and has no paging parameters.

| Column | What it holds |
|---|---|
| `Clicked At` | The click time in site time, `Y-m-d H:i:s`. |
| `Link Slug` | The link's slug. |
| `Target URL` | The destination URL recorded for the click. |
| `Country` | The two-letter country code, or empty. |
| `Referrer Domain` | The referring host, or empty. |
| `Referrer URL` | The referring URL, or empty. |
| `Device` | `Desktop`, `Mobile`, `Tablet` or `Other`. |
| `OS` | The OS family. |
| `Browser` | The browser family. |
| `Unique` | `yes` or `no`. |
| `Post ID` | The post the click came from, or empty. |
| `UTM Source` | The inbound `utm_source` value, or empty. |
| `UTM Medium` | The inbound `utm_medium` value, or empty. |
| `UTM Campaign` | The inbound `utm_campaign` value, or empty. |
| `UTM Term` | The inbound `utm_term` value, or empty. |
| `UTM Content` | The inbound `utm_content` value, or empty. |

A cell that starts with `=`, `+`, `-`, `@`, a tab or a carriage return gets a leading single quote, so spreadsheets do not run it as a formula.

```bash
curl -u USER:APPLICATION_PASSWORD -o clicks.csv \
  "https://example.com/wp-json/link-manager-pro/v1/links/12/report/export?start=2026-10-01"
```

```text
Clicked At,"Link Slug","Target URL",Country,"Referrer Domain","Referrer URL",Device,OS,Browser,Unique,"Post ID","UTM Source","UTM Medium","UTM Campaign","UTM Term","UTM Content"
"2026-10-22 18:40:03",spring-sale,https://example.com/shop/spring,US,example.com,https://example.com/spring-picks/,Desktop,Windows,Chrome,yes,77,newsletter,,,,
```

**Errors:**

| Status | When |
|---|---|
| `404` | No short link has that ID. The error comes before any CSV is sent. |

### List a short link's recent clicks

`GET /links/{id}/report/recent`

Returns a page of the newest clicks of one short link, with no date filter.

**Permission:** `link_manager_pro_access` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Path parameters:**

| Name | Type | What it is |
|---|---|---|
| `id` | integer | The short link ID. |

**Query parameters:**

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `page` | integer | No | `1` | The page to return. See [Pagination](https://docs.linkmanagerpro.com/developers/rest-api/#pagination). |
| `per_page` | integer | No | `10` | Rows per page, kept between 1 and 100. When you send the parameter, a missing, empty, zero, negative or non-numeric value gives the default `10`. The cap of 100 stays. |

**Response:** `200`. `data` is a page object ([Pagination](https://docs.linkmanagerpro.com/developers/rest-api/#pagination)). Each row of `data.data` is [a recent click object](#the-recent-click-object). Rows are ordered newest first by click ID.

```json
{
  "success": true,
  "data": {
    "current_page": 1,
    "data": [
      {
        "clicked_at": "2026-10-22 18:40:03",
        "is_unique": true,
        "post_id": 0,
        "device": "Mobile",
        "os": "iOS",
        "referrer_domain": null,
        "referrer_url": null,
        "browser": "Safari",
        "country_code": "DE",
        "post_title": null
      }
    ],
    "per_page": 10,
    "total": 31,
    "last_page": 4
  }
}
```

The example shows part of the page object. The other page fields are listed in the [REST API overview](https://docs.linkmanagerpro.com/developers/rest-api/#pagination).

**Errors:**

| Status | When |
|---|---|
| `404` | No short link has that ID. |

## Site-wide stats

### Get the dashboard stats

`GET /stats`

Returns the dashboard numbers: all-time totals, the click KPIs for a date range and the previous period, link health counts and the site calendar.

**Permission:** `link_manager_pro_access` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Query parameters:**

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `start` | string | No | 30 days ending on `end` | First day, `Y-m-d`, site-local. See [Date ranges](#date-ranges). |
| `end` | string | No | the site's today | Last day, `Y-m-d`, site-local. |

**Response:** `200`. `data` holds:

| Field | Type | What it holds |
|---|---|---|
| `totalLinks` | integer | Short links, not counting the hidden links behind redirects. |
| `totalClicks` | integer | All recorded short-link clicks. |
| `uniqueClicks` | integer | Unique recorded short-link clicks. |
| `brokenCount` | integer | Active link health issues (content plus traffic). |
| `notFoundCount` | integer | Active traffic issues, which are visitor 404s. |
| `range` | object | Click KPIs for the resolved range, fields below. |
| `previous` | object | The same KPIs for the equal-length period right before `start`. |
| `all_time` | object | All-time totals, fields below. |
| `health` | object | `content` (integer), `traffic` (integer), `traffic_24h` (integer, distinct 404 pages seen in the last 24 hours) and `last_scan`. |
| `window` | object | The resolved range: `start`, `end` (strings), `days` (integer) and `previous` with the same three fields. |
| `site` | object | `today` (string `Y-m-d`), `presets`, `timezone` (string) and `startOfWeek` (integer, `0` is Sunday). |

`health.last_scan` is `null` until a link health scan has finished. Otherwise it holds `posts`, `checked`, `unchecked` and `started_at` (integers) and `finished_at` (string).

`site.presets` has the keys `7d`, `14d` and `30d`. Each holds the same fields as `window`, resolved against the site's today.

`range`:

| Field | Type | What it holds |
|---|---|---|
| `start`, `end` | string | The resolved days, `Y-m-d`. |
| `days` | integer | Days in the range, both ends included. |
| `clicks` | integer | Short-link clicks in the range. |
| `unique` | integer | Unique clicks in the range. |
| `from_posts` | integer | Clicks that came from a post. |
| `partner_clicks` | integer | Clicks on short links that go to an affiliate's domain. |
| `links_total` | integer | Short links, not counting the hidden redirect links. |

`previous` holds `start`, `end`, `days` (the previous period, as in `window.previous`), `clicks`, `unique`, `from_posts` and `partner_clicks`.

`all_time`:

| Field | Type | What it holds |
|---|---|---|
| `clicks_today` | integer | Clicks since midnight, site time. |
| `clicks` | integer | All recorded short-link clicks. |
| `unique` | integer | Unique clicks. |
| `unique_share` | number | Unique clicks as a percent of clicks, one decimal. |
| `partner_clicks` | integer | Clicks on short links to affiliate domains. |
| `partner_share` | number | Partner clicks as a percent of clicks, one decimal. |
| `partners_total` | integer | Affiliates. |
| `partners_active` | integer | Affiliates with at least one click. |
| `partner_links` | integer | Short links that go to an affiliate domain. |
| `autolink_links` | integer | Short links with auto-linking on. |
| `autolink_partners` | integer | Affiliates with auto-linking on. |
| `links_total` | integer | Short links, not counting the hidden redirect links. |
| `redirect_hits` | integer | Hits on redirects. |
| `internal_clicks` | integer or null | Internal-link clicks. `null` unless click tracking and internal-link tracking are both on. |
| `lifetime_clicks` | integer | The sum of the links' lifetime counters. |
| `tracking_enabled` | boolean | Whether click tracking is on. |
| `retention_days` | integer | Days of click history kept. `0` keeps it forever. |
| `autolink_site_enabled` | boolean | Whether auto-linking is on for the site. |
| `history_trusted` | boolean | Whether the last 90 days were fully recorded. |
| `history_reason` | string | The reason when not trusted, as in the report's `tracking` object. |

```json
{
  "success": true,
  "data": {
    "totalLinks": 48,
    "totalClicks": 1520,
    "uniqueClicks": 1104,
    "brokenCount": 3,
    "notFoundCount": 1,
    "range": {
      "start": "2026-09-24", "end": "2026-10-23", "days": 30,
      "clicks": 410, "unique": 301, "from_posts": 255,
      "partner_clicks": 96, "links_total": 48
    },
    "previous": {
      "start": "2026-08-25", "end": "2026-09-23", "days": 30,
      "clicks": 380, "unique": 280, "from_posts": 240, "partner_clicks": 88
    },
    "all_time": {
      "clicks_today": 7, "clicks": 1520, "unique": 1104, "unique_share": 72.6,
      "partner_clicks": 410, "partner_share": 27.0, "partners_total": 5,
      "partners_active": 4, "partner_links": 12, "autolink_links": 9,
      "autolink_partners": 3, "links_total": 48, "redirect_hits": 62,
      "internal_clicks": null, "lifetime_clicks": 1530,
      "tracking_enabled": true, "retention_days": 0,
      "autolink_site_enabled": true, "history_trusted": true, "history_reason": ""
    },
    "health": {
      "content": 2, "traffic": 1, "traffic_24h": 1,
      "last_scan": { "posts": 120, "finished_at": "2026-10-22 03:10:00", "checked": 640, "unchecked": 0, "started_at": 1792638000 }
    },
    "window": {
      "start": "2026-09-24", "end": "2026-10-23", "days": 30,
      "previous": { "start": "2026-08-25", "end": "2026-09-23", "days": 30 }
    },
    "site": {
      "today": "2026-10-23",
      "presets": { "7d": {}, "14d": {}, "30d": {} },
      "timezone": "Europe/Berlin",
      "startOfWeek": 1
    }
  }
}
```

In the example, each `presets` entry is shortened to `{}`. The code fills it with the `window` fields.

**Errors:** None specific to this route.

### List the top partners

`GET /stats/affiliates`

Returns up to 10 affiliates with the most recorded clicks on short links that go to their domain.

**Permission:** `link_manager_pro_access` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Query parameters:** None.

**Response:** `200`. `data` is an array of at most 10 items, most clicks first. Ties go to the older affiliate (lower ID). Affiliates with no clicks are included while there are fewer than 10.

| Field | Type | What it holds |
|---|---|---|
| `id` | integer | The affiliate ID. |
| `site_name` | string | The affiliate's name. |
| `domain` | string | The affiliate's domain. |
| `click_count` | integer | Recorded clicks on short links to that domain, all time. It is not the stored counter on the affiliate. |
| `enable_autolink` | boolean | Whether auto-linking uses this affiliate. |

```json
{
  "success": true,
  "data": [
    { "id": 3, "site_name": "Example Store", "domain": "store.example.com", "click_count": 210, "enable_autolink": true }
  ]
}
```

**Errors:** None specific to this route.

### Get the site clicks series

`GET /stats/clicks`

Returns one row per day with total and unique short-link clicks for the chart on the dashboard.

**Permission:** `link_manager_pro_access` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Query parameters:**

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `start` | string | No | 30 days ending on `end` | First day, `Y-m-d`, site-local. See [Date ranges](#date-ranges). |
| `end` | string | No | the site's today | Last day, `Y-m-d`, site-local. |

**Response:** `200`. `data` is an array with one item per day, oldest first, with zeros for quiet days. Each item has the fields of the [short link series](#get-a-short-link-s-click-series): `date`, `total` and `unique`.

```json
{
  "success": true,
  "data": [
    { "date": "2026-10-22", "total": 9, "unique": 7 },
    { "date": "2026-10-23", "total": 7, "unique": 5 }
  ]
}
```

**Errors:** None specific to this route.

### Get clicks by country

`GET /stats/countries`

Returns short-link clicks grouped by country code, for the geo map.

**Permission:** `link_manager_pro_access` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Query parameters:**

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `start` | string | No | 30 days ending on `end` | First day, `Y-m-d`, site-local. See [Date ranges](#date-ranges). The route never returns all time. |
| `end` | string | No | the site's today | Last day, `Y-m-d`, site-local. |

**Response:** `200`. `data` holds:

| Field | Type | What it holds |
|---|---|---|
| `rows` | array | Countries, most clicks first. Each item has `code` (string, upper-case two letters) and `total` (integer). |
| `total` | integer | All clicks in the range, including the unresolved ones. |
| `unresolved` | integer | Clicks with an empty code, the unknown code `XX` or a code that is not two letters. They have no row. |

```json
{
  "success": true,
  "data": {
    "rows": [{ "code": "US", "total": 190 }, { "code": "DE", "total": 64 }],
    "total": 270,
    "unresolved": 16
  }
}
```

**Errors:** None specific to this route.

### Get the click heatmap

`GET /stats/heatmap`

Returns short-link clicks grouped by weekday and hour, in site time, for the dashboard heatmap.

**Permission:** `link_manager_pro_access` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Query parameters:**

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `start` | string | No | 30 days ending on `end` | First day, `Y-m-d`, site-local. See [Date ranges](#date-ranges). |
| `end` | string | No | the site's today | Last day, `Y-m-d`, site-local. |

**Response:** `200`. `data` holds:

| Field | Type | What it holds |
|---|---|---|
| `cells` | array | Only the cells that have clicks. Each cell is `[hour, weekday, count]`: `hour` is 0 to 23, `weekday` is 0 (Monday) to 6 (Sunday). |
| `max` | integer | The largest cell count. |
| `total` | integer | All clicks in the cells. |
| `by_day` | array of 7 integers | Clicks per weekday, Monday first. |
| `by_hour` | array of 24 integers | Clicks per hour, midnight first. |
| `busiest` | object or null | `null` when `total` is `0`. Otherwise `days` and `hours`, each with `from` and `to` (inclusive bucket indexes; `to` below `from` means the span wraps past Sunday or 23:00), `clicks` (integer) and `share` (percent of all clicks, one decimal). |

A busiest span starts at the peak and grows to a neighbor that has at least 75 percent of the peak. It spans at most 3 weekdays or 3 hours.

```json
{
  "success": true,
  "data": {
    "cells": [[9, 1, 14], [10, 1, 22]],
    "max": 22,
    "total": 36,
    "by_day": [0, 36, 0, 0, 0, 0, 0],
    "by_hour": [0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    "busiest": {
      "days": { "from": 1, "to": 1, "clicks": 36, "share": 100.0 },
      "hours": { "from": 9, "to": 10, "clicks": 36, "share": 100.0 }
    }
  }
}
```

**Errors:** None specific to this route.

### List the top short links

`GET /stats/links`

Returns the short links with the most recorded clicks. The wp-admin dashboard widget reads the same list.

**Permission:** `link_manager_pro_access` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Query parameters:**

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `limit` | integer | No | `5` | How many links to return. A value of 0 or less, or one that is not a number, gives `5`. A value above `100` is cut to `100`. |

**Response:** `200`. `data` is an array, most clicks first and newest link first on ties. Links with no recorded click and the hidden redirect links are left out. The list is cached for 60 seconds per `limit`.

| Field | Type | What it holds |
|---|---|---|
| `id` | integer | The link ID. |
| `slug` | string | The slug. |
| `target_url` | string | The destination URL. |
| `click_count` | integer | Recorded clicks in the kept history. It is not the lifetime counter. |

```json
{
  "success": true,
  "data": [
    { "id": 12, "slug": "spring-sale", "target_url": "https://example.com/shop/spring", "click_count": 240 }
  ]
}
```

**Errors:** None specific to this route.

### List the top posts

`GET /stats/posts`

Returns up to 10 posts that sent the most short-link clicks.

**Permission:** `link_manager_pro_access` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Query parameters:** None.

**Response:** `200`. `data` is an array, most clicks first. Redirect hits and internal-link clicks do not count. Posts that no longer exist are left out by the query, so it returns 10 posts whenever 10 existing posts sent clicks.

| Field | Type | What it holds |
|---|---|---|
| `post_id` | integer | The post ID. |
| `post_title` | string | The post's stored title. |
| `click_count` | integer | Clicks sent from that post, all time. |
| `status` | string or null | A label such as "Draft", or `null` for a public post. |
| `url` | string or null | The permalink. `null` for a post that isn't public or private. |
| `edit_url` | string or null | The edit link. `null` for a trashed post. |

```json
{
  "success": true,
  "data": [
    {
      "post_id": 77,
      "post_title": "Spring picks",
      "click_count": 132,
      "status": null,
      "url": "https://example.com/spring-picks/",
      "edit_url": "https://example.com/wp-admin/post.php?post=77&action=edit"
    }
  ]
}
```

**Errors:** None specific to this route.

### Get a top list

`GET /stats/top`

Returns the ranked rows for one "What's working" list: short links, campaigns, partners or posts, all time.

**Permission:** `link_manager_pro_access` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Query parameters:**

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `type` | string | Yes | none | `links`, `campaigns`, `partners` or `posts`. Case does not matter and spaces around it are trimmed. |
| `limit` | integer | No | `10` | Rows to return, kept between 1 and 25. A value below 1 or not a whole number gives `10`. |

**Response:** `200`. `data` holds:

| Field | Type | What it holds |
|---|---|---|
| `type` | string | The list that was asked for. |
| `total` | integer | How many items the list ranks: all short links, all campaigns, all partners, or posts with at least one click. |
| `clicks_total` | integer | All recorded short-link clicks, for working out each row's share. |
| `rows` | array | Largest first. Items with no clicks are left out. Fields depend on `type`, below. |

Rows for `links`: `id` (integer), `slug`, `short_url`, `target_url` (strings) and `clicks` (integer).

Rows for `campaigns`: `slug`, `name` (strings), `managed` (boolean), `link_count`, `clicks` and `unique_clicks` (integers). They come from the first page of the campaign list, sorted by total clicks.

Rows for `partners`: `id` (integer), `name`, `domain` (strings), `clicks`, `unique_clicks` and `link_count` (integers). Ties go to the lower ID.

Rows for `posts`: `post_id` (integer), `title` (string, empty when the post is gone), `exists` (boolean), `status` (string or null, WordPress's label for a status such as Draft, `null` for a public post or a missing one), `url` (string or null, only for a public or private post), `edit_url` (string or null, never for a trashed or deleted post) and `clicks` (integer). Ties go to the lower post ID.

The list is cached for 60 seconds per `type` and `limit`.

```json
{
  "success": true,
  "data": {
    "type": "links",
    "total": 48,
    "clicks_total": 1520,
    "rows": [
      {
        "id": 12,
        "slug": "spring-sale",
        "short_url": "https://example.com/link/spring-sale",
        "target_url": "https://example.com/shop/spring",
        "clicks": 240
      }
    ]
  }
}
```

**Errors:**

| Status | When |
|---|---|
| `422` | `type` is missing or not one of the four values. The error is on the field `type`: "Choose links, campaigns, partners or posts." |

## Related

- [REST API overview](https://docs.linkmanagerpro.com/developers/rest-api/)
- [Short links endpoints](https://docs.linkmanagerpro.com/developers/rest-api/links)
- [Campaigns and UTM endpoints](https://docs.linkmanagerpro.com/developers/rest-api/campaigns)
- [Affiliates endpoints](https://docs.linkmanagerpro.com/developers/rest-api/affiliates)
- [Database tables](https://docs.linkmanagerpro.com/developers/data/tables)
