---
title: "Get a keyword's health"
method: GET
path: "/v1/keywords/{id}/health"
tags: ["Keywords"]
---

# Get a keyword's health

`GET /v1/keywords/{id}/health`

## Request

- Base URL: `https://api.mentio.dev`
- URL: `https://api.mentio.dev/v1/keywords/{id}/health`
- Auth: HTTP bearer

Whether the keyword earns what it costs over a trailing window (`range`, default 30d): a status (healthy, noisy, quiet, capped, paused, new) with the reasons in plain words, its numbers by platform and week, what it cost, the words and authors its noise is made of, and suggestions. Each suggestion carries a `patch` to send to PATCH /v1/keywords/{id} as is, and the effect it would have had, measured by running the matcher's own rules over the window's posts. `ai=true` adds a context rewritten by a language model (cached a day, at most 20 model calls an hour per workspace). Read only and never billed; the report is cached for 5 minutes, and a change to the keyword starts a fresh one. At most 30 reads a minute per workspace.

## Path parameters

- `id` string, required: Keyword id (kw_...).

## Query parameters

- `range` '7d' | '30d' | '90d': Trailing window of UTC days ending today, by match time: 7d, 30d, 90d (default 30d).
- `ai` boolean: true: also ask a language model for a rewritten context (cached a day per keyword and window, at most 20 model calls an hour per workspace). Default false: every suggestion comes from the rules alone.

## Response `200`

The keyword's health report

