The Microsoft Clarity Data Export API: a field reference
Every field the Clarity Data Export API actually returns, with the real metric names, the real units, and the four traps that silently corrupt an integration. Verified against live responses, not transcribed from the docs.
Microsoft Clarity's Data Export API is the only programmatic way to get Clarity data out. Microsoft documents that it exists. What Microsoft does not document is what the response actually looks like, and the gap between the two is where integrations quietly break.
This page is the field reference we needed and could not find. Everything below was verified against real responses from a live project, and where something is still unconfirmed it says so rather than guessing.
The endpoint
GET https://www.clarity.ms/export-data/api/v1/project-live-insights
Auth is a bearer JWT in the header:
curl --location 'https://www.clarity.ms/export-data/api/v1/project-live-insights?numOfDays=1&dimension1=URL' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_TOKEN'
Only a project admin can generate the token, under Settings, Data Export, Generate new API token. Three constraints on the token name that are easy to trip over:
- 4 to 32 characters
- alphanumeric plus
-,_and., no spaces - unique per project
Rotate it the moment anyone with project access leaves. There is no per-user scoping.
Parameters
| Param | Values | Meaning |
|---|---|---|
numOfDays |
1, 2, 3 |
Data window: the last 24, 48 or 72 hours. This is the entire history you can ever pull. |
dimension1 |
see below | First breakdown dimension |
dimension2 |
see below | Second breakdown (optional) |
dimension3 |
see below | Third breakdown (optional) |
Available dimensions, maximum three per request: Browser, Device, Country/Region, OS, Source, Medium, Campaign, Channel, URL.
For per-page analysis, dimension1=URL is the one that matters.
Trap 1: the metric names are not what the docs call them
This is the one that costs people days.
Microsoft's documentation writes the metrics in prose: "Rage Click Count", "Dead Click Count". The API returns them in PascalCase with no spaces:
DeadClickCount ExcessiveScroll RageClickCount
QuickbackClick ScriptErrorCount ErrorClickCount
ScrollDepth Traffic EngagementTime
Nine metric objects, in that order, confirmed against a real numOfDays=3&dimension1=URL call.
If you match on the documented prose names, every friction lookup returns nothing. Your code does not throw. It records zero rage clicks, zero dead clicks, zero errors, and reports that every page on your site is healthy. The request succeeded, the data landed, and the conclusion is wrong.
Match on the strings above, and treat an unrecognised metricName as a loud failure rather than a zero. A metric you do not recognise is a metric you are not measuring.
Trap 2: there is no raw count field
The obvious mental model is that RageClickCount returns a count of rage clicks, which you divide by sessions to get a rate. That field does not exist.
Here is a real friction row:
{
"sessionsCount": "2",
"sessionsWithMetricPercentage": 0,
"sessionsWithoutMetricPercentage": 100,
"pagesViews": "0",
"subTotal": "0",
"Url": "https://example.com/"
}
sessionsWithMetricPercentage is Clarity's own normalised figure: the percentage of sessions in which that friction occurred. It is already the rate. Do not compute one.
Two genuinely different measures are available and they answer different questions:
- Incidence:
sessionsWithMetricPercentage. How many sessions hit this at all. - Intensity:
subTotal / sessionsCount. Average events per session.
"5% of sessions rage-clicked" is incidence. "Sessions that rage-clicked did so 4 times on average" is intensity. A page can be bad at one and fine at the other.
Note the scale: it is 0 to 100, not 0 to 1. A threshold written as 0.05 expecting a fraction will never fire.
That shape is shared by all six friction metrics: DeadClickCount, RageClickCount, QuickbackClick, ExcessiveScroll, ScriptErrorCount, ErrorClickCount.
The other three row shapes
The dimension echo is Url with that exact capitalisation, and its value is an absolute URL including scheme and host, not a path.
ScrollDepth:
{ "averageScrollDepth": 73, "Url": "https://example.com/" }
0 to 100, confirmed by observed values of 73, 6, 65, 71 and 100. Not 0 to 1.
Traffic:
{
"totalSessionCount": "2",
"totalBotSessionCount": "1",
"distinctUserCount": "3",
"pagesPerSessionPercentage": 1,
"Url": "https://example.com/"
}
totalBotSessionCount is included in totalSessionCount, not additional to it. If you want human sessions you must subtract. Skipping that inflates your denominator and makes every rate on the page look better than it is.
Also note the field is distinctUserCount, spelled correctly. Some third-party write-ups repeat a typo'd variant.
EngagementTime:
{ "totalTime": "13", "activeTime": "165", "Url": "https://example.com/" }
Seconds, and they are sums across sessions, not averages. The naming is honest about this: averageScrollDepth says average, these say total. Per-session engagement is totalTime / sessions. activeTime is the engaged subset of totalTime.
Trap 3: the payload mixes strings and numbers
Counts come back as JSON strings ("2", "165"). Percentages and averageScrollDepth come back as numbers (0, 73).
Cast everything explicitly on the way in. In a loosely typed language this is the worst kind of bug: PHP will happily add a numeric string, JavaScript will happily concatenate one, and neither tells you. You get a plausible number that is wrong.
Trap 4: you cannot backfill, and not for the reason you think
The window is the last 1 to 3 days. The natural workaround is to miss a day, then pull numOfDays=2 tomorrow and split the result. That does not work, and it is worth understanding exactly why before you build on the assumption.
Two facts, both from real responses:
- The rows carry no date field at all.
numOfDays=Nreturns a single N-day aggregate, not N daily buckets.
So a 48-hour response is one blended number covering two days. There is no arithmetic that recovers the two days from it, because the information is not in the payload. A missed day is gone permanently.
If you store multi-day pulls at all, label them. Storing a 2-day blend in the same column as 1-day rows silently corrupts every trend you compute afterwards.
The hard limits
| Limit | Value | What it means in practice |
|---|---|---|
| Requests per project per day | 10 | One daily dimension1=URL snapshot costs 1. Plenty of headroom. |
| Data window | last 1 to 3 days | No backfill, ever. History only exists if you store it. |
| Dimensions per request | 3 | Rarely a constraint. |
| Rows per response | 1000, no pagination | A site with more than 1000 URLs needs a segmentation strategy. |
Error codes: 401 bad or expired token, 403 token not authorised for the project, 400 bad parameters, 429 TooManyRequests once the daily limit is hit.
If you are near the 1000-row cap, log loudly when you hit it. A silently truncated response looks exactly like a site that got smaller.
Retention and the API window are different things
This distinction gets stated wrongly a lot, including by people selling tools built on this API.
| Value | |
|---|---|
| Export API window | previous 1 to 3 days |
| Recording retention | 30 days, or 9 months if labelled or favourited |
| Heatmap retention | 9 months |
Clarity does not forget your data after three days. It keeps it for months and its own dashboard will show it to you. What is limited to three days is programmatic access.
The true statement, which is still enough to justify building a store of your own: the numbers exist, but the only machine-readable route to them covers three days, ten calls and 1000 unpaginated rows. So per-page history and before-and-after comparison require you to snapshot daily and keep it.
Anyone who has used Clarity will catch the stronger claim immediately, so it is worth getting right.
What the API does not return
Setting expectations honestly, because these come up constantly:
- No heatmap coordinates. Where on the page people clicked is dashboard-only.
- No session recording content. The official Clarity MCP server can list recordings by filter; it cannot return their contents as data either.
- Nothing older than 3 days through this API. No exceptions, no parameter that changes it.
- No conversion or revenue data. That is a different tool's job.
What is still unverified
Every friction value in the samples we have observed was 0, so a non-zero friction row has not been seen in the wild yet. The row shape is confirmed. The populated values are not.
If you are integrating against a site with real friction, watch the first response carefully and confirm subTotal and sessionsWithMetricPercentage behave as described above. If they do not, we would genuinely like to know.
Why we wrote this
We build FrictionScope, which snapshots this API every day, stores the history it will not give you, and classifies each page into a problem verdict you can act on. Every trap on this page is one we hit while building it.
The reference exists because the alternative was letting the next person lose the same week.