# AppTail – ASO & Keyword Research (remote · mcp.apptail.io)

App Store keyword rankings, competitors, markets, reviews and analytics for iOS apps, from your AI.

- Trust score: 78/100 (medium)
- Change this week: +1
- Registry status: active
- Liveness: live
- Owner verified: no
- Last scored: 2026-11-05

## Components

- remote · `mcp.apptail.io`: 78/100 (this document), [markdown](https://verifymcp.io/servers/io-apptail-apptail/mcp.md), [page](https://verifymcp.io/servers/io-apptail-apptail/mcp)

## Channel facts

- Endpoint: `https://mcp.apptail.io/mcp`
- Transports: `streamable-http`
- Auth: `none`
- Version: `1.0.0`

## Trust breakdown

How this component scores in each security and reliability category. Every signal is checked automatically against the live server, and we only credit what we can confirm. Scores are 0–100 per category. Scoring method: https://verifymcp.io/docs/scoring (what has changed: https://verifymcp.io/docs/scoring/changelog)

Scored 2026-11-05.

- **Endpoint Security**: 57/100
  - The endpoint's TLS certificate is valid, in date, and uses a strong key.
  - Authorisation check failed: no authorisation is required to call this server, and it exposes a tool marked destructive (remove_keywords).
  - HTTPS is enforced; there's no plaintext access path.
  - HSTS check failed: the Strict-Transport-Security header is absent.
  - DNSSEC check failed: this domain isn't protected by DNSSEC.
- **Transport & Reachability**: 100/100
  - Verified streamable-http transport via a live MCP handshake.
- **Schema Quality & AI Usability**: 74/100
  - 100% of prompts and resources have a non-trivial description (not blank, and not just the item's name).
  - AI-judged instruction clarity (excellent).
  - Context-footprint check failed: tool/resource definitions use about 13063 tokens (~522/item across 25 items; 25 tools + 0 resources), over budget; trim descriptions and params.
  - Usage-examples check failed: none of the tools include examples.
- **Stability & Change Management**: 100/100
  - No destabilizing schema changes in the last 30 days.
- **Tool Coverage**: 100/100
  - 100% of tools have a non-trivial description (not blank, and not just the tool's name).
  - 100% of tool parameters carry a description.
  - Structured output schemas are declared (100% of tools); any adoption earns full credit.
- **Tool Safety**: 100/100
  - No prompt-injection markers were found in the server instructions, tool names or descriptions we captured.
  - All 3 tool(s) whose name or description implies an irreversible operation declare an MCP destructiveHint annotation.
  - An AI judge read all 26 captured unit(s) of tool text and found none that tries to manipulate the model reading it.
- **Capabilities**: 100/100
  - Implements a supported MCP spec version (2025-11-25); the latest is 2026-07-28.

## Install

### How do I install the AppTail – ASO & Keyword Research MCP server?

AppTail – ASO & Keyword Research is a hosted endpoint at https://mcp.apptail.io/mcp, so there is nothing to install locally. Ready-made configuration for Claude, Cursor, VS Code, Codex and 5 more is on this page, copied from each client's own documentation.

### Claude

```bash
claude mcp add --transport http io-apptail-apptail 'https://mcp.apptail.io/mcp'
```

### Cursor

```json
{
  "mcpServers": {
    "io-apptail-apptail": {
      "url": "https://mcp.apptail.io/mcp"
    }
  }
}
```

### VS Code

```json
{
  "servers": {
    "io-apptail-apptail": {
      "type": "http",
      "url": "https://mcp.apptail.io/mcp"
    }
  }
}
```

### Codex

```toml
[mcp_servers.io-apptail-apptail]
url = "https://mcp.apptail.io/mcp"
```

### opencode

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "io-apptail-apptail": {
      "type": "remote",
      "url": "https://mcp.apptail.io/mcp",
      "enabled": true
    }
  }
}
```

### OpenClaw

```bash
openclaw mcp add io-apptail-apptail --url 'https://mcp.apptail.io/mcp' --transport streamable-http
```

### Hermes

```yaml
mcp_servers:
  io-apptail-apptail:
    url: "https://mcp.apptail.io/mcp"
```

### Netclaw

```json
{
  "McpServers": {
    "io-apptail-apptail": {
      "Transport": "http",
      "Url": "https://mcp.apptail.io/mcp"
    }
  }
}
```

### Vellum

```bash
assistant mcp add io-apptail-apptail -t streamable-http -u 'https://mcp.apptail.io/mcp'
```

### Other

```json
{
  "mcpServers": {
    "io-apptail-apptail": {
      "type": "http",
      "url": "https://mcp.apptail.io/mcp"
    }
  }
}
```

The mcpServers block is a cross-client convention. Remote transports vary, so check your client's docs.

## Changelog

Every change recorded for this component, newest first. Days that predate change tracking, or that we cannot explain, say so: "we were watching and nothing happened" and "we were not watching" are different claims.

### 2026-10-31 (score 78, 0)

- [security] Stability: 0.97 → pass

### 2026-10-30 (score 78, +1)

No change was recorded against any check on this day. Stability & Change Management went from 93 to 97. That category is still filling its 30-day observation window: 28 days of observed history at the previous scan, 29 at this one. The score rises as the window fills, whether or not the server changes.

### 2026-10-28 (score 77, +1)

No change was recorded against any check on this day. Stability & Change Management went from 87 to 90. That category is still filling its 30-day observation window: 26 days of observed history at the previous scan, 27 at this one. The score rises as the window fills, whether or not the server changes.

### 2026-10-26 (score 76, +1)

No change was recorded against any check on this day. Stability & Change Management went from 80 to 83. That category is still filling its 30-day observation window: 24 days of observed history at the previous scan, 25 at this one. The score rises as the window fills, whether or not the server changes.

### 2026-10-24 (score 75, +1)

No change was recorded against any check on this day. Stability & Change Management went from 73 to 77. That category is still filling its 30-day observation window: 22 days of observed history at the previous scan, 23 at this one. The score rises as the window fills, whether or not the server changes.

### 2026-10-21 (score 74, +1)

No change was recorded against any check on this day. Stability & Change Management went from 63 to 67. That category is still filling its 30-day observation window: 19 days of observed history at the previous scan, 20 at this one. The score rises as the window fills, whether or not the server changes.

### 2026-10-19 (score 73, +1)

No change was recorded against any check on this day. Stability & Change Management went from 57 to 60. That category is still filling its 30-day observation window: 17 days of observed history at the previous scan, 18 at this one. The score rises as the window fills, whether or not the server changes.

### 2026-10-17 (score 72, +1)

No change was recorded against any check on this day. Stability & Change Management went from 50 to 53. That category is still filling its 30-day observation window: 15 days of observed history at the previous scan, 16 at this one. The score rises as the window fills, whether or not the server changes.

## MCP tools (25)

### `get_account` (~212 tokens)

Get Account

The account: its apps with Apptail ids, its plan and the limits that can refuse a write, App Store Connect health per app, how fresh the first-party data is, and the storefronts it focuses on. Call this first whenever the request is about "my app" or "my keywords" — every other tool needs an app_id, and the focus countries and hidden apps here are what keep your answer agreeing with what the customer sees in the console.

Input parameters:

- `include_competitors` (boolean): Also return the rival apps tracked under this account's apps, each marked `is_competitor`. Default false: including them makes any portfolio total wrong, and get_competitors is the tool that answers…
- `include_hidden` (boolean): Also return apps the owner has set aside. Default true, because they are still tracked and still answerable — each one is marked `hidden: true`, and they must be left out of portfolio totals. Set fal…

Output parameters:

- `apps` (array): The account's apps. `app_id` here is what every other tool takes; the numeric id in an App Store URL is `apple_app_id` and will not work.
- `connection` (object): App Store Connect health for the account. A `failed` or `stale` connection looks exactly like a quiet fortnight in the numbers, which is why it is stated separately rather than left to be inferred. `…
- `count` (integer): Number of apps returned.
- `data_through` (string|null): Most recent day App Store Connect reported anywhere in the account. Per-app dates are on each app, and they differ — one healthy app keeps this recent while another has been silent for weeks.
- `focus_countries` (array): The storefronts this account actually watches, in its own order. Every console screen narrows to these. Narrow your answers to them too unless the user asks otherwise — an answer across all 122 store…
- `plan` (object): The plan and its ceilings, read from the services that enforce them — so a refusal cannot disagree with this. Tell the user what was capped rather than working around a limit.

### `explain_period` (~394 tokens)

Explain Period

Return an account or owned-app summary for a requested period: available measured App Store Connect performance, storefront changes, tracked keyword movements, recorded signals, review volume and the last sent digest. Returns comparison data, coverage and caveats identifying unavailable sections. Reads account data without changing tracking or publishing content.

Input parameters:

- `app_id` (integer): Narrow to one of the account's own apps — get_account lists them. Omit for the whole portfolio, which excludes apps the owner has hidden.
- `compare` (string): What to read the period against. `previous` (the default) is the period immediately before — for a calendar month, the calendar month before. `year_ago` is the same dates a year earlier, for comparis…
- `country` (string): Storefront for the keyword half only; the money is reported across all of them with a per-storefront split. Defaults to the app's primary storefront. Ranks only exist per storefront.
- `from` (string): Start date, `YYYY-MM-DD`, inclusive. Overrides `period` when both are sent. Send it alone and the window runs to today.
- `period` (string): The window to report on. A calendar month as `2026-07`, a calendar year as `2026`, `last_month`, `this_month`, `last_year`, or a trailing window: `today`, `7d`, `14d`, `30d`, `90d`, `180d`, `365d`, `…
- `to` (string): End date, `YYYY-MM-DD`, inclusive. Send it alone and the window is the three years before it.

Output parameters:

- `app_ids` (array): The apps this covers.
- `caveats` (array): Everything the reader has to hear before this reads as a complete picture: apps with no connection, a window shortened to the last day Apple reported, a corpus larger than the movers shown. **Pass th…
- `comparison_window` (object|null): What everything here is read against. Null when `compare` was `none`.
- `digest` (object|null): **What the customer was last mailed** — the weekly digest, sent Wednesdays, that leads with the measured week and at most three moves. Null when none has gone out yet. Anything here is breakfast read…
- `headline` (array): The two or three sentences a person would open with, each carrying its own figures. Lead with these; everything below is the evidence for them.
- `keywords` (object|null): The keyword half. Null when the account tracks no apps.
- `performance` (object|null): The measured half. **Null when the account has no apps at all**, and present with null figures when it has apps but no App Store Connect connection — those are different answers, and the caveats say…
- `reviews` (object): Review arrivals — a flow, counted over the period. Not the backlog of unanswered reviews, which has no window and is on the console's reviews screen.
- `signals` (object): What the product itself detected. A stored record, not a reconstruction — quote these sentences rather than re-deriving them from the numbers.
- `window` (object): The window actually read, which is not always the one asked for: an inverted range is swapped, a range past three years is shortened, and first-party figures are pulled back to the last day App Store…

### `get_performance` (~598 tokens)

Get Performance

Read measured App Store Connect impressions, store views, downloads, sales and proceeds for apps connected to the authenticated account. Supports an owned app or portfolio, documented time windows, period comparisons and breakdowns by storefront or traffic source. Returns the actual reporting window, connection coverage and caveats. Does not provide private analytics for apps outside the account.

Input parameters:

- `app_id` (integer): Apptail app id, required when `scope` is `app`. Must be one of the user's own apps — get_account lists them. Measured analytics require an authorised App Store Connect connection for the account.
- `compare` (string): What to read the window against. `previous` (the default) is the period immediately before — for a calendar month that is the calendar month before, not the same number of days. `year_ago` is the sam…
- `from` (string): Start date, `YYYY-MM-DD`, inclusive. Overrides `period` when both are sent. Send it alone and the window runs to today.
- `grain` (string): Bucket size for the series. Omit and it is chosen to fit the window — a day for a month, a week for a year. A grain too fine or too coarse for the window is ignored rather than refused.
- `include_apps` (boolean): On `portfolio` scope, also return one row per app so a portfolio move can be attributed to the app that caused it. Default false.
- `period` (string): The window to report on. A calendar month as `2026-07`, a calendar year as `2026`, `last_month`, `this_month`, `last_year`, or a trailing window: `today`, `7d`, `14d`, `30d`, `90d`, `180d`, `365d`, `…
- `scope` (string): `portfolio` (the default) sums every app the account owns; `app` reports one, and then `app_id` is required. Hidden apps are in the portfolio sum only if the account has not hidden them — they are ex…
- `split` (string): Break the totals down. `country` gives one row per storefront Apple named; `source` gives Apple's own attribution — App Store search, browse, app and web referrers. `source` reports the attribution a…
- `to` (string): End date, `YYYY-MM-DD`, inclusive. Send it alone and the window is the three years before it.

Output parameters:

- `app_ids` (array): The apps behind these figures.
- `apps` (array): One row per app, when `include_apps` was set on a portfolio call. Empty otherwise.
- `caveats` (array): Sentences the reader has to hear before the numbers mean what they look like they mean — apps left out, a window shortened, a breakdown that does not sum. Pass these on; do not summarise them away.
- `comparison_window` (object|null): The window `previous` on each metric was measured over. Null when `compare` was `none`.
- `coverage` (object): What the totals do and do not cover. Quote this whenever `connected` is below `apps`.
- `data_through` (string|null): Last day App Store Connect has reported across these apps. Apple lands analytics one to two days late, so this is usually a day or two behind today; a gap of a week or more is a broken connection rat…
- `grain` (string): Bucket size the series came back at.
- `requested_window` (object): What the call asked for, when that differs from what was read. Compare with `window` before quoting a total as covering the period the user named.
- `scope` (string): Which scope answered: `portfolio` or `app`.
- `series` (array): The same metrics bucketed across the window, oldest first. Empty when nothing is connected.
- `split` (string): Which breakdown was returned: `none`, `country` or `source`.
- `split_rows` (array): The breakdown, biggest by downloads first. Empty when `split` is `none`.
- `split_total` (integer): How many rows the breakdown has in total, which can exceed the number returned.
- `totals` (array): Window totals, one entry per metric, each carrying where it came from and what it may be compared with.
- `truncated` (boolean): True when `split_rows` is shorter than `split_total`. Say so rather than presenting the returned rows as the whole breakdown.
- `window` (object): The window actually read, which is not always the one asked for: an inverted range is swapped, a range past three years is shortened, and first-party figures are pulled back to the last day App Store…

### `get_signals` (~601 tokens)

Get Signals

What actually happened, from the record the product keeps: rank moves on tracked terms, chart entries, review spikes, competitor releases and price changes, plus — for the user's own apps only — week-over-week traffic and conversion shifts, download collapses, new reviews and rating moves. Each carries the two numbers behind it and the sentence that states the finding. This is a stored table read, not a reconstruction, so it is both cheaper and more reliable than diffing rank histories yourself. Covers the user's own apps and the competitors tracked under them; a rival's finding never `asks_action`.

Input parameters:

- `app_id` (integer): Narrow to one of the user's own apps — get_account lists them. Findings about the competitors tracked under it are included, because a rival taking your places is something that happened to you. Omit…
- `from` (string): Start date, `YYYY-MM-DD`, inclusive. Overrides `period` when both are sent. Send it alone and the window runs to today.
- `include_sent` (boolean): Mark the findings that already went out in an alert digest the user has read. Default false. Set it whenever you are about to summarise a period for someone: without it you cannot tell news from some…
- `limit` (integer): Max findings, newest first. Default 50, capped at 200. The answer always says how many the window actually holds.
- `min_severity` (string): Floor on how loudly a finding speaks. Default `neutral`, which is everything. `warning` drops a rival shipping a point release, which is worth knowing and not worth acting on. It is NOT the same as "…
- `period` (string): The window to report on. A calendar month as `2026-07`, a calendar year as `2026`, `last_month`, `this_month`, `last_year`, or a trailing window: `today`, `7d`, `14d`, `30d`, `90d`, `180d`, `365d`, `…
- `to` (string): End date, `YYYY-MM-DD`, inclusive. Send it alone and the window is the three years before it.
- `types` (array): Only these kinds of finding. Omit for all of them. An unknown name is refused rather than ignored, so a filtered answer is never returned as though it were unfiltered.

Output parameters:

- `app_ids` (array): The apps of the user's these findings hang under.
- `by_severity` (object): The severities among the findings returned. Counted over the returned rows, not the window, so it describes the list in hand.
- `by_type` (object): How many of each kind the window holds — the shape of the period, counted over everything rather than over the capped list. A key is absent when there were none.
- `count` (integer): Findings returned.
- `digest` (object|null): What the product has already told this person, when `include_sent` was set. Null otherwise, which means "not asked" rather than "never mailed".
- `signals` (array): The findings, newest first. Each carries the sentence that states it and the two numbers behind it. Empty is a real answer: it means nothing crossed a detector's threshold, which is the only way a th…
- `total` (integer): Findings the window holds in total. Larger than `count` when the list was capped.
- `truncated` (boolean): True when `count` is below `total`. Say the list is partial rather than presenting it as everything that happened.
- `window` (object): The window actually read, which is not always the one asked for: an inverted range is swapped, a range past three years is shortened, and first-party figures are pulled back to the last day App Store…

### `search_apps` (~300 tokens)

Search Apps

Search for iOS apps by name, bundle ID, or App Store URL. Supports all languages including Cyrillic, Chinese, etc. If the app is not in our database, it will be fetched from the App Store, persisted as a shared public app record, and queued for enrichment. This does not add the app to your account or competitor watchlist. Auto-detects the likely App Store region from the query language (e.g. Cyrillic → Russia, Chinese → China). To add what you find, add_competitors and add_app take the same name or URL directly — you do not have to look up an app_id first.

Input parameters:

- `country` (string): Storefront to search (e.g. "us", "ru"). Omit and it is guessed from the script the query is written in — Cyrillic implies ru, and so on. Pass it explicitly when the user names a market, because the g…
- `limit` (integer): Max results. Default 10, capped at 25.
- `query` (string, required): App name in any language, a bundle id (com.example.app), or a full App Store URL. Searches Apptail first and falls back to the App Store, importing anything it finds — so this returns a usable app_id…

Output parameters:

- `apps` (array): Matching apps, best match first. Empty when nothing was found.
- `count` (integer): Number of apps returned.
- `country` (string|null): Storefront that was searched. Null when answered from the local database, which is not country-specific.
- `hint` (string|null): Present when nothing was found: suggests how to retry.
- `note` (string|null): Present when the results need a caveat, e.g. data still being fetched in the background.
- `source` (string): Where the results came from. "app_store" means they were just imported and are still being enriched.

### `get_app` (~570 tokens)

Get App

Everything AppTail holds about one app, at the depth you ask for. The base answer is the store listing **in one storefront** — the localised title and subtitle a shopper in `country` actually reads, the price and its currency, version, category, release dates, the publisher's ids. `include` adds sections: `prices` (what it costs in every storefront), `ratings` (every storefront's rating and vote count, pooled honestly, plus a daily series and per-storefront movement with `history_days`), `charts` (the charts it currently ranks in), `popularity` (organic reach), `developer` (the publisher and its portfolio), `versions` (release cadence) and `screenshots`. Works for ANY app in the store, not only the user's own. It does NOT return impressions, downloads or revenue — get_performance does, and only for the user's own connected apps.

Input parameters:

- `app_id` (integer, required): Apptail app id. get_account for the user's own apps, search_apps for any other app in the store. Not the numeric id in an App Store URL (that is apple_app_id).
- `country` (string): Storefront to read the listing in (e.g. US, GB, DE). Defaults to the app's primary storefront. Title, subtitle, price, rating and screenshots all differ per storefront, so this changes the answer rat…
- `history_days` (integer): With `include: ["ratings"]`, also return the rating and the vote count day by day, this many days back from today, **and how each storefront moved over that window** — `ratings_gained` and `rating_ch…
- `include` (array): Extra sections to read. Omit for the listing alone, which is the cheap answer. `ratings`, `charts` and `popularity` need a Starter or Pro plan and are refused by name without one — the rest are free.…

Output parameters:

- `app_id` (integer): Apptail app ID. This is the `app_id` every other tool expects.
- `apple_app_id` (integer|null): Apple's own numeric app ID, as it appears in App Store URLs. Not interchangeable with `app_id`.
- `bundle_id` (string|null): Store bundle identifier, e.g. "com.burbn.instagram".
- `charts` (array): Charts the app currently ranks in, across all countries and categories. Present only with `include: ["charts"]`; empty means it charts nowhere.
- `console_url` (string|null): Where to open this app in the AppTail console — the owner's own dashboard when `is_mine`, the research view otherwise. Null where this environment serves no console. End an answer about an app with i…
- `country` (string|null): Storefront the metrics below were read from. Defaults to `primary_country`.
- `currency` (string|null): ISO code of the currency `price` is in, derived from `country`. A price without it is a number, not an amount — several storefronts print a "$" and mean different money.
- `current_version_release_date` (string|null): Date the current version shipped (YYYY-MM-DD). Useful as an update-cadence signal.
- `developer` (object|null): The publisher and its portfolio — with the cap on `top_apps` declared. Present only with `include: ["developer"]`; null there when AppTail holds no profile for the publisher.
- `developer_id` (integer|null): Apptail developer ID. Pass this as `developer_id` to get_developer_info. Null if we hold no profile for the publisher.
- `has_inapps` (boolean|null): Whether the app offers in-app purchases.
- `icon` (string|null): Absolute URL of the app icon.
- `includes` (array): The sections actually read. A section absent from this list is absent from the answer because nobody asked for it — which is not the same as it being empty.
- `is_mine` (boolean|null): True when this is one of the asking account's own apps. Null when the call carried no account — get_top_charts answers without a token — which is "not known here", not "no".
- `is_tracked_competitor` (boolean|null): True when the account already tracks this app as a competitor of one of theirs. False on a high-ranking rival is a suggestion worth making: add_competitors starts watching it. Null when the call carr…
- `listing_storefronts` (array): One storefront per language the listing is localized in, `primary_country` first, e.g. ["ru", "us"]. An app published in two languages is searched for in two storefronts; keyword discovery and tracki…
- `name` (string|null): The app's title **as published in `country`** — the string a shopper in that storefront reads, which is also the string Apple indexes. It is not a translation of one canonical name: an app localised…
- `popularity` (object|null): Organic reach in `country`. Present only with `include: ["popularity"]`, and **null there when no storefront could be resolved** — a key that is present and null means "asked for, nothing to read", w…
- `price` (number|null): Download price in `country`, in that storefront's own currency and in major units — 9.99, never 999. 0 means free, which is what most apps are; `currency` names the money. Null when this storefront h…
- `prices` (object|null): What the app costs to download, per storefront. Present only with `include: ["prices"]`. This is the download price alone: `has_inapps` says whether there is more to pay after that, and get_app does…
- `primary_category` (integer|null): Store category ID. Pass this as `category` to get_top_charts.
- `primary_category_name` (string|null): Human-readable name of `primary_category`, e.g. "Health Fitness".
- `primary_country` (string|null): Lowercase ISO country code of the app's main storefront, e.g. "us".
- `privacy_policy_url` (string|null): Privacy policy URL.
- `rating_average` (number|null): Mean star rating (1-5) in `country`. Null if the app has no ratings there yet.
- `rating_count` (integer|null): Number of ratings in `country`.
- `ratings` (object|null): Present only when `ratings` is in `include` — a section you did not ask for is absent from the answer entirely, which is not the same as it being empty.
- `release_date` (string|null): Date the app first appeared in the store (YYYY-MM-DD).
- `screenshots` (object|null): Present only with `include: ["screenshots"]`, and null there when no storefront could be resolved.
- `seller_url` (string|null): Publisher's marketing website.
- `store` (string|null): Which store the app belongs to.
- `store_developer_id` (integer|null): The store's own publisher ID, as it appears in store URLs. Not interchangeable with `developer_id`.
- `subtitle` (string|null): The subtitle under the title in `country` — thirty characters Apple indexes as heavily as the title, and the field most competitors leave generic. Null when the storefront listing has none, which is…
- `support_url` (string|null): Support page URL.
- `tier` (integer|null): Apptail activity tier for `country`: 1 = currently charting, 2 = shipped more than two updates in the last 180 days.
- `version` (string|null): Latest published version string.
- `versions` (object|null): Release history. Present only with `include: ["versions"]`. Cadence is the one competitor signal readable straight off the store: four releases in six weeks then eight months of nothing is a team tha…

### `get_keywords` (~1078 tokens)

Get Keywords

The account's tracked keywords for an app — or across the whole portfolio — over any window: where each term ranked at the start and end, how far it moved, how many days it held and how many days it was measured, and optionally its day-by-day history, the same terms measured for named competitors, Search Visibility (a 0–100 score per storefront with the terms that moved it), and the terms two of your own apps are both on. Sorted worst movement first by default, so "what dropped" is at the top of a 300-term corpus — which also makes the returned rows a selection rather than a sample: `movers` and `coverage` are counted over the whole corpus and are what a summary comes from. Filter by `contains` to ask about one term by name. This is the tool for every question about tracked terms; it replaces get_tracked_keywords, get_keyword_positions and compare_keyword_positions.

Input parameters:

- `app_id` (integer): Apptail app id for one of YOUR apps — get_account lists them. Required for `scope: "app"`. Not the numeric id in an App Store URL (that is apple_app_id).
- `compare_with` (array): Apptail app ids to measure on these same terms — from get_competitors or search_apps. Rivals are measured on YOUR corpus, not theirs. At most 5 are used and the answer names which; send the ones that…
- `contains` (string): Only terms whose text carries this substring, matched case-insensitively and applied before the sort and the cap. This is how to ask about a term by name — "how is `mpg` doing" — without looking up i…
- `country` (string): Storefront to read ranks in (e.g. US, GB). Defaults to the app's primary storefront. Ranks only exist per storefront — the same term ranks differently in each — so one call answers for one market.
- `from` (string): Start date, `YYYY-MM-DD`, inclusive. Overrides `period` when both are sent. Send it alone and the window runs to today.
- `include` (array): Extra blocks. `history` adds a day-by-day series per term, capped at 25 terms — pair it with `keyword_ids` for specific terms. `visibility` adds **Search Visibility** — the 0–100 answer to "is our AS…
- `keyword_ids` (array): Only these terms, by Apptail keyword id — from this tool's own output or discover_keywords. Ids the account does not track are still answered when the scope is one app, because "how is my app doing f…
- `limit` (integer): Max terms returned, after sorting. Default 100, capped at 300. The answer always says how many the corpus holds.
- `period` (string): The window to report on. A calendar month as `2026-07`, a calendar year as `2026`, `last_month`, `this_month`, `last_year`, or a trailing window: `today`, `7d`, `14d`, `30d`, `90d`, `180d`, `365d`, `…
- `scope` (string): `app` (the default) reads one app's corpus and needs `app_id`. `portfolio` reads every app the account owns and reports each term under whichever of them ranks best for it — that is the scope `includ…
- `sort` (string): `movement` (the default) puts terms that left the results first, then real drops by size — the top of the list is what to ask about. `rank` is best position first, `volume` is most searched (Apple po…
- `tag_id` (integer): Only keywords carrying this tag. Tag ids come back on the keywords in this tool's own output. Omit for the whole corpus.
- `to` (string): End date, `YYYY-MM-DD`, inclusive. Send it alone and the window is the three years before it.

Output parameters:

- `app_ids` (array): The apps these ranks were read for.
- `caveats` (array): Sentences the reader has to hear before the list means what it looks like it means — a capped list and what it was selected for, terms nobody crawled, ranks that are weeks old, a history returned for…
- `compare_truncated` (boolean): True when competitor ids were dropped for exceeding the cap of 5.
- `compared_app_ids` (array): The competitor ids actually measured. Shorter than what was sent when the cap or an unknown id dropped one.
- `count` (integer): Terms returned.
- `country` (string): Storefront the ranks were measured in.
- `coverage` (object): How much of this corpus was actually looked at in this window — the answer to "is this a drop or a gap in your crawling", which is otherwise unanswerable from ranks alone and has been guessed at wron…
- `keywords` (array): The terms, in the order `sort` asked for. **A selection, not a sample**: this is the top of a sorted list, so when `truncated` is true these rows are whatever the sort favoured — with the default sor…
- `movers` (object): The shape of the window in four numbers, **counted over every term in the corpus, not over the rows returned**. This is the only honest basis for a sentence about the corpus, and it is the one to lea…
- `scope` (string): Which scope answered: `app` or `portfolio`.
- `shared` (array): Terms two of the account's own apps are both on, worst first, when `include: ["shared"]` was asked for at portfolio scope. Empty otherwise. Read `competing` before calling anything a conflict.
- `sort` (string): The order the list came back in.
- `total` (integer): Terms the corpus holds in this storefront. Larger than `count` when the list was capped.
- `truncated` (boolean): True when `count` is below `total`. Say the list is partial rather than reporting it as the whole corpus.
- `visibility` (object|null): When `include: ["visibility"]` was asked for: Search Visibility (`score`, `storefronts`) and the daily band counts. **The band counts are not over this corpus**: they count every term the app appeare…
- `window` (object): The window actually read, which is not always the one asked for: an inverted range is swapped, a range past three years is shortened, and first-party figures are pulled back to the last day App Store…

### `get_keyword_serp` (~518 tokens)

Get Keyword Serp

The App Store search results for one term in one storefront, on one day: who ranked, in what order, with their names, subtitles and ratings. This is how you find out WHO took the places an app lost — a rank that fell is a fact, and the results page on the day it fell is the reason. Works for ANY term in the store, tracked or not, known to us or not: pass `keyword_id` for a term you already have an id for, or `keyword` + `country` for words a user typed, which resolves the term and crawls the storefront live when what we hold is over a day old. Omit the date for the most recent crawl. A lookup can persist a new shared keyword record and queue a background crawl that updates stored search observations. It does not track the keyword for your account.

Input parameters:

- `country` (string): Storefront for `keyword`, e.g. "us", "jp". Required with `keyword` and ignored with `keyword_id`, because a keyword already names its storefront. There is no worldwide search results page — every App…
- `date` (string): The day to read, `YYYY-MM-DD`. Omit for the most recent crawl, which is the only mode that ever crawls live — naming a day is a question about the past and is answered from what was recorded. A day n…
- `keyword` (string): The search term itself, for words a user typed rather than an id — "sleep tracker", "заметки". Needs `country`. A term nobody has ever looked up is created and the storefront is read live, so this an…
- `keyword_id` (integer): Apptail keyword id — from get_keywords or discover_keywords, or off a `rank.move` signal. The storefront is fixed by the keyword itself; a term is tracked per country. Give this OR `keyword` + `count…
- `limit` (integer): How deep to read. Default 25, capped at 50. Below about 25 nobody is competing with you for the term.

Output parameters:

- `apps` (array): The results in rank order, best first. `is_mine` marks the account's own apps and `is_tracked_competitor` marks rivals it already watches — a high-ranking app that is neither is worth suggesting they…
- `console_url` (string|null): Where this search is shown in the console, with the icons and screenshots this payload leaves out.
- `count` (integer): Apps returned.
- `country` (string|null): Storefront these results come from. Fixed by the keyword.
- `crawled` (boolean): Whether that day had a crawl at all. False with an empty list means nobody measured, **not** that nobody ranked.
- `crawled_days` (array): Days in the last 30 that were crawled, newest first. Name one in `date` to see how the results looked then — comparing two days is what shows who took a place.
- `date` (string|null): The day actually read, which is the day asked for or the most recent crawl. Null when nothing has been crawled for this term.
- `keyword` (string): The search term itself.
- `keyword_id` (integer): The term that was read.
- `my_position` (integer|null): The best rank held here by one of the account's own apps, or null when none of them are in these results. Null is not -1: it means no app of theirs appears in the depth that was read, which is not th…
- `popularity` (integer|null): Apple search popularity for the term, roughly 5-100. Higher means more searched. Read it before calling a position valuable.
- `refreshed` (boolean): True when this call read the App Store rather than the database — a term looked up for the first time, or one whose last crawl was over a day old. False means these are stored results; `date` says wh…
- `results_count` (integer|null): How many apps the store returns for this term in total. The results below are the top of that.

### `discover_keywords` (~571 tokens)

Discover Keywords

Terms an app could be tracking but is not, merged from every source and each labelled with where it came from: what tracked competitors rank for, what the store's similar apps rank for, what those competitors put in their own titles and subtitles, what the store's own mining surfaced, and — reading the listing cold — what the app's own title, subtitle and description suggest. The last is the only source that works on an app added minutes ago; `listing` is the only one that can surface a phrase nobody has ranked for yet, and it offers a phrase only when two or more rivals publish it and Apple reports somebody searching it. Pass `contains` to also match terms already in AppTail's database. Already-tracked and already-dismissed terms are never returned. This is a list of candidates, not a measure of coverage: for how much of a niche's vocabulary the account is missing, and which holes matter most, call get_landscape and read `term_gap`. Discovery can persist shared suggested keyword records, cache generated suggestions, and queue background popularity collection. It does not add those terms to account tracking.

Input parameters:

- `app_id` (integer, required): Apptail app id to suggest for. get_account for the user's own apps. Suggestions are derived from this app's listing, its rivals and its neighbours, so they are only meaningful for the app they were a…
- `contains` (string): Only terms containing this text. With `source: ["corpus"]` this is a search of AppTail's keyword database. Note it searches terms AppTail holds, not the App Store: a phrase nobody has ever tracked re…
- `country` (string): Storefront to suggest for (e.g. US, DE). Defaults to the app's primary storefront — which is only one of them: an app localized into several languages is searched for in several storefronts, so call…
- `limit` (integer): Max suggestions, most-searched first. Default 60, capped at 200. The answer always says how many there were.
- `source` (array): Which sources to read. Omit for all five that need no argument — `competitors`, `similar`, `listing`, `store`, `ai`. `corpus` is a text match against terms AppTail already holds and does nothing with…

Output parameters:

- `app_id` (integer): The app these suggestions are for.
- `app_name` (string|null): Its name.
- `by_source` (object): How many suggestions each source produced, keyed by source name; a source with none is absent. **Read this before reporting an empty answer.** Five of the six need something on file — rivals, the sto…
- `console_url` (string|null): The app's keyword screen in the console, where the same list can be reviewed and added by hand.
- `count` (integer): Suggestions returned.
- `country` (string): The storefront they apply to.
- `sources` (array): Sources actually read. A source not listed here was not consulted.
- `suggestions` (array): The terms, most-searched first. Add them with add_keywords using their `keyword_id`, never their text.
- `total` (integer): Suggestions found before the cap.
- `truncated` (boolean): True when `count` is below `total`. Say the list is partial rather than presenting it as everything worth tracking.

### `add_keywords` (~443 tokens)

Add Keywords

Start tracking search terms for one of your apps, in one storefront. Send terms, or `keyword_ids` for suggestions returned by discover_keywords — a suggestion MUST be added by id, because re-resolving its term can land on a different keyword row. Counts against the plan's per-app keyword limit; `dry_run` reports what would happen without writing anything. Terms already in the corpus come back under `already_tracked` rather than as an error — that is the state the caller asked for.

Input parameters:

- `app_id` (integer, required): Apptail app id for one of YOUR apps — get it from get_account. Not the numeric id in an App Store URL (that is apple_app_id). Keywords are tracked per app.
- `country` (string, required): Storefront to track these terms in, ISO 3166-1 alpha-2 (e.g. US, GB, DE). Required and not defaulted: a term tracked in the wrong storefront is a wasted keyword slot. Ranks are per storefront — the s…
- `dry_run` (boolean): Report what would be added, and how many slots are left, without tracking anything. Nothing is written and nothing counts against the plan.
- `keyword_ids` (array): Apptail keyword ids to start tracking — the `keyword_id` on a discover_keywords row. Use this rather than the term whenever you have an id: a term is re-resolved and can land on a different keyword r…
- `keywords` (array): Search terms to start tracking, as a user would type them into the App Store (e.g. ["fitness tracker", "workout app"]). Send the whole batch in one call rather than one term per call. Terms we have n…

Output parameters:

- `added` (array): Terms now being tracked. Under `dry_run` this is what would be added and nothing was written.
- `added_count` (integer): Size of `added`.
- `already_tracked` (array): The subset of `skipped` that the app was already tracking. **This is the end state that was asked for, not a fault** — a term already in the corpus needs no retry and no apology. What is in `skipped`…
- `app_id` (integer): The app these terms are tracked for.
- `country` (string): The storefront they were tracked in.
- `dry_run` (boolean): True when nothing was written.
- `limit` (integer): Keywords this plan allows per app. -1 means unlimited.
- `limit_reached` (string|null): Set when the account's plan limit stopped the run before the whole list was added. A ceiling, not a fault — say what was capped rather than retrying.
- `remaining` (integer|null): Keyword slots left for this app on this plan after the call. Null when the plan is unlimited. Zero means the next add is refused.
- `skipped` (array): Everything not added: the union of `already_tracked` and the terms that could not be resolved in that storefront or that ran out of plan room. Do not report this count as a failure without reading `a…
- `skipped_count` (integer): Size of `skipped`, both reasons together. Subtract `already_tracked` before calling anything a problem.

### `remove_keywords` (~151 tokens)

Remove Keywords

Remove tracked keywords from an app. Frees the slots against the plan limit; position history is kept, so re-adding a term later does not start from nothing.

Input parameters:

- `app_id` (integer, required): Apptail app id for one of YOUR apps — get it from get_account. Not the numeric id in an App Store URL (that is apple_app_id).
- `keyword_ids` (array, required): Apptail keyword ids to stop tracking, as returned by get_keywords. Ids, not terms — two apps can track the same word and only these rows are removed. Frees the slots against your plan limit. Position…

Output parameters:

- `app_id` (integer): The app these terms were tracked for.
- `remaining` (integer): How many keywords this app still tracks, across every storefront.
- `removed_count` (integer): How many keywords were actually untracked. Lower than `requested_count` when some IDs were not tracked for this app.
- `requested_count` (integer): How many keyword IDs were passed in.

### `tag_keywords` (~479 tokens)

Tag Keywords

Put tracked keywords under a tag, in bulk — or clear their tag — and/or say whether they are brand terms for Search Visibility. The tag is named, not looked up: a name that does not exist yet is created. A keyword carries at most one tag, so assigning replaces whatever it had. `brand` is separate from tags: `own` (your app's name) and `competitor` (a rival's name) leave the terms out of the visibility score, `none` counts them whatever detection says, `auto` hands them back to detection. Send `brand` without `tag` to change only that — the tags stay as they are. This is the tool for organising a large corpus after reading it with get_keywords, which is drudgery in the console and one call here.

Input parameters:

- `app_id` (integer, required): Apptail app id for one of YOUR apps — from get_account. Tags belong to an app.
- `brand` (string): Whether these terms are somebody's name, for Search Visibility (get_keywords `include: ["visibility"]`). `own` = your app's name, `competitor` = a rival's: both are listed and left out of the score,…
- `color` (string): Hex colour for a tag being created, e.g. "#2563eb". Ignored when the tag already exists. Omit and one is picked from the palette that the app is not already using.
- `keyword_ids` (array, required): Apptail keyword ids to tag, as returned by get_keywords. Ids, not terms. Up to 500 in one call. A keyword the app does not track is reported back rather than silently ignored.
- `tag` (string): The tag name to put them under. Created if it does not exist. Omit — or send null — to clear the tag off these keywords instead.

Output parameters:

- `action` (string): `assigned` when a tag was set, `cleared` when the tag was taken off, `untouched` when only `brand` was sent and the tags were left as they were.
- `app_id` (integer): The app whose corpus was changed.
- `brand` (string|null): The brand decision applied (`own`, `competitor`, `none`, `auto`), or null when none was sent. Search Visibility recomputes on the next get_keywords read.
- `brand_changed_count` (integer): How many keywords took the brand decision. 0 when `brand` was not sent.
- `changed_count` (integer): How many keywords had their tag changed. 0 on a brand-only call.
- `requested_count` (integer): How many keyword ids were sent.
- `tag` (object|null): The tag the keywords now carry. Null when the action was `cleared`.
- `tag_created` (boolean): True when this call created the tag rather than reusing one. Worth mentioning: a typo makes a second tag rather than an error.
- `tags` (array): Every tag this app now has, so a follow-up call does not have to guess at names.
- `unknown_ids` (array): Ids this app does not track, so nothing was changed for them. Empty on a clean run. Never present these as tagged.

### `get_competitors` (~165 tokens)

Get Competitors

List tracked competitor apps for one of your apps, with how many more the plan allows — and `suggested`: up to ten rivals the account does not track yet, ranked by how many of the app's own search terms they sit in the top ten for. Works on an app with no keywords: its listing is read for terms and the store is crawled for them. Read `suggested_basis.terms_pending` before calling a short list complete. Suggestions can persist shared keyword records and queue background crawls. Suggested apps are not added to your competitor watchlist.

Input parameters:

- `app_id` (integer, required): Apptail app id for one of YOUR apps — get it from get_account. Not the numeric id in an App Store URL (that is apple_app_id).

Output parameters:

- `app_id` (integer): The app these rivals are tracked against.
- `competitors` (array): Apps tracked as competitors of the given app. Empty if none are tracked yet.
- `count` (integer): Number of competitors returned.
- `limit` (integer): How many competitors this plan allows per app. -1 means unlimited.
- `remaining` (integer|null): Slots left before add_competitors is refused. Null when the plan is unlimited.
- `suggested` (array): Rivals worth tracking that are not on the list, strongest evidence first: apps in the top ten of the terms this app competes on (tracked, already ranked for, or read off its listing when it has neith…
- `suggested_basis` (object|null): What the suggestions were derived from. Null when the app is not one of the account's own — a suggestion needs your terms to read.

### `get_landscape` (~412 tokens)

Get Landscape

Who the App Store shows beside one of your apps for the terms it competes on, in one storefront: every app in the niche with how many of your terms it ranks for and owns the top 10 for, its ratings, its review flow and its size, plus which apps arrived or fell out this window — and `term_gap`, how much of the niche's own vocabulary you do not track yet, with the biggest holes ready for add_keywords. This is the niche, not your watchlist: most rows are apps nobody added, and get_competitors answers the other question.

Input parameters:

- `app_id` (integer, required): Apptail app id for one of YOUR apps — get it from get_account. Not the numeric id in an App Store URL (that is apple_app_id).
- `country` (string): Storefront to read the niche in (e.g. US, GB). A niche exists per storefront and is never blended across them. Defaults to the first of the account's focus_countries this app is actually tracked in,…
- `from` (string): Start date, `YYYY-MM-DD`, inclusive. Overrides `period` when both are sent. Send it alone and the window runs to today.
- `period` (string): The window to report on. A calendar month as `2026-07`, a calendar year as `2026`, `last_month`, `this_month`, `last_year`, or a trailing window: `today`, `7d`, `14d`, `30d`, `90d`, `180d`, `365d`, `…
- `to` (string): End date, `YYYY-MM-DD`, inclusive. Send it alone and the window is the three years before it.

Output parameters:

- `app_id` (integer): The app whose niche this is.
- `app_name` (string|null): Its name.
- `caveats` (array): Sentences the reader has to hear before the numbers mean what they look like they mean — a cut list, hidden rows, an unmeasurable gap, modelled sizes. Pass these on; do not summarise them away.
- `console_url` (string|null): This landscape in the console, in this storefront.
- `count` (integer): Members returned.
- `country` (string): The storefront actually read, which may not be the one asked for.
- `hidden_count` (integer): Apps the account owner has hidden from this landscape in the console. They are excluded from `members`, from the movers and from `term_gap` alike.
- `members` (array): The niche, most of the account's terms owned first. `entered` and `left` mark this window's movers.
- `term_gap` (object): How much of the niche's vocabulary the account is not watching.
- `terms` (integer): How many terms defined this niche: the account's tracked terms for this app in this storefront, plus the terms the app already holds a top-10 position for. Every `terms` and `top10_terms` figure belo…
- `total` (integer): Members the niche actually holds. Larger than `count` means the list was cut.
- `truncated` (boolean): True when `count` is less than `total`. Say the list is partial rather than presenting it as the whole niche.
- `visibility` (object|null): The basis of each member's `visibility`. Null when it could not be read just now — then every member's `visibility` is null too, which is unknown, not zero.
- `window` (object): The window used, and what the change figures are measured against.

### `add_competitors` (~316 tokens)

Add Competitors

Track one or more rival apps against one of YOUR apps. Each competitor can be given as an Apptail app_id, an App Store URL, or just a name — names and URLs are resolved and imported the way search_apps does, so you do NOT need to look up an app_id first. Returns one outcome per item: what was added, what was already tracked, and what a plan limit refused.

Input parameters:

- `app_id` (integer, required): Apptail app id for one of YOUR apps — from get_account. The rivals are tracked FOR this app: competitor sets belong to an app, not to the account, so removing one later affects only this app.
- `competitors` (array, required): The rivals, up to 20. Each item is an Apptail app_id, a full App Store URL, or an app name in any language. Send all of them in one call — the plan limit is checked per item, so a batch that runs out…
- `country` (string): Storefront to resolve names in, ISO 3166-1 alpha-2 (e.g. US, DE). Omit and it is guessed from the script the name is written in. Ignored for items given as an app_id.
- `dry_run` (boolean): Resolve every item and report what would happen, without tracking anything. Use it when the user named apps ambiguously and you want to confirm you found the right ones before changing their account.

Output parameters:

- `added_count` (integer): How many are now tracked that were not before.
- `app_id` (integer): The app these rivals are tracked against.
- `dry_run` (boolean): True when nothing was written. Every outcome then reads `would_add`.
- `outcomes` (array): One row per item, in the order they were sent. Read `status` per row: a batch is rarely all one thing, and reporting it as "done" when two of five were refused is the failure this shape exists to pre…
- `remaining` (integer|null): Competitor slots left for this app on this plan after the call. Null when the plan is unlimited. Zero means the next add will be refused — say so rather than letting the user find out.
- `requested_count` (integer): How many items were sent.

### `remove_competitors` (~179 tokens)

Remove Competitors

Remove one or more competitor tracking relationships for an app owned by the authenticated account. Accepts AppTail app ids, App Store URLs or names. Name and URL resolution can query the public App Store and import shared app records. Removes the selected pairing only; public apps and pairings for other apps remain. Requires write authorisation and returns one outcome per item.

Input parameters:

- `app_id` (integer, required): Apptail app id for one of YOUR apps — from get_account. Only this app's competitor set is touched.
- `competitors` (array, required): The rivals to stop tracking, up to 20. Each item is an Apptail app_id (as returned by get_competitors), a store URL, or a name. Ids are the reliable form here — a name is resolved against the whole s…

Output parameters:

- `app_id` (integer): The app these rivals were tracked against.
- `outcomes` (array): One row per item. `not_tracked` means it was not a competitor of this app to begin with — the end state is what was asked for, but do not report it as a removal.
- `remaining` (integer): How many competitors this app still tracks.
- `removed_count` (integer): How many pairings were actually removed.
- `requested_count` (integer): How many items were sent.

### `add_app` (~233 tokens)

Add App

Add an app to the account as one of the user's own. Takes an Apptail app_id, an App Store URL, or a name — resolved and imported the way search_apps does. Tracking starts immediately (rankings, reviews, competitors); measured downloads and proceeds still require connecting App Store Connect in the console, which cannot be done from here.

Input parameters:

- `app` (string, required): The app to claim: an Apptail app_id, a full App Store URL, or the app's name. A URL is the reliable form — a name resolves against the whole store, and two apps can share one.
- `country` (string): Primary storefront for this app, ISO 3166-1 alpha-2 (e.g. US, DE). Defaults to US. This is the storefront its keywords and rankings are tracked in first; more can be added in the console.
- `dry_run` (boolean): Resolve the app and report what would happen without claiming it. Worth doing when the user gave a name rather than a URL, so they can confirm it is the right app before it counts against their plan.

Output parameters:

- `app` (object|null): The app as it now appears in the account. Null under dry_run only if nothing resolved.
- `apps_limit` (integer): How many the plan allows. -1 means unlimited.
- `apps_used` (integer): Own apps in the account after this call.
- `connected` (boolean): Whether App Store Connect reports for this app. False for almost every freshly added app — say so, because get_performance will return nothing for it until it is connected in the console.
- `console_url` (string|null): Where to open this app in the AppTail console.
- `country` (string): The storefront it was added for.
- `note` (string|null): What happens next, when there is something the user should know.
- `status` (string): `added` when it is now in the account, `already` when it was there before this call, `would_add` under dry_run.

### `get_market` (~871 tokens)

Get Market

Read iOS App Store niche data by saved market_id or by query and countries. Returns storefront competition, search visibility, app membership and optional corpus or movement data. Phrase research can create shared keyword records and persist crawled search observations; it does not save an account market. Downloads and revenue are labelled third-party rolling 30-day estimates. Reports per-storefront values, coverage and caveats.

Input parameters:

- `app_id` (integer): Scopes `include: ["app_terms"]` to one app — an Apptail app id from `members`, `search_apps` or `get_landscape`. This is the audit trail for membership: it returns every (term, storefront) place that…
- `budget_seconds` (integer): How long a live `query` may spend crawling, shared across ALL storefronts rather than each. Default 5, max 30. It is rarely the binding limit — at most 12 terms are crawled per call whatever the cloc…
- `countries` (array): Storefronts to ask the question in, ISO 3166-1 alpha-2 (e.g. ["US","GB","DE"]). Required with `query`. Each storefront gets its own corpus seeded from the same phrase; that is deliberate, and a phras…
- `from` (string): Start date, `YYYY-MM-DD`, inclusive. Overrides `period` when both are sent. Send it alone and the window runs to today.
- `include` (array): Extra sections, each costing a query: `members` (the leaderboard), `corpus` (every term with why it is in), `movement` (who arrived and who left), `app_terms` (which terms ONE app holds here and whet…
- `market_id` (integer): Apptail market id for a market this account has saved — from list_markets. A saved market is a pure read: no crawling, sub-second. Send this OR `query`, not both.
- `new_within_days` (integer): Keep only apps FIRST RELEASED inside this many days — "what has launched into this niche lately". Applied to every app ranking for the corpus before the list is cut, so these are the new apps in the…
- `period` (string): The window to report on. A calendar month as `2026-07`, a calendar year as `2026`, `last_month`, `this_month`, `last_year`, or a trailing window: `today`, `7d`, `14d`, `30d`, `90d`, `180d`, `365d`, `…
- `query` (string): A phrase to resolve into a market right now, e.g. "AI calorie tracking". Requires `countries`. Can persist shared keywords and search observations. No market is saved to your account — call save_mark…
- `store` (string): Read only this one storefront of a saved market. Omit for every storefront compared, which is where `overlap` and the contest range come from.
- `to` (string): End date, `YYYY-MM-DD`, inclusive. Send it alone and the window is the three years before it.

Output parameters:

- `app_terms` (array|null): Every (term, storefront) place that app holds here, best place first — the evidence `app_verdict` was taken over. Present only with include: ["app_terms"].
- `app_terms_total` (integer|null): Places the app actually holds. Larger than the list means it was cut.
- `app_verdict` (object|null): Whether the app named by `app_id` is competing in this market, and which clause of the rule decided. Present only with include: ["app_terms"].
- `apps` (integer): UNION: apps COMPETING in at least one storefront, counted ONCE however many they rank in. Competing means a top-10 place on at least one of this market's terms AND a top-50 place on at least two of t…
- `apps_capped` (boolean): True when the census stopped at its depth before it ran out of apps. `apps`, `newcomers`, `departures`, `downloads_30d` and `revenue_30d` are then floors, because all of them are summed over the coun…
- `built_at` (string|null): The OLDEST of the storefronts' build times: a market is only as fresh as its worst.
- `caveats` (array): Sentences the reader has to hear before the numbers mean what they look like they mean. Pass them on; do not summarise them away.
- `console_url` (string|null): This market in the console.
- `contest_high` (number|null): The most contested storefront's. A RANGE, deliberately: there is no market-level contest to average.
- `contest_low` (number|null): The least contested storefront's share of top-10 slots held by its top three, 0-100.
- `corpus` (array|null): Every term, with its storefront and why it is in the corpus. Present only with include: ["corpus"].
- `corpus_total` (integer|null): Terms the market holds. Larger than the list means it was cut.
- `departures` (integer|null): Distinct apps that fell out of every storefront they held. Null on the same terms as `newcomers`.
- `downloads_30d` (integer|null): ESTIMATE. A third-party model, worldwide and 30-day, summed over the UNION with each app counted once. Adding the per-storefront totals would count the same worldwide figure once per storefront and b…
- `entered` (array|null): Apps that arrived somewhere in this market this window. include: ["movement"].
- `fringe` (integer): Apps that rank somewhere in this market's search results and are NOT competing in it: one term only, or no top-10 place anywhere. Typically several times `apps` — measured on a nine-storefront market…
- `left` (array|null): Apps that fell out of every storefront they held. include: ["movement"].
- `market_id` (integer|null): Apptail market id, or NULL when this reading was resolved from a phrase and not saved. Pass it to save_market to keep it, or to get_market to read it again for free.
- `members` (array|null): The leaderboard, ordered by rating count — the one size figure we measure for every app. Present only with include: ["members"].
- `members_total` (integer|null): Members the market actually holds. Larger than the list means it was cut.
- `name` (string|null): What the market is called. Null for an unsaved reading.
- `newcomers` (integer|null): Distinct apps that arrived in at least one storefront this window. Arriving in `de` while already ranking in `us` counts as arriving in this market. **NULL means no storefront had a previous window t…
- `overlap` (number|null): Of the apps holding a top-10 place in ANY storefront, the share holding one in EVERY storefront, 0-100. High means one leaderboard travels and a newcomer meets it wherever they launch; low means each…
- `pending_storefronts` (array): Storefronts that ran out of budget before they were read at all. Name them; a partial answer that reads as complete is the failure this field exists to prevent.
- `pending_terms` (array): Terms in the corpus that could not be crawled inside the budget. Their ranks are missing from this reading, so it is a floor. Saving the market and asking again returns a deeper answer.
- `revenue_30d` (integer|null): ESTIMATE, same basis as downloads_30d.
- `size_covered` (integer): How many of the `apps` above carry an estimate at all, INCLUDING the ones the vendor would only bound as "under 1,000" / "under 5,000". Those contribute nothing to the sums, so a market with many sma…
- `slug` (string|null): Its address in the console.
- `spread` (string|null): open | contested | locked | mixed. `mixed` is the finding rather than a fudge: a niche locked in the US and open in Germany is the answer to "where should I launch this first". Storefronts in the sam…
- `stale_terms` (integer): Corpus terms across the market past their 72-hour refresh.
- `status` (string): draft | building | ready | failed. A market is `building` while ANY of its storefronts is — never report it ready because most of them landed. `ready` beats `failed`, so a market can read `ready` and…
- `storefront_count` (integer): How many places this question is asked in.
- `storefronts` (array): One entry per storefront, and the only level at which contest, demand and depth exist. Compare these rather than averaging them.
- `terms` (integer): SUM of the per-storefront corpora. The corpora are disjoint by construction — a term belongs to one storefront — so nothing is counted twice, and this is exactly what the market costs to crawl.
- `truncated` (boolean): True when any list above was cut. Say so rather than presenting a partial list as the whole thing.
- `window` (object): The window read, and what `newcomers`, `departures` and the change figures are measured against.

### `list_markets` (~247 tokens)

List Markets

List markets saved by the authenticated account, including market ids, storefronts, competition ranges, app counts and observed changes for the requested period. Reads saved account configuration and market observations without creating, updating or deleting markets. Returned ids can be used with get_market.

Input parameters:

- `from` (string): Start date, `YYYY-MM-DD`, inclusive. Overrides `period` when both are sent. Send it alone and the window runs to today.
- `period` (string): The window to report on. A calendar month as `2026-07`, a calendar year as `2026`, `last_month`, `this_month`, `last_year`, or a trailing window: `today`, `7d`, `14d`, `30d`, `90d`, `180d`, `365d`, `…
- `to` (string): End date, `YYYY-MM-DD`, inclusive. Send it alone and the window is the three years before it.

Output parameters:

- `count` (integer): Markets this account keeps.
- `limit` (integer): How many its plan allows. Equal to `count` means the next save is refused; say so before the user tries.
- `markets` (array): One entry per market, newest first. Pass a `market_id` to get_market with include: ["members"] for the leaderboard behind any of them.
- `window` (object): The window every change figure below is measured over.

### `save_market` (~342 tokens)

Save Market

Create or update a saved market in the authenticated account from a query, countries and optional per-storefront seed terms. An existing name updates that market, adding storefronts or replacing seed configuration and rebuilding requested storefronts. Persists account configuration and queues background corpus collection using public App Store data. Requires write authorisation. Returns market_id, build status, affected storefronts and caveats; collection may still be pending.

Input parameters:

- `countries` (array, required): Storefronts to ask it in, ISO 3166-1 alpha-2 (e.g. ["US","DE"]). Each keeps its own corpus; nothing is blended across them.
- `name` (string): What to call it. Defaults to `query`. Passing the name of a market this account already has ADDS the new storefronts to it rather than creating a second one.
- `query` (string, required): The phrase to build the market from, e.g. "AI calorie tracking". Used as the seed in every storefront unless `seeds` overrides it.
- `seeds` (object): Per-storefront seed terms, keyed by country code: {"de": ["kalorienzähler"]}. OPTIONAL, and optional per storefront. A storefront you leave out is not seeded from `query`: its terms are read off the…

Output parameters:

- `added_storefronts` (array): The ones this call added or rebuilt.
- `caveats` (array): What the caller needs to hear — that the build is not finished, or that a storefront was seeded from an English phrase.
- `console_url` (string|null): The market in the console.
- `created` (boolean): True for a new market, false when storefronts were added to one that already existed.
- `market_id` (integer): Pass it to get_market to read the market once it has built.
- `name` (string)
- `slug` (string): Its address in the console.
- `status` (string): `building` immediately after a save. A market is building while ANY storefront is; each one lands separately. A build that dies leaves the storefront `failed` (or `ready` with `failed_at` set when a…
- `storefronts` (array): Every storefront this market is now asked in, including ones it already had.

### `edit_market_corpus` (~335 tokens)

Edit Market Corpus

Add terms to one storefront's corpus, or take terms out of it. Add for a term the corpus SHOULD hold and does not — a rival's brand, a phrase Apple has never measured the popularity of, the term a thin storefront is really about; a pinned term is kept through every rebuild and refreshed on the same 72-hour cadence as the rest. Remove for a term that does not belong: ANY term can go, and one the heuristic found is also kept out of future rebuilds rather than returning within 72 hours. This is NOT `save_market`: seeds are what the corpus is expanded FROM and changing one re-runs the whole heuristic over that storefront, where these two writes act on single terms in the result.

Input parameters:

- `add` (array): Terms to pin, as somebody would type them into the App Store. Send the batch in one call. Terms already in the corpus come back under `already_present` rather than as an error.
- `country` (string, required): Which storefront's corpus, ISO 3166-1 alpha-2 (e.g. US, DE). Required and not defaulted: a corpus belongs to one storefront, and there is no market-wide term list to add to.
- `market_id` (integer, required): Apptail market id — from list_markets or save_market.
- `remove` (array): Keyword ids to take out of the corpus — the `keyword_id` on ANY get_market corpus row. A term the heuristic found is also recorded as excluded so rebuilds do not re-derive it; `add` on the same term…

Output parameters:

- `already_present` (array): Terms that were already in this corpus, and where they came from. Not an error — it is the state the caller asked for, and a `competitor` term is NOT re-recorded as `manual`, because the origin says…
- `cap` (integer): The most terms this storefront may hold, from the account's plan. A market's storefronts each get their own — the cap is per storefront, not shared across the market.
- `caveats` (array): What the caller needs to hear before reporting success — that ranks are not in yet, or that the cap cut the list.
- `console_url` (string|null): This market's corpus in the console.
- `country` (string): The storefront that was edited. A corpus belongs to one storefront and is never shared between them.
- `excluded` (array): The subset of `removed` that will ALSO be kept out of every future rebuild. A term the heuristic found is re-derived from the seeds every 72 hours, so removing it has to be recorded outside the corpu…
- `market_id` (integer): The market this corpus belongs to.
- `not_removed` (array): Ids that were sent to `remove` and are still in the corpus, each with the reason.
- `pinned` (array): Terms added to the corpus by this call, with `origin: "manual"`. A term Apptail has never seen is created and queued for crawling, so its ranks appear within minutes rather than immediately.
- `refused` (array): Terms not added because the storefront is at `cap`. Unpin something or drop a storefront; the account's plan sets the number.
- `removed` (array): Keyword ids taken out of the corpus by this call.
- `terms` (integer): How many terms this storefront's corpus holds now.

### `remove_market` (~153 tokens)

Remove Market

Delete a saved market, or just one of its storefronts. Pass `store` to drop a single storefront and leave the rest of the market with its corpora and its build clocks untouched; omit it to delete the whole market. Deleting frees a slot on the account's plan. The terms themselves are not deleted — they are shared with any other market or tracked app using them.

Input parameters:

- `market_id` (integer, required): Apptail market id — from list_markets.
- `store` (string): Drop only this storefront, ISO 3166-1 alpha-2. Omit to delete the whole market. Dropping the last storefront deletes the market too, because a market asked nowhere is not a market.

Output parameters:

- `deleted` (boolean): True when the whole market is gone. False when only a storefront was dropped and the market remains.
- `market_id` (integer)
- `markets_remaining` (integer): Markets left on the account after this call.
- `name` (string)
- `remaining_storefronts` (array): What the market is still asked in.
- `removed_storefronts` (array): What this call removed.

### `get_top_charts` (~262 tokens)

Get Top Charts

Get top app charts (free, paid, or grossing) for a store, country, and category.

Input parameters:

- `category` (string): Apple category id as a number, e.g. 6014 Games, 6015 Finance, 6017 Education, 6023 Food & Drink, 6013 Health & Fitness, 6012 Lifestyle. Omit for the overall chart across all categories. An id we do n…
- `chart_type` (string): Which chart. Default free. Use grossing for revenue questions — top free ranks by downloads and says nothing about money. An unrecognised value falls back to free and the reply says which chart it re…
- `country` (string): Storefront to read the chart for (e.g. US, GB, JP). Defaults to US. An unrecognised code falls back to US rather than failing — the reply echoes the storefront actually used, so check it.
- `limit` (integer): How deep to read the chart. Default 25, capped at 100.
- `store` (string): Only the App Store is covered. Google Play is in the data model but nothing is crawled for it, so omit this.

Output parameters:

- `apps` (array): Chart entries, rank 1 first. Empty when no chart data exists for this country/category combination.
- `chart_type` (string): Which chart was read.
- `count` (integer): Number of entries returned.
- `country` (string): Storefront the chart was read from.

### `get_reviews` (~792 tokens)

Get Reviews

Reviews for any app over a window, filtered by storefront, star rating, text, or whether the developer has replied — plus the shape of the period: how many arrived per bucket, what they averaged, and where the app's pooled store rating stood while they did. Reviews exist for every app whether or not App Store Connect is connected, because they are read from the public store. This is the tool for "what are people complaining about": filter it and read the reviews yourself rather than asking for a summary. Every review says whether it can be answered (`can_reply`) and where an existing reply stands (`reply_state`); `reply_to_reviews` is what answers them.

Input parameters:

- `app_id` (integer, required): Apptail app id. get_account for the user's own apps, search_apps for any other. Reviews are readable for any app in the store, including competitors.
- `contains` (string): Only reviews whose title or body contains this text, case-insensitively. Title as well as body, because a complaint is as often the headline as the paragraph. This is how you check a hypothesis — "cr…
- `country` (string): Only reviews from this storefront (e.g. de). Omit for every storefront, which is usually right — one storefront's reviews are a thin sample. Send it when `ratings.by_country` on get_app has pointed a…
- `from` (string): Start date, `YYYY-MM-DD`, inclusive. Overrides `period` when both are sent. Send it alone and the window runs to today.
- `limit` (integer): Max reviews, newest first. Default 25, capped at 200. The answer always says how many matched.
- `period` (string): The window to report on. A calendar month as `2026-07`, a calendar year as `2026`, `last_month`, `this_month`, `last_year`, or a trailing window: `today`, `7d`, `14d`, `30d`, `90d`, `180d`, `365d`, `…
- `rating` (array): Only these star ratings, e.g. `[1, 2]` for the critical ones. Omit for all five. The `summary` counts are deliberately NOT filtered by this, so a call asking only for one-stars still learns how many…
- `to` (string): End date, `YYYY-MM-DD`, inclusive. Send it alone and the window is the three years before it.
- `unanswered` (boolean): Only reviews the developer has never replied to. False or omitted for all of them. Combined with `rating: [1, 2]` this is the work queue the console leads with — and **a backlog has no window**, so s…

Output parameters:

- `app_id` (integer): The app the reviews belong to.
- `app_name` (string|null): Its name.
- `console_url` (string|null): The app's Reviews screen in the console.
- `count` (integer): Reviews returned.
- `country` (string|null): Storefront the list was narrowed to, or null for every storefront.
- `filtered` (boolean): True when any of `country`, `rating`, `contains` or `unanswered` narrowed the list. Worth saying out loud: "no reviews" under a filter is not "no reviews".
- `replies` (object): Whether replying is available to this account at all. Read it together with `can_reply` on each review: this is about the account's key, that is about the individual review having an App Store Connec…
- `reviews` (array): The matching reviews, newest first, in the language they were written in. Read them; do not ask for a summary you can produce better yourself.
- `summary` (object): The period in figures. `arrived*`, `critical*` and `average_rating*` are flows *in the window*; `all_time`, `unanswered` and `store_rating*` are the app's standing position and ignore it. Mixing the…
- `total` (integer): Reviews matching the filters in this window. Larger than `count` when the list was capped.
- `trend` (object): The shape of the period. Compare `average_rating` with `store_rating` bucket by bucket: the first is the mood of what arrived, the second is the running total the store ranks on, and the gap between…
- `truncated` (boolean): True when `count` is below `total`. Say the list is a sample rather than presenting it as everything that was written.
- `window` (object): The window actually read, which is not always the one asked for: an inverted range is swapped, a range past three years is shortened, and first-party figures are pulled back to the last day App Store…

### `reply_to_reviews` (~205 tokens)

Reply To Reviews

Create, update or remove public App Store review responses for apps owned by the authenticated account, using its connected App Store Connect API key. Requires write authorisation and eligible review ids. Submitted text is sent verbatim under the developer's name. Obtain approval for the exact text or requested removal before submitting. dry_run validates without publication. Accepts up to ten items and returns per-item outcomes; pending means Apple has accepted a response but has not published it.

Input parameters:

- `dry_run` (boolean): Check every item and report what would happen, without publishing anything. **Worth using by default here** — it confirms the reviews resolve, the account's key reaches their apps and the text fits,…
- `replies` (array, required): The replies, up to 10 per call. Send them in one call rather than one at a time: each is answered separately, so a batch where one app is not covered still posts the rest and says which it did not.

Output parameters:

- `dry_run` (boolean): True when nothing was sent to Apple. Every outcome is then what *would* have happened.
- `failed_count` (integer): How many did not do what was asked. Never report a batch as done while this is above zero — say which ones and why.
- `max_items` (integer): The per-call cap. A request above it is refused as a whole rather than silently trimmed.
- `outcomes` (array): One row per item, in the order they were sent. Read `status` per row, then `state` on the ones that posted.
- `posted_count` (integer): How many replies Apple took. Read each one's `state` before calling any of them published.
- `removed_count` (integer): How many replies were taken down.
- `requested_count` (integer): How many items were sent.

## Diagnostics

Captured diagnostic sections: TLS, DNSSEC, Authorisation, Transports. The full working is on the page: https://verifymcp.io/servers/io-apptail-apptail/mcp#diagnostics

## Score history

- 2026-11-05: 78
- 2026-11-04: 78
- 2026-11-03: 78
- 2026-11-02: 78
- 2026-11-01: 78
- 2026-10-31: 78
- 2026-10-30: 78
- 2026-10-29: 77
- 2026-10-28: 77
- 2026-10-27: 76
- 2026-10-26: 76
- 2026-10-25: 75
- 2026-10-24: 75
- 2026-10-23: 74
- 2026-10-22: 74
- 2026-10-21: 74
- 2026-10-20: 73
- 2026-10-19: 73
- 2026-10-18: 72
- 2026-10-17: 72
- 2026-10-16: 71
- 2026-10-15: 71
- 2026-10-14: 70
- 2026-10-13: 70
- 2026-10-12: 69
- 2026-10-10: 68
- 2026-10-09: 68
- 2026-10-05: 66
- 2026-10-04: 66
- 2026-10-03: 65

## Common questions

### What is the AppTail – ASO & Keyword Research MCP server?

AppTail – ASO & Keyword Research is an MCP server listed in the public MCP registry as io.apptail/apptail. App Store keyword rankings, competitors, markets, reviews and analytics for iOS apps, from your AI. This page covers its hosted endpoint (https://mcp.apptail.io/mcp).

### Is the AppTail – ASO & Keyword Research MCP server safe to use?

AppTail – ASO & Keyword Research scores 78 out of 100 on VerifyMCP. That is a record of what we were able to check automatically, not an endorsement. The category breakdown on this page shows every signal behind the number, including the ones we could not confirm.

### What tools does the AppTail – ASO & Keyword Research MCP server expose?

AppTail – ASO & Keyword Research exposes 25 tools: get_account, explain_period, get_performance, get_signals, search_apps, and 20 more. Their descriptions and schemas cost roughly 10,427 tokens of context every time the server is loaded.

### Does the AppTail – ASO & Keyword Research MCP server require authentication?

No. We connected to AppTail – ASO & Keyword Research without credentials and it answered, so anything it exposes is reachable by anyone who knows the address.

### Is the AppTail – ASO & Keyword Research MCP server still maintained?

AppTail – ASO & Keyword Research is still listed as active in the MCP registry. We last reached this channel on 5 November 2026. Those dates come from our own scans of the registry and the channel itself, not from anything the publisher announced.

## Links

- Remote endpoint: https://mcp.apptail.io/mcp
- Website: https://apptail.io/mcp
- Changelog RSS feed: https://verifymcp.io/servers/io-apptail-apptail/mcp.xml
- Changelog JSON feed: https://verifymcp.io/servers/io-apptail-apptail/mcp.json
- HTML version of this page: https://verifymcp.io/servers/io-apptail-apptail/mcp
