API & MCP
API usage
Read traffic reports from your sites and networks with the available API endpoints.
The API provides read-only traffic reports for sites, custom views, and networks. Every request uses a team API token and returns a JSON response with a data and meta object.
If you want an AI assistant to read these reports instead of writing the calls yourself, see the MCP server. It is a thin wrapper over these very endpoints: every one of its tools calls one of them, and the two share one rate limit.
#Authentication
Create a team token from Settings → API tokens and grant it the read permission for the resource you want to query:
sites.readforsite:{siteUuid}andsite:{siteUuid}@{customViewId}networks.readfornetwork:{networkId}
Send it as a bearer token:
Authorization: Bearer YOUR_TEAM_TOKEN
Accept: application/json
The token belongs to the team, so the addressed site or network must belong to that team. The team must also have API access enabled by its plan.
Permissions cascade: networks.read also grants sites.read, so a token that can read a network can read the sites inside it.
Those two are the only permissions a token can carry. The API is read-only, so there is nothing to grant beyond reading. Older tokens issued with a sites.edit or networks.edit permission still work, and grant the read access those included.
#Available endpoints
Every endpoint below a site group is addressed by a wireable: site:{siteUuid}, site:{siteUuid}@{customViewId} or network:{networkId}. The first endpoint is the one that hands you those addresses, so start there.
#Your sites
GET /api/v1/sites
The only endpoint without a wireable, because it is the one that produces them. Without parameters it lists every site, custom view and network the token can read, each with the address the other endpoints expect and the timezone its dates are reported in:
{
"data": {
"sites": [
{
"wireable": "site:ab12cd",
"domain": "example.com",
"timezone": "Europe/Rome",
"custom_views": [
{ "wireable": "site:ab12cd@7", "name": "Blog only" }
]
}
],
"networks": [
{ "wireable": "network:3", "name": "All properties" }
]
},
"meta": { "sites": 1, "networks": 1 }
}
networks is listed only for a token carrying networks.read; with sites.read alone it comes back empty rather than as an error. The endpoint itself requires at least sites.read.
To look up one entry in a long list, add search with part of a domain or network name, or a site's exact uuid, and type=sites or type=networks to list only one kind. With limit (at most 500) and offset the list comes in pages, and meta adds has_more and next_offset.
#Field dictionary
GET /api/v1/site:SITE_UUID/fields
The metric and dimension names that site group accepts, each with its display name, type and meaning, plus the allowed values of the fields that have a closed set:
{
"data": {
"default_metrics": ["views", "sessions", "viewers", "avg_time_on_page", "bounce_rate_sessions"],
"metrics": [
{ "name": "views", "display_name": "Pageviews", "type": "number", "description": "…" }
],
"dimensions": [
{ "name": "pathname", "display_name": "Pathname", "type": "string" },
{
"name": "verified_state",
"display_name": "Macro Category",
"type": "select",
"allowed_values": [
{ "value": "-1", "label": "Human" },
{ "value": "2", "label": "Malicious Bot" }
]
}
]
},
"meta": { "wireable": "site:ab12cd", "timezone": "Europe/Rome" }
}
This is the same list the stats endpoints validate against, so anything it returns is accepted and anything it omits is rejected with 422. It is also worth reading because the list depends on the address: site_id, for instance, exists only when the address covers more than one site. allowed_values appears only on closed-set fields, and carries the value the filter expects next to the label that explains it — on verified_state, month and day_name that value is an internal code.
The Metrics and Dimensions tables further down are the same vocabulary written out for a reader; this endpoint is the machine-readable copy, and it cannot fall out of date.
The raw-logs endpoint has its own dictionary, returned with ?source=raw_logs: the columns of the raw event table, each usable both in columns and in where_expression, the default_columns, and the raw-logs limits under meta.limits. Raw logs have no metrics.
#Time series
GET /api/v1/site:SITE_UUID/stats/views
Useful query parameters:
start,end: optional date or Unix timestamp rangerange: a relative period such aslast_7_days, instead ofstartandend. See Dates and defaultscompare_start,compare_end: optional comparison range, both required togethercompare_range:previous_period,previous_monthorprevious_year, instead ofcompare_startandcompare_end. See Dates and defaultsgranularity:minute,hour,day,week,month, oryear. Defaults todaymetrics: comma-separated metric names, for exampleviews,allowed_requestswhere_expression,having_expression: filter expressions
The response gives one label per time bucket and one series per metric. Buckets with no traffic are filled with zeros, so every series has exactly one value per label:
{
"data": {
"labels": ["2026-08-01", "2026-08-02"],
"series": [
{ "label": "Pageviews", "data": [120, 98], "compared": false }
]
},
"meta": { "start": 1785542400, "end": 1785715200, "timezone": "Europe/Rome", "granularity": "day" }
}
Note that label is the metric's display name, not its key: views comes back as Pageviews. The table below maps the two. When a comparison range is requested the response also carries compare_labels, and the extra series are marked "compared": true.
#Dimension report
GET /api/v1/site:SITE_UUID/stats/pages
Useful query parameters:
start,endorrange: the reporting window, as for the time seriescompare_startandcompare_end, orcompare_range: optional comparison range, as for the time seriesdimension: primary dimension, for examplepathname. Defaults topathnamedimensions: comma-separated secondary dimensions, for examplehostname,country_codemetrics: comma-separated metrics, for exampleviews,entrancessort,sort_order: sorting field andascordesc. Defaults toviewsanddesclimit,offset: pagination controls.limitaccepts 1 to 1000 and defaults to 20where_expression,having_expression: filter expressionswith_totals:trueto also returnmeta.totals. Defaults tofalse
Each row carries the grouped dimensions followed by the requested metrics:
{
"data": [
{ "pathname": "/pricing", "views": 412, "sessions": 380 },
{ "pathname": "/", "views": 388, "sessions": 351 }
],
"meta": {
"start": 1785542400, "end": 1785715200, "timezone": "Europe/Rome",
"dimension": "pathname", "dimensions": [], "limit": 20, "offset": 0
}
}
With a comparison range, each row also carries compared_{metric} keys.
With with_totals=true, meta.totals holds the requested metrics aggregated over every group that matches the filters, not only the rows on the current page. Rates and averages are recomputed over the whole set rather than summed. With a comparison range, the totals carry compared_{metric} keys as well:
"meta": {
"dimension": "pathname", "limit": 20, "offset": 0,
"totals": { "pathname": "", "views": 12840, "sessions": 9921 }
}
There is no total row count, so to page through a report keep raising offset until a page comes back shorter than limit.
sort is the one parameter that is not checked against the dictionary: a name that is neither one of the requested metrics nor the dimension is ignored, and the report is sorted by the first metric instead.
#Raw logs
GET /api/v1/site:SITE_UUID/raw-logs
The individual requests behind the reports, one row per request, newest first. Use it to look at specific hits, such as the exact URLs a crawler fetched or the user agent of a bot, and the report endpoints above for counts and trends: they are cheaper and complete.
Useful query parameters:
start,end: the window, in the same formats as the other endpoints. Withoutstartit is the 24 hours beforeendrange:today,yesterdayorlast_7_days, instead ofstartandendcolumns: comma-separated names fromGET /api/v1/{wireable}/fields?source=raw_logs. Defaults todatetime_second,bot_name,country_code,full_url,full_user_agentwhere_expression: filter expression over the same namesorder_direction:desc(newest first, the default) orasclimit,offset: pagination controls.limitdefaults to 20
{
"data": [
{
"datetime_second": "2026-09-29 07:58:12",
"bot_name": "GPTBot",
"country_code": "US",
"full_url": "example.com/pricing",
"full_user_agent": "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.2; +https://openai.com/gptbot)"
}
],
"meta": {
"start": 1790582400, "end": 1790668800, "timezone": "Europe/Rome",
"columns": ["datetime_second", "bot_name", "country_code", "full_url", "full_user_agent"],
"order_direction": "desc", "limit": 20, "offset": 0, "has_more": true, "next_offset": 20
}
}
When has_more is true, ask for the next page with offset set to next_offset, and with start and end set to the meta.start and meta.end of the first page: an omitted end means "now", which would move the window between one page and the next. next_offset is null once the next page would go past the offset limit; narrow the window or add filters to reach the rows beyond it.
Raw logs read events one by one instead of aggregates, so they come with limits the report endpoints do not have. A request outside them is rejected with 422, never cut short silently:
| Limit | Value |
|---|---|
Window between start and end |
at most 8 days (192 hours) |
Rows per request (limit) |
at most 1000 |
offset |
at most 10000 |
columns |
at most 20 |
Conditions in where_expression |
at most 20 |
GET /api/v1/{wireable}/fields?source=raw_logs repeats these values under meta.limits. A query that still takes too long is stopped and answered with 504: narrow the window, lower the offset or add filters. Comparison ranges and having_expression are not available on raw logs.
#Raw logs time series
GET /api/v1/site:SITE_UUID/raw-logs/timeseries
The number of individual requests ("hits") per time bucket, over the same raw events as the raw-logs endpoint: the Hits chart of the Raw Logs page. Every bucket of the window is present, with 0 when it had no traffic.
Useful query parameters:
start,endorrange: the window, exactly as for raw logsgranularity:1m,5m,15m,1hor1d. Without it, the finest one available for the windowwhere_expression: filter expression over the raw-logs names
{
"data": [
{ "bucket": "2026-10-05 10:00:00", "timestamp": 1791187200, "hits": 412 },
{ "bucket": "2026-10-05 11:00:00", "timestamp": 1791190800, "hits": 0 }
],
"meta": {
"start": 1791187200, "end": 1791194400, "timezone": "Europe/Rome",
"granularity": "1h", "total_hits": 412
}
}
bucket is the start of the bucket in the site's timezone; timestamp is the same instant as a Unix timestamp, which stays unambiguous when a daylight-saving change repeats an hour. Day buckets start at local midnight.
The time series has the same window and filter limits as raw logs, and the same 504 when a query takes too long. Rows, offsets and columns do not apply. Short buckets are only available on short windows, so that a series never grows to thousands of points:
| Granularity | Longest window |
|---|---|
1m |
4 hours |
5m |
24 hours |
15m |
3 days |
1h, 1d |
any window within the raw-logs limit |
A granularity too fine for the window is rejected with 422.
The same endpoints accept a network or custom-view address:
GET /api/v1/network:NETWORK_ID/stats/pages?dimension=pathname&metrics=views
GET /api/v1/site:SITE_UUID@VIEW_ID/stats/views?granularity=day&metrics=views
All parameters belong in the URL. Lists can be written as comma-separated values, so a GET request does not need a request body. Repeated array parameters such as metrics[]=views&metrics[]=entrances remain supported as well.
#Dates and defaults
start, end, compare_start and compare_end accept either Unix timestamps or any ordinary date string, such as 2026-08-01 or 2026-08-01T10:00:00Z. They are interpreted in the site's timezone, which every response repeats in meta.timezone.
Instead of start and end you can send range with a relative period, resolved in the site's timezone each time the call runs. A call scheduled every morning with range=yesterday always reports the day before, with no dates to update. Sending range together with start or end is rejected with 422. The response repeats the period in meta.range, next to the meta.start and meta.end it resolved to.
The two report endpoints accept the periods of the dashboard date picker. They count whole days and end yesterday at the latest, like the picker, because today's figures are still incomplete:
range |
Period |
|---|---|
yesterday |
yesterday |
last_7_days, last_14_days, last_30_days, last_90_days, last_365_days |
that many days, ending yesterday |
this_month |
from the 1st of the month to yesterday |
last_month |
the whole previous month |
this_year |
from 1 January to yesterday |
last_year |
the whole previous year |
Raw logs and their time series accept the periods of the Raw Logs page instead, which run up to the moment of the call: today (from midnight to now), yesterday (the whole day) and last_7_days (from midnight seven days ago to now, so up to 8 days).
The comparison range has its own relative form, compare_range, which takes the comparison periods of the date picker. They are relative to the main window, whether it comes from range or from start and end, so with range the comparison moves along with it:
compare_range |
Comparison period |
|---|---|
previous_period |
as many days as the main window, ending the day before it starts |
previous_month |
the same days one month earlier |
previous_year |
the same days one year earlier |
Sending compare_range together with compare_start or compare_end is rejected with 422, and the response repeats it in meta.compare_range. Raw logs have no comparison range.
When neither range nor start and end are sent the window is the last 7 days; raw logs are the exception, with the 24 hours before end and a window of at most 8 days. The two report endpoints report by whole days, so granularity=hour and granularity=minute only add detail on very recent windows. Requests reaching further back than your plan's data retention are answered with the data that is still available.
#Metrics
Omitting metrics returns views, sessions, viewers, avg_time_on_page and bounce_rate_sessions. GET /api/v1/{wireable}/fields returns this same table for a given address, including the descriptions.
| Metric | Display name | Type |
|---|---|---|
views |
Pageviews | number |
viewers |
Unique Pageviews | number |
sessions |
Involved Sessions | number |
entrances |
Entrances | number |
unique_entrances |
Unique Entrances | number |
exits |
Exits | number |
unique_exits |
Unique Exits | number |
views_per_session |
Pageviews per Sessions | number |
sessions_per_user |
Sessions per Users | number |
views_per_entrance |
Pageviews per Entrances | number |
entrances_per_user |
Entrances per Unique Entrances | number |
avg_time_on_page |
Avg. Time on Page | seconds |
avg_time |
Avg. Session Duration (Entrances) | seconds |
avg_time_sessions |
Avg. Session Duration | seconds |
bounce_rate |
Bounce Rate (Entrances) | percentage |
bounce_rate_sessions |
Bounce Rate | percentage |
avg_response_time |
Avg. Response Time | seconds |
avg_bytes_sent |
Average Bytes Sent | number |
allowed_requests |
2xx Requests | number |
redirect_requests |
3xx Requests | number |
denied_requests |
4xx Requests | number |
error_requests |
5xx Requests | number |
last_visit |
Last Visit | date |
#Dimensions
Usable as dimension, in dimensions, and on the left-hand side of where_expression. GET /api/v1/{wireable}/fields returns this same table for a given address, with the allowed values of every closed set spelled out.
| Dimension | Display name | Type |
|---|---|---|
pathname |
Pathname | string |
full_url |
Full URL | string |
hostname |
Hostname | string |
entry_pathname |
Entry Page | string |
pagepath_level1 … pagepath_level5 |
Pagepath Level 1 … 5 | string |
query_string |
Query String | string |
referrer |
Referrer | string |
referrer_source |
Referrer + Source | string |
source_medium |
Source / Medium | string |
utm_source, utm_medium, utm_campaign, utm_content, utm_term |
UTM parameters | string |
country_code |
Country | closed set: ISO country codes, plus Unknown |
browser |
Browser | string |
operating_system |
Operating System | string |
accept_language |
Accept Language | string |
bot_name |
Bot Name | string |
bot_type |
Bot Type | string |
verified_state |
Macro Category | closed set: -1 Human, 0 Suspicious Bot, 1 Verified Bot, 2 Malicious Bot |
asn_verified |
ASN Verified | closed set: true, false |
is_datacenter |
Is Datacenter | closed set: true, false |
is_vpn |
Is VPN | closed set: true, false |
user_agent_group |
User Agent Group | string |
full_user_agent |
Full User Agent | string |
request_method |
Request Method | string |
status_code |
Status Code | number |
date |
Date | date |
datetime |
Datetime | datetime |
hourminute |
Hour + Minute | hourminute |
year |
Year | number |
month |
Month | closed set: 1 to 12 |
day_name |
Day Name | closed set: 1 (Monday) to 7 (Sunday) |
When the address covers more than one site, a site_id dimension is also available, filtered by domain name; in the report it comes back as site_domain.
#Filter expressions
where_expression filters on dimensions, having_expression on metric values. Both take the same shape: an OR of AND-groups, with the joining words written in between.
[[["pathname", "=", "/pricing"], "AND", ["country_code", "=", "IT"]], "OR", [["pathname", "=", "/about"]]]
A single condition is still a group inside a list: [[["verified_state", "=", "2"]]].
The parameter accepts that JSON directly, or the URL-safe encoded form that dashboard links use. The Copy API call button on the chart and report table, and on the Raw Logs chart and table, includes the filters currently applied to that component. It lets you choose the period of the copied call: the exact dates selected on the page, or, when the page is on one of the periods listed under Dates and defaults, that period as range, so the call keeps following the calendar. When the dashboard compares two periods, the copied call carries the comparison too: as compare_range when it is one of the date picker's comparison periods, otherwise as compare_start and compare_end. The copied URL omits pagination limits and offsets.
Which operators are allowed depends on the field:
| Field type | Operators |
|---|---|
| String | =, !=, IN, NOT IN, LIKE, NOT LIKE, STARTS WITH, ENDS WITH, EMPTY, NOT EMPTY |
| Number, percentage, seconds, date | =, !=, >, >=, <, <=, IN, NOT IN |
| Closed set | =, !=, IN, NOT IN |
| Boolean | =, != |
IN and NOT IN take an array as the third element, with at most 50 values. LIKE and NOT LIKE surround the value with wildcards for you, and STARTS WITH and ENDS WITH add one on the correct side, so do not write % yourself. EMPTY and NOT EMPTY ignore the value: pass an empty string.
Percentages are given as they are displayed, so ["bounce_rate", ">", 50] means more than 50%. Durations accept either a number of seconds or a HH:MM:SS string.
#Rate limits
The API limit is measured in requests per minute for the team and for the caller IP. When the plan limit is reached, the API returns 429 Too Many Requests with the retry interval both in the message and in the Retry-After header. A plan without API access returns 403.
Every endpoint counts the same, sites and fields included. Neither of those changes between calls, though, so read them once and keep the answer rather than asking again before each report.
Calls made through the MCP server are counted here too: it is one shared allowance, not two.
#Errors
Every error returns a JSON body with a message, and validation errors add an errors object listing the offending fields.
| Status | Meaning |
|---|---|
401 |
The token is missing, malformed or has been deleted |
403 |
A user token was used instead of a team token, the token lacks the required permission, the site belongs to another team, the plan has no API access, or the subscription is inactive or in overage |
404 |
No such site, custom view or network |
422 |
A parameter failed validation, for example an unknown metric or dimension name, a malformed filter expression, or a raw-logs request outside its limits |
429 |
The per-minute rate limit was reached |
503 |
The analytics database is temporarily unreachable: retry after the Retry-After interval |
504 |
The query took too long or read too much data: narrow the window, lower the offset or add filters |