Endpoint list
Here is a list of Publytics endpoints. Every path starts with https://publytics.net/api, and every request needs the token described in the introduction. All the queries resemble the structure of the queries made with the dashboard interface, and they accept the same dimensions, metrics and filters.
The names of the dimensions and metrics are listed in Dimensions and metrics; Describe a Site returns the complete list for a site, custom ones included.
The responses below show the exact format of each endpoint, with example values.
List Sites
GET /sites
Lists the sites, custom views and networks your token can read, with the identifier to use as {siteId} in the other endpoints. This call is not charged to the API / MCP calls per hour.
Response
{
"sites": [
{
"domain": "example.com",
"timezone": "Europe/Rome",
"owned": true,
"views": [
{ "site": "EXMPL1", "name": "Standard view" },
{ "site": "EXMPL1@12", "name": "Italy only" }
]
}
],
"networks": [
{ "site": "network=3", "name": "My network", "owned": true, "domains": ["example.com", "example.org"] }
]
}
Describe a Site
GET /site/{siteId}/meta
Describes a site: the report sections you can read, and every dimension and metric the site accepts, standard and custom, with the endpoints and datasets they work with. Call it before building a query, to know the names to use in metricsCols, customCols, sort and in the filters. Custom dimensions and metrics have "custom": true; the dimensions that the plan of the site does not include are left out.
Response
| Field | Content |
|---|---|
| site | Identifier, type (site, custom_view or network), domain, name, timezone and data retention of the site |
| sections | The report sections your user can read: realtime, content, acquisition, events, reports |
| endpoints | For each endpoint, the dataset it reads, the section it needs (/content and /events also accept the reports section, in also_with_section) and its default metrics |
| dimensions | Every dimension, with its name, label, type, whether it is custom, the filter operators it accepts and the datasets it belongs to. Dimensions with a fixed list of values also list them (or their number, when they are more than 60) |
| metrics | Every metric, with its name, label, type, whether it is custom and the datasets it belongs to. The daily unique counts carry a caveat |
| filters | The syntax of the filters, with an example |
{
"site": {
"site": "EXMPL1",
"type": "site",
"domain": "example.com",
"name": "Standard view",
"timezone": "Europe/Rome",
"data_retention_years": "unlimited",
"sites": [{ "domain": "example.com", "view": "standard view", "uuid_view": "EXMPL1" }]
},
"sections": ["realtime", "content", "acquisition", "events", "reports"],
"endpoints": {
"/content": {
"dataset": "content",
"section": "content",
"also_with_section": "reports",
"default_metrics": ["views", "sessions", "viewers", "avg_time_on_page", "bounce_rate_sessions"],
"primaryProp": "the primary dimension: any dimension of the content dataset (pathname by default)",
"totals": "/content/totals"
},
...
},
"dimensions": [
{
"name": "pathname",
"label": "Pathname",
"type": "string",
"custom": false,
"operators": ["=", "!=", "LIKE", "NOT LIKE", "EMPTY", "NOT EMPTY", "STARTS WITH", "ENDS WITH"],
"datasets": ["content", "audience", "events", "realtime"]
},
{
"name": "screen_size",
"label": "Device",
"type": "string",
"custom": false,
"operators": ["=", "!="],
"values": ["Desktop", "Mobile", "Tablet", ""],
"datasets": ["content", "audience", "events", "realtime"]
},
{
"name": "article_id",
"label": "Custom Dimension: article_id",
"type": "number",
"custom": true,
"operators": ["=", "!=", ">", ">=", "<", "<="],
"datasets": ["content", "audience", "events", "realtime"]
},
...
],
"metrics": [
{
"name": "views",
"label": "Pageviews",
"type": "number",
"custom": false,
"datasets": ["content", "audience", "events"]
},
{
"name": "viewers",
"label": "Unique Pageviews",
"type": "number",
"custom": false,
"caveat": "Daily unique count: over a period of more days it is the sum of the daily values, so a returning person is counted once per day.",
"datasets": ["content", "events"]
},
...
],
"filters": { "syntax": "...", "example": [...], "metric_operators": ["=", "!=", ">", ">=", "<", "<="] }
}
Common Query Parameters
The Content, Entry/Exit, Acquisition, Audience Stats and Events endpoints accept these query parameters. Dates are days in the format YYYY-MM-DD, and both start and end are included. JSON values must be URL-encoded.
| Parameter | Description | Default Value |
|---|---|---|
| start | The first day of the period. | Current date - 7 days |
| end | The last day of the period. | Current date - 1 day |
| metricsCols | A JSON array with the metrics to include, e.g. ["views","viewers"]. If omitted or empty, the default metrics of the endpoint are used. |
Default metrics |
| customCols | A JSON array with secondary dimensions: each row is split by them too, e.g. ["country_code"]. Up to 10 dimensions, the primary one included. |
Empty array |
| dimensionsFilters | A JSON array of conditions on the dimensions (the equivalent of WHERE). See Filters. | No filter |
| metricsFilters | A JSON array of conditions on the metrics (the equivalent of HAVING). See Filters. | No filter |
| sort | The metric or dimension to sort the rows by. It must be one of the requested metrics or dimensions, otherwise the first metric is used. | "views" |
| sortOrder | desc (descending) or asc (ascending). |
"desc" |
| limit | The number of rows per page. The maximum allowed limit is 1000. | 20 |
| offset | The page number, starting from 1: with limit=20, offset=2 returns the rows from 21 to 40. |
1 |
| compareStart | The first day of a period to compare with. | null |
| compareEnd | The last day of a period to compare with. | null |
| primaryPropFilter, customFilters, customPropFilter (deprecated) | Legacy filter formats, still accepted. Use dimensionsFilters and metricsFilters instead. |
Empty array |
Response
A JSON array with one object per row, containing the dimensions of the row and the requested metrics. Shares (bounce rate, % quality pageviews, scroll depth) are fractions between 0 and 1, and times are in seconds.
[
{
"pathname": "/blog/how-to-start",
"views": 6768,
"sessions": 6201,
"viewers": 6053,
"avg_time_on_page": 96.08,
"bounce_rate_sessions": 0.0565
},
...
]
With customCols, each row also contains the secondary dimensions, and there is one row per combination:
[
{ "pathname": "/blog/how-to-start", "country_code": "IT", "views": 6198 },
{ "pathname": "/blog/what-is-new", "country_code": "IT", "views": 5331 },
...
]
When compareStart and compareEnd are set, each row also contains a compared_{metric} field for each metric, with its value in the comparison period (0 when the row has no data then). The rows are the ones of the main period:
[
{
"pathname": "/blog/how-to-start",
"views": 6768,
"viewers": 6053,
"compared_views": 5120,
"compared_viewers": 4610
},
...
]
Unique counts (viewers, unique_entrances, unique_exits) are computed per day: over a period of more days they are the sum of the daily values, so a returning visitor is counted once per day.
Time series: to get one row per day, use the date dimension as the primary one and sort by it, e.g. GET /site/{siteId}/acquisition/date?sort=date&sortOrder=asc for the daily page views, sessions and users of the site, or GET /site/{siteId}/content?primaryProp=date&sort=date&sortOrder=asc.
[
{ "date": "2026-03-01", "views": 6016, "unique_entrances": 4545 },
{ "date": "2026-03-02", "views": 11155, "unique_entrances": 7689 },
{ "date": "2026-03-03", "views": 7611, "unique_entrances": 5626 }
]
Filters
dimensionsFilters and metricsFilters have the same syntax, but act at two different moments:
dimensionsFilters(the SQLWHERE) chooses which data is counted, before the rows are grouped: e.g. only the page views from Italy. It accepts any dimension of the dataset, even one that is not in the rows.metricsFilters(the SQLHAVING) chooses which rows are returned, after they are counted: e.g. only the pages with more than 1,000 page views. It accepts any metric of the dataset, even one that is not inmetricsCols, and tests it on each row over the whole period.
Structure
A filter always has exactly two levels: a list of groups joined by "OR", where each group is a list of conditions joined by "AND". Each condition is [name, operator, value].
[ group , "OR", group , "OR", ... ] the outer list: groups joined by "OR"
group = [ condition, "AND", condition, ... ] a group: conditions joined by "AND"
condition = [name, operator, value]
The joining words are strings written between the items, and each one belongs to its own level: "OR" only between groups, "AND" only between the conditions of a group. A row matches when all the conditions of at least one group are true: in SQL terms, (a AND b) OR (c) OR (d AND e).
Dimension filters (WHERE)
One condition: the data from Italy. A single condition still needs both levels of brackets.
[[["country_code", "=", "IT"]]]
→ country_code = 'IT'
AND: the blog pages read on a mobile phone. The two conditions are in the same group.
[[["pathname", "STARTS WITH", "/blog"], "AND", ["screen_size", "=", "Mobile"]]]
→ pathname LIKE '/blog%' AND screen_size = 'Mobile'
OR: the visits from Google or from Bing. Each condition is a group of its own.
[[["referrer_source", "=", "Google"]], "OR", [["referrer_source", "=", "Bing"]]]
→ referrer_source = 'Google' OR referrer_source = 'Bing'
AND inside OR: the blog pages read from Italy, plus any page reached from Google.
[[["pathname", "STARTS WITH", "/blog"], "AND", ["country_code", "=", "IT"]], "OR", [["referrer_source", "=", "Google"]]]
→ (pathname LIKE '/blog%' AND country_code = 'IT') OR referrer_source = 'Google'
OR inside AND: there is no third level, so a filter like (A OR B) AND C is written by repeating C in each group, as (A AND C) OR (B AND C). The visits from Google or Bing, only from Italy:
[[["referrer_source", "=", "Google"], "AND", ["country_code", "=", "IT"]], "OR", [["referrer_source", "=", "Bing"], "AND", ["country_code", "=", "IT"]]]
→ (referrer_source = 'Google' OR referrer_source = 'Bing') AND country_code = 'IT'
In the same way, (A OR B) AND (C OR D) becomes four groups: (A AND C) OR (A AND D) OR (B AND C) OR (B AND D).
Exclusions: != and NOT LIKE in the same group. All the pages except the home page and the tag pages:
[[["pathname", "!=", "/"], "AND", ["pathname", "NOT LIKE", "/tag/"]]]
→ pathname != '/' AND pathname NOT LIKE '%/tag/%'
Metric filters (HAVING)
The same syntax, with the names of the metrics and the operators =, !=, >, >=, <, <=. Shares are fractions between 0 and 1: a 10% bounce rate is 0.1.
One condition: only the rows with more than 1,000 page views.
[[["views", ">", 1000]]]
AND: the rows with between 100 and 1,000 page views.
[[["views", ">=", 100], "AND", ["views", "<=", 1000]]]
AND inside OR: the pages with more than 1,000 page views and a bounce rate under 10%, plus the pages with more than 6,000 page views whatever their bounce rate.
[[["views", ">", 1000], "AND", ["bounce_rate_sessions", "<", 0.1]], "OR", [["views", ">", 6000]]]
→ HAVING (views > 1000 AND bounce_rate_sessions < 0.1) OR views > 6000
Both together
The blog pages read from Italy in September, only those with at least 100 page views. With curl, -G and --data-urlencode take care of the encoding:
curl -G 'https://publytics.net/api/site/{siteId}/content' \
--header 'Authorization: Bearer {token}' \
--data-urlencode 'start=2026-09-01' \
--data-urlencode 'end=2026-09-30' \
--data-urlencode 'metricsCols=["views","viewers"]' \
--data-urlencode 'dimensionsFilters=[[["pathname","STARTS WITH","/blog"],"AND",["country_code","=","IT"]]]' \
--data-urlencode 'metricsFilters=[[["views",">=",100]]]'
Operators
| Type | Operators |
|---|---|
| Text dimensions | =, !=, LIKE, NOT LIKE, STARTS WITH, ENDS WITH, EMPTY, NOT EMPTY |
| Dimensions with a fixed list of values | =, != |
| Numbers, dates and metrics | =, !=, >, >=, <, <= |
LIKE and NOT LIKE match anywhere in the value (no % needed), EMPTY and NOT EMPTY take an empty string as value, and dates are written as YYYY-MM-DD. The Describe a Site endpoint lists the operators accepted by each dimension.
Limits and errors
A filter can have up to 100 conditions in dimensionsFilters and 50 in metricsFilters; a query can group by up to 10 dimensions. Above them the API answers 422 with the reason. A filter that is not well formed (a joining word at the wrong level, a dimension or metric that does not exist, an operator the dimension does not accept, a value outside its fixed list) answers 500.
Get Content Pages
GET /site/{siteId}/content
Retrieves the most read articles of a site, or its content grouped by any other dimension. Requires the Content or the Reports section.
Query parameters
All the common query parameters, plus:
| Parameter | Description | Default Value |
|---|---|---|
| primaryProp | The dimension each row represents. Any dimension of the content dataset works, custom ones included (e.g. author). |
"pathname" |
Default metrics: views, sessions, viewers, avg_time_on_page, bounce_rate_sessions.
Response
[
{
"pathname": "/blog/how-to-start",
"views": 8157,
"sessions": 7518,
"viewers": 7346,
"avg_time_on_page": 96.09,
"bounce_rate_sessions": 0.2322
},
...
]
Get Entry/Exit Pages
GET /site/{siteId}/acquisition/entry & GET /site/{siteId}/acquisition/exit
Retrieves the entry/exit pages of a site, one row per page (pathname). Requires the Acquisition section.
Query parameters
All the common query parameters, except sort: the rows are always sorted by entrances (entry) or exits (exit), or by the first metric of metricsCols when that one is not requested. sortOrder can still reverse the order.
Default metrics: entrances, unique_entrances, bounce_rate (entry); exits, unique_exits (exit).
Response (entry)
[
{ "pathname": "/blog/how-to-start", "entrances": 5847, "unique_entrances": 5333, "bounce_rate": 0.0614 },
{ "pathname": "/blog/what-is-new", "entrances": 5313, "unique_entrances": 4940, "bounce_rate": 0.2136 },
...
]
Get Acquisition Pages (group by a specific dimension)
GET /site/{siteId}/acquisition/{dimension}
Retrieves the acquisition stats of a site grouped by a specific dimension. Requires the Acquisition section.
Parameter Description
dimension (required) The dimension each row represents: any dimension of the audience dataset, custom ones included. The most used are:
- UTM Medium: "utm_medium"
- UTM Source: "utm_source"
- UTM Campaign: "utm_campaign"
- UTM Content: "utm_content"
- UTM Term: "utm_term"
- Referrer: "referrer"
- Referrer + Source: "referrer_source"
- Source / Medium: "source_medium"
- Entry page: "entry_pathname"
- Date: "date", for a daily series of the whole site
A dimension that is not in the audience dataset answers 400.
Query parameters
All the common query parameters.
Default metrics: views, entrances, unique_entrances, avg_time, bounce_rate.
Response
[
{
"referrer_source": "Facebook",
"views": 41209,
"entrances": 33258,
"unique_entrances": 30546,
"avg_time": 59.04,
"bounce_rate": 0.1657
},
{
"referrer_source": "Google",
"views": 4794,
"entrances": 3374,
"unique_entrances": 3136,
"avg_time": 86.32,
"bounce_rate": 0.2217
},
...
]
In the audience dataset entrances are the sessions and unique_entrances the users, as labelled in the dashboard.
Get Audience Overview: Users, Sessions, Pageviews and Bounce Rate
GET /site/{siteId}/visitors
Retrieves the totals of the site in a period: users, sessions, page views and bounce rate.
Query parameters
| Parameter | Description | Default Value |
|---|---|---|
| start | The first day of the period. | Current date - 7 days |
| end | The last day of the period. | Current date - 1 day |
No other parameter is supported: filters and comparison periods are not available on this endpoint. For totals with filters, use Get Totals.
Response
{ "viewers": 38352, "views": 53000, "sessions": 41834, "bounceRate": 0.1812 }
viewers is the sum of the daily unique visitors, and bounceRate is the share of single-page sessions, between 0 and 1.
Get Audience Stats (group by a specific dimension)
GET /site/{siteId}/visitors/{dimension}
Retrieves the Audience stats of a site grouped by a specific dimension. Requires the Acquisition section.
Parameter Description
dimension (required) The dimension each row represents. The following dimensions are supported:
- Device: "device" (rows contain the
screen_sizefield) - Country: "country" (rows contain the
country_codefield) - Operating system: "operating_system"
- Browser: "browser"
- Hostname: "hostname"
Any other audience dimension (e.g. city) can be read with Get Acquisition Pages.
Query parameters
All the common query parameters.
Default metrics: views, entrances, unique_entrances, avg_time, bounce_rate.
Response (/visitors/country)
[
{
"country_code": "IT",
"views": 48712,
"entrances": 38499,
"unique_entrances": 35347,
"avg_time": 61.41,
"bounce_rate": 0.1816
},
...
]
Get Events Report
GET /site/{siteId}/events
Retrieves the events of a site. Requires the Events or the Reports section.
Query parameters
All the common query parameters, plus:
| Parameter | Description | Default Value |
|---|---|---|
| primaryProp | The dimension each row represents: name for one row per event, pathname for one row per page, or any other dimension of the events dataset, custom ones included (e.g. country_code). |
"pathname" |
To get the events by name, set primaryProp=name.
Default metrics: views, sessions, viewers. In the events dataset views is the number of times the event happened (Hits).
Response (primaryProp=name)
[
{ "name": "newsletter_signup", "views": 7212, "sessions": 4208, "viewers": 4109 },
{ "name": "share_click", "views": 4301, "sessions": 4184, "viewers": 4092 },
...
]
Get AI Traffic
GET /site/{siteId}/ai-traffic
Retrieves the visits brought by each AI assistant: the table of the AI Traffic overview of the dashboard, with the same sources, filters and numbers. Requires the Content section.
Query parameters
| Parameter | Description | Default Value |
|---|---|---|
| start | The first day of the period. | Current date - 7 days |
| end | The last day of the period. | Current date - 1 day |
| compareStart | The first day of a period to compare with. | null |
| compareEnd | The last day of a period to compare with. | null |
Response
{
"sources": [
{
"name": "ChatGPT",
"views": 305,
"entrances": 281,
"unique_entrances": 270,
"dimensionsFilters": [[["referrer_source", "=", "chatgpt.com"]]]
},
{
"name": "Perplexity",
"views": 478,
"entrances": 402,
"unique_entrances": 390,
"dimensionsFilters": [[["referrer_source", "=", "perplexity"]], "OR", [["referrer_source", "=", "perplexity.ai"]]]
},
...
],
"totals": { "views": 2831, "entrances": 2390, "unique_entrances": 2301 },
"dimensionsFilters": [[["referrer_source", "=", "Google AI Overview"]], "OR", [["referrer_source", "=", "chatgpt.com"]], "OR", ...]
}
| Field | Content |
|---|---|
| sources | Every AI source (Google AI Overview, ChatGPT, DeepSeek, Mistral, Claude, Perplexity, Microsoft Copilot, Google Gemini), sorted by views, also the ones with no visit. views are the page views, entrances the sessions and unique_entrances the users |
| totals | The sum of all the sources |
| dimensionsFilters | The filter that recognises all the AI traffic; each source also has its own |
| compared_metric | With compareStart and compareEnd, in every source and in totals |
The filters can be passed as they are to the other endpoints, to analyse the AI traffic any way you like. For example the pages most visited from an AI assistant, with /content, or the AI visits day by day:
curl -G 'https://publytics.net/api/site/{siteId}/acquisition/date' \
--header 'Authorization: Bearer {token}' \
--data-urlencode 'sort=date' \
--data-urlencode 'sortOrder=asc' \
--data-urlencode 'dimensionsFilters=[[["referrer_source","=","chatgpt.com"]]]'
To combine an AI filter with another condition, add the condition to each of its groups, as explained in Filters: ChatGPT or Claude visits from Italy are [[["referrer_source","=","chatgpt.com"],"AND",["country_code","=","IT"]],"OR",[["referrer_source","=","claude.ai"],"AND",["country_code","=","IT"]]].
Get Totals
GET /site/{siteId}/content/totals, GET /site/{siteId}/acquisition/entry/totals, GET /site/{siteId}/acquisition/exit/totals, GET /site/{siteId}/acquisition/{dimension}/totals, GET /site/{siteId}/visitors/{dimension}/totals & GET /site/{siteId}/events/totals
Every list endpoint above has a /totals version that takes the same parameters and returns, in a single object, how many rows match them and the metrics over all those rows: the same numbers as the totals row of the dashboard tables. Use it instead of reading every page of the list to add up the rows. limit, offset, sort and sortOrder are ignored. Each call counts as one call, like the list.
For example, the views of all the pages under /blog read from Italy last week:
curl 'https://publytics.net/api/site/{siteId}/content/totals?start=2026-11-02&end=2026-11-08&metricsCols=%5B%22views%22%5D&dimensionsFilters=%5B%5B%5B%22pathname%22%2C%22STARTS%20WITH%22%2C%22%2Fblog%22%5D%2C%22AND%22%2C%5B%22country_code%22%2C%22%3D%22%2C%22IT%22%5D%5D%5D' \
--header 'Authorization: Bearer {token}'
Response
{
"rows": 348,
"views": 12840
}
With the default metrics and a comparison period (/content/totals?compareStart=...&compareEnd=...):
{
"rows": 633,
"views": 53000,
"sessions": 48778,
"viewers": 47136,
"avg_time_on_page": 108.8,
"bounce_rate_sessions": 0.1554,
"compared_rows": 618,
"compared_views": 52855,
"compared_sessions": 48414,
"compared_viewers": 46830,
"compared_avg_time_on_page": 107.01,
"compared_bounce_rate_sessions": 0.1635
}
| Field | Content |
|---|---|
| rows | The number of rows the list endpoint returns with the same parameters, across all its pages. With customCols a row is a combination of the primary and secondary dimensions. |
| metrics | Each requested metric (the default ones of the list when metricsCols is omitted) over all the rows: counts are summed, while rates and averages (bounce rate, average time) are weighted as in the dashboard. A ratio is null when no row matches. |
| compared_rows, compared_metric | With compareStart and compareEnd, the same totals for the comparison period. |
Unique counts (viewers, unique_entrances, unique_exits) are summed over the rows too: a person who read three pages counts three times in the viewers of /content/totals, on top of being counted once per day. For the visitors of the whole site use Get Audience Overview.
Get Realtime Overview
GET /site/{siteId}/realtime/overview
Retrieves what is happening on the site right now: the active users or the page views of the last minutes, split by one or more dimensions. It is the data of the Real Time section of the dashboard. Requires the Realtime section.
Query parameters
| Parameter | Description | Default Value |
|---|---|---|
| select | A JSON array with the dimensions each row is split by, e.g. ["referrer_source"] or ["pathname","country_code"]. Up to 10 dimensions of the realtime dataset (see Dimensions and metrics: every standard dimension except exit_pathname, plus the custom ones). |
["pathname"] |
| realtimeMetric | What to count: activeUsers (distinct visitors) or pageViews. |
"activeUsers" |
| realtimeRange | How far back to look, in seconds. The maximum allowed range is 1800 (30 minutes). | 300 |
| dimensionsFilters | A JSON array of conditions on the dimensions. See Filters. | No filter |
| limit | The number of rows to return. The maximum allowed limit is 1000. | 20 |
No other parameter is supported: there are no dates, metric filters, sorting or paging.
Response
A JSON array with one object per row, sorted by count, containing the dimensions in select and:
- views: the active users or the page views, according to
realtimeMetric - percentage: the share of the total, from 0 to 100
- avg_engagement_time: the average engagement time in seconds, or 0 when engagement time is not tracked
curl -G 'https://publytics.net/api/site/{siteId}/realtime/overview' \
--header 'Authorization: Bearer {token}' \
--data-urlencode 'select=["pathname"]' \
--data-urlencode 'realtimeRange=1800' \
--data-urlencode 'limit=3'
[
{ "pathname": "/blog/how-to-start", "views": 42, "percentage": 21.5, "avg_engagement_time": 48.2 },
{ "pathname": "/", "views": 31, "percentage": 15.9, "avg_engagement_time": 12.7 },
{ "pathname": "/blog/what-is-new", "views": 18, "percentage": 9.2, "avg_engagement_time": 0 }
]
The array is empty when nobody is on the site. With activeUsers, a visitor who viewed two pages is counted on both rows, so the rows can add up to more than the active users of the site: for the total, split by a dimension that has a single value, e.g. select=["hostname"] on a site with one domain. Traffic sources follow the Real-Time Attribution Model of your preferences (event-based by default).