--- name: itunes-app-store-scraper description: Read iOS, iPad and Mac App Store listings as structured records via the Apify Actor arman-bd/itunes-app-store-scraper. Returns app name, bundle ID, developer, numeric and localised price, lifetime and current-version ratings, category list, version, release and update dates, release notes, full description, minimum OS, file size, age rating, languages, screenshots, icon and store URL. Use for keyword and ASO research, competitor release tracking, cross-storefront price comparison and enriching a list of app or bundle IDs. Not for user review text, chart positions, download estimates or revenue. --- # App Store Scraper: iOS Apps, Ratings & Pricing Apify Actor `arman-bd/itunes-app-store-scraper`. Give it keywords, app IDs, store URLs or bundle IDs, and get one dataset record per app. It runs without credentials, and a 200-app keyword sweep is a single request. ## When to use it - Seeing which apps rank for a keyword, in the order a shopper sees them. - Polling `version` and `currentVersionReleaseDate` to track a competitor's ship cadence. - Comparing price, currency, rating and availability for the same apps across storefronts. - Turning a list of bundle IDs from an MDM or analytics export into full store records. - Category benchmarking: pull up to 200 apps for a keyword and chart rating distribution. ## When not to use it - Individual user review text. Only the aggregate rating and rating count are returned. - Chart positions such as Top Free or Top Paid, which come from a different feed. - Download counts, install estimates or revenue. Apple does not publish those here. - Deep result sets for a broad keyword. A term yields at most 200 apps and there is no page two, so more specific terms are the only way to go wider. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/itunes-app-store-scraper').call({ appIds: ['618783545', 'com.burbn.instagram'], country: 'GB', entity: 'software', maxResultsPerTerm: 50, }); const { items } = await client.dataset(run.defaultDatasetId).listItems(); const { value: summary } = await client .keyValueStore(run.defaultKeyValueStoreId) .getRecord('RUN_SUMMARY'); ``` One-shot over HTTP, when you want the rows back in the same request: ```bash curl -X POST "https://api.apify.com/v2/acts/arman-bd~itunes-app-store-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"searchTerms":["habit tracker"],"country":"US","entity":"software","maxResultsPerTerm":50}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/itunes-app-store-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `searchTerms` | string[] | no | `[]` | Keywords, one search pass each, matching app names, developers and keyword metadata in store ranking order. Duplicates and blanks are dropped. | | `appIds` | string[] | no | `[]` | Specific apps. Accepts a numeric App Store ID (`618783545`), a full store URL containing `/id618783545`, or a reverse-DNS bundle ID (`com.burbn.instagram`). Numeric IDs are batched 50 per request; each bundle ID costs its own request. Unrecognisable entries are logged and skipped. | | `country` | string | no | `"US"` | Two-letter storefront code, case-insensitive on the way in and uppercased. Anything that is not two letters throws before any request. | | `entity` | string | no | `"software"` | `software` for iPhone, `iPadSoftware`, or `macSoftware`. These are separate catalogues, not a flag on one result set. Any other value throws. | | `maxResultsPerTerm` | integer | no | `50` | Apps taken per search term, 1 to 200. Values above 200 are clamped with a warning. Ignored for `appIds`, which always return every match. | At least one of `searchTerms` and `appIds` must be non-empty, otherwise the run throws immediately. **The storefront changes the data, not just the currency.** `country` decides price, currency, availability, the rating aggregate and the language of release notes and description, so the same `trackId` in `US` and `DE` is genuinely two different records and is the intended way to compare markets. The second thing to plan is request shape: keywords are one request each regardless of `maxResultsPerTerm`, numeric IDs are one request per fifty, and bundle IDs are one request each, so resolving numeric IDs once and reusing them is much cheaper than repeatedly looking up bundle IDs. ## Output One record per app saved, de-duplicated on `trackId` across every search and lookup in the run. | Field | Type | Notes | |---|---|---| | `trackId` | number \| null | Apple's numeric app ID. The join key. | | `trackName` | string \| null | App name as it appears in this storefront. | | `bundleId` | string \| null | Reverse-DNS bundle identifier. | | `sellerName` | string \| null | Legal seller name, often in capitals. | | `artistName` | string \| null | Display developer name. Frequently differs from `sellerName`. | | `artistId` | number \| null | Developer ID. Group by this to find one publisher's portfolio. | | `price` | number \| null | Numeric price in `currency`. `0` for free apps, and `null` if absent. | | `formattedPrice` | string \| null | Localised string such as `Free` or `£5.99`. | | `currency` | string \| null | ISO currency code for the storefront. | | `averageUserRating` | number \| null | Lifetime mean rating, unrounded, for example `4.10675`. | | `userRatingCount` | number \| null | Lifetime rating count. | | `averageUserRatingForCurrentVersion` | number \| null | Same, scoped to the shipping version. | | `userRatingCountForCurrentVersion` | number \| null | Same, scoped to the shipping version. | | `primaryGenre` | string \| null | Primary category name. | | `genres` | string[] | Every category the app is listed under, primary first. | | `version` | string \| null | Shipping version string. | | `releaseDate` | string \| null | First release, ISO 8601. | | `currentVersionReleaseDate` | string \| null | When the shipping version went live, ISO 8601. This is the freshness signal. | | `releaseNotes` | string \| null | The full "What's New" text, in the storefront language. | | `description` | string \| null | Full store description. Can be several kilobytes. | | `minimumOsVersion` | string \| null | Minimum OS, for example `17.0`. | | `fileSizeBytes` | number \| null | Download size in bytes, coerced from the string the source sends. `null` when unparseable. | | `contentAdvisoryRating` | string \| null | Age rating such as `17+`. | | `languages` | string[] | Two-letter language codes. `[]` when none are declared. | | `screenshotUrls` | string[] | iPhone screenshots. `[]` for Mac-only apps. | | `ipadScreenshotUrls` | string[] | iPad screenshots. `[]` when the app is iPhone only. | | `artworkUrl512` | string \| null | 512px icon. | | `trackViewUrl` | string \| null | Store listing URL. | | `sellerUrl` | string \| null | Developer's own site, when declared. | | `country` | string | The storefront used for this run, uppercased. Part of the identity of the row. | | `source` | string | Which input produced the row: the search term string, or the literal `appIds`. Join back to your input on this. | | `scrapedAt` | string | Run timestamp, ISO 8601. | A real record, long strings trimmed: ```json { "trackId": 618783545, "trackName": "Slack", "bundleId": "com.tinyspeck.chatlyio", "sellerName": "SLACK TECHNOLOGIES L.L.C.", "artistName": "Slack Technologies, Inc.", "artistId": 453420243, "price": 0, "formattedPrice": "Free", "currency": "USD", "averageUserRating": 4.10675, "userRatingCount": 49068, "averageUserRatingForCurrentVersion": 4.10675, "userRatingCountForCurrentVersion": 49068, "primaryGenre": "Business", "genres": ["Business", "Productivity"], "version": "26.08.10", "releaseDate": "2013-03-20T19:23:34Z", "currentVersionReleaseDate": "2026-08-05T19:26:20Z", "releaseNotes": "Bug Fixes\n• Photoshop files pulled a disappearing act when shared …", "description": "Companies in every industry around the world grow their business in Slack …", "minimumOsVersion": "17.0", "fileSizeBytes": 345516032, "contentAdvisoryRating": "17+", "languages": ["EN", "FR", "DE", "IT", "JA", "KO", "PT", "ZH", "ES"], "screenshotUrls": ["https://is1-ssl.mzstatic.com/image/thumb/…/392x696bb.png"], "ipadScreenshotUrls": ["https://is1-ssl.mzstatic.com/image/thumb/…/552x414bb.jpg"], "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/…/512x512bb.jpg", "trackViewUrl": "https://apps.apple.com/us/app/slack/id618783545?uo=4", "sellerUrl": "https://slack.com/is", "country": "US", "source": "appIds", "scrapedAt": "2026-08-06T11:37:21.807Z" } ``` ## RUN_SUMMARY Written to the run's key-value store under the key `RUN_SUMMARY`. **Read it.** It is where a partial run admits that it was partial. ```json { "targetsRequested": 3, "targetsFailed": 0, "failures": [], "appsSaved": 52, "filters": { "searchTerms": ["habit tracker"], "appIds": ["618783545", "com.burbn.instagram"], "country": "US", "entity": "software", "maxResultsPerTerm": 50 }, "finishedAt": "2026-08-06T11:37:22.104Z" } ``` A target is one search term, one batch of up to fifty numeric IDs, or one bundle ID, so `targetsRequested` will not match the length of your input lists. `failures` entries carry `target` (the human label) and `error`; a run where every target failed throws instead of finishing. An app that simply is not in the storefront produces no failure and no row at all, so compare `appsSaved` against what you asked for rather than trusting an empty `failures` array to mean everything resolved. Note that `filters.maxResultsPerTerm` is the clamped value actually used, not what you sent. ## Behaviour to plan around - **200 apps per term is the ceiling and there is no page two.** Paging parameters are accepted upstream and then ignored. Widen coverage with more specific terms, never with a bigger cap. - **Row order is the ranking.** Search results arrive in the store's own relevance order for that keyword, so the array index is the ASO position. Anything that reorders the dataset destroys that signal, so capture the index before sorting. - **A missing app is silent.** A lookup that matches nothing returns an empty result set with a success status, not an error. The absence of a `trackId` in the dataset is the only evidence. - **`entity` does not apply to bundle ID lookups.** Numeric ID and keyword targets are scoped to the chosen catalogue; a bundle ID resolves against the storefront alone. A Mac-only run can therefore return an iPhone record for a bundle ID. - **De-duplication is global within a run.** An app reached by both a search and a lookup is saved once, with the `source` of whichever target found it first. - **`country` is part of the row identity.** Two storefronts for the same app are two rows only across two runs; within one run there is a single storefront. Do not merge rows from different runs on `trackId` alone. - **Current-version ratings often equal lifetime ratings.** Apple resets the per-version aggregate only when a developer asks it to, so equal values are normal and are not a sign of a copy error. - **`sellerName` and `artistName` disagree routinely.** The first is the legal entity, the second the display name. Use `artistId` for grouping, never a name string. - **A rejected storefront code fails that target, not the run.** Transient failures get three attempts with linear backoff; rejections are final and are not retried. - **Bundle IDs are the expensive input.** One request each, versus fifty numeric IDs per request. Resolve bundle IDs to `trackId` once, then work from numeric IDs. ## Recipes **Keyword ASO snapshot.** One request per term, maximum depth. ```json { "searchTerms": ["habit tracker", "budget app"], "country": "US", "entity": "software", "maxResultsPerTerm": 200 } ``` Keep the dataset order per `source` value: that index is the keyword ranking. **Cross-storefront price and rating comparison.** Run this once per storefront and join the datasets on `trackId`, keeping `country` in the key. ```json { "appIds": ["618783545", "389801252", "284882215"], "country": "GB", "entity": "software", "maxResultsPerTerm": 50 } ``` **Enrich an MDM inventory of bundle IDs.** ```json { "appIds": ["com.tinyspeck.chatlyio", "com.burbn.instagram", "com.google.Maps"], "country": "US", "entity": "software", "maxResultsPerTerm": 50 } ``` Store the returned `trackId` values and use those next time; the run will be far shorter. **Daily competitor release watch.** ```json { "appIds": ["618783545", "310633997"], "country": "US", "entity": "software", "maxResultsPerTerm": 50 } ``` Alert when `version` or `currentVersionReleaseDate` changes, and read `releaseNotes` for what shipped.