# App Store Listing Change Feed - iOS ASO & Price Change Tracker (`mibedk/app-store-listing-change-feed`) Actor

Watches iOS apps across App Store storefronts and emits only what changed since the last run: renamed title, edited description (word-level diff), new version and release notes, price change, new icon or screenshots, category move, delisting or new storefront.

- **URL**: https://apify.com/mibedk/app-store-listing-change-feed.md
- **Developed by:** [Mikkel Bech-Hansen](https://apify.com/mibedk) (community)
- **Categories:** Marketing, SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $12.00 / 1,000 listing change detecteds

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 Store Listing Change Feed - iOS ASO & Price Change Tracker

Give this actor a list of iOS apps and the App Store storefronts you care about, and it tells you what changed since the last time it ran. Not a snapshot of the listing — the *diff*. Every other App Store scraper hands you today's title, description and price and leaves you to store the results and compare them yourself; this one keeps that memory for you and returns only the edits: a competitor renamed their app, rewrote their description, shipped a new version, dropped their price, replaced their icon or screenshots, moved category, raised their minimum iOS, got delisted from a storefront, or launched in a new one. Each change comes with the value before, the value after, a word-level diff for text edits, and the rating and review count at the moment the change was spotted. Schedule it daily and it becomes a competitor ASO alert feed.

### How it works

On the first run the actor records what each app's listing looks like in each storefront and returns one `baseline` record per app so you can confirm the watchlist is right. From then on, every run compares against what it saw last time and returns only what moved. **On a quiet watchlist an empty dataset is the correct, expected result** — that is the point of a change feed. It reads Apple's public iTunes Lookup endpoint, batching up to 100 apps per request, so a 500-app watchlist is a handful of HTTP calls.

### Input

Every field has a working default. Run it with no configuration at all and it watches Facebook, Calm and ChatGPT in the US storefront.

| Field | Type | Default | Notes |
|---|---|---|---|
| `appIds` | array of strings | Facebook, Calm, ChatGPT | App Store URLs (`https://apps.apple.com/us/app/id284882215`), bare IDs (`284882215`) or `id284882215` fragments — all accepted |
| `countries` | array of strings | `["us"]` | Two-letter storefront codes: `us`, `gb`, `de`, `jp`, `br`… Each app is diffed separately per storefront |
| `seedFromChart` | select | `none` | Also watch the current Apple `top-free` / `top-paid` / `top-grossing` chart for the first storefront |
| `chartSize` | integer | `25` | Chart positions to take, 1–200 |
| `searchTerms` | array of strings | `[]` | Keywords, e.g. `habit tracker`. Adds the top App Store search results for each to the watchlist |
| `maxAppsPerSearchTerm` | integer | `25` | 1–200 |
| `maxAppsPerRun` | integer | `200` | Hard cap on app × storefront checks per run, 1–5000. 50 apps × 3 storefronts = 150 checks |
| `trackedFields` | multi-select | all except `supportedDevices` | `title`, `description`, `version`, `price`, `icon`, `screenshots`, `genres`, `minimumOs`, `supportedDevices`, `seller`, `availability` |
| `minDescriptionWordsChanged` | integer | `1` | Report a description edit only when at least this many words moved. Raise it to silence typo fixes |
| `storefrontLanguage` | string | `""` (storefront default) | Locale for returned text, e.g. `en_us`, `de_de`, `ja_jp` |
| `maxChangeEvents` | integer | `500` | Cap on change events returned and charged per run |
| `markDelistedAfterMisses` | integer | `2` | Consecutive runs an app must be missing before a `delisted` event fires |
| `includeBaselineRecords` | boolean | `true` | Emit one uncharged `baseline` record the first time an app is seen |
| `forceBaseline` | boolean | `false` | Discard stored state and treat every app as new. Use after changing `storefrontLanguage` |
| `stateKeyValueStoreName` | string | `app-store-change-feed-state` | Named key-value store holding the memory. Change it to run two independent watchlists |
| `requestsPerMinute` | integer | `18` | Apple rate-limits at roughly 20/min per IP. 1–60 |
| `requestTimeoutSecs` | integer | `30` | 5–120 |
| `maxRequestRetries` | integer | `3` | 0–10. A batch that fails every retry is skipped — not charged, and never read as a delisting |
| `proxyConfiguration` | object | no proxy | Optional. Apple's public endpoints do not IP-block; most watchlists never need this |

### Output

One record per typed change, per app, per storefront. `changeType` is one of `title`, `description`, `version`, `price`, `icon`, `screenshots`, `genres`, `minimumOs`, `supportedDevices`, `seller`, `delisted`, `appeared`, or `baseline`.

A real record, from a run where the watched app had shipped a new version since the previous run:

```json
{
  "changeType": "version",
  "appId": 571800810,
  "appName": "Calm",
  "bundleId": "com.calm.calmapp",
  "country": "us",
  "appStoreUrl": "https://apps.apple.com/us/app/calm/id571800810",
  "developerName": "Calm.com",
  "developerId": 571800813,
  "field": "version",
  "previousValue": "6.9.0",
  "newValue": "7.0.5",
  "addedWords": [],
  "removedWords": [],
  "wordsAdded": 0,
  "wordsRemoved": 0,
  "wordDiff": null,
  "addedItems": [],
  "removedItems": [],
  "version": "7.0.5",
  "previousVersion": "6.9.0",
  "releaseNotes": "Thanks for using Calm, the #1 app to help you sleep more, stress less and live mindfully...\n\nThis update contains multiple bug fixes and performance improvements.",
  "currentVersionReleaseDate": "2026-08-30T04:19:50Z",
  "price": 0,
  "formattedPrice": "Free",
  "currency": "USD",
  "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/68/23/11/6823110c-7640-5606-1cae-0a453864343e/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg",
  "ratingAtChange": 4.77282,
  "userRatingCountAtChange": 1977970,
  "previousObservedAt": "2026-09-02T23:35:00.688Z",
  "observedAt": "2026-09-02T23:35:13.955Z"
}
```

Field notes:

- `previousValue` / `newValue` — the before and after, as text, for every change type.
- `wordDiff` — inline word-level rendering for `title` and `description` edits, e.g. `Calm is the [-number one-] {+#1+} app for [-sleep-] {+sleep, meditation,+} and relaxation. Find anxiety relief, stress …`. Long unchanged stretches are elided.
- `addedWords` / `removedWords` — the individual words, capped at 60 each; `wordsAdded` / `wordsRemoved` carry the true counts.
- `addedItems` / `removedItems` — for list fields: the screenshot URLs added and removed, the genres gained and lost, the device identifiers added and dropped.
- `ratingAtChange` / `userRatingCountAtChange` — the star rating and review count at the moment the change was observed. Ratings are never a change trigger (they move constantly); they are context for the change.
- `delisted` records carry the last known listing values; `newValue` is `null`.
- `baseline` records are the first sighting and are never billed as a change event.

### Cost

Two pay-per-event charges: `app-checked` ($0.0008) for each app polled in each storefront, and `change-detected` ($0.012) for each change event delivered. `baseline` records are free.

| Run | Charges |
|---|---|
| Zero-config first run (3 apps, 1 storefront) | 3 × `app-checked` = **$0.0024** |
| 100 apps, 1 storefront, quiet day | 100 × `app-checked` = **$0.08** |
| 100 apps, 1 storefront, typical day (~4 changes) | $0.08 + 4 × $0.012 = **$0.13** |
| 100 apps, 3 storefronts, daily for a month | ~300 checks/day → **$7.20/mo** + changes |
| 500 apps, 1 storefront, weekly | 500 × $0.0008 = **$0.40 per run** |

A first run never charges a change event — it has nothing to compare against, so it charges only for the polling.

### Known limitations

Read these before you buy; they are the honest edges of what the public endpoint exposes.

- **No subtitle, promotional text, in-app purchase list or keyword field.** Apple's Lookup endpoint does not return them. Getting them would mean scraping and parsing the App Store's client-rendered web page, which is fragile; this actor deliberately does not. If the subtitle is the ASO field you care about most, this is not your tool.
- **Price is the base price only.** Subscription and in-app purchase price changes are not visible to this endpoint, so an app going from a $9.99/mo to a $4.99/mo subscription shows no `price` event.
- **The first run for any app in any storefront cannot report a change.** It records a baseline. Real change events start on the second run.
- **Detection is only as fine-grained as your schedule.** A title that is changed and changed back between two runs is invisible. Daily is the useful cadence; hourly rarely finds more.
- **Screenshot changes are detected by URL.** Apple issues a new URL when a developer re-uploads an image, which is the signal used here. Re-uploading a byte-identical screenshot therefore counts as a change, and the actor cannot tell you what is visually different — only which URLs appeared and disappeared.
- **`delisted` is inferred from absence, not announced.** An app pulled from a storefront and an app that has become region-locked look identical from here. The actor waits for `markDelistedAfterMisses` consecutive runs before reporting, and a failed HTTP request is never read as a delisting — but a genuinely long Apple outage could still produce a false `delisted`.
- **`supportedDevices` is noisy and off by default.** Apple adds a new device identifier to most apps at once when new hardware ships, which produces a change event for the whole watchlist on the same day.
- **Rate limits.** Apple allows roughly 20 requests per minute per IP on the Lookup endpoint. Because each request carries 100 app IDs, the default of 18/minute covers a 1,800-app watchlist per minute. Very large, very frequent watchlists may need the proxy input.
- **Chart and search seeding occasionally returns an ID the Lookup endpoint will not resolve.** Those apps are skipped, not charged for, and never reported as delisted.
- **iOS and iPadOS apps only.** Mac App Store, tvOS-only apps, and Google Play are out of scope.
- **Changing `storefrontLanguage` re-baselines every localised field.** Turn on `forceBaseline` for the run where you change it, or you will get one wave of bogus "edits".

### Legal

This actor is not affiliated with, endorsed by, or sponsored by Apple Inc. "App Store", "iTunes", "iOS" and "iPadOS" are trademarks of Apple Inc.

It reads two public, unauthenticated Apple endpoints — the iTunes Lookup/Search API at `itunes.apple.com` and the Apple Marketing Tools RSS chart feed at `rss.marketingtools.apple.com`. Both are documented by Apple, but they are published for Apple's affiliate and marketing-tools programs, and Apple's terms for those programs and for its websites generally restrict automated access, impose request-rate limits, and constrain how the returned data may be stored, redistributed and displayed. Using this actor may not comply with those terms, regardless of the fact that the underlying listing data is publicly visible on the App Store — "public" and "permitted by the terms of service" are separate questions. You are responsible for reviewing Apple's applicable terms, for your own compliance with them, and for how you use, store and redistribute the data this actor returns.

The actor returns product and company information only: app listings and the developer entity that publishes them. It does not collect reviews, reviewer names, or any other personal data, and it does not access anything behind a login.

# Actor input Schema

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

App Store URLs (https://apps.apple.com/us/app/facebook/id284882215), bare numeric App Store IDs (284882215), or 'id284882215' fragments - all three are accepted. This is the watchlist; each app is checked in every storefront listed below.

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

Two-letter App Store storefront codes, e.g. us, gb, de, jp, br. Each app is diffed independently per storefront, because titles, descriptions, prices and screenshots are localised. More storefronts means proportionally more 'app-checked' events.

## `seedFromChart` (type: `string`):

Optionally add the current Apple top chart for the first storefront to the watchlist. Use this to watch a whole category of competitors without listing them by hand. The chart is re-read on every run, so apps that climb into it are picked up automatically.

## `chartSize` (type: `integer`):

How many apps to take from the chart, from position 1 downwards. Ignored when 'Also watch a top chart' is 'none'.

## `searchTerms` (type: `array`):

Optional keywords, e.g. 'habit tracker'. The top App Store search results for each keyword in the first storefront are added to the watchlist. This is the fastest way to build a competitor set for an ASO keyword you care about.

## `maxAppsPerSearchTerm` (type: `integer`):

How many search results to take per keyword. Ignored when no search terms are given.

## `maxAppsPerRun` (type: `integer`):

Hard cap on how many app-and-storefront pairs are checked - and charged as 'app-checked' - in a single run. 50 apps across 3 storefronts is 150 checks. Protects you from an accidental 200-app x 10-storefront watchlist. Apps you listed by hand are always checked before seeded ones.

## `trackedFields` (type: `array`):

Which listing fields produce a change event. 'Supported devices' is off by default because Apple adds new device models to nearly every app at the same time, which would bury the fields you actually care about.

## `minDescriptionWordsChanged` (type: `integer`):

Descriptions pick up small edits constantly. A description change is only reported when at least this many words were added or removed in total; smaller edits update the stored copy silently so the next real rewrite still diffs against what is published today. 1 reports every edit.

## `storefrontLanguage` (type: `string`):

Optional locale for the returned text, e.g. en_us, de_de, ja_jp. Leave empty to get each storefront's own default language. Changing this after the first run will make every localised field look edited once - turn on 'Reset stored state' for that run.

## `maxChangeEvents` (type: `integer`):

Hard cap on the number of change events returned and charged for in one run. A normal run on a stable watchlist returns single digits; this only matters on the run after you widen the watchlist.

## `markDelistedAfterMisses` (type: `integer`):

The iTunes Lookup endpoint returns nothing for an app that is pulled from a storefront - and also, occasionally, for one that is simply having a bad minute. The actor waits for this many consecutive runs with the app missing before emitting a 'delisted' event, so a blip does not create a false alarm. Set to 1 to report immediately.

## `includeBaselineRecords` (type: `boolean`):

The first run has nothing to compare against, so it cannot report changes. With this on, that run returns one 'baseline' record per app/storefront showing the listing as it stands now, so you can see the watchlist was picked up correctly. Baseline records are never charged as change events. Turn it off if you only ever want edits.

## `forceBaseline` (type: `boolean`):

Discards the remembered listing for every app in this run and treats them all as seen for the first time. Use after changing 'Storefront language', or to clear out state you no longer trust. Leave off for normal scheduled runs, otherwise the actor can never detect a change.

## `stateKeyValueStoreName` (type: `string`):

Name of the named key-value store holding the last-seen listing for each app and storefront. Runs sharing this name share a memory of the watchlist. Change it to run two independent watchlists side by side under one account.

## `requestsPerMinute` (type: `integer`):

Apple rate-limits the iTunes Lookup endpoint at roughly 20 requests per minute per IP. Since one request carries up to 100 app IDs, the default of 18 is enough for a 1,800-app watchlist per minute. Lower it if you see HTTP 403 or 429 warnings.

## `requestTimeoutSecs` (type: `integer`):

Per-request timeout for calls to Apple's lookup, search and chart endpoints.

## `maxRequestRetries` (type: `integer`):

How many times to retry a failing Apple request before giving up on it. A batch that fails every retry is skipped for the run - its apps are not charged for, and no delisting is inferred from the failure.

## `proxyConfiguration` (type: `object`):

Optional, and off by default so the actor is cheap to try. Apple's public endpoints do not IP-block, and batching keeps the request count low, so most watchlists never need a proxy. Turn it on if you run very large watchlists frequently and start seeing 403 or 429 warnings.

## Actor input object example

```json
{
  "appIds": [
    "https://apps.apple.com/us/app/id284882215",
    "https://apps.apple.com/us/app/id571800810",
    "https://apps.apple.com/us/app/id6448311069"
  ],
  "countries": [
    "us"
  ],
  "seedFromChart": "none",
  "chartSize": 25,
  "searchTerms": [],
  "maxAppsPerSearchTerm": 25,
  "maxAppsPerRun": 200,
  "trackedFields": [
    "title",
    "description",
    "version",
    "price",
    "icon",
    "screenshots",
    "genres",
    "minimumOs",
    "seller",
    "availability"
  ],
  "minDescriptionWordsChanged": 1,
  "storefrontLanguage": "",
  "maxChangeEvents": 500,
  "markDelistedAfterMisses": 2,
  "includeBaselineRecords": true,
  "forceBaseline": false,
  "stateKeyValueStoreName": "app-store-change-feed-state",
  "requestsPerMinute": 18,
  "requestTimeoutSecs": 30,
  "maxRequestRetries": 3,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `changeEvents` (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 = {
    "appIds": [
        "https://apps.apple.com/us/app/id284882215",
        "https://apps.apple.com/us/app/id571800810",
        "https://apps.apple.com/us/app/id6448311069"
    ],
    "countries": [
        "us"
    ],
    "searchTerms": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("mibedk/app-store-listing-change-feed").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 = {
    "appIds": [
        "https://apps.apple.com/us/app/id284882215",
        "https://apps.apple.com/us/app/id571800810",
        "https://apps.apple.com/us/app/id6448311069",
    ],
    "countries": ["us"],
    "searchTerms": [],
}

# Run the Actor and wait for it to finish
run = client.actor("mibedk/app-store-listing-change-feed").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 '{
  "appIds": [
    "https://apps.apple.com/us/app/id284882215",
    "https://apps.apple.com/us/app/id571800810",
    "https://apps.apple.com/us/app/id6448311069"
  ],
  "countries": [
    "us"
  ],
  "searchTerms": []
}' |
apify call mibedk/app-store-listing-change-feed --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mibedk/app-store-listing-change-feed"
        }
    }
}
```

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/oi6iMUZXjdDR8L0n7/builds/N3dUhzwl2Z0HbNuB1/openapi.json
