# App Discovery Radar (`kamnev.apps/app-discovery-radar`) Actor

Find iOS apps for niche research, track competitor metadata, and compare country listings. For app makers, ASO teams and publishers. Includes readable reports, CSV, saved comparisons and Apple source links. Bounded sample. $0.25 per completed report, including zero matches.

- **URL**: https://apify.com/kamnev.apps/app-discovery-radar.md
- **Developed by:** [KamnevApps](https://apify.com/kamnev.apps) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.25 / completed report

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## App Discovery Radar

Find iOS apps in a niche, follow a competitor watchlist, or compare the same apps across countries. Each run gives you a readable report, CSV exports and the source evidence behind every match.

For independent app makers preparing a shortlist, ASO teams reviewing competitors, and publishers checking country listings. Start from a ready-made task and take the report into your existing research workflow.

Keep the same saved search to see what changed since its previous successful run. The first run records your starting point; later runs separate new matches, metadata changes and gaps in the source.

**Launch price: $0.25 per completed report**, with no additional Actor start, per-row or run-usage fee. Four reports cost $1; 30 reports cost $7.50. A completed report with no matches also costs $0.25. Check the [live Pricing tab](https://apify.com/kamnev.apps/app-discovery-radar/pricing) before running.

### Choose a starting point

| Your task | Ready-made example | What to change |
| --- | --- | --- |
| Research a niche before you build | [Find travel apps with fewer than 2,000 ratings](https://apify.com/kamnev.apps/app-discovery-radar/examples/find-travel-apps-with-fewer-than-2-000-ratings) | Two travel queries, US and UK, Travel category, fewer than 2,000 ratings per storefront. Replace the queries and filters for your niche. |
| Prepare a competitor review | [Track changes in a productivity app watchlist](https://apify.com/kamnev.apps/app-discovery-radar/examples/track-changes-in-a-productivity-app-watchlist) | Todoist, TickTick and Things 3 in the US. Replace their Apple app IDs with your own IDs or App Store links. |
| Review listings across countries | [Compare apps across three App Store countries](https://apify.com/kamnev.apps/app-discovery-radar/examples/compare-apps-across-three-app-store-countries) | The same three apps in the US, UK and Spain, with separate prices, ratings and source records. |

Each example copies into your account. Your runs and history remain private. These are starting configurations, not a subscription or an active schedule. The example apps are independent products; their inclusion does not imply endorsement.

**See a finished report before running:** [niche shortlist](https://api.apify.com/v2/key-value-stores/kAlnwHLXhjHPFZBiI/records/SAMPLE.html), [app watchlist](https://api.apify.com/v2/key-value-stores/WzQDyjH4oTttgT1l7/records/SAMPLE.html), or [country comparison](https://api.apify.com/v2/key-value-stores/CKXOVRAoWteRC1MGr/records/SAMPLE.html). These completed samples from 27 October 2026 are free to view and download; opening them does not start a run.

**Coverage is a bounded, ranked sample from Apple's iTunes Search/Lookup API.** It is not a complete App Store index. The source has no documented search pagination; a recent-release filter does not discover every recent release.

### Bring your own apps

Paste numeric Apple IDs or HTTPS `apps.apple.com` app links directly into **App IDs or App Store links**. For example, `572688855` and `https://apps.apple.com/us/app/todoist/id572688855?platform=iphone` select the same app. Links with or without a two-letter country prefix are accepted; query strings and fragments are ignored. Radar extracts the ID without opening the supplied URL. Your selected `countries` control which storefronts are checked; a link's country does not change them. Replacing an ID with its equivalent link preserves the same search criteria and history.

For a ready-to-copy configuration, the [free input helper](https://kamnevapps.com/app-discovery-radar/#bring-your-apps) turns 1–3 HTTPS App Store links or numeric Apple app IDs into Actor input. Choose any of US, UK and Spain, then copy the JSON or download `INPUT.json`. It works locally in your browser without looking up apps, uploading entries or starting a run. Its three-app limit applies only to the helper; this Actor accepts up to 100 IDs or links.

Paste the JSON into the Actor's Input tab in JSON mode and save a new task. Keep the same history name (`savedSearchId`) for later comparisons. The helper omits `requestId`, so rerunning the task collects a fresh observation.

### Quick start

1. Choose an example above, or enter your own keywords, Apple app IDs or App Store links.
2. Pick 1–3 countries and a limit on app-country checks. Add filters only when you need them.
3. Run, then open **Report** in the Output tab. Use the report's local search and filters to inspect the shortlist. They do not start another run or add another report charge.
4. Keep the same `savedSearchId` for the next observation. Leave `requestId` empty for a fresh check.

For example, this input builds the travel shortlist above:

```json
{
  "queries": ["travel planner", "trip itinerary"],
  "appIds": [],
  "countries": ["US", "GB"],
  "categoryIds": ["6003"],
  "maxRatingCount": 1999,
  "maxApps": 160,
  "resultsPerQuery": 100,
  "savedSearchId": "travel-niche-shortlist"
}
```

`maxApps` counts **app-country checks**, not unique apps. This example permits at most 80 candidate apps across two countries. Filters narrow the returned sample; they do not expand source coverage. For a watchlist without keyword discovery, set `queries` to `[]` and supply `appIds`.

### Input

| Field | Meaning and limits |
| --- | --- |
| `queries` | Up to 5 nonempty phrases, at most 120 characters each. Provide queries, `appIds`, or both. |
| `appIds` | Up to 100 positive Apple app IDs (at most 15 digits) or HTTPS `apps.apple.com` app links. Links become numeric IDs before deduplication and comparison; their query strings and fragments are ignored. Explicit seeds are selected first. |
| `countries` | 1–3 of US, GB, ES, DE, FR, CA, AU, IT, NL, JP. Default US. These storefront selections take precedence over any country in an app link. |
| `categoryIds` | Up to 20 Apple genre IDs, each 4–5 digits. Any listed genre may match. Travel is `6003`. |
| `languages` | Up to 10 supported app language codes of 2–3 letters reported by Apple in `languageCodesISO2A`, such as EN or ES. All listed codes must match. These are not App Store listing localizations or the language of a listing title. |
| `releaseDateFrom`, `releaseDateTo` | Inclusive source-reported initial-release date bounds in `YYYY-MM-DD`. |
| `updatedDateFrom`, `updatedDateTo` | Inclusive current-version release date bounds in `YYYY-MM-DD`. |
| `minRatingCount`, `maxRatingCount` | Inclusive nonnegative safe-integer storefront rating-count bounds. |
| `minRating`, `maxRating` | Inclusive average-rating bounds from 0 to 5. |
| `minPrice`, `maxPrice` | Inclusive bounds from 0 to 10,000 in each storefront's currency. No currency conversion. |
| `requireAllCountries` | Require the app to be returned by lookup in every selected country. Other metadata filters still apply to each storefront record. Default false. |
| `maxApps` | Integer from the number of selected countries to 500. Must cover every seed-country pair. Default 500. |
| `resultsPerQuery` | 1–200 requested ranked results per query-country pair. Default 50. |
| `savedSearchId` | History identifier of up to 64 letters, digits, underscores or dashes. Default `default`. |
| `requestId` | Optional same-operation retry identifier of up to 100 letters, digits, underscores or dashes. Omit for recurring observations. |

The unique-candidate cap is `floor(maxApps / countries.length)`. After reserving explicit seeds, selection interleaves ranks across query-country result lists in round-robin order. Duplicate app IDs share one candidate slot. Coverage records show selected and excluded candidates; round-robin selection does not make the sample exhaustive.

Source access is sequential, paced at least 3.5 seconds apart, and limited to 60 HTTP attempts including retries. Eligible failures receive at most two retries. Each request has a 15-second timeout, responses are capped at 10 MiB, and source collection has a 10-minute deadline. Required metadata that is missing cannot pass its filter; the report records the reason.

### Output

The default dataset contains matching **app-country records**. One app can produce several storefront rows. The HTML report groups them by app and supports local search, filters, sorting and export of the visible shortlist without another source request. All evidence remains readable when JavaScript is disabled. The downloaded HTML also embeds downloads of the other six report files, so those downloads do not depend on access to sibling storage URLs.

The run's key-value store contains seven artifacts:

| Artifact | Contents |
| --- | --- |
| `RESULTS.json` | Criteria, coverage, observations including rejected records, matches, availability evidence, changes, source statistics and billing state. |
| `MATCHES.csv` | All matching app-country records with source fields and filter explanations. |
| `REPORT.html` | Readable report, storefront register, local filters and expandable source evidence. |
| `REPORT.md` | Concise text report. |
| `CHANGES.csv` | Change records with reasons and before/after values where available. |
| `ALERT.json` | Default notification decision, stable event ID and eligible change evidence. |
| `INPUT.json` | Rerunnable criteria without the previous `requestId`. |

#### Download the report through the API

The generated API examples return a run and its dataset. The HTML report and CSV are in that run’s key-value store, including when a completed report has zero matches. Use the same authenticated client to access your private output:

```js
import { writeFile } from 'node:fs/promises';

// Continue after the Apify API example has returned `run` and `client`.
if (run.status !== 'SUCCEEDED') throw new Error(`Run ${run.id}: ${run.status}`);
const store = client.keyValueStore(run.defaultKeyValueStoreId);
for (const name of ['REPORT.html', 'MATCHES.csv']) {
    const record = await store.getRecord(name, { buffer: true });
    if (!record) throw new Error(`Missing artifact: ${name}`);
    await writeFile(name, record.value);
}
```

This downloads an existing report; it does not start another run or create another report charge. Keep `savedSearchId` stable and omit `requestId` when you want a fresh observation.

`releaseDate` is the source's app release date. `updatedAt` is its current-version release date. `firstSeenAt` is the first observation by this saved search, not a launch date. Rating counts are storefront ratings, not text-review counts, downloads or revenue. Missing scalar values are `null` with an explanation in `missingReasons`. Review text, artwork, download/revenue estimates and in-app purchase catalogs are not collected.

### History and change evidence

Keep the same `savedSearchId` and criteria to compare with the previous successfully completed observation. The first observation establishes a baseline. Changing criteria starts a new comparison baseline while retaining first-seen history.

Change evidence separates newly passing records, substantive metadata changes, routine rating movement, missing/recovered source fields, records that no longer pass filters, and known apps returning to the sample. Inspect each change's reason: source-data recovery is different from a newly discovered app.

A successful lookup that returns no record is a **lookup gap**, not confirmed delisting. An app absent from the bounded candidate sample has unknown current availability. Network failures, exhausted retries and invalid source responses fail collection; they do not become availability claims, replace the previous successful baseline, or trigger a completed-report charge.

History is scoped by trusted platform user and Actor identity, then `savedSearchId`. Runs for the same user and Actor are serialized across their searches; an overlap can fail with `SEARCH_BUSY`. Keep schedules non-overlapping. Each saved search is limited to 1,000 logical reports and 10,000 observed app-country identities. At a limit, export the history and use a new `savedSearchId`; history is not silently evicted.

### Tasks, schedules and notifications

Save the input as an [Apify task](https://docs.apify.com/actors/running/tasks) and attach a [schedule](https://docs.apify.com/actors/running/schedules). Use a stable `savedSearchId` and **omit `requestId`** for fresh recurring observations. A fixed `requestId` replays the original logical report without fresh collection or a new charge attempt. Reusing it with different criteria is rejected.

To receive change notifications, configure a native [`ACTOR.RUN.SUCCEEDED` webhook](https://docs.apify.com/integrations/webhooks/events) to your own consumer. That consumer reads the run's `ALERT.json`, sends a notification only when `shouldNotify` is true, and deduplicates `eventId` within the customer account and Actor scope. Success webhooks can also arrive for unchanged reports, zero matches and replays.

The default signal excludes baselines, criteria resets, replays, routine rating movement, known apps returning and source-data recovery. The Actor itself does not send email or chat messages. Your consumer controls delivery and can inspect `RESULTS.json` for different rules.

### Pricing

You pay for **one completed bounded report**, including a valid zero-match report. Technical collection failures do not trigger a completed-report charge. Replaying the same logical operation returns its saved report without a new charge attempt or source check. A fresh observation is a new report, even when nothing changed.

The report and exports are saved before billing. The [live Pricing tab](https://apify.com/kamnev.apps/app-discovery-radar/pricing) shows the current event price. Apify account charges or separate services you configure are governed by their own terms.

Reports use metadata from Apple's iTunes Search/Lookup API. No artwork, previews or review text are collected, and no Apple affiliation or endorsement is implied. This product does not use or resell AppFigures API data.

# Actor input Schema

## `queries` (type: `array`):

Enter up to 5 phrases (120 characters each), or leave empty when tracking known app IDs. Keywords find candidates; your filters are checked after lookup.

## `appIds` (type: `array`):

Optional watchlist of up to 100 positive Apple app IDs (up to 15 digits) or HTTPS apps.apple.com app links, such as https://apps.apple.com/us/app/id572688855. Links are converted to IDs without opening them; query strings and fragments are ignored. Selected App Store countries apply regardless of the country in a link. For watchlist-only research, leave Search queries empty. Unique app IDs multiplied by countries must fit the check limit.

## `countries` (type: `array`):

Choose 1–3 storefronts. These selections apply even when an app link contains a different country. Country describes storefront availability and rating/price observations, not app language.

## `maxApps` (type: `integer`):

Total app-country budget, not unique app count: 120 checks across 3 countries permits at most 40 candidate apps. Must be at least country count; maximum 500. Seed apps consume this budget first.

## `resultsPerQuery` (type: `integer`):

Request 1–200 ranked results from each keyword/storefront search. This is a source limit, not a completeness guarantee or a pagination size. The combined candidate list is capped by maxApps.

## `categoryIds` (type: `array`):

Optional OR filter across up to 20 Apple genre IDs, for example 6003 for Travel. Matches any app genre ID, not just its primary category. Missing category data fails this filter.

## `languages` (type: `array`):

Optional AND filter for up to 10 two- or three-letter supported app language codes reported by Apple in languageCodesISO2A, such as EN or ES. These are not App Store listing localizations or the language of a listing title. Missing data does not pass this filter.

## `releaseDateFrom` (type: `string`):

Inclusive YYYY-MM-DD lower bound on the source-reported app release date. This is distinct from first discovery and current-version release. Missing data fails this filter.

## `releaseDateTo` (type: `string`):

Inclusive YYYY-MM-DD upper bound on source-reported app release date. Must be on or after releaseDateFrom.

## `updatedDateFrom` (type: `string`):

Inclusive YYYY-MM-DD lower bound on currentVersionReleaseDate. Distinct from the initial app release date. Missing data fails this filter.

## `updatedDateTo` (type: `string`):

Inclusive YYYY-MM-DD upper bound on current-version release date. Must be on or after updatedDateFrom.

## `minRatingCount` (type: `integer`):

Inclusive storefront userRatingCount lower bound, a count of ratings rather than text reviews. Missing data fails this filter.

## `maxRatingCount` (type: `integer`):

Inclusive storefront rating-count upper bound. Must be at least minRatingCount.

## `minRating` (type: `number`):

Inclusive averageUserRating lower bound from 0 to 5. Missing data fails this filter.

## `maxRating` (type: `number`):

Inclusive averageUserRating upper bound from 0 to 5. Must be at least minRating.

## `minPrice` (type: `number`):

Inclusive price lower bound in each storefront's returned currency. No currency conversion; use a single country when comparing monetary amounts. Missing data fails this filter.

## `maxPrice` (type: `number`):

Inclusive price upper bound in each storefront's returned currency. Must be at least minPrice. In-app purchase and subscription prices are not provided.

## `requireAllCountries` (type: `boolean`):

Only match apps returned by successful lookup in every requested storefront. A missing lookup result does not establish permanent delisting. Technical lookup failures fail the report.

## `savedSearchId` (type: `string`):

Reuse to preserve first-seen history and compare successful runs. Changing criteria starts a comparison baseline; first-seen history remains. Use a new ID for an independent search. History is isolated by trusted platform user/Actor identity.

## `requestId` (type: `string`):

Use the same ID only when retrying the same logical report. Replays return the saved report without a new source check or charge attempt. Omit for recurring tasks/schedules so each run observes fresh data. Reuse with different criteria is rejected.

## Actor input object example

```json
{
  "queries": [
    "travel planner"
  ],
  "countries": [
    "US"
  ],
  "maxApps": 500,
  "resultsPerQuery": 50,
  "requireAllCountries": false,
  "savedSearchId": "default"
}
```

# Actor output Schema

## `report` (type: `string`):

No description

## `evidence` (type: `string`):

No description

## `csv` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `matches` (type: `string`):

No description

## `changes` (type: `string`):

No description

## `notification` (type: `string`):

No description

## `rerunInput` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "queries": [
        "travel planner"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("kamnev.apps/app-discovery-radar").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "queries": ["travel planner"] }

# Run the Actor and wait for it to finish
run = client.actor("kamnev.apps/app-discovery-radar").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "queries": [
    "travel planner"
  ]
}' |
apify call kamnev.apps/app-discovery-radar --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kamnev.apps/app-discovery-radar"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/olU1llXqGE04eyJyc/builds/VQicHf6jhEfyRNHkS/openapi.json
