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.read for site:{siteUuid} and site:{siteUuid}@{customViewId}
  • networks.read for network:{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 range
  • range: a relative period such as last_7_days, instead of start and end. See Dates and defaults
  • compare_start, compare_end: optional comparison range, both required together
  • compare_range: previous_period, previous_month or previous_year, instead of compare_start and compare_end. See Dates and defaults
  • granularity: minute, hour, day, week, month, or year. Defaults to day
  • metrics: comma-separated metric names, for example views,allowed_requests
  • where_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, end or range: the reporting window, as for the time series
  • compare_start and compare_end, or compare_range: optional comparison range, as for the time series
  • dimension: primary dimension, for example pathname. Defaults to pathname
  • dimensions: comma-separated secondary dimensions, for example hostname,country_code
  • metrics: comma-separated metrics, for example views,entrances
  • sort, sort_order: sorting field and asc or desc. Defaults to views and desc
  • limit, offset: pagination controls. limit accepts 1 to 1000 and defaults to 20
  • where_expression, having_expression: filter expressions
  • with_totals: true to also return meta.totals. Defaults to false

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. Without start it is the 24 hours before end
  • range: today, yesterday or last_7_days, instead of start and end
  • columns: comma-separated names from GET /api/v1/{wireable}/fields?source=raw_logs. Defaults to datetime_second,bot_name,country_code,full_url,full_user_agent
  • where_expression: filter expression over the same names
  • order_direction: desc (newest first, the default) or asc
  • limit, offset: pagination controls. limit defaults 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, end or range: the window, exactly as for raw logs
  • granularity: 1m, 5m, 15m, 1h or 1d. Without it, the finest one available for the window
  • where_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