# Analytics

_Category: REST API reference_

Pull your help center's numbers into your own reports, dashboards and scripts. Three read-only reports give you a traffic summary, the performance of each article, and what readers searched for and didn't find. They read the same data as the statistics in your dashboard (see [Read your help center statistics](https://self.helpcenter.io/content/statistics)).

The reports are available on every plan. Any API key can read them, **Read only** included; OAuth apps need the `analytics.read` scope. An API key reports on the help center it was created in, and an OAuth app names the help center with the `X-HCio-Site` header (see [Choose which help center a request acts on](https://developers.helpcenter.io/content/choosing-a-help-center)).

## Endpoints

| Method and path | What it returns |
| --- | --- |
| `GET /v1/analytics/summary` | Article views, visitors and searches for a period, each with the change against the period before, plus ratings and your team's authoring activity. |
| `GET /v1/analytics/content` | Views and ratings for each article read in the period. |
| `GET /v1/analytics/searches` | The searches that failed, the most frequent searches and the most common search words. |

All three allow 6 requests a minute per API key or OAuth app, shared between them (see Limits below).

## Choose a period

All three endpoints take the same optional parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `from` | date | The first day of the period, for example `2026-09-01`. A time of day is ignored. Default: 6 days before `to`. |
| `to` | date | The last day of the period, included. Default: today. |
| `lang` | string | Only figures for this language. It must be a language of your help center. Default: all languages. |

Without `from` and `to`, you get the last 7 days, today included. Days are counted in UTC. A period can be up to 92 days long: a longer one is refused with `422 Unprocessable Entity` rather than cut short, so ask for several periods in turn. Every response says which period it measured in `range`, for example `{"from": "2026-09-24", "to": "2026-09-30", "days": 7}`.

## Get a summary

GET`/v1/analytics/summary`

Traffic, ratings and authoring activity for a period. Traffic comes with the change against the period before.

| Field | Description |
| --- | --- |
| `traffic.article_views` | Article views in the period. |
| `traffic.visitors` | Visitors in the period. |
| `traffic.searches` | Searches in the period. This is the total the search report does not give. |
| `traffic.article_views_per_visitor` | Article views divided by visitors, rounded up. |
| `ratings.total` | Ratings readers gave your articles in the period. |
| `activity` | Your team's work in the period: `articles_created`, `articles_updated`, `articles_published` (drafts that were published), `articles_unpublished` (published articles turned back into drafts) and `active_team_members` (people who edited articles). `lang` doesn't filter these. |
| `totals.published_articles` | Articles published now, whatever the period. With `lang` set to another language than your default, the published articles translated into it. |

Each `traffic` figure has a `count` and a `change_pct`: the change in percent against the preceding period of the same length, rounded to one decimal. For the last 7 days, that is the 7 days before them. `change_pct` is `0` when the preceding period had nothing to compare with. Numbers with nothing after the decimal point come without one (`0`, not `0.0`), so read them as numbers, not as a fixed format.

## Get article performance

GET`/v1/analytics/content`

Views and ratings for each article read in the period. Sort by rating to find the articles readers like least.

| Parameter | Type | Description |
| --- | --- | --- |
| `sort` | string | `views_desc` (default) for the most read first, `views_asc` for the least read first, or `rating_asc` for the largest share of thumbs down first, with ties broken toward the most read article. |
| `category_id` | integer | Only articles filed directly in this category, not in its subcategories. It must be a category of your help center. A category with no articles gives an empty list, and so do `0` and `-1`: they don't select uncategorized articles. |
| `page`, `per_page` | integer | The page, from 1, and articles per page, 1 to 100. Default `per_page`: 25. |

- **Only articles read in the period are listed**, as `excludes_never_viewed` reminds you. To find the articles nobody opened, list your articles with the [Articles](https://developers.helpcenter.io/content/articles-api) endpoints and compare. Drafts and articles in the Trash never appear.
- `views_in_range` counts views in the period. `total_views` is an all-time counter that no longer counts new views, so it can be lower than `views_in_range`. Use `views_in_range` for current traffic.
- **Ratings are percentages of all the ratings the article ever received**, not only those in the period, and not counts. Each is rounded up, so they can add up to more than 100 (67 and 34 in the example). One thumbs down reads as 100%, so weigh ratings against views. With `lang`, only ratings in that language count.
- `title` and `slug` are always the English ones: an article without an English title comes back with both empty. Match rows to your articles with `article_id`; `_links.view` is the article's address on your help center in any case.
- `updated_at` is when the article was last changed.

## Get search demand and content gaps

GET`/v1/analytics/searches`

What readers searched for: the searches that failed, the most frequent searches and the most common words.

Use `limit` to set how many rows each list returns: 1 to 50, 20 by default. There is no paging: each list holds the top rows only, as `meta.lists_are_top_n` says.

- `content_gaps` lists the searches that failed most, `frequent_queries` the most frequent searches (with the same fields), and `common_terms` the single words most used in searches.
- **Rows are counted per day, not over the period.** Each row holds one query on one day, and a query searched in several languages, or both on your help center and in the widget, gets a row for each. The same query can therefore appear in several rows of one list, like "cancel annual plan refund" in the example: add the rows up for a period total.
- A search fails when it returns no results or nobody opens a result: `failed_searches` is `zero_result_searches` plus `no_click_searches`, and `failure_rate` is `failed_searches` divided by `searches`, rounded to two decimals.
- `reason` tells you what to fix. `0 results`: nothing matched, so write the article. `no click`: results came back but nobody opened one, so the article probably exists and its title doesn't match what readers type. `mixed`: both happened. It is `null` in `frequent_queries`.
- `top_hit` is the first result readers got for the query, and `article_clicked` the article a reader opened from the results, each taken from the latest search that had one. Each is `null` or `{article_id, title, status}`, where `status` can also be `trashed`, or `deleted` with no `title`.
- `last_searched_at` is when the query was last searched that day, in UTC. `query` is the text as readers typed it.
- For the total number of searches, use `traffic.searches` from the summary.

## How fresh the numbers are

Views, visitors, searches and the search lists are computed once a day. Today's figures are incomplete until the next day, and the most recent days can still change as the daily run processes them again. Ratings, authoring activity and published totals are counted when you ask.

So fetch a report once and keep it. Asking for the same period again during the day returns the same traffic figures and spends your limit.

## Limits

- The analytics endpoints allow **6 requests a minute** per API key or OAuth app, shared between the three, on top of the general limit of 300 a minute (see [Rate limits](https://developers.helpcenter.io/content/rate-limits)). Requests refused with `422` count too.
- Over the limit, the API answers `429 Too Many Requests` with the code `analytics_rate_limited`, and `Retry-After` gives the seconds to wait. Wait instead of retrying in a loop.
- When the reports can't be computed, the API answers `503 Service Unavailable` with the code `analytics_unavailable` and `Retry-After: 300`. It isn't a problem with your request: try again after five minutes.

## What the reports include

Only totals, counts and rates: no visitor IP addresses, no visitor identifiers, no individual searches and no reader journeys. Search `query` text is returned as readers typed it, so treat it like any text readers send you.

Private articles, and articles in private categories, appear with their titles like every other article: a key or token sees all of its help center's articles, as it does through the Articles endpoints.

## Errors

| Status | When |
| --- | --- |
| `401 Unauthorized` | The key or token is missing or not valid: `{"status": "unauthorized"}`. |
| `403 Forbidden` | `insufficient_scope`: the OAuth token doesn't have `analytics.read`. |
| `422 Unprocessable Entity` | A parameter is not valid: a period over 92 days, `to` before `from`, a date the API can't read, a language that isn't enabled, a category of another help center, or `sort`, `per_page` or `limit` out of range. `errors` names the parameter. |
| `429 Too Many Requests` | `analytics_rate_limited`: more than 6 analytics requests in a minute. |
| `503 Service Unavailable` | `analytics_unavailable`: try again after the seconds in `Retry-After`. |

## Related

- [Rate limits](https://developers.helpcenter.io/content/rate-limits)
- [Articles](https://developers.helpcenter.io/content/articles-api)