- KeywordHealth
  - `keyword` object, required: The keyword the report is about.
    - `id` string, required: Keyword id (kw_...).
    - `term` string, required: The term as typed.
    - `group` GroupRef, required: The group the keyword belongs to.
      - `id` string, required: Group id (grp_...).
      - `name` string, required: The group's name.
      - `externalId` string, nullable, required: Your own id for the group, or null.
      - `isDefault` boolean, required: The workspace's default group, where a keyword lands when no group is named.
  - `window` object, required: The window the report reads, by match time, in UTC days.
    - `from` string, required: First day, YYYY-MM-DD, inclusive, UTC.
    - `to` string, required: Last day, inclusive: today.
    - `days` integer, required: Length of the window in days.
  - `status` 'healthy' | 'noisy' | 'quiet' | 'capped' | 'paused' | 'new', required: healthy: nothing to fix. noisy: 20 or more scored matches in the window and under 30% of them relevant. quiet: 7 days or older with no relevant match in the window. capped: at its monthly mention cap. paused: muted (by you, the wallet or the noise brake). new: under 7 days old and not noisy yet, or changed in the last 7 days with under 20 scored matches since the change. When the keyword's matching rules, platforms or context changed (or it was unmuted) inside the window, the status reads only the matches since then (stats.judgedSince).
  - `reasons` string[], required: Why, in plain words; the first line explains the status.
  - `stats` object, required: Computed over the keyword's matches in the window.
    - `matches` integer, required: Matches recorded in the window, relevant or not.
    - `relevant` integer, required: Scored at or above the relevance line (40).
    - `filtered` integer, required: Scored under the line: the noise. Billed like any match.
    - `unscored` integer, required: Not scored yet, or failed to score (never billed).
    - `noiseShare` number, nullable, required: filtered / (relevant + filtered); null with nothing scored.
    - `workspaceShare` number, nullable, required: This keyword's share of the workspace's matches in the window; null when the workspace matched nothing.
    - `judgedSince` string, nullable, required: When the keyword last changed its matching rules, platforms or context (or was unmuted), when that is inside the window: the status, the reasons, noiseTerms, noiseAuthors and suggestions read only the matches since then, while these numbers keep the whole window. Null: everything reads the whole window.
    - `byPlatform` object[], required: One row per platform it matched on, most matches first.
      - `platform` 'bluesky' | 'hackernews' | 'github' | 'stackoverflow' | 'devto' | 'reddit' | 'x' | 'youtube' | 'news' | 'linkedin' | 'tiktok' | 'instagram' | 'appstore' | 'googleplay' | 'trustpilot' | 'googlemaps' | 'rss', required: Platform: bluesky, hackernews, github, stackoverflow, devto, reddit, x, youtube, news, linkedin, tiktok, instagram, appstore (App Store reviews), googleplay (Google Play reviews), trustpilot (Trustpilot reviews), googlemaps (Google reviews of a place), rss (RSS and Atom feeds a keyword reads).
      - `matches` integer, required: Matches on this platform in the window.
      - `relevant` integer, required: Of those, scored at or above the relevance line (40).
      - `filtered` integer, required: Of those, scored under the line: the noise.
      - `noiseShare` number, nullable, required: filtered / (relevant + filtered); null with nothing scored.
    - `weekly` object[], required: The trend, one row per 7 days of the window, oldest first; the last row may be shorter.
      - `from` string, required: First day of the 7, YYYY-MM-DD (counted from the window's first day).
      - `matches` integer, required: Matches in those 7 days.
      - `relevant` integer, required: Of those, relevant.
    - `cost` object, required: What the keyword cost over the window at list price.
      - `keywordDays` integer, required: Days in the window the keyword was charged for.
      - `keywordCents` integer, required: Those days at the keyword rate.
      - `billableMentions` integer, required: Matches billed in the window, by the time they were scored.
      - `mentionCents` integer, required: Those matches at the mention rate.
      - `billableComments` integer, required: Comments billed in the window under the keyword (0074).
      - `commentCents` integer, required: Those comments at the comment rate.
      - `totalCents` integer, required: keywordCents, mentionCents and commentCents: its row in GET /v1/usage/breakdown for the same range.
  - `sample` object, required: The posts behind noiseTerms, noiseAuthors and the effects, after the keyword's current rules (a post an older rule let in is not counted). Review platforms are matched by app, not by text, and are left out.
    - `noise` integer, required: Noise posts read (the newest of the window, or since stats.judgedSince, at most 200), on platforms matched by text. None under 20 scored matches since a change.
    - `relevant` integer, required: Relevant posts read, likewise.
  - `noiseTerms` object[], required: Up to 10 words or phrases over-represented in the noise against the relevant posts, strongest first. The keyword's own words are left out.
    - `term` string, required: A word or two-word phrase.
    - `noisePosts` integer, required: Sampled noise posts that carry it.
    - `relevantPosts` integer, required: Sampled relevant posts that carry it.
    - `lift` number, nullable, required: How many times more common it is in noise than in relevant posts (smoothed); null with no relevant post to compare.
  - `noiseAuthors` object[], required: Authors with 3 or more noise posts in the sample and no relevant one.
    - `name` string, required: The author as the platform shows them.
    - `entry` string, required: What matching.excludedAuthors would store for them.
    - `noisePosts` integer, required: Sampled noise posts by them.
  - `suggestions` KeywordSuggestion[], required: Changes that would cut the noise, each ready for PATCH /v1/keywords/{id}. Empty when nothing is worth changing, or under 20 scored matches since the last change (a reason says so).
    - `type` 'excluded_terms' | 'excluded_authors' | 'required_terms' | 'platforms' | 'context', required: excluded_terms and excluded_authors add to the matching rules; required_terms sets them; platforms drops the platforms that are almost all noise; context rewrites the sentence the classifier reads.
    - `values` string[], required: What it adds (terms, authors), drops (platforms) or writes (the context).
    - `why` string, required: The reason and the measured effect, in plain words.
    - `patch` object, required: The body to send to PATCH /v1/keywords/{id} as is to apply it. A list holds the whole new list, current entries kept.
      - `term` string: Renames the keyword: the new term is tracked from now on, with no look-back, and the keyword keeps its id, its settings and the mentions the old term found. Wrap it in double quotes for the exact phrase only. A 409 when its group already tracks the new term.
      - `kind` 'brand' | 'competitor' | 'topic': Reclassify it as brand, competitor or topic.
      - `muted` boolean: A muted keyword stops polling and matching; its mentions stay.
      - `platforms` string[], nullable: Replaces the platform list; null means every platform, [] none (reviews only, when the keyword has reviewSources).
      - `context` string, nullable: A sentence the classifier reads for this keyword only, on top of the company profile or the group's own description (at most 300 characters): what the term means here, what to ignore. "Arc is our browser; ignore the geometry word." Null clears it.
      - `matching` object: Omitted fields are untouched; an empty list clears one.
        - `requiredTerms` string[]: The post must ALSO contain these terms, any one of them or all of them per requiredMode. A `*` at the start or the end of an entry is a wildcard (alert* matches alerts and alerting). Empty: no requirement.
        - `requiredMode` 'any' | 'all': any: at least one required term must appear. all: every one must.
        - `requiredGroups` array[]: Combinations of terms, at most 50 of 10 terms each: the post must ALSO contain every term of at least one group (an OR of ANDs). For the keyword inreach, [["weather", "alert*"], ["tsunami"], ["flash flood"]] keeps a post about inReach weather alerts, tsunamis or flash floods, one keyword instead of three. Same phrase and wildcard rules as requiredTerms; when both are set a post must pass both. Empty: no combinations.
          - string[]
        - `excludedTerms` string[]: A post containing any of these is dropped. A `*` at the start or the end of an entry is a wildcard (beta.* matches beta.0.1; *bot matches nightlybot).
        - `excludedAuthors` string[]: Posts by these authors are dropped: profile or post links, @handles, u/names, Bluesky DIDs or display names, stored in canonical form like an alert's muted list.
        - `caseSensitive` boolean: true: the term must appear in the case it was typed (RAG, never rag). Default false.
        - `exactPhrase` boolean: true: only the exact phrase matches. false (default): a multi-word keyword also matches a post holding its words close together, in any order, plurals and spellings (non-profit, nonprofit) included; such a match is kept and billed only when the classifier scores it relevant. A term sent in double quotes sets this to true.
        - `repositories` object: GitHub only: each field is replaced when sent, kept when omitted; null clears a minimum.
          - `events` string[]: The repository events this keyword takes. traction: a repository created in the last 30 days reached 10 stars. stars: one with 100 stars or more passed a milestone (100, 250, 500, 1,000, 2,000, 5,000 ...). growth: it gained stars fast in a week. new: every repository created, even with no stars (off by default: nearly all of them are noise for a broad keyword). Default: traction, stars, growth. Empty: no repository events.
          - `minStars` integer, nullable: Only repositories with at least this many stars; null for no minimum.
          - `minWeeklyStars` integer, nullable: Only repositories that gained at least this many stars in the last 7 days (25 or more). It applies to every event, and one whose last week is not known yet does not pass. null: a growth event needs 100 stars and 20% of the repository's stars in a week, the other events need no growth.
        - `subreddits` object: Reddit only, for this keyword alone: each list is replaced when sent, kept when omitted.
          - `only` string[]: Replaces the keyword's allowlist; [] clears it.
          - `excluded` string[]: Replaces the keyword's deny list; [] clears it.
      - `cap` object, nullable: Replaces the mention cap and its period (per, default month); null removes it. A cap above the current period's count resumes a capped keyword at once, one at or under it pauses it.
        - `mentions` integer, required: Charged items allowed per period (see per): matched mentions plus the thread comments delivered under them. Every match counts, relevant or not, the look-back a new keyword gets included, because every match bills.
        - `per` 'day' | 'week' | 'month': The period the cap counts in, in UTC: day (from 00:00), week (from Monday 00:00) or month (the calendar month, the default). At the cap the keyword stops until the next period starts, so a daily cap keeps mentions coming every day with a fixed ceiling on spend.
      - `comments` object: Comments under this keyword's mentions; omitted fields are untouched (on create: off, 20 per post).
        - `enabled` boolean: Read the comments under this keyword's relevant mentions, from 30 minutes after each post, as long as its platform's conversations last (a day to a month). Each comment delivered costs $0.008 (comments of fewer than three words are dropped, never billed) (the comments line of the bill). Off by default.
        - `maxPerPost` integer: The newest comments of one thread you receive, 20 by default, 100 at most: the ceiling on what one mention's comments can cost. A comment that names the keyword is a mention too, but only among these newest ones, so it is never billed past the ceiling.
      - `groupId` string: Moves the keyword to this group (grp_...). A 409 when that group already tracks the term.
      - `reviewSources` object[]: Replaces the list of apps whose reviews this keyword collects; [] disconnects them all (their reviews stay). An app or country added here gets the free 30-day look-back; one already listed keeps its place.
        - `url` string: The review page's link: an App Store or Google Play app (https://apps.apple.com/us/app/notion/id1232780281, https://play.google.com/store/apps/details?id=notion.id), a Trustpilot page (https://www.trustpilot.com/review/notion.so) or a Google Maps place (its full link, or a maps.app.goo.gl share link). Or give platform and id.
        - `platform` 'appstore' | 'googleplay' | 'trustpilot' | 'googlemaps': appstore (Apple App Store), googleplay (Google Play), trustpilot (a company's Trustpilot page) or googlemaps (a place's Google reviews).
        - `id` string: The id on the platform: the digits after "id" on the App Store, the package name on Google Play, the company's domain on Trustpilot (notion.so), a Place ID (ChIJ...) on Google Maps.
        - `countries` string[]: App Store and Google Play only: storefronts to read, two-letter codes, at most 20. Default: the one in the link, else us. Each is one more poll a day; the same review seen in two storefronts is one mention. Trustpilot and Google Maps have one page for everyone and take none.
        - `language` string: Google Play only: the language of the reviews to read (en, es, de, pt-BR); Google Play answers one language at a time. Default: the link's hl, else en.
      - `feeds` object[]: Replaces the feeds this keyword reads; [] disconnects them all (their mentions stay). A feed added here is checked now and brings its newest 10 matching items of the last 30 days; one already listed keeps its place.
        - `url` string, required: A feed's address (https://forum.example.com/posts.rss), or a page's (https://forum.example.com): the feed the page advertises is used.
      - `readsWorkspaceFeeds` boolean: true: the keyword also reads the workspace's feeds (GET /v1/feeds), live from now on. false: its own feeds alone.
    - `effect` object, nullable, required: What the change would have done over the window, measured with the matcher's own rules. Null for a context, which changes scores, not matches.
      - `noiseRemoved` integer, required: Noise matches of the window (since stats.judgedSince when set) the change would have removed (estimated from the sample unless exact).
      - `relevantRemoved` integer, required: Relevant matches of the window it would have removed.
      - `centsSaved` integer, required: What the removed matches cost at the mention rate: what the change would have saved over the window.
      - `sample` object, required: The measurement behind the estimate: the matcher's own rules run over the sampled posts.
        - `noiseRemoved` integer, required: Sampled noise posts the change rejects.
        - `noise` integer, required: Noise posts sampled.
        - `relevantRemoved` integer, required: Sampled relevant posts the change rejects.
        - `relevant` integer, required: Relevant posts sampled.
      - `exact` boolean, required: The sample was the whole window, so the counts are measured, not estimated.
    - `source` 'rules' | 'ai', required: rules: computed from the window's posts. ai: written by a language model (ai=true).
  - `ai` object, required: The optional language model half of the report.
    - `requested` boolean, required: ai=true was asked.
    - `status` 'off' | 'generated' | 'cached' | 'unavailable' | 'rate_limited', required: off: not asked. generated: written now. cached: written earlier today for the same keyword, window and settings. unavailable: the model failed, answered nothing usable or is not configured. rate_limited: the workspace's 20 model calls this hour are spent.
  - `generatedAt` string, required: When the report was computed; it is cached for 5 minutes, and a change to the keyword starts a fresh one.

## Other responses

- `400`: Invalid query parameters
- `401`: Missing or invalid API key
- `404`: Keyword not found
- `429`: More than 30 health reads this minute (rate_limited)

---

[API](https://skmtc.dev/mentio/apis/mentio-api.md) · [All operations](https://skmtc.dev/mentio/apis/mentio-api/llms.txt) · [OpenAPI document](https://skmtc.dev/mentio/apis/mentio-api/revisions/b16a21d0ed02?raw)
