# Plans and refreshes Peek and Panda plans, requested refreshes, and how to understand refresh availability. PeekPanda offers two plans: Peek and Panda. Both open the same iOS catalogue and case files. Peek is PeekPanda-only. Panda adds a shared workspace and includes the whole Genviral Creator plan. ## Peek and Panda | | Peek | Panda | | --- | --- | --- | | Monthly | $59 | $99 | | Yearly | $590 | $990 | | Shared workspace | No | Yes, members inherit the owner plan | | Genviral | Not included | Personal AI assistant, images, video, slideshows, posting and scheduling | | Requested refreshes per day | 1 | 3, shared across the workspace | Refreshes are not the reason the plans cost different amounts. Browsing stored evidence never spends one. Yearly billing includes the equivalent of two months free compared with monthly billing. These are launch prices and may change before general availability. Sign in at [genviral.io](https://genviral.io) with the same account after a Panda purchase. Choose a plan at `/pricing`, then select monthly or yearly billing. If checkout is not available in your environment, PeekPanda says so clearly. ## What a requested refresh is PeekPanda normally shows stored public observations. Opening a case file, a chart, or a category does not itself request new source data. A **requested refresh** asks PeekPanda to seek a newer public observation. It can return a pending state while collection completes. The latest valid observation remains visible until a newer one is available. To keep results reliable, current observations can be reused and repeated requests may be temporarily unavailable. A completed refresh means PeekPanda recorded newer public evidence; it does not guarantee that the underlying app or market changed. ## Reading refresh availability In the ASO workspace, the refresh control explains whether an update is available, in progress, recently completed, or unnecessary because the evidence is current. If nothing needs collecting, PeekPanda says so rather than spending an allowance. Plan usage for requested catalogue refreshes appears on your Account page. ASO and keyword collection are documented separately in [refreshing keywords](/docs/refreshing-keywords). ## Where your plan comes from The **Plan** card identifies whether your access is billed on PeekPanda, included with an eligible plan, or provided as preview access. It also shows the current billing period and, where available, a link to manage billing. ## Related - [Start peeking in 4 steps](/docs/start-peeking) - [What is PeekPanda](/docs/what-is-peekpanda) - [How to read the evidence](/docs/reading-the-evidence) - [Freshness and source dates](/docs/freshness-and-source-dates) --- # How to read the evidence Read observation dates, estimates, unavailable values, and suggested matches with appropriate care. PeekPanda shows public evidence with its date and context. Read the source context, observation date, and label together; a number without that context is easy to over-read. ## Every fact has a date Case files identify when public listing information, charts, search evidence, ads, and organic matches were observed. Different sections can have different dates because they describe different public observations. That is intentional. Use the date to decide whether evidence is current enough for your question. Older evidence remains evidence of the time it was recorded, not a claim about today. ## Estimates are estimates Downloads and revenue are labelled as estimates because they are derived from public signals. They are not measured downloads, developer-reported revenue, or financial statements. See [how estimates are computed](/docs/how-estimates-are-computed). Compare estimates only within a comparable storefront, category, and observation period. Use first-party data for decisions with material financial consequences. ## A dash means unavailable PeekPanda never treats unavailable evidence as zero. - A missing metric shows an explicit unavailable state. - A gap in a history remains a gap rather than dropping to zero. - Unavailable values are kept separate from observed values when sorting. - A first observation has no earlier observation to compare against. An empty ASO section means the listing was not found in the collected query set. It does not mean the app has no keyword presence. ## Suggested is not confirmed Organic matches are public-evidence suggestions. They can help you find leads, but a name match does not prove that an account belongs to the company behind an app. Open the original source before treating a match as confirmed. ## Related - [Revenue and download estimates](/docs/revenue-and-download-estimates) - [Rank history and charts](/docs/rank-history-and-charts) - [Open a case file](/docs/open-a-case-file) - [Freshness and source dates](/docs/freshness-and-source-dates) --- # Start peeking in 4 steps Find an app, open its case file, read the evidence, and track it so changes come to you. Four steps take you from an empty screen to an app you are following. Each one links to the guide that goes deeper. ![Home, with the case board and what changed since the last visit.](/docs-media/home.png?w=1728&h=1084) ## 1/ Find an app Press `⌘K` on a Mac, `Ctrl+K` on Windows, or `/` anywhere you are not already typing in a field. The search spotlight opens. Type a name. Results come from the catalogue as you type. Arrow up and down to move through them, `Enter` to open the highlighted app, `Escape` to close. Clicking a result works too. You can also browse instead of searching. **Apps** in the sidebar opens the public charts. The switch at the top right of that page moves between **Leaderboards**, **Search** and **Categories**. Categories lists the shelves in the catalogue snapshot and the apps recorded in each. If nothing matches, the spotlight tells you plainly that no app by that name is in the catalogue. It does not guess. ## 2/ Open its case file Picking a result takes you to `/apps/ios/`. The top of the page is the app's identity: icon, name, developer, rating with the date of the store lookup behind it, and chips for category, price, age rating, release and update dates, and its best observed chart rank. **View listing** opens the app on the App Store. Below that are five tabs: Overview, Paid ads, Organic, ASO, and Listing & about. Overview is where you land. [Open a case file](/docs/open-a-case-file) walks through every panel. ## 3/ Read the evidence Start with **Latest evidence** at the top of Overview. Downloads leads, with revenue, store ratings and chart rank beside it. Each estimate shows the latest supported value, the change since the previous valid observation, and the date behind it. If today's rating sample cannot support a new estimate, the last valid value stays visible with its original date. Paid ads shows the exact stored total rather than treating an ad's historical start date as its collection date. Two things to hold on to while you read: - Downloads and revenue are **estimates**, derived from ratings movement and grossing rank. They are not measured downloads and not a calendar-month total. - A dash means nothing has been collected for that metric, not that the value is zero. Scroll on for what is new this month versus last, the keywords the app ranks for, its store screenshots, and strips of the stored ads and organic posts tied to it. [How to read the evidence](/docs/reading-the-evidence) covers source dates, estimates and unknowns in full. ## 4/ Track it Open **ASO** in the sidebar and use **Track another app**. Type the name, pick the app, and choose whether it is yours or a competitor. Tracking keeps public keyword evidence associated with your app, and **Home** can show observed movement, rival context, and recent creative evidence around its case file. Your workspace shows its current tracking allowance. You can also track a rival straight from the workstation. In the Competitors panel, any app that keeps meeting yours in search can be tracked with one click, and compared keyword by keyword. > Tracking a competitor is separate from saving content. In Organic Content and Paid Ads, **Save** keeps an individual post or ad, and the **Saved** filter brings your selections back. ## Related - [What is PeekPanda](/docs/what-is-peekpanda) - [Open a case file](/docs/open-a-case-file) - [How to read the evidence](/docs/reading-the-evidence) - [Finding similar apps](/docs/similar-apps) --- # What is PeekPanda PeekPanda is an iOS competitor catalogue where every app has a case file of dated, stored evidence. PeekPanda is a research tool for iOS apps. You look up an app, open its case file, and read what has actually been collected about it: store listing facts, ratings, App Store chart positions, search positions, matched creatives, and estimates. The rule the product is built around: every stored fact carries the date it was observed. Nothing is presented as more certain than it is. An estimate is labelled an estimate. A number nobody has collected shows as a dash, not a zero. ## What a case file contains A case file lives at `/apps/ios/`, where the store id is the app's public App Store identifier. It opens on an identity block and five tabs. The identity block carries the icon, app name, developer, star rating and rating count with the date of the store lookup behind them, and a row of chips: platform, category, price, age rating, release date, last store update, and the app's best observed chart rank with the storefront and day it was seen. A **View listing** button opens the app on the App Store. | Tab | What is in it | | --- | --- | | Overview | Latest supported download, revenue, store-rating and chart-rank readings, the exact stored paid-ad total, recent organic evidence, keywords, store screenshots, and creative previews | | Paid ads | Stored ad observations linked to this exact store identity, each with the platform, status and the date it was observed | | Organic | Suggested organic posts matched on product name, with the evidence for each match and the source snapshot date | | ASO | Collected public searches the app appears in, its observed position and movement, the popularity, difficulty and opportunity of each phrase, and the apps ranking around it | | Listing & about | The store description, monetization facts, screenshots and the store observation behind them | Where a panel has nothing collected, it says so and offers a **Request data** button that records your interest. It is not a promise of collection; an ASO request can begin building keyword evidence for that app. ## What is covered The catalogue is iOS only. There is no Android coverage. Chart coverage is available for selected storefronts, categories, and chart types. The selected context is always shown with the observation. ## What is not covered Some things are honest unknowns and stay that way: - Ad spend, budgets and onboarding journeys. - Native App Store keyword rank. What you see is a position observed in public search results for a collected query. - Search volume and any invented difficulty score. - Listing history. Changes to screenshots, descriptions and versions have not been collected. - Android, at any level. ## Who it is for Indie developers, mobile marketers, growth teams and founders who want to look at what a competitor is actually doing before deciding what to build, price or say. Everything on a case file is also readable as JSON through the PeekPanda API and MCP tool profile, so an agent can do the same reading you can. ## Related - [Start peeking in 4 steps](/docs/start-peeking) - [How to read the evidence](/docs/reading-the-evidence) - [Open a case file](/docs/open-a-case-file) - [Plans and refreshes](/docs/plans-and-refreshes) --- # Open a case file Every panel on an app's page, what it holds, and how to move between them. A case file is one app's page. It lives at `/apps/ios/`, where the store id is the app's public App Store identifier. Search results, chart rows, rival lists and category shelves all lead here. ![A case file on Overview: the identity block, monthly estimates, and the latest collected ads.](/docs-media/case-file.png?w=1728&h=1084) The page is an identity block and five tabs. Everything below the identity block is evidence. The overview comes from one read of the case file, and every other tab reads its own evidence, so one empty panel never blanks the rest. ## The identity block At the top: the app icon, a **Case file · iOS** tag, the app name and developer. Under the name, the star rating and rating count with the words *Store lookup* and the date those numbers were read. Then a row of chips, each one a store fact: - Platform and category - Price - Age rating, when the store publishes one - Released and Updated dates - The best chart rank observed for the app, with the storefront and day it was seen On the right, **View listing** opens the app on the App Store in a new tab. Above the identity block, breadcrumbs run **Apps / Category / App name**. The category crumb takes you to the public chart for that shelf. ## Moving between tabs The tab strip sits under the identity block: **Overview**, **ASO**, **Paid ads**, **Organic**, **Listing & about**. The active tab carries a green underline. Some tabs carry a small badge. ASO shows how many collected searches this app appears in. Organic shows how many suggested matches were found, with a `+` when the match scan reached its limit. The overview renders from the case file alone. Every other panel loads when you open its tab, so evidence you never open is never read. While a panel loads, a placeholder holds its place. Each tab has its own address, named by the `tab` query parameter: `?tab=aso`, `?tab=paid`, `?tab=organic` or `?tab=listing`. The overview is the default and needs no parameter. A link with one of these opens the page on that tab. Switching tabs keeps the rest of the address, such as the chart country and type, and Back and Forward step through the tabs you opened. Tiles and previews inside the page link to the tab that holds the full evidence. Those links also carry a `#tab-` fragment, which scrolls the tab strip into view. ## Overview ### Latest evidence The lead panel. Downloads is the headline figure, with a timeline underneath when more than one observation exists. Beside it: Revenue, Store ratings, and Chart rank. Each estimate shows the latest supported value and how it moved since the previous valid observation. The caption makes clear that each point is a 30-day estimate derived from two dated ratings observations, not a calendar-month total. When a newer rating sample cannot support a new estimate, the last valid value remains and its caption names both the estimate date and newer ratings date. Downloads and revenue are estimates. See [Revenue and download estimates](/docs/revenue-and-download-estimates). The Chart rank card shows the latest observed rank, its movement, a sparkline of the collected days, and how many days have been observed. If the app has never been seen on the selected chart, it says "No chart history collected" rather than showing a zero. The chart behind that card defaults to your app's likely home: the paid chart if the store price is above zero, the free chart otherwise, in the United States, Overall. Arriving from a leaderboard carries that board's country, shelf and chart type through, so the rank you clicked is the rank you see. ### This month vs last Two counters, New ads and New organic, comparing what was collected this month against last month, with the date the count was taken as of. Undated items are counted separately rather than assumed into a month. ### Keywords A rail listing the searches this app ranks for, best position first, each with a position pill. The heading beside it says how many of the collected queries matched, and how many of those are on page one. If nothing has been collected, the rail says so and offers **Request data** to begin building relevant public keyword evidence. ### Screenshots, Paid ads, Organic posts Three horizontal strips of stored media, in that order. Screenshots come from the store listing. Paid ads and organic posts are the same evidence the dedicated tabs hold, in preview form. Each strip's heading states how many items are stored and how many are new this month. ## Paid ads Stored ad observations linked to this app's exact App Store identity, not matched by name. Each card carries the creative, its platform, its status, the date it was observed, and a link to the source where one is stored. If nothing has been stored, the panel says the panda has not stored a paid creative for this store identity. It does not imply the app runs no ads. ## Organic Suggested organic posts, matched against a copied public corpus by product name. Every card is badged **Suggested match** and shows the evidence for the match plus its source snapshot and copy dates. A name match does not prove the app owns the publishing account. If the candidate scan reached its configured limit, the panel says so, because more matches may exist beyond what is shown. ## ASO The search picture is a keyword table built from collected public searches that contain this listing. Each row shows the query, the observation date, the observed position with how far it moved since the run before, its popularity, difficulty and opportunity, and a preview that opens the full list of apps in ranking for that search. Positions here are where the app appeared in collected public search results. They are not native App Store ranks, and they are not every keyword the app ranks for. ![The ASO tab of a case file, listing collected searches and the observed position in each.](/docs-media/case-file-aso.png?w=1728&h=1084) ## Listing & about The store description as published in the catalogue snapshot, with that snapshot's date. Then **Monetization**: download price, whether the listing declares in-app purchases, whether it declares ads, and an installs figure. When that figure was derived from ratings it is labelled `Historical install proxy` and marked as not measured installs. Below that, the stored screenshots and the store observation behind them. Listing history, meaning changes to screenshots, descriptions and versions over time, has not been collected. That panel is marked as such and carries a **Request data** button. ## When a panel cannot load If the case file itself cannot be read, the page shows a single "Not loaded" state with the reason. Individual panels behave the same way: a panel that could not be read says so, rather than rendering an empty result that looks like a real zero. ## Related - [How to read the evidence](/docs/reading-the-evidence) - [Revenue and download estimates](/docs/revenue-and-download-estimates) - [Rank history and charts](/docs/rank-history-and-charts) - [Finding similar apps](/docs/similar-apps) --- # Rank history and App Store charts Browse stored App Store charts and follow one app’s observed chart position over time. Use the **Apps** section to browse PeekPanda’s stored App Store chart observations. Open an app to view its case file or its history for the selected chart. Chart pages display the latest available observation and its date. They do not present browsing as a live market check. ## Choosing a chart Choose a storefront, category, chart type, and view. The selected choices live in the URL, so a shared link opens the same chart context. Grossing rank is a relative position, not a revenue figure. See [revenue and download estimates](/docs/revenue-and-download-estimates) for how PeekPanda presents any associated estimate. ## The table Each row describes a stored observation for an app: its chart position, available movement, listing context, public rating information, and estimated downloads or revenue where evidence supports an estimate. You can sort supported columns. Values that are unavailable remain separate from observed values, so they do not read as the smallest result. ## One app’s rank history Open a chart position to review an app’s recorded history for that exact chart. The page shows the latest observed position, the best and worst recorded positions, the observation period, and a timeline of the stored positions. A line connects comparable observations. A missing observation remains a gap. An app that has not appeared in the selected chart shows an explicit unavailable state, not a rank of zero. Where the same app has observations in other supported storefronts, the page shows each with its own date. Compare only observations from comparable periods and chart contexts. ## Related - [Leaderboards](/docs/leaderboards) - [Revenue and download estimates](/docs/revenue-and-download-estimates) - [Open a case file](/docs/open-a-case-file) - [How to read the evidence](/docs/reading-the-evidence) --- # Revenue and download estimates How to read the case-file download and revenue estimates without mistaking them for measured results. Case files can show estimated monthly downloads and revenue. Both are clearly labelled estimates and are derived from public App Store signals, not private developer reporting. ![Monthly download and revenue estimates on a case file, each marked Estimate.](/docs-media/case-file.png?w=1728&h=1084) Use them to investigate relative scale and changes over time. Do not use them as an app’s actual downloads, reported revenue, profit, or valuation. ## Download estimates Download estimates use public rating activity observed over time. They are directional: an app’s rating behavior, audience, and review prompts can change the signal without a matching change in real downloads. Where the rating history has never produced a valid increase, the estimate remains unavailable. That is not a zero. After a valid estimate exists, a missed collection or an unchanged or lower rating count keeps that estimate visible with its original **As of** date until newer evidence can support another one. ## Revenue estimates Revenue estimates use public grossing-chart signals where those signals are available. A chart position is not revenue, and apps without suitable chart evidence may have no revenue estimate. Compare estimates only in a comparable context, such as the same storefront, category, and observation period. For a financial decision, validate the finding with first-party evidence. ## Reading the panel Each point on an estimate timeline is tied to an observation date. When a prior estimate is available, the displayed change compares it with the preceding valid estimate. Missing points remain gaps rather than being treated as zero, while summary views retain the latest valid value and disclose its age. On sortable views, unavailable estimates sort separately from available values so they do not read as the smallest result. ## Related - [How estimates are computed](/docs/how-estimates-are-computed) - [How to read the evidence](/docs/reading-the-evidence) - [Rank history and charts](/docs/rank-history-and-charts) - [Open a case file](/docs/open-a-case-file) --- # Finding similar apps Three ways to reach the apps that sit next to the one you are reading, and what each neighbourhood actually means. There is no single "similar apps" button. There are three different neighbourhoods, and they mean different things. Pick the one that matches the question you are asking. | You want | Go to | | --- | --- | | Apps competing on one specific search | Open that keyword's ranking on the ASO tab | | Apps on the same shelf | The category crumb, or the Categories page | | Apps at a similar commercial level | The public chart around its rank | ## Apps in ranking: the neighbours on one search In the keyword table on the ASO tab, click a ranking preview. **Apps in ranking** lists the apps that appeared for that search, in position order, with the app you are reading highlighted. This is the tightest possible neighbourhood: the exact set of apps a person saw when they typed that phrase, on the day it was collected. Any app in the list that has its own case file is a link. When a public response contains more results than the stored observation, the heading explains that the visible result set is incomplete. ## The shelf: same category Two routes into a category: - The **category crumb** above a case file, or the category chip in a chart row, opens the public chart for that shelf. That gives you the shelf ranked by position. - **Apps → Categories** lists every shelf in the catalogue snapshot with how many apps sit on it. Picking one opens the catalogue filtered to that category, ordered by how many people rated each app. Category is the store's own classification, so it is broad. It answers "what else is filed here", not "what else competes with this". ## The chart: same commercial level From a chart row, clicking the rank opens that app's rank history; going back to the board shows you the apps immediately above and below it on the same chart, on the same day. For grossing charts in particular, that is a useful peer set: apps holding a similar position on the same shelf, in the same storefront. ## Tracking a competitor When a rival is worth following rather than just reading, track it. In the ASO workstation, the **Competitors** panel lists the apps that keep meeting your tracked app in search, and each one can be tracked in place. Once tracked, you can run a keyword gap against it and its movement shows up on **Home**. ## For agents The API and MCP profile expose a stored similar-listings read, `get_similar_competitors`, at `GET /v1/competitors/{platform}/{storeId}/similar`. It returns the same paginated listing envelope as the competitor list, taking an optional cursor and limit. It is a stored read: it never triggers collection. ## Related - [Open a case file](/docs/open-a-case-file) - [Rank history and charts](/docs/rank-history-and-charts) - [How to read the evidence](/docs/reading-the-evidence) - [Start peeking in 4 steps](/docs/start-peeking) --- # Catalogue and search Find iOS apps in the PeekPanda catalogue and understand the results you are viewing. Press `⌘K` (or `Ctrl+K`) anywhere in PeekPanda to search the iOS catalogue. Choose an app to open its case file. The catalogue is iOS only. Each row represents an App Store listing, not a cross-platform product record. ## Search from anywhere Open search with the keyboard shortcut, the **Search** control in the Apps view, or the field on the catalogue page. Type an app name, use the arrow keys to move through matches, press Enter to open the selected app, and press Esc to close search. Search prioritizes app-name matches. The result row shows the available public listing context, such as the app name, icon, developer, rating activity, and category. No match means PeekPanda does not currently hold a matching listing. It does not mean the app does not exist. You can use [Request an app](/docs/request-an-app) to add a specific App Store listing for investigation. ## The catalogue table The catalogue view lists stored app observations and lets you narrow them by app name or category. Its URL keeps the active filters, making a filtered view easy to share. ![Catalogue search for “recipe”, limited to iOS.](/docs-media/catalogue.png?w=1728&h=1084) | Column | What it holds | | --- | --- | | # | Position in the current list, not a chart rank. | | App | The app’s icon, name, and available developer information. | | Category | The public App Store category. | | Rating | Observed public star rating and rating activity. | | Installs | A download estimate derived from public signals, when available. | | Released | The listing’s public release date. | | Updated | When PeekPanda last recorded the listing information. | **Installs is an estimate, not a measured download count.** A dash means PeekPanda does not have enough usable evidence to show the value. See [how estimates are computed](/docs/how-estimates-are-computed). ## Freshness Opening the catalogue reads stored observations rather than waiting on an App Store request. If supporting listing information is missing or needs updating, PeekPanda may collect it in the background and update the page when it becomes available. The current observation date always remains visible. ## Related - [Categories](/docs/categories) - [Open a case file](/docs/open-a-case-file) - [Request an app](/docs/request-an-app) - [How estimates are computed](/docs/how-estimates-are-computed) --- # Categories Browse the catalogue by App Store category and see how many listings sit on each shelf. `/apps/categories` lists every App Store category the catalogue holds, with a count next to each one. Click a category to open the catalogue table filtered to it. ![Categories, ordered by how many listings the snapshot recorded.](/docs-media/categories.png?w=1728&h=1084) Reach it from the **Categories** pill in the Apps view switch, or from **All categories** on the catalogue page. ## What a category card tells you Each card carries the category name and the number of listings recorded under it in the snapshot. The count describes what PeekPanda collected, not every listing that may exist in that category. Cards are ordered by count, largest first. Categories with the same count are ordered by name. A listing with no category recorded appears in no card. It is still in the catalogue and still findable by name. ## Opening a category Clicking a card opens `/apps/search?category=`. From there the category is a filter like any other: 1. The page header changes to the category name. 2. The active category shows as a chip in the filter row. The `×` on it clears the filter. 3. Every other catalogue control still applies. Rows stay ordered by rating count, highest first, 24 at a time. You can also reach the same view by clicking the category on any row of the catalogue table. > Category matching is exact. It filters on the category value stored on the listing, so it will not fold two similar shelves into one. ## Categories are not chart shelves Two different lists of categories exist in PeekPanda, and they are not interchangeable. | | Catalogue categories | Chart shelves | | --- | --- | --- | | Where | `/apps/categories` and the catalogue table | The **Shelf** filter on the leaderboards | | What they group | Listings stored in the catalogue | A stored public chart for one storefront | | How many | However many the collected listings carry | Overall plus 27 App Store categories | | Counts mean | Listings collected under that category | Not a count; the shelf picks which chart to read | Use catalogue categories to browse what has been collected. Use the shelf filter on [Leaderboards](/docs/leaderboards) to read a specific daily chart. ## Related - [Catalogue and search](/docs/catalogue-and-search) - [Leaderboards](/docs/leaderboards) - [Open a case file](/docs/open-a-case-file) --- # Emerging apps Find recently launched iOS apps with early public signals and understand what the emerging-app board means. Open `/emerging` to explore apps that are early in their public lifecycle and may not yet belong in the established catalogue. ![Emerging apps, with the add and scan controls and an empty board.](/docs-media/emerging.png?w=1728&h=1084) ## Three ways an app can appear PeekPanda uses separate intake lanes for established catalogue coverage, emerging-app discovery, and direct requests. Each lane has a different purpose: - **Established** covers apps with an established public footprint. - **Emerging** surfaces recently launched apps with enough public evidence to begin observing them. - **Requested** lets you add a specific App Store listing you want to investigate. An emerging or requested app is not rejected simply because it does not meet the standards used for established coverage. ## Finding fresh launches Use **Look for fresh launches** to enter search terms that describe the market you are researching. PeekPanda checks the public results for recently released apps that meet the emerging lane’s evidence requirements, then adds qualifying apps to the board. Discovery is focused on the terms you provide. It is not a claim that PeekPanda has searched every new app, category, or market. ## Reading the board Each row identifies the app, its public launch information, and the earliest recorded rating context. A traction label helps you distinguish an app being watched from one that later showed higher public rating activity. **Observed growth** means a later public observation was higher than the earlier reference point. It is not proof of downloads, revenue, product-market fit, or sustained momentum. The baseline stays visible so the comparison remains interpretable. ## Adding an app you already know **Add an app you found** accepts an App Store link or app identifier for the requested lane. The requested lane does not require the app to clear an established or emerging threshold. See [Request an app](/docs/request-an-app). ## Related - [Request an app](/docs/request-an-app) - [Catalogue and search](/docs/catalogue-and-search) - [Open a case file](/docs/open-a-case-file) - [Reading the evidence](/docs/reading-the-evidence) --- # Leaderboards Browse stored App Store charts by storefront, category, and chart type, then interpret rank changes responsibly. `/apps` opens PeekPanda’s leaderboards. They show stored public App Store chart observations by storefront, category, and chart type. Browsing a chart reads the latest available observation. The displayed date tells you when it was recorded. ![The Apps leaderboard, with chart movement, advertisers, viral posts, and search owners.](/docs-media/leaderboards.png?w=1728&h=1084) ## Choosing a chart Use the controls above the table to choose a storefront, category, chart type, and view. The selected state is kept in the URL, so you can share the exact board you are looking at. Available views include rankings, gainers, losers, new entrants, and recently released apps. A view can be unavailable when there is not yet enough comparable history. ## Reading a row | Column | What it holds | | --- | --- | | # | Observed chart position and, where available, its change. | | App | Icon, name, and available developer information. | | Category | The App Store category on the listing. | | Downloads est. | A public-signal estimate, when available. | | Revenue est. | A public-signal estimate, when available. | | Rank trend | Recently observed chart positions. | | Rating | Public star rating and rating activity. | | Released | Public listing release information. | Download and revenue values are estimates, not measured results. A dash means no suitable estimate is available, not zero. See [revenue and download estimates](/docs/revenue-and-download-estimates). ## What rank movement means A movement compares the app’s observed position with its preceding comparable observation for the same chart. It describes a change in position, not why the position changed or whether the app’s business performance improved. Grossing rank is a relative position, not a revenue amount. A chart with no earlier comparable observation shows no movement rather than inventing a zero. ## Sorting and insight panels You can sort supported columns and share the resulting view from its URL. Values without suitable evidence remain separate from observed values when sorting. The surrounding panels summarize the currently selected chart, such as its category mix, release-age context, and available storefront coverage. They describe the observed chart, not the entire App Store market. ## Related - [Rank history and charts](/docs/rank-history-and-charts) - [Revenue and download estimates](/docs/revenue-and-download-estimates) - [Categories](/docs/categories) - [Open a case file](/docs/open-a-case-file) --- # Request an app Add an App Store listing the catalogue does not hold yet, and ask for evidence that has not been collected for one it does. Two different requests exist, and they do different jobs. - **The app is not in the catalogue.** Add it yourself with a link. It is admitted immediately. - **The app is in the catalogue but a tab is empty.** Record a data request. It saves demand; it does not collect on the spot. ## Adding an app that is missing Go to `/emerging` and use **Add an app you found**. Paste one of these: - A numeric App Store ID, for example `1499198946`. - An App Store URL containing the app's `/id` segment, for example `https://apps.apple.com/us/app/example/id1499198946`. Then press **Add to the board**. This is the requested lane. It has no rating floor and no launch window, so a small or brand new app is welcome. See [Emerging apps](/docs/emerging-apps) for how the lanes differ. ### What is accepted Only App Store listings are accepted. A URL from another host, or a URL without an app identifier, is rejected with a clear message. Text that is neither a number nor a URL asks you for one of the two. The app is then checked in the US storefront. If the listing is unavailable there, PeekPanda explains that nothing was added. ### What happens next On success the message line reads *Observed and added to the board* and the app appears on the board straight away. At that moment the app is admitted with what the lookup returned: its name, its release date, and its rating count. That rating count becomes the app's baseline, and the app starts as *Watching*. The app is now in the catalogue. It is findable by name in search, and it has a [case file](/docs/open-a-case-file). The rest of the listing detail, such as category and developer, fills in when store metadata is collected for it. Nothing is invented in the meantime. Adding the same app twice does not create a duplicate. The existing record takes a new observation instead. ## Asking for evidence that has not been collected Open an app's case file. Where a tab has nothing collected, it says so and offers a **Request data** button. The button appears on: | Where | What it asks for | | --- | --- | | Overview | Paid ads, organic content, ASO | | Organic tab | Organic content | | ASO tab | ASO | | Listing and about tab | Listing history | Press it once. It changes to **Requested** and stays that way. Repeating the request is harmless; it records the same demand rather than a second one. Each account can register one request per kind per app. If the request cannot be saved you get *Could not save your request. Please try again.* ### What a data request actually does It records that you want that evidence for that app. That is the whole promise. It does not guarantee collection or put a date on when information may become available. An ASO request on an iOS listing can begin building keyword evidence for that app. Every other kind records demand only. > A request never invents data. Until something is actually collected, the tab keeps saying nothing was collected rather than showing a guess. [Reading the evidence](/docs/reading-the-evidence) covers how absences are shown. ## Related - [Emerging apps](/docs/emerging-apps) - [Open a case file](/docs/open-a-case-file) - [Reading the evidence](/docs/reading-the-evidence) - [Catalogue and search](/docs/catalogue-and-search) --- # Organic content library Search stored public organic posts, inspect their dated engagement evidence, and save useful creative references. **Organic Content** opens `/library?source=organic`. It is a searchable library of stored public posts, including available media, caption, hook, and engagement evidence. ![The organic content library.](/docs-media/organic.png?w=1728&h=1084) The figures you see are observations with dates, not live readings of a post’s current performance. ## API and MCP parity The same stored corpus is available through `GET /v1/organic` and the `search_organic_content` MCP tool. Read one item with `GET /v1/organic/{postId}` or `get_organic_content`; list the authenticated caller’s saved ids with `GET /v1/organic/saved` or `list_saved_organic_content`. These reads preserve the same bounded filters, stored evidence, and owner-scoped saved state as the library. ## Searching and filters Search the available post text and sort by relevance, observation date, publication date, engagement, or content shape. Filters let you narrow by content type, business type, source platform, category, creator, product, and date range. Active filters appear as removable chips. The results describe the stored corpus that matches your current view, not every post published on a platform. ## Opening a post Open a card to view its media, account context, publication date, available engagement evidence, hook, caption, and detected monetization signals. Metrics that have not been observed are omitted rather than shown as zero. Where available, you can save a post, copy its original link, or open the original post. The primary action reflects the kind of creative evidence available. ## What the library is and is not Product matching is based on public evidence and can be a useful lead. It does not verify that a creator account belongs to a product or company. Engagement is tied to its observation date. A post can change after it was recorded, so read the date before comparing it with another post. ## Related - [Reading a creative card](/docs/reading-a-creative-card) - [Saving creatives](/docs/saving-creatives) - [Paid ads library](/docs/paid-ads-library) - [Reading the evidence](/docs/reading-the-evidence) --- # Paid ads library Search dated public paid-ad observations, inspect creative evidence, and understand what the library does not measure. **Paid Ads** opens `/library?source=paid`. It is a searchable library of public paid-ad observations, including creative media, copy, and the app or site each ad points to. ![The paid ads library, with filters and active video cards.](/docs-media/paid-ads.png?w=1728&h=1084) The library shows stored evidence. Browsing it does not claim to perform a live check of an advertiser’s current campaigns. ## Searching and filters Search matches the available advertiser, creative, and destination information. A search for a known category term such as `food` or `self improvement` also includes ads linked to the matching canonical category, while unrelated searches stay literal. Filters narrow by source platform, canonical App Store category, observed status, creative format, start date, observed run length, CTA presence, primary-text length, reported reach, and verified app match. You can sort by when an ad was found, when it started where that date was disclosed, or by reported reach where available. The category picker uses this fixed English vocabulary: Health & Wellness, Personal Development, Relationships & Lifestyle, Arts, Hobbies & Entertainment, Education & Knowledge, Fashion & Beauty, Spirituality & Beliefs, Business & Finance, Travel, Food & Cooking, Home & Design, Fitness, Technology, Gaming & Esports, and Productivity & Organization. PeekPanda keeps the source category unchanged and maps only known separator, case, diacritic, and documented multilingual aliases into these labels. An empty state means no stored observation matches the current search and filters. It does not prove that no matching ad exists. ## Matching an ad to an app PeekPanda links an ad to an app when the ad’s observed destination resolves to the same App Store identity. When the evidence does not support that link, the card uses the available destination or advertiser context instead. No matching ad is not a claim that an app has never advertised. ## Reading ad dates and status An ad card may show a disclosed start date, an end date, an observed active status, or the date PeekPanda first recorded it. These are source observations, not a live delivery guarantee. Time running describes the period disclosed by the public record. It does not reveal budget, spend, impressions, or profitability. ## Reported reach Some public records disclose reach for particular regions. Where available, PeekPanda shows it as a source-reported figure. It is not an impression count, spend figure, or PeekPanda estimate. If it is unavailable, the card says so rather than showing zero. ## Opening an ad Open a card to inspect available copy, creative media, source context, and destination link. You can save or remove an item from your own collection, and open the original public record when available. Transcript and AI-analysis views may be available for eligible creative evidence. If a result cannot be generated or is still in progress, PeekPanda keeps the prior evidence unchanged and explains the state. ## Related - [Reading a creative card](/docs/reading-a-creative-card) - [Saving creatives](/docs/saving-creatives) - [Organic content library](/docs/organic-content-library) - [Open a case file](/docs/open-a-case-file) --- # Reading a creative card What every part of a creative card means, in both the paid ads library and the organic content library. Both libraries render the same card. A paid ad and an organic post are translated into one shape first, so the layout never changes under you. What differs is what each half of the card is allowed to say. ![Paid-ad cards: identity, media, how long the ad has been observed, and the footer.](/docs-media/paid-ads.png?w=1728&h=1084) The card has four parts, top to bottom: the identity, the media, the facts, and the footer. ## The identity row The icon, the name, one line of context, and the bookmark button. **The name** is whatever most specifically identifies the creative. | Library | Name shown | | --- | --- | | Paid | The linked product's name, or the advertiser's name when the ad resolves to no product | | Organic | The product name, or the creator's channel name, or the account handle | Where no icon was collected, the card draws a coloured tile with the first letter instead. The colour is derived from the name, so the same product always gets the same tile. **The subline** is the one line of context under the name. | Library | Subline | Reading | | --- | --- | --- | | Paid | `Active · source platform` | Observed active, with any disclosed timing context | | Paid | `Ended · source platform` | No longer observed active, where an end date is disclosed | | Paid | `Source platform` | No timing information was disclosed | | Organic | `@account · source platform` | The account that posted it and the source context | A green dot before the subline means the ad's observed status is active. Organic posts never carry it. The bookmark button on the right saves the creative. [Saving creatives](/docs/saving-creatives) covers where saved items go. ## The media The creative itself, with up to three markers on it. - **The format chip**, top left: `Video`, `Slideshow`, `Carousel`, `Image`, `Dynamic` or `Unknown`. - **A position chip** such as `2/6` when the creative has more than one image. Arrows step through them, and the dot strip underneath prints at most five dots before counting the rest as `+n`. A video card shows a play glyph over its poster frame. Hovering or focusing the card plays the clip muted and looping. The video file is only fetched at that moment, and a browser set to reduce motion never starts it. ## The signal on a paid ad Meta discloses reach for almost none of the ads it archives, so a paid card does not print reach as a fact. It prints how long the ad has been paid for, which every ad has, and whether it is still running. - **The stripe** under the creative fills with run length and is full at 180 days. Green means the ad is live; grey means it was pulled. Scan a page and the longest-running ads stand out. - **The day count** is the big number: `122 days live`, or `20 days, then pulled` for an ad that ended. - **The status pill** at the right reads Active, Inactive or Unknown. - **A reach tag** such as `EU · 265.5K` appears beside the count only when Meta reported it. When nothing was reported, nothing is shown; the card never invents a number. The line under the advertiser name gives the platform and the **Started** date the platform disclosed. When there is none it reads **Seen**, the date the ad was first collected, and the day count reads `—` with *run length unknown*. ## The facts row on an organic post Up to four short facts: Views, Likes, Comments, Posted. **A fact that was never collected is dropped.** A post with no comment count shows three facts rather than a fourth reading zero. ## The footer - **See details →** opens the full record: the inspector for a paid ad, the preview and details panel for an organic post. - The link button opens the destination in a new tab. For a paid ad that is the product it points at, or its ad library entry when there is no product link. For an organic post it is the original post. - The download button saves the creative's media file: the video when there is one, otherwise the first image. The file is named after the creative. When the browser will not read the file directly, it opens in a new tab instead of failing quietly. ## Related - [Paid ads library](/docs/paid-ads-library) - [Organic content library](/docs/organic-content-library) - [Saving creatives](/docs/saving-creatives) - [Reading the evidence](/docs/reading-the-evidence) --- # Saving creatives Save ads and posts to your account, and use the Saved views to get back to them. Click the bookmark on any creative card to save it. The card fills in immediately. Click it again to remove it. ![The bookmark sits on the creative card, in the paid ads library and the organic library.](/docs-media/paid-ads.png?w=1728&h=1084) Saved items belong to your account. They follow you between sessions and devices, and nobody else sees them. ## Saving an organic post Two controls save the same post: - The bookmark on the card in the grid. - The heart at the top of the details panel when the post is open. ## Saving a paid ad - The bookmark on the card in the grid. - **Save** in the inspector when the ad is open. It reads **Remove** once the ad is saved. ## Getting back to what you saved The two libraries keep their own Saved view. There is no single combined list. | Library | How to get back | What you see | | --- | --- | --- | | Organic | The **Saved** pill in the header switch, or `/library?source=saved` | The organic feed narrowed to your saved posts | | Paid | The **♡ Saved** toggle in the paid filter bar | The paid list narrowed to your saved ads | Both are filters, not separate collections. Every other control still works inside them. ### The organic Saved view `/library?source=saved` is the organic library with one extra condition applied. Search, sort, content type, platform, category, creator, product, date range: all of it still narrows the results, and the saved condition survives every change you make. Switching back to **Organic** drops it. ### The paid Saved toggle The **Saved** toggle sits in the paid ads filter bar. Turn it on and the list keeps only your saved ads, and the count changes from `N ads` to `N saved ads`. Removing an ad while the toggle is on takes it out of the list straight away, which is what you want when you are pruning a shortlist. ## When a save does not go through Saving is optimistic: the card updates before the write is confirmed, so the list stays responsive while you work through a page. If the write fails, the card returns to its previous state. In the paid library a line also appears saying *Could not update the saved ad. Try again.* Saving something twice is harmless. The second save records the same bookmark rather than a duplicate. ## What removing does Removing a bookmark removes your bookmark. It does not delete the ad or the post, and it does not change anything for anyone else. The creative stays in the library and you can save it again. ## Related - [Paid ads library](/docs/paid-ads-library) - [Organic content library](/docs/organic-content-library) - [Reading a creative card](/docs/reading-a-creative-card) - [Plans and refreshes](/docs/plans-and-refreshes) --- # ASO workstation The ASO board holds the iOS apps you track and opens one case file per app, showing every collected search it appears in. The ASO workstation lives at `/aso`. It holds the iOS apps you track, yours and your rivals', and opens a case file for each one showing the App Store searches it appears in, how crowded each search is, and who sits ahead. Use folders to keep research grouped, or add a temporary app idea before it has an App Store listing. ![The ASO workstation for one tracked app, with collected searches, positions, and difficulty.](/docs-media/aso.png?w=1728&h=1084) Everything here is US App Store only. Positions are the positions observed in the store's public search results, not native App Store ranks. Home reads a bounded compact ASO evidence view for up to six tracked apps. API and MCP clients can use `get_aso_case_evidence` for the same stored summary, rivals, keyword position history, and difficulty; this read never starts collection. ## What the board shows With nothing tracked, the board is a single field: type an app's name and pick it. With apps tracked, it shows: - A **Track another app** card at the top. - **Your apps**: everything you added as *My app*. - **Competitors**: everything you added as *Competitor*. - A count in the header, `N of 25 apps`. Each card carries the app icon, its name, the developer, when it was last refreshed, and a keyword count. That count is the number of collected searches the app currently appears in. It is not the number of keywords you typed in. A card that has never finished a refresh reads **Collecting…** instead of a refresh time. ## Inside one app Open a card and you get the case file for that app. **Header.** The case-file tag says `Case file · my app` or `Case file · competitor`. Next to the title sit the refresh pill and **Stop tracking**. If the app also has a catalogue profile, a `Profile ↗` link appears beside the name. **Search presence.** Four counts over the keywords collected so far: | Tile | What it counts | | --- | --- | | Keywords tracked | Every collected keyword row, with how many of them you watch on every refresh | | Page one | Keywords where the app sits at position 1 to 10 | | Best position | The best collected position, and the keyword it belongs to | | Moved since last run | Keywords whose position changed between the last two collection runs | A tile with nothing behind it reads **Not collected** and shows an em dash, never a zero. *Moved since last run* stays empty until some keyword has two runs. **Keyword workspace.** A list on the left, ordered by Newest, Opportunity or Position. Each row shows the derived opportunity score, the keyword, `#position · N apps`, and a small line of its position history. Click a row to open that keyword's research in a popup without leaving the page. **Keyword table.** One row per keyword shows its position with how far it moved since the run before, its opportunity, popularity and difficulty, the apps in its ranking, and its last update. Add private notes and tags to keep decisions with the keyword. Click a column heading to sort it. **Suggestions.** Phrases the app does not rank for yet, drawn from the vocabulary of the titles ranking around it and from the store's own suggestions. Open Suggestions to review them, then add selected terms to collection. **Who it meets in search.** The apps that keep turning up on the same searches, with a one-click keyword gap against any of them. ## What you need to open it The workstation needs an active PeekPanda entitlement. Without one the page shows a resting panda and the reason. Without a signed-in session it asks you to sign in with your Genviral account. ## Limits - 25 tracked apps per account. - 100 tracked keywords per app. - Removing an app removes its tracked keywords too. ## Related - [Track your app](/docs/track-your-app) - [Keyword explorer](/docs/keyword-explorer) - [Refreshing keywords](/docs/refreshing-keywords) - [Keyword gap](/docs/keyword-gap) --- # Keyword explorer Explore an App Store phrase, its public search evidence, and PeekPanda’s directional keyword signals. At `/aso/explore`, enter an App Store phrase to review the public search evidence behind it. The same view opens from a keyword in a tracked app’s case file. ![Keyword explorer for “meal planner”, with popularity, difficulty, and the apps on the first page.](/docs-media/keyword-explorer.png?w=1728&h=1084) Keyword observations currently cover the US storefront in English. Positions are observed public search-result positions, not guaranteed native App Store ranks. ## The four signals | Signal | What it tells you | | --- | --- | | Opportunity | A directional shortlisting signal that weighs popularity against difficulty. | | Popularity | How much the phrase is searched, from 0 to 100. It is a relative reading, not search volume. | | Difficulty | How established the leading apps in the collected result set appear to be. | | Ranking apps | The apps returned in the collected public result set. | Each signal identifies whether it is observed, derived, or unavailable. An unavailable signal is not shown as zero. ## Opportunity Opportunity helps you prioritize phrases that are searched more and defended less. It is a consistent PeekPanda signal, not a ranking promise, conversion forecast, or statement that a phrase is right for your app. It is unavailable when its supporting evidence is unavailable, and it should be read with the oldest supporting observation date in mind. ## Popularity and difficulty Popularity reads Apple’s own weekly search popularity for the phrase where PeekPanda has collected it, and falls back to how prominently the store suggests the phrase when it has not. Difficulty summarizes the observed establishment of leading apps in the collected result set. Both are directional proxies, and each score names its own source. Use the underlying suggestions and result list to judge relevance before acting. ## The ranking Use the tabs to review leading results, the full collected result set, and related public suggestions. Each row identifies its observed position and available public listing context. A result links to a case file when PeekPanda has one for that app. ## Nothing collected yet If a phrase has not been collected, choose **Peek at this keyword** to request public evidence. The page reports whether the request is pending, complete, or unnecessary because current evidence is already available. ## Related - [Refreshing keywords](/docs/refreshing-keywords) - [Keyword metrics explained](/docs/keyword-metrics-explained) - [ASO workstation](/docs/aso-workstation) - [Keyword gap](/docs/keyword-gap) --- # Keyword gap Compare the searches your app ranks for against a rival's, split into theirs only, both, and yours only. Open a tracked app and look at **Who it meets in search**. Press **Compare** next to any rival and you get three columns: the searches only they rank for, the searches you both rank for, and the searches only you rank for. ## Who shows up as a rival Rivals are the apps that keep appearing on the same collected searches as this app. They are derived from stored snapshots, so they only appear after the first refresh has collected something. Before that the panel says so. Each row shows: - The rival's icon and name. - `N shared`: how many collected searches you both appear in. - `ahead on M`: how many of those shared searches they sit above you on. - `best #P`: their best collected position across the shared set, when there is one. The panel lists six rivals and offers **Show all** for the rest. Any tracked competitor of yours also appears in the list, even if it has no shared keywords yet. Next to **Compare**, each row carries **Track** if you do not track that app yet, or **Open** if you do. Track adds it as a *Competitor* on your board. ## Reading the comparison | Column | What is in it | | --- | --- | | Only *rival* | Searches the rival ranks for where your app is absent, with their position | | Both | Searches you both rank for, your position first, theirs second | | Only this app | Searches only your app ranks for, with your position | Every keyword in the three columns links to the [keyword explorer](/docs/keyword-explorer), so you can see the whole ranking behind a gap before you act on it. An empty column says so in plain words rather than showing zero rows: *Nothing they rank for that this app misses*, *No shared keywords*, *Nothing this app ranks for alone*. ## What the comparison is, and is not Both sides are built from stored search snapshots, not from a live query at the moment you press Compare. So: - A gap means the rival was in a collected search result where your app was not. It does not mean the store would rank it that way this second. - An empty side means those searches were not in the collected set for that app, not that the app ranks for nothing. - The two apps can have been collected at different times. Refreshing both before you compare makes the two sides describe the same period. Positions on both sides are observed store search positions, never native App Store ranks. ## Related - [ASO workstation](/docs/aso-workstation) - [Keyword explorer](/docs/keyword-explorer) - [Refreshing keywords](/docs/refreshing-keywords) - [Track your app](/docs/track-your-app) --- # Refreshing keywords What a keyword refresh does, how to read its state, and what happens when fresh data is not yet available. The refresh control asks PeekPanda to collect newer public keyword observations where they are needed. Most keyword pages read observations already stored, so a refresh can take time to complete. You can continue using the workspace while it runs. ![Refresh sits at the top of a tracked app in the ASO workstation.](/docs-media/aso.png?w=1728&h=1084) ## Refresh states | State | Meaning | | --- | --- | | Refresh | Some keyword evidence can be updated. | | In progress | PeekPanda has accepted the request and is collecting public observations. | | Recently refreshed | The available evidence is already recent, or a new request is temporarily unavailable. | | Everything is current | Nothing needs collecting right now. | The page updates when results land. If a request cannot be completed, PeekPanda keeps the last valid observation and explains when you can try again. ## What a refresh can collect For a tracked app, PeekPanda refreshes watched keywords and relevant public search observations already associated with that app. It can also surface related terms based on the app’s public listing, so you are not limited to phrases you thought of first. For an individual keyword, the refresh focuses on that phrase and the public evidence needed to explain it. Current observations are reused rather than needlessly collected again. ## What the result means After requesting a refresh, you may see a pending state, updated dates, or an explanation that no collection was needed. A completed refresh confirms that PeekPanda recorded a newer public observation; it does not guarantee that the store has changed or that a ranking will improve. If a value remains unavailable after a refresh, the public response did not provide enough evidence to show it responsibly. PeekPanda does not turn that into a zero or a guessed figure. ## Related - [Keyword explorer](/docs/keyword-explorer) - [ASO workstation](/docs/aso-workstation) - [Usage and limits](/docs/usage-and-limits) - [Plans and refreshes](/docs/plans-and-refreshes) --- # Track your app Add an iOS app to the ASO workspace, identify it as yours or a competitor’s, and watch relevant keywords. On `/aso`, search for an app by name and select the correct public App Store listing. PeekPanda begins gathering relevant public keyword evidence for the app. ![Tracked apps in the ASO workstation, split between your apps and competitors.](/docs-media/aso.png?w=1728&h=1084) ## Adding an app 1. Open `/aso`. 2. Type an app name and choose a match from the available public App Store results. 3. Mark it as **My app** or **Competitor** before selecting it. 4. Open the app’s case file to review its keyword workspace. The initial collection uses the public app title to identify relevant keyword evidence. It may take time for new observations to appear. If the listing is no longer available from the public store, PeekPanda explains that the app cannot be added. ## My app or competitor The role is an account label used to organize your ASO board and case files. Both roles receive the same public observations. You can change the label by removing the tracked app and adding it again with the preferred role. ## Watching keywords Watch a keyword from the keyword table, an idea, or the **Watch a keyword** control. You can add one phrase or a short list. Watched keywords are retained for your app’s future ASO refreshes, including phrases the app does not yet appear for. PeekPanda standardizes keyword input before saving it so duplicate spellings do not create separate watches. Unwatching a keyword removes it from your personal watch list; it does not erase historical public observations. If a request would exceed your workspace allowance, PeekPanda explains the limit and preserves the existing list. ## Removing an app Use **Stop tracking** in the app header and confirm the choice. The app’s watched-keyword setup is removed from your account and the app no longer appears on your board. Public catalogue observations are not presented as your private data and remain governed by their own retention and availability rules. ## Related - [ASO workstation](/docs/aso-workstation) - [Refreshing keywords](/docs/refreshing-keywords) - [Keyword explorer](/docs/keyword-explorer) - [Keyword gap](/docs/keyword-gap) --- # API keys Create a named API key, copy its secret once, and revoke keys you no longer trust. Manage API keys at the bottom of `/account`. Give a key a name, select **Create key**, and copy the secret immediately. The full secret is displayed once only. ![API keys at the bottom of Account. A secret is shown only at the moment you create it.](/docs-media/account.png?w=1728&h=1084) ## Creating a key 1. Open `/account` and scroll to **API keys**. 2. Enter a descriptive name. 3. Select **Create key** and copy the secret from the confirmation panel. Store the secret in a secure credential manager. If it is lost or exposed, revoke the key and create a replacement; PeekPanda cannot show the full secret again. Creating a key requires an active plan. Without one, the account page explains how to choose a plan. ## Your keys The key list shows its name, a safe identifier preview, creation date, latest use, and current status. Unused and revoked keys are clearly identified. ## Revoking a key Select **Delete**, then confirm the choice. The key stops working immediately. Its record remains visible as revoked so you can understand your account’s credential history. ## Using a key Keys authenticate requests made to PeekPanda’s public API. Treat a key like a password: do not place it in client-side code, share it in messages, or commit it to source control. See [authentication](/docs/authentication) and [API reference](/docs/api-reference) before integrating. ## Related - [Profile and plan](/docs/profile-and-plan) - [Usage and limits](/docs/usage-and-limits) - [Authentication](/docs/authentication) - [API reference](/docs/api-reference) --- # Billing Peek and Panda launch pricing, access sources, and where to manage a subscription. PeekPanda offers Peek and Panda plans. If checkout is not available, the product says so clearly rather than starting an incomplete purchase flow. ![The plan card on Account.](/docs-media/account.png?w=1728&h=1084) ## The launch plans | Plan | Monthly | Yearly | Requested refreshes per day | | --- | --- | --- | --- | | Peek | $59 | $590 | 1 | | Panda | $99 | $990 | 3 | Yearly billing includes the equivalent of two months free compared with monthly billing. These are launch prices and may change before general availability. The requested-refresh allowance applies to requested catalogue refreshes. Other product areas explain their own availability and refresh state where you use them. ## Access Your Account page identifies whether access is billed on PeekPanda, included with an eligible plan, or provided as preview access. An active entitlement gives access to the relevant product areas and API-key controls. ## Managing a subscription When subscription management is available for your account, **Manage billing** opens the secure billing portal. PeekPanda does not collect payment details directly in the product interface. If there is no subscription to manage, the control is not shown. Any checkout or billing issue is explained with a clear status message. ## Related - [Profile and plan](/docs/profile-and-plan) - [Usage and limits](/docs/usage-and-limits) - [Plans and refreshes](/docs/plans-and-refreshes) - [API keys](/docs/api-keys) --- # Profile and plan The account desk holds your name, your mark, your plan chip and where that plan came from. `/account` is the paperwork desk. It has four things on it: your profile, today's usage, your plan, and your API keys. This page covers the first and the third. ![Account, with the profile card and the plan chip.](/docs-media/account.png?w=1728&h=1084) The chip next to the page title is your plan: **Peek**, **Panda**, or **Free access** when no entitlement is on file. ## Profile Three rows, one card. | Row | What you can do | | --- | --- | | Name | Edit it. Press *Edit name*, type, save. 1 to 80 characters | | Email | Read only. It comes from your Genviral account | | Mark | Nothing. Your initials are taken from your name; PeekPanda does not store a photo | The name you save is the one the sidebar and the header use. If you have never set one, PeekPanda falls back to the part of your email before the `@`. Saving is a single request. If it fails the card shows the reason and keeps your typing. ## Plan Four rows, one card, with the subscription status as the section aside: **Active**, **Included**, **Preview** or **None**. | Row | What it says | | --- | --- | | Current | Peek, Panda, or Free access | | Access | Where the entitlement came from | | Period | The end of the current billing period, or *No billing period on file* | | Billing | *Manage billing* when a billing customer exists, plus a link to the plans | The **Access** row is the interesting one. There are three sources: - **Billed on PeekPanda.** You have an active PeekPanda subscription. - **Peek comes with your Genviral Business or Scale plan.** A qualifying Genviral team subscription carries Peek across. - **Admin preview.** Internal access during the preview. With no entitlement at all, the row reads *No Peek or Panda subscription is on file*, and the link offers to choose a plan. ## What you cannot change here - Your email. It belongs to the Genviral account behind the session. - Your plan name or its allowance. Those follow the entitlement, not a setting. - Your avatar. Initials are the mark. **Manage billing** only appears once a billing customer is on file for your account. It opens the provider's own portal in place. Until then, the row shows only the link to the plans. ## Related - [Usage and limits](/docs/usage-and-limits) - [API keys](/docs/api-keys) - [Billing](/docs/billing) --- # Usage and limits What the Account usage area shows, what your plan covers, and how PeekPanda handles a limit. The usage area on `/account` shows the activity and access information relevant to your PeekPanda account. ![Usage on Account: refreshes today, the daily allotment, and active keys.](/docs-media/account.png?w=1728&h=1084) | Item | What it means | | --- | --- | | Refreshes today | Requested catalogue refreshes used during the current allowance period. | | Daily allotment | The requested-refresh allowance included with your active plan. | | Active keys | API keys that can currently make requests. | | Last key use | The most recent recorded use of an active API key. | Without an active plan, plan-based usage is shown as unavailable rather than zero. A key that has never been used is identified as such. ## What plan usage covers The plan allotment applies to requested catalogue refreshes. It does not turn ordinary browsing into a billable action: case files, charts, and stored observations remain readable without a refresh request. Keyword and ASO refreshes have their own operational safeguards so public-source collection remains fair and reliable. Their status is shown where you request the refresh. See [refreshing keywords](/docs/refreshing-keywords). ## When you reach a limit PeekPanda explains the relevant limit and, where applicable, when you can try again. It does not partially apply a request in a way that could make the result misleading. Existing valid observations remain available with their dates. API-key creation requires the access included with an active entitlement. Manage keys from [API keys](/docs/api-keys). ## Reading usage honestly Usage counts describe recorded account activity. A plan allowance describes what your active plan includes. When there is no result to show, PeekPanda uses an explicit unavailable state rather than presenting an empty value as zero. ## Related - [Refreshing keywords](/docs/refreshing-keywords) - [Profile and plan](/docs/profile-and-plan) - [API keys](/docs/api-keys) - [Plans and refreshes](/docs/plans-and-refreshes) --- # Freshness and source dates How to read observation dates, source dates, stale data, and a requested refresh. Every fact PeekPanda shows has a date. The date tells you when the public information was observed; it is not a claim that the fact is still true right now. PeekPanda normally serves stored observations so pages can load without waiting on a public source. Read the date with the result, especially when you are comparing ranks, estimates, or keyword signals. ## Observation dates and source dates An **observed at** date is when PeekPanda recorded public information. A **source updated** date, where a source provides one, is when that source says its information changed. A source can be updated before PeekPanda records it, so the two dates may differ. Some facts are point-in-time observations, such as a chart position or a search result. Others describe the latest public listing information available to PeekPanda. The case file labels the date so you can tell which you are reading. ## History and comparisons When PeekPanda has more than one observation, it may show a change from the preceding available observation. That is not always a day-to-day comparison: the interval can differ if there was no comparable earlier observation. The first available observation has no prior comparison. PeekPanda shows the value and its date rather than inventing a zero change. ## What stale means Stale means the observation is older than PeekPanda’s normal freshness expectation for that type of data, or has not been collected yet. It does not mean the observation is false. It remains evidence of the date on which it was recorded. Different data types age at different rates. Rankings, public listing information, and keyword signals each have their own freshness expectations because they change differently. A never-collected value is clearly distinguished from an older one. ## What a refresh changes A refresh asks PeekPanda to collect a newer public observation when one is appropriate. It does not rewrite the past or guarantee that a source has changed. The prior observation remains the historical context for later comparisons. Some refreshes complete asynchronously. In that case, PeekPanda confirms that the request is pending and updates the page when newer evidence is available. If collection is temporarily unavailable, the last valid observation stays visible with its date. ## Reading dates well 1. Compare facts observed in comparable periods. 2. Check the prior-observation date before interpreting a change. 3. Treat a derived value as only as current as the public observations supporting it. 4. Use an unavailable value as an honest gap in evidence, not a zero. ## Related - [How estimates are computed](/docs/how-estimates-are-computed) - [Where the data comes from](/docs/where-the-data-comes-from) - [Plans and refreshes](/docs/plans-and-refreshes) - [Honest unknowns](/docs/honest-unknowns) --- # Honest unknowns What PeekPanda does not know about an app, and why unavailable evidence is more useful than a confident guess. PeekPanda works from public information. When the available evidence cannot support a responsible value, PeekPanda shows it as unavailable rather than guessing. > Unknown and zero are different facts. A missing value means PeekPanda does not have suitable evidence; it does not mean the underlying result is zero. ## Advertising spend and performance Public advertising libraries can show creative, copy, destination information, source dates, and—in some cases—source-reported reach. They do not provide an advertiser’s budget, spend, return on ad spend, or complete delivery performance. PeekPanda does not model those missing figures. A reported reach figure, where available, remains a source-reported disclosure rather than a PeekPanda measurement. ## What happens inside an app PeekPanda does not observe private product activity, including onboarding, paywalls, conversion, retention, experiments, subscription sales, or user behavior. It does not connect to an app, install an SDK, or access a developer’s analytics. ## Measured downloads and reported revenue PeekPanda’s download and revenue figures are estimates based on public App Store signals. They are not measured downloads, developer-reported revenue, or audited financial data. An unavailable estimate does not mean an app has no downloads or revenue. It means the public observations do not support a responsible estimate for that app, storefront, or period. See [how estimates are computed](/docs/how-estimates-are-computed). ## Native keyword rank and search volume Keyword positions are observed public search-result positions, not a guarantee of the rank a particular person sees in the App Store. An empty keyword section means the app was not found in the collected query set; it does not prove the app ranks for nothing. PeekPanda does not claim App Store search volume. Its keyword signals are directional aids based on public search observations and suggestions. See [keyword metrics explained](/docs/keyword-metrics-explained). ## Coverage boundaries The catalogue is iOS-only. Coverage also varies by information type and storefront, so always read the observation date and available source context before comparing apps. Organic social matches are suggestions based on public evidence. A name match does not prove that an account belongs to the company behind an app. ## Requesting more evidence Where a case-file section lacks evidence, **Request data** records the kind of information you want to see. It is not a promise that the information exists or will become available. Until PeekPanda has collected suitable public evidence, the section remains explicitly unavailable. ## Related - [How estimates are computed](/docs/how-estimates-are-computed) - [Where the data comes from](/docs/where-the-data-comes-from) - [Keyword metrics explained](/docs/keyword-metrics-explained) - [Freshness and source dates](/docs/freshness-and-source-dates) --- # App revenue and download estimates What PeekPanda’s app revenue and download estimates mean, which public signals inform them, and when an estimate is unavailable. As of 2026-09-15, PeekPanda provides directional app revenue and download estimates from public App Store signals. They are not company reporting. Apple says developers can view their own sales and usage data in [App Store Connect Analytics](https://developer.apple.com/app-store-connect/analytics/). PeekPanda never presents an estimate as a developer’s measured result. Use these estimates to compare similar apps and investigate movement. They are not financial statements, profit figures, valuations, or a substitute for first-party reporting. ## What PeekPanda estimates PeekPanda may show a monthly download range and a monthly revenue range. Each range has a low, middle, and high value, an evidence group, a sample size, and the date of the observation. A rating-velocity reading may also appear as a trend signal. It is not used as a download estimate. The model is trained on independent third-party labels. Those labels describe worldwide, last-month results and are rounded to broad values. A result shown for a storefront is therefore a public-signal estimate anchored by that evidence, not an audited country total. ## How downloads are estimated Downloads use the app’s lifetime Apple rating count. PeekPanda fits a separate relationship for a storefront and, where the evidence supports it, the app’s App Store genre. The relationship is a power law: apps with more lifetime ratings generally have more monthly downloads, but the relationship varies widely between apps. The rating tape is still collected. Its day-to-day movement is shown as a trend signal when there are enough dated observations; it no longer multiplies into the download estimate. There is no universal “ratings × constant” rule. ## How revenue is estimated Revenue uses a two-part rule. When the app has a current US grossing-chart rank, the rank supplies the strongest public revenue signal. When it has no grossing rank, PeekPanda uses the download estimate and a typical revenue-per-download value for the app’s monetization type. Monetization is read from listing facts: a paid app is treated as one-time, an app with an in-app purchase is treated as subscription, and a free app without an in-app purchase is treated as none. Apple does not reveal whether every in-app purchase recurs, so this is a practical classification rather than a billing record. An unranked app classified as none has no observable revenue signal and its revenue is left unavailable. Its downloads can still be estimated. This avoids presenting $0 for apps that earn through marketplaces, advertising, or billing outside Apple. ## Why every result is a range The range is widened by the model group’s measured multiplicative error on held-out labelled apps. PeekPanda applies a minimum width because the training labels themselves are rounded; a narrower band would suggest precision the source does not contain. The sample size tells you how many labelled apps support the group. A zero sample size means the storefront adjustment is unmeasured against local labels. An accepted genre gets its own group only when it has enough labelled apps and performs better on held-out apps than the all-genre fallback. Otherwise the app uses the storefront’s all-genre group. If no usable group exists, PeekPanda leaves the estimate unavailable. The practical accuracy claim is typically within about 2–3×. That is a guide to the expected scale of error, not a promise for each app. ## When an estimate is unavailable An unavailable estimate appears as `null`, a dash, or a clear “not available” state. It does **not** mean zero. Common reasons are: - the listing has no usable lifetime rating count; - no grossing rank and no supported monetization signal exist for revenue; - no usable model parameters cover the storefront and genre; or - the relevant observation is missing or too old to support a responsible reading. PeekPanda does not fill these gaps with a plausible-looking fallback. ## How to read changes Compare the observation date, storefront, and category with the value. A previous value is the preceding available observation, not a market baseline. A change can reflect a change in ratings, chart presence, pricing, audience, or the timing and coverage of collection. Public ratings are not downloads, and chart position is not revenue. Use the ranges for directional comparison and trend discovery. For decisions with material financial consequences, verify the finding with first-party data or the developer’s own reporting. ## Frequently asked questions ### Are PeekPanda’s download estimates actual downloads? No. They are model estimates based on public lifetime rating counts and independent rounded labels. Developers’ measured download data lives in their own analytics tools. ### Are app revenue estimates the same as reported revenue? No. They are directional ranges based on public chart and listing signals. They do not reveal revenue reported by an app developer. ### Why does an app have no revenue or download estimate? PeekPanda only serves a value when the available evidence supports one. Missing means insufficient usable evidence, not zero. ### Can I compare estimates between apps? Yes, with care. Comparisons are strongest when apps share a storefront, genre, and observation period. Treat a large difference as a reason to investigate, not proof of an exact business result. ### How current are the estimates? Each observation carries its own date. An older estimate remains evidence of its recorded moment and may no longer describe today. ## Related - [Where the data comes from](/docs/where-the-data-comes-from) - [Freshness and source dates](/docs/freshness-and-source-dates) - [Honest unknowns](/docs/honest-unknowns) - [Reading the evidence](/docs/reading-the-evidence) --- # Keyword metrics explained What Popularity, Difficulty, Opportunity, and Ranking apps mean in PeekPanda’s ASO surfaces. PeekPanda reads a keyword with four signals: Popularity, Difficulty, Ranking apps, and Opportunity. The app case file and the ASO workstation both use these words for the same numbers. They help you shortlist phrases to investigate; they are not App Store search volume, paid-advertising data, or a promise of ranking results. All current keyword observations are for the US storefront in English. ## Popularity Popularity is how much a phrase is searched, from 0 to 100. It has two possible readings, and every score says which one produced it in its tooltip. Where PeekPanda has collected Apple’s own weekly search popularity for the phrase, that is the reading. It is Apple’s relative score for that search term in the US App Store over one completed Sunday to Saturday week, and the score carries that week with it. It is relative popularity, never a count of searches. Where no such observation exists, PeekPanda falls back to a weaker reading: how prominently the store suggests the phrase and closely related phrases when people type. That is Apple’s autocomplete ordering, not a measurement of search activity. Popularity is unavailable when neither reading has been collected. An unavailable value is shown as a dash, never as a zero. Those are different states. ## Difficulty Difficulty describes how established the leading apps in a collected search result appear to be, from 0 to 100. It is the median rating count of the apps on page one of the observed result set, on a log scale where a million ratings reads as 100. A higher number generally means more established competitors are present. Below three rated apps on page one, PeekPanda says the evidence is weak. It does not measure relevance, conversion, product quality, or the effort your own app will need. Older apps can remain highly rated even when their current momentum is limited. ## Opportunity Opportunity weighs the two against each other: `popularity × (100 − difficulty) ÷ 100`. A phrase people search a lot but nobody owns scores near its Popularity; the same phrase behind a page one of million-rating apps scores near zero. It is a directional shortlisting aid, most useful for comparing terms from the same observation period, then opening the underlying results to check relevance and the apps actually present. Opportunity is derived, never collected, so it is only as fresh as the older of its two inputs. It is unavailable when either input is unavailable. It does not tell you whether a keyword will convert, whether your app is a fit, or whether the resulting traffic is valuable. ## Ranking apps Ranking apps is the number of apps returned in the collected public search result. It is a count of that observation, not a count of every app competing for the phrase. The store may limit what a single response reveals. ## Position Position is where an app sat in the collected search result, with how far it moved since the run before it. A keyword with only one collection run shows the position alone, because there is nothing yet to compare it against. Positions are always observed search-result positions. They are never presented as a guaranteed native App Store rank. ## Reading a keyword well - Check the observation date before comparing phrases. - Read the visible result set, not only the score. - Check which reading produced a Popularity score before leaning on it. - Use an unavailable value as a reason to collect better evidence, not as a zero. - Validate a promising phrase against your product, listing, and customer language. ## Related - [The keyword explorer](/docs/keyword-explorer) - [Refreshing keywords](/docs/refreshing-keywords) - [Freshness and source dates](/docs/freshness-and-source-dates) - [Honest unknowns](/docs/honest-unknowns) --- # Where the data comes from The public sources PeekPanda observes, what each contributes, and what PeekPanda does not collect. PeekPanda observes public information. It does not receive an app’s private analytics, customer data, installed-device data, or a company’s internal reporting. Everything in the catalogue is iOS-focused. PeekPanda does not offer Android coverage or cross-platform totals. ## Public App Store information PeekPanda records publicly available listing and chart information, such as an app’s name, developer, description, release information, price, category, public ratings, and publicly available media links. It also observes public rankings and search results where available. Case files and charts normally display stored observations rather than fetching a source while you wait. Each observation carries a date so you can judge its currency. Some user-requested actions may start a refresh; those actions return a pending state while the new observation is collected. Public ratings can inform download estimates, and public grossing-chart signals can inform revenue estimates. Neither is private reporting. See [how estimates are computed](/docs/how-estimates-are-computed). ## Keyword data The ASO workspace uses public App Store search results and suggestions for the US storefront in English. A search position is the position observed in that public response; it is not a native rank guaranteed by the store, search volume, or an advertising-platform popularity score. Keyword pages show stored observations and their dates. A refresh may collect newer results when appropriate. Search observations are shared: a query collected once can improve the evidence available to later readers without revealing who requested it. An empty keyword section means the app was not found in the collected query set. It does not prove that the app ranks for no keywords. ## Public advertising libraries PeekPanda may show public advertising-library observations, including creative text and media, destination links, source dates, and the original public record where available. A destination match is presented as evidence, not proof of company ownership. Spend, budget, and performance are not inferred when they are not publicly disclosed. If a reported reach figure is available, it remains a source-reported figure—not a PeekPanda measurement. ## How apps enter the catalogue Apps can enter through established catalogue coverage, emerging-app discovery, or a direct request. These are separate lanes: an emerging or requested app is not judged by the same admission rule as a long-established listing. PeekPanda documents the lane so you know why an app is present. A listing’s presence is not an endorsement, a quality score, or proof that it is currently growing. ## What tracking your app changes Tracking an app tells PeekPanda which public queries and listing to watch for your account. It does not connect to the app, install software, access private analytics, or collect information about its users. Your tracked-app list and watched keywords remain account-specific. ## What is not a source - No private developer analytics or revenue reporting - No device panel, SDK, or installed-app telemetry - No invented data for a missing public observation - No claim that a name match verifies social-account ownership ## Related - [How estimates are computed](/docs/how-estimates-are-computed) - [Keyword metrics explained](/docs/keyword-metrics-explained) - [Freshness and source dates](/docs/freshness-and-source-dates) - [Honest unknowns](/docs/honest-unknowns) --- # Get started Connect an assistant to PeekPanda so it can ask about competitors, creatives, keywords, and charts. You need a PeekPanda account. ## Before you connect Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. The contract audience is https://mcp.peekpanda.com/mcp. ## Choose how to connect Choose one method. Both use the remote MCP server. OAuth is the path for clients that can sign you in. An API key is the path when the client asks for a bearer token. ## 1. Connect with Claude Code Add the remote server, open Claude Code, run /mcp, and complete sign-in. Then ask it to search for iOS calorie-tracking competitors. ``` claude mcp add --transport http peekpanda https://mcp.peekpanda.com/mcp claude /mcp ``` ## 2. Connect with Codex Add the remote server, sign in, then launch Codex and ask: “Use PeekPanda to search for iOS calorie-tracking competitors.” ``` codex mcp add peekpanda --url https://mcp.peekpanda.com/mcp codex mcp login peekpanda codex ``` ## 3. Connect with Cursor Add the server URL in Cursor’s MCP settings and sign in when Cursor asks. For a bearer token, create an API key and pass it from the environment. Do not paste the key into chat or source control. ``` { "mcpServers": { "peekpanda": { "url": "https://mcp.peekpanda.com/mcp", "headers": { "Authorization": "Bearer ${env:PEEKPANDA_TOKEN}" } } } } ``` ## Try it The first successful request should call search_apps with action search. Confirm the app and the date range on the result before you treat it as current. Your plan and scopes still apply. ``` { "name": "search_apps", "arguments": { "request": { "action": "search", "query": { "platform": "ios", "search": "calorie", "limit": 24 } } } } ``` ## Safety Start with context:read. A bearer token can use every permission you gave it until you revoke the key. Revoke keys from Account → API keys. MCP cannot change billing or delete your account. ## Next steps Read the tool list, then the OAuth notes if the client signs in for you. [Tool reference](/docs/mcp-tools) --- # Tool reference Every MCP call names a tool and passes request.action. That action is one HTTP route, not a second catalogue. ## Tools Reads use context:read. Save, remove, and data-request commands use content:write. The server checks the credential, the scope, and the owner before it runs the action. | Tool | Action | HTTP route | Scope | | --- | --- | --- | --- | | `search_apps` | `search` | `GET /v1/competitors` | context:read | | `search_apps` | `categories` | `GET /v1/competitors/categories` | context:read | | `search_apps` | `similar` | `GET /v1/competitors/{platform}/{storeId}/similar` | context:read | | `search_apps` | `emerging` | `GET /v1/competitors/intake/emerging` | context:read | | `get_app` | `case_file` | `GET /v1/competitors/{platform}/{storeId}/case-file` | context:read | | `get_app` | `get` | `GET /v1/competitors/{platform}/{storeId}` | context:read | | `get_app` | `aso` | `GET /v1/competitors/{platform}/{storeId}/aso` | context:read | | `get_app` | `keywords` | `GET /v1/competitors/{platform}/{storeId}/aso/keywords` | context:read | | `get_app` | `aso_status` | `GET /v1/competitors/{platform}/{storeId}/aso/collection` | context:read | | `get_app` | `organic` | `GET /v1/competitors/{platform}/{storeId}/organic` | context:read | | `get_app` | `estimates` | `GET /v1/competitors/{platform}/{storeId}/estimates/history` | context:read | | `get_app` | `technology` | `GET /v1/competitors/{platform}/{storeId}/technology` | context:read | | `get_app` | `media` | `GET /v1/competitors/{platform}/{storeId}/media` | context:read | | `get_app` | `data_requests` | `GET /v1/competitors/{platform}/{storeId}/data-requests` | context:read | | `get_app` | `chart_history` | `GET /v1/charts/apps/{storeId}/history` | context:read | | `get_ads` | `search` | `GET /v1/paid-ads` | context:read | | `get_ads` | `get` | `GET /v1/paid-ads/{platform}/{archiveId}` | context:read | | `get_ads` | `intelligence` | `GET /v1/paid-ads/intelligence` | context:read | | `get_ads` | `rails` | `GET /v1/paid-ads/rails` | context:read | | `search_organic` | `search` | `GET /v1/organic` | context:read | | `search_organic` | `get` | `GET /v1/organic/{postId}` | context:read | | `search_organic` | `saved` | `GET /v1/organic/saved` | context:read | | `aso` | `list` | `GET /v1/aso/apps` | context:read | | `aso` | `evidence` | `GET /v1/aso/case-evidence` | context:read | | `aso` | `get` | `GET /v1/aso/apps/{appId}` | context:read | | `aso` | `discover` | `GET /v1/aso/apps/{appId}/keyword-discovery` | context:read | | `aso` | `gap` | `GET /v1/aso/apps/{appId}/gap/{rivalStoreId}` | context:read | | `aso` | `explore` | `GET /v1/aso/keywords/{query}` | context:read | | `get_market` | `overview` | `GET /v1/market/overview` | context:read | | `get_market` | `leaderboard` | `GET /v1/market/leaderboard` | context:read | | `get_market` | `signals` | `GET /v1/market/signals` | context:read | | `get_market` | `charts` | `GET /v1/charts` | context:read | | `save` | `organic` | `POST /v1/organic/{postId}/saved` | content:write | | `save` | `unsave_organic` | `DELETE /v1/organic/{postId}/saved` | content:write | | `save` | `ad` | `POST /v1/paid-ads/{platform}/{archiveId}/saved` | content:write | | `save` | `unsave_ad` | `DELETE /v1/paid-ads/{platform}/{archiveId}/saved` | content:write | | `refresh` | `aso_collection` | `POST /v1/competitors/{platform}/{storeId}/aso/collection` | content:write | | `refresh` | `estimates` | `POST /v1/competitors/{platform}/{storeId}/estimates/refresh` | content:write | | `refresh` | `technology` | `POST /v1/competitors/{platform}/{storeId}/technology` | content:write | | `refresh` | `aso_app` | `POST /v1/aso/apps/{appId}/refresh` | content:write | | `refresh` | `aso_keyword` | `POST /v1/aso/keywords/{query}/refresh` | content:write | | `refresh` | `ad_transcript` | `POST /v1/paid-ads/{platform}/{archiveId}/transcript` | content:write | | `refresh` | `ad_analysis` | `POST /v1/paid-ads/{platform}/{archiveId}/analysis` | content:write | | `request_data` | `request` | `POST /v1/competitors/{platform}/{storeId}/data-requests` | content:write | | `request_data` | `intake` | `POST /v1/competitors/intake/requested` | content:write | | `request_data` | `discover_emerging` | `POST /v1/competitors/intake/emerging-discovery` | content:write | | `manage_aso` | `track` | `POST /v1/aso/apps` | content:write | | `manage_aso` | `add_temporary` | `POST /v1/aso/apps/temporary` | content:write | | `manage_aso` | `update` | `PATCH /v1/aso/apps/{appId}` | content:write | | `manage_aso` | `untrack` | `DELETE /v1/aso/apps/{appId}` | content:write | | `manage_aso` | `create_folder` | `POST /v1/aso/folders` | content:write | | `manage_aso` | `update_folder` | `PATCH /v1/aso/folders/{folderId}` | content:write | | `manage_aso` | `remove_folder` | `DELETE /v1/aso/folders/{folderId}` | content:write | | `manage_aso` | `add_keywords` | `POST /v1/aso/apps/{appId}/keywords` | content:write | | `manage_aso` | `update_keyword` | `PATCH /v1/aso/apps/{appId}/keywords/{keywordId}` | content:write | | `manage_aso` | `remove_keyword` | `DELETE /v1/aso/apps/{appId}/keywords/{keywordId}` | content:write | | `manage_aso` | `assign_tag` | `POST /v1/aso/apps/{appId}/keywords/tags` | content:write | | `manage_aso` | `create_tag` | `POST /v1/aso/tags` | content:write | | `manage_aso` | `update_tag` | `PATCH /v1/aso/tags/{tagId}` | content:write | | `manage_aso` | `remove_tag` | `DELETE /v1/aso/tags/{tagId}` | content:write | | `manage_aso` | `search_ios` | `POST /v1/aso/app-search` | content:write | --- # OAuth reference OAuth lets a client sign in to PeekPanda and receive an access token for the MCP resource. The token is not your dashboard session. ## Resource The protected resource is https://mcp.peekpanda.com/mcp. Tokens are audience-pinned to that resource. A token issued for another product cannot call PeekPanda. Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Sign in and approve access The client opens the shared issuer. You sign in with your PeekPanda account, review the requested scopes, and allow access. The client stores the access token and refreshes it. Your password is not shared with the client. ## Scopes context:read covers catalogue, creative, keyword, chart, and market reads. content:write covers save, remove, and data-request commands. Missing scopes fail before the operation runs. [Scopes and tenancy](/docs/scopes-and-tenancy) ## What the token cannot do An MCP token cannot delete your account, change billing, or stand in for a dashboard session. A website or app id in a tool request is a target to check, not proof of access. ## API keys Clients that cannot complete OAuth use an API key as a bearer token on the same MCP URL. Create, copy, and revoke keys from Account. The secret is shown once. [API keys](/docs/api-keys) --- # Get started Read stored iOS competitor evidence from your backend, a script, or an AI agent. ## Before you connect Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Which surface to use HTTP and MCP read the same catalogue. Pick the surface that matches the client. | Surface | Credential | Use it for | | --- | --- | --- | | REST API | API key | Read analytics-style catalogue data, save items, and send commands from your backend | | MCP | OAuth or the same API key | Ask an assistant about competitors, creatives, keywords, and charts | ## Base URL Every v1 route hangs off this origin. Send the tenancy selector with the credential. ``` https://api.peekpanda.com/v1/ ``` ## Authenticate requests Send a PeekPanda API key as a bearer token. Create one under Account → API keys. It is shown once. Do not put it in frontend code or a public repository. [Create an API key](/docs/api-keys) ``` Authorization: Bearer YOUR_API_KEY ``` ## Try a read Search stored iOS calorie-tracking competitors. Responses are paginated with an opaque next cursor. Keep the original filters when you follow it. ``` curl --get 'https://api.peekpanda.com/v1/competitors' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --data-urlencode 'platform=ios' \ --data-urlencode 'search=calorie' \ --data-urlencode 'limit=24' ``` ## Errors Failed requests return an HTTP status and a safe error body. Invalid input is 400, a missing credential is 401, a missing subscription is 402, a missing scope is 403, a missing record is 404, a conflict is 409, and a throttle is 429. Honor Retry-After when it is present. [Errors and retries](/docs/errors-and-retries) ## Next steps Connect an assistant, or open the endpoint index and follow one operation. [Get started with MCP](/docs/mcp) --- # Authentication Use a PeekPanda API credential or the shared OAuth issuer without exposing internal secrets. ## Bearer credentials Send a PeekPanda API credential in the Authorization header. Credentials are owner scoped and the backend resolves their entitlement; a browser supplied actor ID or tenancy header cannot grant access. ## OAuth and MCP MCP clients connect to the remote server and complete consent with the shared issuer for the PeekPanda resource. Tokens are audience pinned. Never put an internal actor signing secret in an MCP configuration. ## Rollout status The public hostname, API key provisioning and production OAuth consent flow are rollout work. The local/admin preview is not a claim that these addresses are live. --- # Scopes and tenancy Understand permissions and the owner scope attached to each request. ## Scopes Reads use context:read. Save, remove and data-request commands use content:write. The server rejects missing scopes before the domain operation runs. ## Tenancy selector Use x-genviral-tenancy: personal or an authorized workspace UUID. This selects the owner context; it does not create membership or turn public catalogue evidence into private customer data. ## Saved items Bookmarks belong to the canonical authenticated user and are shared with Genviral. They are private account data and are never copied into the global competitor catalogue. --- # All endpoints The initial versioned HTTP surface, generated from the shared endpoint contract. ## Competitors GET /competitors, /competitors/categories, /competitors/{platform}/{storeId}, /similar, /organic and /aso read stored iOS catalogue evidence. GET /charts reads the latest bounded Apple chart snapshot, and GET /charts/apps/{storeId}/history reads that app’s dated rank history. POST /data-requests records deduplicated demand; it never starts collection. ## Creative library GET /organic and /organic/{postId} search the copied public organic corpus. GET /paid-ads searches stored paid-ad observations. Saved organic and paid-ad commands use POST or DELETE on their /saved paths. ## Market GET /market/overview reads one storefront as a whole: the market pulse, the advertiser, viral and search-owner boards, the hottest and widest-open searches, and typed findings. GET /market/signals annotates up to 600 store IDs with chart, paid-ad, organic and keyword signals. A stream that was never collected for an app is null, not zero. ## Contract source Each row in the endpoint table links to the request curl, JSON response and source for that operation. The shared public contract is the source of truth. This reference does not invent fields for spend, onboarding or native search volume. ## Endpoints | Method | Path | Scope | MCP job | | --- | --- | --- | --- | | GET | `/v1/competitors` | `context:read` | `search_apps search` | | GET | `/v1/competitors/categories` | `context:read` | `search_apps categories` | | GET | `/v1/competitors/{platform}/{storeId}` | `context:read` | `get_app get` | | GET | `/v1/competitors/{platform}/{storeId}/similar` | `context:read` | `search_apps similar` | | GET | `/v1/competitors/{platform}/{storeId}/organic` | `context:read` | `get_app organic` | | GET | `/v1/competitors/{platform}/{storeId}/case-file` | `context:read` | `get_app case_file` | | GET | `/v1/competitors/{platform}/{storeId}/aso` | `context:read` | `get_app aso` | | GET | `/v1/competitors/{platform}/{storeId}/aso/keywords` | `context:read` | `get_app keywords` | | GET | `/v1/competitors/{platform}/{storeId}/aso/collection` | `context:read` | `get_app aso_status` | | POST | `/v1/competitors/{platform}/{storeId}/aso/collection` | `content:write` | `refresh aso_collection` | | GET | `/v1/competitors/{platform}/{storeId}/data-requests` | `context:read` | `get_app data_requests` | | POST | `/v1/competitors/{platform}/{storeId}/data-requests` | `content:write` | `request_data request` | | GET | `/v1/organic` | `context:read` | `search_organic search` | | GET | `/v1/organic/{postId}` | `context:read` | `search_organic get` | | GET | `/v1/organic/saved` | `context:read` | `search_organic saved` | | POST | `/v1/organic/{postId}/saved` | `content:write` | `save organic` | | DELETE | `/v1/organic/{postId}/saved` | `content:write` | `save unsave_organic` | | GET | `/v1/paid-ads` | `context:read` | `get_ads search` | | GET | `/v1/paid-ads/intelligence` | `context:read` | `get_ads intelligence` | | GET | `/v1/paid-ads/rails` | `context:read` | `get_ads rails` | | GET | `/v1/paid-ads/{platform}/{archiveId}` | `context:read` | `get_ads get` | | POST | `/v1/paid-ads/{platform}/{archiveId}/transcript` | `content:write` | `refresh ad_transcript` | | POST | `/v1/paid-ads/{platform}/{archiveId}/analysis` | `content:write` | `refresh ad_analysis` | | POST | `/v1/paid-ads/{platform}/{archiveId}/saved` | `content:write` | `save ad` | | DELETE | `/v1/paid-ads/{platform}/{archiveId}/saved` | `content:write` | `save unsave_ad` | | GET | `/v1/aso/apps` | `context:read` | `aso list` | | GET | `/v1/aso/case-evidence` | `context:read` | `aso evidence` | | POST | `/v1/aso/folders` | `content:write` | `manage_aso create_folder` | | PATCH | `/v1/aso/folders/{folderId}` | `content:write` | `manage_aso update_folder` | | DELETE | `/v1/aso/folders/{folderId}` | `content:write` | `manage_aso remove_folder` | | POST | `/v1/aso/apps` | `content:write` | `manage_aso track` | | POST | `/v1/aso/apps/temporary` | `content:write` | `manage_aso add_temporary` | | GET | `/v1/aso/apps/{appId}` | `context:read` | `aso get` | | GET | `/v1/aso/apps/{appId}/keyword-discovery` | `context:read` | `aso discover` | | PATCH | `/v1/aso/apps/{appId}` | `content:write` | `manage_aso update` | | DELETE | `/v1/aso/apps/{appId}` | `content:write` | `manage_aso untrack` | | POST | `/v1/aso/apps/{appId}/keywords` | `content:write` | `manage_aso add_keywords` | | POST | `/v1/aso/tags` | `content:write` | `manage_aso create_tag` | | PATCH | `/v1/aso/tags/{tagId}` | `content:write` | `manage_aso update_tag` | | DELETE | `/v1/aso/tags/{tagId}` | `content:write` | `manage_aso remove_tag` | | PATCH | `/v1/aso/apps/{appId}/keywords/{keywordId}` | `content:write` | `manage_aso update_keyword` | | POST | `/v1/aso/apps/{appId}/keywords/tags` | `content:write` | `manage_aso assign_tag` | | DELETE | `/v1/aso/apps/{appId}/keywords/{keywordId}` | `content:write` | `manage_aso remove_keyword` | | POST | `/v1/aso/apps/{appId}/refresh` | `content:write` | `refresh aso_app` | | GET | `/v1/aso/apps/{appId}/gap/{rivalStoreId}` | `context:read` | `aso gap` | | POST | `/v1/aso/app-search` | `content:write` | `manage_aso search_ios` | | GET | `/v1/aso/keywords/{query}` | `context:read` | `aso explore` | | POST | `/v1/aso/keywords/{query}/refresh` | `content:write` | `refresh aso_keyword` | | POST | `/v1/competitors/intake/requested` | `content:write` | `request_data intake` | | POST | `/v1/competitors/intake/emerging-discovery` | `content:write` | `request_data discover_emerging` | | GET | `/v1/competitors/{platform}/{storeId}/estimates/history` | `context:read` | `get_app estimates` | | POST | `/v1/competitors/{platform}/{storeId}/estimates/refresh` | `content:write` | `refresh estimates` | | GET | `/v1/competitors/{platform}/{storeId}/technology` | `context:read` | `get_app technology` | | POST | `/v1/competitors/{platform}/{storeId}/technology` | `content:write` | `refresh technology` | | GET | `/v1/competitors/{platform}/{storeId}/media` | `context:read` | `get_app media` | | GET | `/v1/competitors/intake/emerging` | `context:read` | `search_apps emerging` | | GET | `/v1/charts` | `context:read` | `get_market charts` | | GET | `/v1/charts/apps/{storeId}/history` | `context:read` | `get_app chart_history` | | GET | `/v1/market/overview` | `context:read` | `get_market overview` | | GET | `/v1/market/leaderboard` | `context:read` | `get_market leaderboard` | | GET | `/v1/market/signals` | `context:read` | `get_market signals` | --- # Errors and retries Handle safe envelopes, authorization failures and rate limits predictably. ## Status codes The public adapter maps invalid input to 400, missing credentials to 401, missing subscription to 402, authorization failures to 403, missing records to 404, conflicts to 409 and throttles to 429. Unexpected failures are sanitized. ## Retry-After Honor Retry-After when supplied. Do not retry unchanged 400, 401, 402 or 403 responses. Never retry an uncertain provider-writing command unless its idempotency or job-status contract makes that safe. ## Freshness A successful HTTP response proves transport and authorization only. It does not make a provider observation current beyond the source date included in the payload. --- # Search competitors Search the stored iOS competitor catalogue with bounded, cursor-based filters. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/competitors - Method: `GET` - Path: `/v1/competitors` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `search_apps` - MCP action: `search` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `search` | query | string, max 120 | no | Product-name search text. | | `category` | query | string, 1–120 | no | Exact catalogue category filter. | | `platform` | query | ios | no | The preview supports iOS only. | | `cursor` | query | opaque string | no | Cursor returned by the previous page. | | `limit` | query | integer, 1–100 | no | Page size; defaults to 24. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Send filters as query parameters. Keep them unchanged when following an opaque cursor. ``` curl --get 'https://api.peekpanda.com/v1/competitors' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --data-urlencode 'platform=ios' \ --data-urlencode 'search=calorie tracker' \ --data-urlencode 'limit=24' ``` ## MCP request Call search_apps with action search. Path fields go in params, filters in query, and a write body in body. ``` { "name": "search_apps", "arguments": { "request": { "action": "search" } } } ``` ## Response Paginated catalogue listings. 0000000000 is a synthetic valid-format store ID. ``` { "data": [ { "platform": "ios", "storeId": "0000000000", "appName": "Example Calorie Tracker", "category": "Health & Fitness", "developerId": "example-developer", "price": 0, "adSupported": null, "inAppPurchases": true, "totalRatings": 12450, "rating": 4.7, "installs": { "kind": "ratings_proxy", "value": 622500 }, "releaseDate": "2024-05-01", "storeUpdatedAt": "2026-09-06", "sourceUpdatedAt": "2026-09-06", "descriptionUpdatedAt": null, "description": null, "shortDescription": null, "source": "itunes", "snapshotDate": "2026-03-01" } ], "pagination": { "nextCursor": null, "hasMore": false } } ``` ## Source Dated public App Store listing observations. Installs, when present, are directional estimates rather than store-reported downloads. ## Refresh Reads return stored observations. When newer listing data is requested, collection continues asynchronously. --- # List competitor categories List stored competitor-catalogue category strings and their listing counts. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/competitors/categories - Method: `GET` - Path: `/v1/competitors/categories` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `search_apps` - MCP action: `categories` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request This read has no query parameters. ``` curl --get 'https://api.peekpanda.com/v1/competitors/categories' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call search_apps with action categories. Path fields go in params, filters in query, and a write body in body. ``` { "name": "search_apps", "arguments": { "request": { "action": "categories" } } } ``` ## Response Each row is a stored category string and how many listings use it. ``` { "data": [ { "category": "Health & Fitness", "count": 120 } ] } ``` ## Source Competitor catalogue category strings on stored listings. ## Refresh This read returns stored observations and does not start collection. --- # Get competitor Read one stored iOS competitor-catalogue listing. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/competitors/{platform}/{storeId} - Method: `GET` - Path: `/v1/competitors/{platform}/{storeId}` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_app` - MCP action: `get` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | ios | yes | The preview supports iOS only. | | `storeId` | path | numeric string | yes | Apple App Store identifier. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Identify the listing with platform ios and a numeric Apple store ID. ``` curl --get 'https://api.peekpanda.com/v1/competitors/ios/0000000000' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call get_app with action get. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_app", "arguments": { "request": { "action": "get", "params": { "platform": "", "storeId": "" } } } } ``` ## Response Returns one listing object. When collected, storeInAppPurchases contains dated Apple purchase names and prices for the US store. A missing field means not checked. An empty items array means the checked public page showed no purchases. Apple shows at most 10 items; this is not a full product catalogue or proof of subscription billing. ``` { "platform": "ios", "storeId": "0000000000", "appName": "Example Calorie Tracker", "category": "Health & Fitness", "developerId": "example-developer", "price": 0, "adSupported": null, "inAppPurchases": true, "totalRatings": 12450, "rating": 4.7, "installs": { "kind": "ratings_proxy", "value": 622500 }, "releaseDate": "2024-05-01", "storeUpdatedAt": "2026-09-06", "sourceUpdatedAt": "2026-09-06", "descriptionUpdatedAt": null, "description": null, "shortDescription": null, "source": "itunes", "snapshotDate": "2026-03-01", "storeInAppPurchases": { "appleId": "0000000000", "country": "us", "fetchedAt": "2026-09-06T00:00:00.000Z", "total": 1, "source": "app_store_product_page", "items": [ { "name": "Premium", "price": "$9.99", "amount": 9.99, "currency": "USD", "kind": "unknown" } ] } } ``` ## Source Dated public App Store listing observations. Installs, when present, are directional estimates rather than store-reported downloads. ## Refresh Reads return stored observations. When newer listing data is requested, collection continues asynchronously. --- # Get similar competitors Read similar stored iOS listings for one competitor, with the list page envelope. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/competitors/{platform}/{storeId}/similar - Method: `GET` - Path: `/v1/competitors/{platform}/{storeId}/similar` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `search_apps` - MCP action: `similar` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | ios | yes | The preview supports iOS only. | | `storeId` | path | numeric string | yes | Apple App Store identifier. | | `cursor` | query | opaque string | no | Cursor returned by the previous page. | | `limit` | query | integer, 1–100 | no | Page size; defaults to 24. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Send an optional opaque cursor and limit. Defaults match the competitor list page. ``` curl --get 'https://api.peekpanda.com/v1/competitors/ios/0000000000/similar' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --data-urlencode 'limit=24' ``` ## MCP request Call search_apps with action similar. Path fields go in params, filters in query, and a write body in body. ``` { "name": "search_apps", "arguments": { "request": { "action": "similar", "params": { "platform": "", "storeId": "" } } } } ``` ## Response The same paginated listing envelope as GET /v1/competitors. ``` { "data": [ { "platform": "ios", "storeId": "0000000000", "appName": "Example Calorie Tracker", "category": "Health & Fitness", "developerId": "example-developer", "price": 0, "adSupported": null, "inAppPurchases": true, "totalRatings": 12450, "rating": 4.7, "installs": { "kind": "ratings_proxy", "value": 622500 }, "releaseDate": "2024-05-01", "storeUpdatedAt": "2026-09-06", "sourceUpdatedAt": "2026-09-06", "descriptionUpdatedAt": null, "description": null, "shortDescription": null, "source": "itunes", "snapshotDate": "2026-03-01" } ], "pagination": { "nextCursor": null, "hasMore": false } } ``` ## Source Dated public App Store listing observations. Installs, when present, are directional estimates rather than store-reported downloads. ## Refresh Reads return stored observations. When newer listing data is requested, collection continues asynchronously. --- # Get competitor case file Read one competitor app’s whole overview in one request. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/competitors/{platform}/{storeId}/case-file - Method: `GET` - Path: `/v1/competitors/{platform}/{storeId}/case-file` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_app` - MCP action: `case_file` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | ios | yes | The preview supports iOS only. | | `storeId` | path | numeric string | yes | Apple App Store identifier. | | `country` | query | two-letter storefront code | no | Chart storefront; defaults to us. | | `categoryId` | query | all or App Store genre id | no | Chart category; defaults to all. | | `chartType` | query | free, paid or grossing | no | Chart for the standing. When omitted, a priced app reads the paid chart and a free app the free chart. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Identify the listing and, optionally, the chart the standing is read from. ``` curl --get 'https://api.peekpanda.com/v1/competitors/ios/0000000000/case-file' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --data-urlencode 'country=us' ``` ## MCP request Call get_app with action case_file. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_app", "arguments": { "request": { "action": "case_file", "params": { "platform": "", "storeId": "" } } } } ``` ## Response paid is ready, updating (a rebuild is pending and nothing is stored yet), none (no paid evidence) or unavailable (its read failed). A ready summary is read at request time over the last 7 days and 12 weeks; latestAds uses the Search paid ads item shape with short-lived media URLs. aso, estimates and rankStanding are null outside iOS; grossing is null when the chart is already grossing. viewer is yours alone. See Section states for sections whose read failed. ``` { "listing": { "platform": "ios", "storeId": "0000000000", "appName": "Example Calorie Tracker", "category": "Health & Fitness", "developerId": "example-developer", "price": 0, "adSupported": null, "inAppPurchases": true, "totalRatings": 12450, "rating": 4.7, "installs": { "kind": "ratings_proxy", "value": 622500 }, "releaseDate": "2024-05-01", "storeUpdatedAt": "2026-09-06", "sourceUpdatedAt": "2026-09-06", "descriptionUpdatedAt": null, "description": null, "shortDescription": null, "source": "itunes", "snapshotDate": "2026-03-01" }, "paid": { "kind": "ready", "summary": { "totalAds": 14, "liveAds": 6, "startedLast7Days": 2, "medianRunDays": 41, "longestRunDays": 188, "liveBeyond60Days": 2, "longevity": [ { "band": "0-6", "count": 1 }, { "band": "7-14", "count": 1 }, { "band": "15-30", "count": 1 }, { "band": "31-60", "count": 1 }, { "band": "61-90", "count": 1 }, { "band": "90+", "count": 1 } ], "weeklyStarts": [ { "weekStart": "2026-06-15", "newAds": 1 }, { "weekStart": "2026-06-22", "newAds": 1 }, { "weekStart": "2026-06-29", "newAds": 1 }, { "weekStart": "2026-07-06", "newAds": 1 }, { "weekStart": "2026-07-13", "newAds": 1 }, { "weekStart": "2026-07-20", "newAds": 1 }, { "weekStart": "2026-07-27", "newAds": 1 }, { "weekStart": "2026-08-03", "newAds": 1 }, { "weekStart": "2026-08-10", "newAds": 1 }, { "weekStart": "2026-08-17", "newAds": 1 }, { "weekStart": "2026-08-24", "newAds": 1 }, { "weekStart": "2026-08-31", "newAds": 2 } ], "formats": { "video": 9, "image": 5, "carousel": 0, "dynamic": 0, "unknown": 0 }, "destinations": { "appStore": 12, "playStore": 0, "web": 2, "unknown": 0 }, "topCta": "Install now", "advertiserCount": 1 }, "advertisers": [ { "advertiserId": "33333333-3333-4333-8333-333333333333", "externalId": "example-page", "name": "Example Calorie Tracker", "totalAds": 14, "liveAds": 6 } ], "latestAds": [], "builtAt": "2026-09-06T00:00:00.000Z", "updating": null }, "aso": { "positionKind": "observed_search_api_position", "storeId": "0000000000", "country": "us", "language": "en_us", "source": "itunes_search", "queriesMatched": 1, "summary": { "top3": 0, "top10": 1, "top50": 1, "bestPosition": { "query": "calorie tracker", "position": 4 }, "medianPosition": 4 }, "bestPositions": [ { "query": "calorie tracker", "position": 4, "popularity": 63 } ] }, "estimates": { "data": [] }, "rankStanding": { "country": "us", "categoryId": "all", "chartType": "free", "chart": { "chartType": "free", "latest": { "day": "2026-09-06", "rank": 18 }, "previous": { "day": "2026-09-05", "rank": 21 } }, "grossing": null, "storefronts": [ { "country": "us", "day": "2026-09-06", "rank": 18 }, { "country": "gb", "day": "2026-09-06", "rank": 33 } ] }, "organic": { "matchedCount": 1, "candidateLimitReached": false, "latest": [ { "sourceId": "organic-post-example", "productName": "Example Calorie Tracker", "caption": "A stored organic clip", "link": "https://www.tiktok.com/@example/video/1", "datePublished": "2026-09-06", "match": { "status": "suggested", "evidence": "store_url", "score": 1 } } ] }, "viewer": { "canGenerateIntelligence": true, "requests": { "data": [ { "id": "11111111-1111-4111-8111-111111111111", "kind": "aso", "status": "requested", "requestedAt": "2026-09-06T00:00:00.000Z" } ] } } } ``` ## Source The stored per-app paid signal (an ad belongs to the app its own verified App Store link names), current US App Store search observations, PeekPanda estimates, stored Apple chart snapshots and the copied public organic corpus. Missing evidence is null or none, never zero. ## Refresh This read returns stored observations and does not start collection. ## Section states Each section says why it holds no evidence. null means the section does not apply to this listing: aso, estimates and rankStanding are null outside iOS. none means PeekPanda holds no paid evidence for the app. unavailable means the section could not be read for this request. It says nothing about the evidence itself, so retry later instead of reading it as empty. paid, aso, estimates, rankStanding, organic and viewer.requests can each be unavailable while the rest of the case file still answers; the listing is the only part whose failure fails the request. The example shows a case file whose paid, estimates, organic and request reads failed. ``` { "listing": { "platform": "ios", "storeId": "0000000000", "appName": "Example Calorie Tracker", "category": "Health & Fitness", "developerId": "example-developer", "price": 0, "adSupported": null, "inAppPurchases": true, "totalRatings": 12450, "rating": 4.7, "installs": { "kind": "ratings_proxy", "value": 622500 }, "releaseDate": "2024-05-01", "storeUpdatedAt": "2026-09-06", "sourceUpdatedAt": "2026-09-06", "descriptionUpdatedAt": null, "description": null, "shortDescription": null, "source": "itunes", "snapshotDate": "2026-03-01" }, "paid": { "kind": "unavailable" }, "aso": { "positionKind": "observed_search_api_position", "storeId": "0000000000", "country": "us", "language": "en_us", "source": "itunes_search", "queriesMatched": 1, "summary": { "top3": 0, "top10": 1, "top50": 1, "bestPosition": { "query": "calorie tracker", "position": 4 }, "medianPosition": 4 }, "bestPositions": [ { "query": "calorie tracker", "position": 4, "popularity": 63 } ] }, "estimates": { "kind": "unavailable" }, "rankStanding": { "country": "us", "categoryId": "all", "chartType": "free", "chart": { "chartType": "free", "latest": { "day": "2026-09-06", "rank": 18 }, "previous": { "day": "2026-09-05", "rank": 21 } }, "grossing": null, "storefronts": [ { "country": "us", "day": "2026-09-06", "rank": 18 }, { "country": "gb", "day": "2026-09-06", "rank": 33 } ] }, "organic": { "kind": "unavailable" }, "viewer": { "canGenerateIntelligence": true, "requests": { "kind": "unavailable" } } } ``` --- # Browse observed competitor ASO keywords Page through the searches an iOS listing currently appears in, most popular first. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/competitors/{platform}/{storeId}/aso/keywords - Method: `GET` - Path: `/v1/competitors/{platform}/{storeId}/aso/keywords` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_app` - MCP action: `keywords` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | ios | yes | The preview supports iOS only. | | `storeId` | path | numeric string | yes | Apple App Store identifier. | | `cursor` | query | opaque string | no | Cursor returned by the previous page. | | `limit` | query | integer, 1–100 | no | Page size; defaults to 50. | | `search` | query | string, up to 200 characters | no | Only queries containing this text. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Identify the listing. Pass the returned cursor to read the next page. ``` curl --get 'https://api.peekpanda.com/v1/competitors/ios/0000000000/aso/keywords' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --data-urlencode 'limit=50' ``` ## MCP request Call get_app with action keywords. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_app", "arguments": { "request": { "action": "keywords", "params": { "platform": "", "storeId": "" } } } } ``` ## Response Position is Search API order, not native App Store rank. popularity is the stored US Apple Ads score, or null when none was collected. ``` { "data": [ { "query": "calorie tracker", "position": 4, "observedAt": "2026-09-06T00:00:00.000Z", "popularity": 63 } ], "nextCursor": null } ``` ## Source Current US English App Store search observations. ## Refresh This read returns stored observations and does not start collection. --- # Get competitor organic content Read stored organic-corpus name-match suggestions for one listing. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/competitors/{platform}/{storeId}/organic - Method: `GET` - Path: `/v1/competitors/{platform}/{storeId}/organic` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_app` - MCP action: `organic` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | ios | yes | The preview supports iOS only. | | `storeId` | path | numeric string | yes | Apple App Store identifier. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Identify the listing. This GET does not take query filters. ``` curl --get 'https://api.peekpanda.com/v1/competitors/ios/0000000000/organic' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call get_app with action organic. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_app", "arguments": { "request": { "action": "organic", "params": { "platform": "", "storeId": "" } } } } ``` ## Response Suggested matches from the copied public organic corpus, not verified ownership. ``` { "data": [ { "sourceId": "organic-post-example", "productName": "Example Calorie Tracker", "caption": "A stored organic clip", "link": "https://www.tiktok.com/@example/video/1", "match": { "status": "suggested", "evidence": "product_name", "score": 0.91 } } ], "candidateLimitReached": false } ``` ## Source Copied public organic corpus; name-match suggestions, not verified ownership. ## Refresh This read returns stored observations and does not start collection. --- # Get competitor ASO intelligence Read stored App Store search observations for one iOS listing. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/competitors/{platform}/{storeId}/aso - Method: `GET` - Path: `/v1/competitors/{platform}/{storeId}/aso` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_app` - MCP action: `aso` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | ios | yes | The preview supports iOS only. | | `storeId` | path | numeric string | yes | Apple App Store identifier. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Identify the listing. This read returns stored observations. ``` curl --get 'https://api.peekpanda.com/v1/competitors/ios/0000000000/aso' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call get_app with action aso. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_app", "arguments": { "request": { "action": "aso", "params": { "platform": "", "storeId": "" } } } } ``` ## Response Position is Search API order, not native App Store rank. There is no Apple Ads popularity. ``` { "positionKind": "observed_search_api_position", "storeId": "0000000000", "coverage": { "queriesMatched": 1, "country": "us", "language": "en_us", "source": "itunes_search", "latestObservedAt": "2026-09-06T00:00:00.000Z", "observationCount": 1 }, "summary": { "top3": 1, "top10": 1, "top50": 1, "bestPosition": { "query": "calorie tracker", "position": 4 }, "medianPosition": 4 }, "keywords": [ { "query": "calorie tracker", "country": "us", "language": "en_us", "source": "itunes_search", "observedAt": "2026-09-06T00:00:00.000Z", "position": 4, "resultCount": 2, "apps": [ { "storeId": "0000000000", "position": 4, "name": "Example Calorie Tracker", "isSubject": true, "inCatalogue": true, "rating": 4.7, "ratingCount": 12450 } ], "history": [ { "observedAt": "2026-09-06T00:00:00.000Z", "position": 4 } ], "demand": { "score": 63, "tier": 3, "country": "us", "periodStart": "2026-08-30", "periodEnd": "2026-09-05", "observedAt": "2026-09-06T00:00:00.000Z" }, "difficulty": { "score": 71 } } ], "rivals": [], "keywordIdeas": [], "insights": [ { "kind": "trend_pending", "title": "One collection run so far", "detail": "Position changes need at least two runs.", "keywords": [], "catalogueStoreId": null } ] } ``` ## Source Dated US English App Store search observations. Position is observed result order, not native App Store rank or search volume. ## Refresh Use the ASO refresh command to request newer coverage. Collection runs asynchronously. --- # Get competitor ASO collection status Read whether ASO coverage is queued, running, complete, or unavailable. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/competitors/{platform}/{storeId}/aso/collection - Method: `GET` - Path: `/v1/competitors/{platform}/{storeId}/aso/collection` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_app` - MCP action: `aso_status` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | ios | yes | The preview supports iOS only. | | `storeId` | path | numeric string | yes | Apple App Store identifier. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Identify the listing. This read never starts collection. ``` curl --get 'https://api.peekpanda.com/v1/competitors/ios/0000000000/aso/collection' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call get_app with action aso_status. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_app", "arguments": { "request": { "action": "aso_status", "params": { "platform": "", "storeId": "" } } } } ``` ## Response The bounded lifecycle status for US/en_us ASO collection. ``` { "storeId": "0000000000", "country": "us", "language": "en_us", "status": "completed", "updatedAt": "2026-09-06T00:00:00.000Z", "retryAt": null } ``` ## Source The collection status for this competitor and storefront. ## Refresh Use the refresh command to request collection. Repeated active requests are coalesced. --- # Refresh competitor ASO collection Refresh ASO coverage for an accessible iOS competitor when collection is available. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: POST /v1/competitors/{platform}/{storeId}/aso/collection - Method: `POST` - Path: `/v1/competitors/{platform}/{storeId}/aso/collection` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `refresh` - MCP action: `aso_collection` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | ios | yes | The preview supports iOS only. | | `storeId` | path | numeric string | yes | Apple App Store identifier. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request The command has no request body; active work is coalesced and completed work respects its refresh cooldown. ``` curl --request POST 'https://api.peekpanda.com/v1/competitors/ios/0000000000/aso/collection' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call refresh with action aso_collection. Path fields go in params, filters in query, and a write body in body. ``` { "name": "refresh", "arguments": { "request": { "action": "aso_collection", "params": { "platform": "", "storeId": "" } } } } ``` ## Response The resulting collection lifecycle status. ``` { "storeId": "0000000000", "country": "us", "language": "en_us", "status": "completed", "updatedAt": "2026-09-06T00:00:00.000Z", "retryAt": null } ``` ## Source Canonical iOS ASO collection owner. ## Refresh Collection runs asynchronously and reports lifecycle, cooldown, and retry timing. --- # List competitor data requests List recorded intelligence-request rows for one listing. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/competitors/{platform}/{storeId}/data-requests - Method: `GET` - Path: `/v1/competitors/{platform}/{storeId}/data-requests` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_app` - MCP action: `data_requests` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | ios | yes | The preview supports iOS only. | | `storeId` | path | numeric string | yes | Apple App Store identifier. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Identify the listing. This GET records nothing. ``` curl --get 'https://api.peekpanda.com/v1/competitors/ios/0000000000/data-requests' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call get_app with action data_requests. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_app", "arguments": { "request": { "action": "data_requests", "params": { "platform": "", "storeId": "" } } } } ``` ## Response Each row is an account-scoped intelligence request with its status and request date. ``` { "data": [ { "id": "11111111-1111-4111-8111-111111111111", "kind": "aso", "status": "requested", "requestedAt": "2026-09-06T00:00:00.000Z" } ] } ``` ## Source Account-scoped requests for missing competitor intelligence. ## Refresh Records demand only. ASO requests may begin an asynchronous collection. --- # Request competitor data Record deduplicated demand for one missing competitor-intelligence kind. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: POST /v1/competitors/{platform}/{storeId}/data-requests - Method: `POST` - Path: `/v1/competitors/{platform}/{storeId}/data-requests` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `request_data` - MCP action: `request` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | ios | yes | The preview supports iOS only. | | `storeId` | path | numeric string | yes | Apple App Store identifier. | | `kind` | body | revenue | downloads | paid_ads | organic | aso | history | yes | The missing-metric kind to record. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request Send one kind. The write records demand only; use the ASO refresh command to start collection. ``` curl --request POST 'https://api.peekpanda.com/v1/competitors/ios/0000000000/data-requests' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --header 'content-type: application/json' \ --data '{"kind":"aso"}' ``` ## MCP request Call request_data with action request. Path fields go in params, filters in query, and a write body in body. ``` { "name": "request_data", "arguments": { "request": { "action": "request", "params": { "platform": "", "storeId": "" } } } } ``` ## Response The persisted request object, including its id and requestedAt. ``` { "id": "11111111-1111-4111-8111-111111111111", "kind": "aso", "status": "requested", "requestedAt": "2026-09-06T00:00:00.000Z" } ``` ## Source Account-scoped requests for missing competitor intelligence. ## Refresh Records demand only. ASO requests may begin an asynchronous collection. --- # Request an iOS competitor Add one explicitly requested iOS app without requiring established-app status. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: POST /v1/competitors/intake/requested - Method: `POST` - Path: `/v1/competitors/intake/requested` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `request_data` - MCP action: `intake` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `app` | body | numeric store ID or apps.apple.com URL | yes | The Apple app to admit. | | `country` | body | us | no | The current intake storefront is us, which is also the default. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request Send a numeric Apple ID or apps.apple.com URL. Country defaults to us. ``` curl --request POST 'https://api.peekpanda.com/v1/competitors/intake/requested' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --header 'content-type: application/json' \ --data '{"app":"0000000000","country":"us"}' ``` ## MCP request Call request_data with action intake. Path fields go in params, filters in query, and a write body in body. ``` { "name": "request_data", "arguments": { "request": { "action": "intake" } } } ``` ## Response The admitted requested listing and its public App Store identity. ``` { "platform": "ios", "storeId": "0000000000", "appName": "Example Calorie Tracker", "storefrontUrl": "https://apps.apple.com/us/app/id0000000000", "releaseDate": "2024-05-01", "country": "us", "reasons": [ "requested" ], "tractionStatus": "candidate", "baselineObservedAt": "2026-09-06T00:00:00.000Z", "baselineRatingCount": 12, "latestObservedAt": "2026-09-06T00:00:00.000Z", "latestRatingCount": 12 } ``` ## Source The public App Store listing identified by the supplied numeric ID or URL. ## Refresh Runs only when explicitly requested. --- # Discover emerging iOS competitors Admit emerging iOS apps from a bounded set of Apple Search queries. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: POST /v1/competitors/intake/emerging-discovery - Method: `POST` - Path: `/v1/competitors/intake/emerging-discovery` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `request_data` - MCP action: `discover_emerging` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `queries` | body | string[], max 12 | yes | Bounded Apple Search terms. | | `country` | body | us | no | The current intake storefront is us, which is also the default. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request Send 1–12 focused search queries. PeekPanda applies the current emerging-app eligibility policy. ``` curl --request POST 'https://api.peekpanda.com/v1/competitors/intake/emerging-discovery' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --header 'content-type: application/json' \ --data '{"queries":["calorie tracker"],"country":"us"}' ``` ## MCP request Call request_data with action discover_emerging. Path fields go in params, filters in query, and a write body in body. ``` { "name": "request_data", "arguments": { "request": { "action": "discover_emerging" } } } ``` ## Response Admitted emerging listings plus how many candidates were rejected. ``` { "admitted": [ { "platform": "ios", "storeId": "0000000000", "appName": "Example Calorie Tracker", "storefrontUrl": "https://apps.apple.com/us/app/id0000000000", "releaseDate": "2024-05-01", "country": "us", "reasons": [ "emerging" ], "tractionStatus": "candidate", "baselineObservedAt": "2026-09-06T00:00:00.000Z", "baselineRatingCount": 12, "latestObservedAt": "2026-09-06T00:00:00.000Z", "latestRatingCount": 12 } ], "rejectedCount": 0 } ``` ## Source Public App Store results for the supplied discovery queries. ## Refresh Runs only when explicitly requested. --- # List emerging iOS competitors Read previously admitted emerging iOS intake listings. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/competitors/intake/emerging - Method: `GET` - Path: `/v1/competitors/intake/emerging` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `search_apps` - MCP action: `emerging` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request This read has no query parameters. ``` curl --get 'https://api.peekpanda.com/v1/competitors/intake/emerging' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call search_apps with action emerging. Path fields go in params, filters in query, and a write body in body. ``` { "name": "search_apps", "arguments": { "request": { "action": "emerging" } } } ``` ## Response Emerging intake listings already stored for the owner. ``` { "data": [ { "platform": "ios", "storeId": "0000000000", "appName": "Example Calorie Tracker", "storefrontUrl": "https://apps.apple.com/us/app/id0000000000", "releaseDate": "2024-05-01", "country": "us", "reasons": [ "emerging" ], "tractionStatus": "candidate", "baselineObservedAt": "2026-09-06T00:00:00.000Z", "baselineRatingCount": 12, "latestObservedAt": "2026-09-06T00:00:00.000Z", "latestRatingCount": 12 } ] } ``` ## Source Previously admitted emerging-app observations. ## Refresh This read returns stored observations and does not start collection. --- # Get competitor media Read stored organic matches and paid-ad observations for one listing. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/competitors/{platform}/{storeId}/media - Method: `GET` - Path: `/v1/competitors/{platform}/{storeId}/media` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_app` - MCP action: `media` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | ios | yes | The preview supports iOS only. | | `storeId` | path | numeric string | yes | Apple App Store identifier. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Identify the listing. This GET does not invent paid-ad rows when none are stored. ``` curl --get 'https://api.peekpanda.com/v1/competitors/ios/0000000000/media' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call get_app with action media. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_app", "arguments": { "request": { "action": "media", "params": { "platform": "", "storeId": "" } } } } ``` ## Response organic uses the organic-match page. paidAds stays empty when no ads are stored, and paidAdDestinations reports where the stored ads sent their clicks. paidAds returns a current cursor page with items, nextCursor and hasMore, without collection totals. ``` { "organic": { "data": [ { "sourceId": "organic-post-example", "productName": "Example Calorie Tracker", "caption": "A stored organic clip", "link": "https://www.tiktok.com/@example/video/1", "match": { "status": "suggested", "evidence": "product_name", "score": 0.91 } } ], "candidateLimitReached": false }, "paidAds": { "items": [], "nextCursor": null, "hasMore": false }, "paidAdDestinations": { "appStore": 0, "playStore": 0, "web": 0, "unknown": 0, "landingPages": [] } } ``` ## Source Copied public organic corpus; name-match suggestions, not verified ownership. ## Refresh This read returns stored observations and does not start collection. --- # Get competitor estimate history Read bounded, dated download and revenue estimates for one iOS app. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/competitors/{platform}/{storeId}/estimates/history - Method: `GET` - Path: `/v1/competitors/{platform}/{storeId}/estimates/history` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_app` - MCP action: `estimates` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | ios | yes | The preview supports iOS only. | | `storeId` | path | numeric string | yes | Apple App Store identifier. | | `limit` | query | integer, 1–366 | no | Maximum observations; defaults to 60. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request This read returns stored PeekPanda observations and never calls the App Store. ``` curl --get 'https://api.peekpanda.com/v1/competitors/ios/0000000000/estimates/history' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --data-urlencode 'limit=60' ``` ## MCP request Call get_app with action estimates. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_app", "arguments": { "request": { "action": "estimates", "params": { "platform": "", "storeId": "" } } } } ``` ## Response Estimate fields are null when the available public evidence cannot support them. ``` { "data": [ { "storeId": "0000000000", "platform": "ios", "source": "peekpanda", "observedAt": "2026-09-06T00:00:00.000Z", "country": "us", "ratingVelocityPerDay": 31.5, "monthlyInstalls": { "range": { "low": 49000, "mid": 129000, "high": 340000 }, "confidence": { "group": "finance", "sampleSize": 149, "errorFactor": 2.589 } }, "previousMonthlyInstalls": null, "monthlyRevenue": { "range": { "low": 932000, "mid": 2000000, "high": 4290000 }, "confidence": { "group": "finance", "sampleSize": 149, "errorFactor": 2.145 } }, "previousMonthlyRevenue": null, "currency": "USD", "modelVersion": 3 } ] } ``` ## Source PeekPanda estimates derived from dated public App Store rating activity and chart observations. Missing evidence returns null. ## Refresh Refreshes run asynchronously and return dated observations when available. --- # Refresh competitor estimates Request one bounded estimate refresh for an accessible iOS app. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: POST /v1/competitors/{platform}/{storeId}/estimates/refresh - Method: `POST` - Path: `/v1/competitors/{platform}/{storeId}/estimates/refresh` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `refresh` - MCP action: `estimates` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | ios | yes | The preview supports iOS only. | | `storeId` | path | numeric string | yes | Apple App Store identifier. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request The command has no request body. Fresh and in-progress work is coalesced. ``` curl --request POST 'https://api.peekpanda.com/v1/competitors/ios/0000000000/estimates/refresh' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call refresh with action estimates. Path fields go in params, filters in query, and a write body in body. ``` { "name": "refresh", "arguments": { "request": { "action": "estimates", "params": { "platform": "", "storeId": "" } } } } ``` ## Response updated includes data; pending includes retryAt and an empty data array. ``` { "state": "pending", "data": [], "retryAt": "2026-09-06T00:15:00.000Z" } ``` ## Source PeekPanda estimates derived from dated public App Store rating activity and chart observations. Missing evidence returns null. ## Refresh Refreshes run asynchronously and return dated observations when available. --- # Get competitor technology Read stored technology evidence and best chart positions for an iOS app. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/competitors/{platform}/{storeId}/technology - Method: `GET` - Path: `/v1/competitors/{platform}/{storeId}/technology` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_app` - MCP action: `technology` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | ios | yes | Technology observations support iOS apps. | | `storeId` | path | numeric string | yes | Apple App Store identifier. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request No request body. Returns existing observations only. ``` curl --get 'https://api.peekpanda.com/v1/competitors/ios/0000000000/technology' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call get_app with action technology. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_app", "arguments": { "request": { "action": "technology", "params": { "platform": "", "storeId": "" } } } } ``` ## Response status is observed or not_collected. SDK and best-rank evidence each have their own collection status and date. Omitted source fields become null, not empty lists. Best ranks cover a rolling 90-day window, not a daily history or current position. ``` { "storeId": "0000000000", "status": "not_collected", "sdk": { "status": "not_collected" }, "bestRanks": { "status": "not_collected" } } ``` ## Source Dated technology observations and source-reported best chart positions. Missing fields remain unknown. An advertising-network identifier does not prove active ad buying. ## Refresh Collection requires an explicit request and is subject to account limits and data availability. Opening this read does not start collection. --- # Refresh competitor technology Explicitly request technology evidence for an accessible iOS app. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: POST /v1/competitors/{platform}/{storeId}/technology - Method: `POST` - Path: `/v1/competitors/{platform}/{storeId}/technology` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `refresh` - MCP action: `technology` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | ios | yes | Technology observations support iOS apps. | | `storeId` | path | numeric string | yes | Apple App Store identifier. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request No request body. Existing fresh observations are reused. ``` curl --request POST 'https://api.peekpanda.com/v1/competitors/ios/0000000000/technology' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call refresh with action technology. Path fields go in params, filters in query, and a write body in body. ``` { "name": "refresh", "arguments": { "request": { "action": "technology", "params": { "platform": "", "storeId": "" } } } } ``` ## Response The receipt state is fresh, updated, pending, limited, failed or unavailable. Always inspect state, even after HTTP success. data retains available evidence; retryAt indicates when a retry can be attempted where supplied. unavailable includes detail. ``` { "state": "limited", "data": { "storeId": "0000000000", "status": "not_collected", "sdk": { "status": "not_collected" }, "bestRanks": { "status": "not_collected" } }, "retryAt": "2026-09-09T08:00:00.000Z" } ``` ## Source Dated technology observations and source-reported best chart positions. Missing fields remain unknown. An advertising-network identifier does not prove active ad buying. ## Refresh Collection requires an explicit request and is subject to account limits and data availability. Opening this read does not start collection. --- # List ASO apps List the owner's tracked iOS ASO apps from storage. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/aso/apps - Method: `GET` - Path: `/v1/aso/apps` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `aso` - MCP action: `list` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Returns tracked apps for the authenticated owner. This GET never calls Apple. At most 25 apps can be tracked. ``` curl --get 'https://api.peekpanda.com/v1/aso/apps' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call aso with action list. Path fields go in params, filters in query, and a write body in body. ``` { "name": "aso", "arguments": { "request": { "action": "list" } } } ``` ## Response Illustrative stored list. `limit` is the tracked-app cap (25), not a request page size. Store id 0000000000 is a synthetic valid-format identifier. ``` { "data": [ { "storeId": "0000000000", "name": "Example Calorie Tracker", "iconUrl": null, "developerName": "Example Labs", "primaryGenre": "Health & Fitness", "rating": 4.7, "ratingCount": 12450, "inCatalogue": true, "id": "11111111-1111-4111-8111-111111111111", "kind": "apple", "folderId": null, "role": "mine", "createdAt": "2026-09-06T00:00:00.000Z", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "keywordCount": 1, "follows": { "chart": false, "ads": false, "organic": false, "keywords": true } } ], "folders": [], "limit": 25 } ``` ## Source Dated public App Store search results and suggestion observations for the US English storefront. Positions are observed result order. Difficulty and demand are directional indicators, not search volume or advertising data. ## Refresh This read returns stored observations. Use the related refresh command when newer evidence is needed. --- # Create an ASO folder Create a named folder for organizing tracked apps. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: POST /v1/aso/folders - Method: `POST` - Path: `/v1/aso/folders` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `manage_aso` - MCP action: `create_folder` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `name` | body | string | yes | Folder name. | | `color` | body | #rrggbb | yes | Folder color. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request Creates a private folder for the authenticated owner. ``` curl --request POST 'https://api.peekpanda.com/v1/aso/folders' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --header 'content-type: application/json' \ --data '{"name":"Research","color":"#22c55e"}' ``` ## MCP request Call manage_aso with action create_folder. Path fields go in params, filters in query, and a write body in body. ``` { "name": "manage_aso", "arguments": { "request": { "action": "create_folder" } } } ``` ## Response The created folder. ``` { "id": "44444444-4444-4444-8444-444444444444", "name": "Research", "color": "#22c55e", "createdAt": "2026-09-06T00:00:00.000Z", "updatedAt": "2026-09-06T00:00:00.000Z" } ``` ## Source Dated public App Store search results and suggestion observations for the US English storefront. Positions are observed result order. Difficulty and demand are directional indicators, not search volume or advertising data. ## Refresh This read returns stored observations. Use the related refresh command when newer evidence is needed. --- # Update an ASO folder Rename or recolor a private ASO folder. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: PATCH /v1/aso/folders/{folderId} - Method: `PATCH` - Path: `/v1/aso/folders/{folderId}` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `manage_aso` - MCP action: `update_folder` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `folderId` | path | uuid | yes | ASO folder id. | | `name` | body | string | yes | Optional name. | | `color` | body | #rrggbb | yes | Optional color. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request Changes the private folder only. ``` curl --request PATCH 'https://api.peekpanda.com/v1/aso/folders/44444444-4444-4444-8444-444444444444' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --header 'content-type: application/json' \ --data '{"name":"Priorities","color":"#2563eb"}' ``` ## MCP request Call manage_aso with action update_folder. Path fields go in params, filters in query, and a write body in body. ``` { "name": "manage_aso", "arguments": { "request": { "action": "update_folder", "params": { "folderId": "" } } } } ``` ## Response The updated folder. ``` { "id": "44444444-4444-4444-8444-444444444444", "name": "Priorities", "color": "#2563eb", "createdAt": "2026-09-06T00:00:00.000Z", "updatedAt": "2026-09-06T00:00:00.000Z" } ``` ## Source Dated public App Store search results and suggestion observations for the US English storefront. Positions are observed result order. Difficulty and demand are directional indicators, not search volume or advertising data. ## Refresh This read returns stored observations. Use the related refresh command when newer evidence is needed. --- # Remove an ASO folder Remove a folder and leave its tracked apps unfiled. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: DELETE /v1/aso/folders/{folderId} - Method: `DELETE` - Path: `/v1/aso/folders/{folderId}` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `manage_aso` - MCP action: `remove_folder` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `folderId` | path | uuid | yes | ASO folder id. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request Removing a folder clears its app assignments but does not remove the tracked apps. ``` curl --request DELETE 'https://api.peekpanda.com/v1/aso/folders/44444444-4444-4444-8444-444444444444' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call manage_aso with action remove_folder. Path fields go in params, filters in query, and a write body in body. ``` { "name": "manage_aso", "arguments": { "request": { "action": "remove_folder", "params": { "folderId": "" } } } } ``` ## Response Empty JSON object. ``` {} ``` ## Source Dated public App Store search results and suggestion observations for the US English storefront. Positions are observed result order. Difficulty and demand are directional indicators, not search volume or advertising data. ## Refresh This read returns stored observations. Use the related refresh command when newer evidence is needed. --- # Create an ASO keyword tag Create a named, coloured label for grouping tracked keywords. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: POST /v1/aso/tags - Method: `POST` - Path: `/v1/aso/tags` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `manage_aso` - MCP action: `create_tag` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `name` | body | string | yes | Tag name, unique per owner. | | `color` | body | #rrggbb | yes | Optional colour; omitted, the next unused palette colour is assigned. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request Creates a private tag for the authenticated owner. Tags belong to the owner, not to one app, so the same tag groups keywords across every tracked app. ``` curl --request POST 'https://api.peekpanda.com/v1/aso/tags' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --header 'content-type: application/json' \ --data '{"name":"Potential"}' ``` ## MCP request Call manage_aso with action create_tag. Path fields go in params, filters in query, and a write body in body. ``` { "name": "manage_aso", "arguments": { "request": { "action": "create_tag" } } } ``` ## Response The created tag. ``` { "id": "55555555-5555-4555-8555-555555555555", "name": "Potential", "color": "#0f9d58", "createdAt": "2026-09-06T00:00:00.000Z", "updatedAt": "2026-09-06T00:00:00.000Z" } ``` ## Source Dated public App Store search results and suggestion observations for the US English storefront. Positions are observed result order. Difficulty and demand are directional indicators, not search volume or advertising data. ## Refresh This read returns stored observations. Use the related refresh command when newer evidence is needed. --- # Update an ASO keyword tag Rename or recolour a keyword tag everywhere it is applied. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: PATCH /v1/aso/tags/{tagId} - Method: `PATCH` - Path: `/v1/aso/tags/{tagId}` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `manage_aso` - MCP action: `update_tag` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `tagId` | path | uuid | yes | Keyword tag id. | | `name` | body | string | yes | Optional name. | | `color` | body | #rrggbb | yes | Optional colour. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request The name lives in one record, so a rename changes the tag on every keyword that carries it. At least one of name or colour is required. ``` curl --request PATCH 'https://api.peekpanda.com/v1/aso/tags/55555555-5555-4555-8555-555555555555' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --header 'content-type: application/json' \ --data '{"name":"Priority","color":"#2563eb"}' ``` ## MCP request Call manage_aso with action update_tag. Path fields go in params, filters in query, and a write body in body. ``` { "name": "manage_aso", "arguments": { "request": { "action": "update_tag", "params": { "tagId": "" } } } } ``` ## Response The updated tag. ``` { "id": "55555555-5555-4555-8555-555555555555", "name": "Priority", "color": "#2563eb", "createdAt": "2026-09-06T00:00:00.000Z", "updatedAt": "2026-09-06T00:00:00.000Z" } ``` ## Source Dated public App Store search results and suggestion observations for the US English storefront. Positions are observed result order. Difficulty and demand are directional indicators, not search volume or advertising data. ## Refresh This read returns stored observations. Use the related refresh command when newer evidence is needed. --- # Remove an ASO keyword tag Delete a tag and lift it from every keyword carrying it. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: DELETE /v1/aso/tags/{tagId} - Method: `DELETE` - Path: `/v1/aso/tags/{tagId}` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `manage_aso` - MCP action: `remove_tag` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `tagId` | path | uuid | yes | Keyword tag id. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request Deleting a tag removes it from every tracked keyword that carries it, across every tracked app. The keywords themselves, and their notes, are untouched. ``` curl --request DELETE 'https://api.peekpanda.com/v1/aso/tags/55555555-5555-4555-8555-555555555555' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call manage_aso with action remove_tag. Path fields go in params, filters in query, and a write body in body. ``` { "name": "manage_aso", "arguments": { "request": { "action": "remove_tag", "params": { "tagId": "" } } } } ``` ## Response Empty JSON object. ``` {} ``` ## Source Dated public App Store search results and suggestion observations for the US English storefront. Positions are observed result order. Difficulty and demand are directional indicators, not search volume or advertising data. ## Refresh This read returns stored observations. Use the related refresh command when newer evidence is needed. --- # Tag ASO keywords Put one tag on a set of tracked keywords, or lift it from them. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: POST /v1/aso/apps/{appId}/keywords/tags - Method: `POST` - Path: `/v1/aso/apps/{appId}/keywords/tags` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `manage_aso` - MCP action: `assign_tag` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `appId` | path | uuid | yes | Tracked ASO app id. | | `tagId` | body | uuid | yes | Keyword tag to apply or lift. | | `keywordIds` | body | uuid[] | yes | Tracked keywords to change, all belonging to this app. | | `action` | body | 'add' | 'remove' | yes | Apply the tag or lift it. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request One command changes the whole set. Applying a tag a keyword already carries changes nothing, so a retry is safe. A keyword id belonging to another app or another owner is rejected outright rather than skipped. ``` curl --request POST 'https://api.peekpanda.com/v1/aso/apps/11111111-1111-4111-8111-111111111111/keywords/tags' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --header 'content-type: application/json' \ --data '{"tagId":"55555555-5555-4555-8555-555555555555","keywordIds":["22222222-2222-4222-8222-222222222222"],"action":"add"}' ``` ## MCP request Call manage_aso with action assign_tag. Path fields go in params, filters in query, and a write body in body. ``` { "name": "manage_aso", "arguments": { "request": { "action": "assign_tag", "params": { "appId": "" } } } } ``` ## Response The updated workstation. ``` { "app": { "storeId": "0000000000", "name": "Example Calorie Tracker", "iconUrl": null, "developerName": "Example Labs", "primaryGenre": "Health & Fitness", "rating": 4.7, "ratingCount": 12450, "inCatalogue": true, "id": "11111111-1111-4111-8111-111111111111", "kind": "apple", "folderId": null, "role": "mine", "createdAt": "2026-09-06T00:00:00.000Z", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "keywordCount": 1, "follows": { "chart": false, "ads": false, "organic": false, "keywords": true } }, "refresh": { "state": "fresh", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "nextAllowedAt": "2026-09-06T01:00:00.000Z", "staleQueryCount": 0, "activeJobId": null, "budget": { "userRefreshesToday": 1, "userDailyLimit": 40 } }, "profile": { "positionKind": "observed_search_api_position", "storeId": "0000000000", "coverage": { "queriesMatched": 0, "country": "us", "language": "en_us", "source": "itunes_search", "latestObservedAt": null, "observationCount": 0 }, "summary": { "top3": 0, "top10": 0, "top50": 0, "bestPosition": null, "medianPosition": null }, "keywords": [], "rivals": [], "keywordIdeas": [], "insights": [ { "kind": "coverage_gap", "title": "Not in the collected query set yet", "detail": "This app did not appear in any collected App Store search. That says nothing about its real rankings.", "keywords": [], "catalogueStoreId": null } ] }, "keywords": [ { "query": "calorie tracker", "position": 4, "trackedKeywordId": "22222222-2222-4222-8222-222222222222", "source": "manual", "note": null, "tags": [ { "id": "55555555-5555-4555-8555-555555555555", "name": "Potential", "color": "#0f9d58", "createdAt": "2026-09-06T00:00:00.000Z", "updatedAt": "2026-09-06T00:00:00.000Z" } ], "observedAt": "2026-09-06T00:00:00.000Z", "isStale": false, "resultCount": 1, "apps": [ { "storeId": "0000000000", "position": 4, "name": "Example Calorie Tracker", "isSubject": true, "inCatalogue": true } ], "history": [ { "observedAt": "2026-09-06T00:00:00.000Z", "position": 4 } ], "difficulty": { "score": 40 }, "demand": { "score": 90, "tier": 5, "country": "us", "periodStart": "2026-08-30", "periodEnd": "2026-09-05", "observedAt": "2026-09-06T00:00:00.000Z" } } ], "tags": [ { "id": "55555555-5555-4555-8555-555555555555", "name": "Potential", "color": "#0f9d58", "createdAt": "2026-09-06T00:00:00.000Z", "updatedAt": "2026-09-06T00:00:00.000Z" } ], "trackedCompetitors": [] } ``` ## Source Dated public App Store search results and suggestion observations for the US English storefront. Positions are observed result order. Difficulty and demand are directional indicators, not search volume or advertising data. ## Refresh This read returns stored observations. Use the related refresh command when newer evidence is needed. --- # Track an ASO app Start tracking an iOS app as mine or a competitor. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: POST /v1/aso/apps - Method: `POST` - Path: `/v1/aso/apps` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `manage_aso` - MCP action: `track` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `storeId` | body | numeric string | yes | Apple App Store identifier. | | `role` | body | mine | competitor | yes | Whether this is your app or a competitor. | | `follows` | body | { chart, ads, organic, keywords } | yes | Optional datasets to follow. Omitted, keywords stay on so existing ASO research is unchanged. At least one must be true. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request Body is `storeId`, `role` (`mine` or `competitor`), and optional `follows`. Tracking is capped at 25 apps. Turning ads on requests collection from that day; it never backfills. Initial keyword evidence collection may continue asynchronously. ``` curl --request POST 'https://api.peekpanda.com/v1/aso/apps' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --header 'content-type: application/json' \ --data '{"storeId":"0000000000","role":"mine"}' ``` ## MCP request Call manage_aso with action track. Path fields go in params, filters in query, and a write body in body. ``` { "name": "manage_aso", "arguments": { "request": { "action": "track" } } } ``` ## Response The created tracked-app object. Store id 0000000000 is a synthetic valid-format identifier. ``` { "storeId": "0000000000", "name": "Example Calorie Tracker", "iconUrl": null, "developerName": "Example Labs", "primaryGenre": "Health & Fitness", "rating": 4.7, "ratingCount": 12450, "inCatalogue": true, "id": "11111111-1111-4111-8111-111111111111", "kind": "apple", "folderId": null, "role": "mine", "createdAt": "2026-09-06T00:00:00.000Z", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "keywordCount": 1, "follows": { "chart": false, "ads": false, "organic": false, "keywords": true } } ``` ## Source Dated public App Store search results and suggestion observations for the US English storefront. Positions are observed result order. Difficulty and demand are directional indicators, not search volume or advertising data. ## Refresh Stored observations are reused while current. Explicit refresh commands run asynchronously and report freshness, progress and any applicable limits. --- # Add a temporary ASO app Add an app idea before it has an App Store listing. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: POST /v1/aso/apps/temporary - Method: `POST` - Path: `/v1/aso/apps/temporary` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `manage_aso` - MCP action: `add_temporary` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `name` | body | string | yes | Temporary app name. | | `folderId` | body | uuid | null | yes | Optional folder. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request Creates a temporary app without an App Store ID. Reuse the same Idempotency-Key when retrying this exact request. ``` curl --request POST 'https://api.peekpanda.com/v1/aso/apps/temporary' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ \ --header 'Idempotency-Key: temporary-app-example-1' --header 'content-type: application/json' \ --data '{"name":"New concept","folderId":"44444444-4444-4444-8444-444444444444"}' ``` ## MCP request Call manage_aso with action add_temporary. Path fields go in params, filters in query, and a write body in body. ``` { "name": "manage_aso", "arguments": { "request": { "action": "add_temporary" } } } ``` ## Response The temporary tracked-app object. ``` { "storeId": null, "name": "New concept", "iconUrl": null, "developerName": null, "primaryGenre": null, "rating": null, "ratingCount": null, "inCatalogue": false, "id": "11111111-1111-4111-8111-111111111111", "kind": "temporary", "folderId": "44444444-4444-4444-8444-444444444444", "role": "mine", "createdAt": "2026-09-06T00:00:00.000Z", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "keywordCount": 1, "follows": { "chart": false, "ads": false, "organic": false, "keywords": true } } ``` ## Source Dated public App Store search results and suggestion observations for the US English storefront. Positions are observed result order. Difficulty and demand are directional indicators, not search volume or advertising data. ## Refresh This read returns stored observations. Use the related refresh command when newer evidence is needed. --- # Get ASO workstation Read the stored ASO workstation for one tracked app. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/aso/apps/{appId} - Method: `GET` - Path: `/v1/aso/apps/{appId}` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `aso` - MCP action: `get` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `appId` | path | uuid | yes | Tracked ASO app id. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Read the stored workstation. Difficulty and demand are directional indicators, not search volume or advertising data. ``` curl --get 'https://api.peekpanda.com/v1/aso/apps/11111111-1111-4111-8111-111111111111' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call aso with action get. Path fields go in params, filters in query, and a write body in body. ``` { "name": "aso", "arguments": { "request": { "action": "get", "params": { "appId": "" } } } } ``` ## Response The response includes the tracked app, its saved keywords, observed positions, rivals, ideas, insights and refresh state for the US English storefront. Every keyword row is one saved keyword with its `trackedKeywordId` and `source`; rankings for searches the app has not saved are listed by keyword discovery, not here. ``` { "app": { "storeId": "0000000000", "name": "Example Calorie Tracker", "iconUrl": null, "developerName": "Example Labs", "primaryGenre": "Health & Fitness", "rating": 4.7, "ratingCount": 12450, "inCatalogue": true, "id": "11111111-1111-4111-8111-111111111111", "kind": "apple", "folderId": null, "role": "mine", "createdAt": "2026-09-06T00:00:00.000Z", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "keywordCount": 1, "follows": { "chart": false, "ads": false, "organic": false, "keywords": true } }, "refresh": { "state": "fresh", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "nextAllowedAt": "2026-09-06T01:00:00.000Z", "staleQueryCount": 0, "activeJobId": null, "budget": { "userRefreshesToday": 1, "userDailyLimit": 40 } }, "profile": { "positionKind": "observed_search_api_position", "storeId": "0000000000", "coverage": { "queriesMatched": 0, "country": "us", "language": "en_us", "source": "itunes_search", "latestObservedAt": null, "observationCount": 0 }, "summary": { "top3": 0, "top10": 0, "top50": 0, "bestPosition": null, "medianPosition": null }, "keywords": [], "rivals": [], "keywordIdeas": [], "insights": [ { "kind": "coverage_gap", "title": "Not in the collected query set yet", "detail": "This app did not appear in any collected App Store search. That says nothing about its real rankings.", "keywords": [], "catalogueStoreId": null } ] }, "keywords": [ { "query": "calorie tracker", "position": 4, "trackedKeywordId": "22222222-2222-4222-8222-222222222222", "source": "manual", "note": null, "tags": [ { "id": "55555555-5555-4555-8555-555555555555", "name": "Potential", "color": "#0f9d58", "createdAt": "2026-09-06T00:00:00.000Z", "updatedAt": "2026-09-06T00:00:00.000Z" } ], "observedAt": "2026-09-06T00:00:00.000Z", "isStale": false, "resultCount": 1, "apps": [ { "storeId": "0000000000", "position": 4, "name": "Example Calorie Tracker", "isSubject": true, "inCatalogue": true } ], "history": [ { "observedAt": "2026-09-06T00:00:00.000Z", "position": 4 } ], "difficulty": { "score": 40 }, "demand": { "score": 90, "tier": 5, "country": "us", "periodStart": "2026-08-30", "periodEnd": "2026-09-05", "observedAt": "2026-09-06T00:00:00.000Z" } } ], "tags": [ { "id": "55555555-5555-4555-8555-555555555555", "name": "Potential", "color": "#0f9d58", "createdAt": "2026-09-06T00:00:00.000Z", "updatedAt": "2026-09-06T00:00:00.000Z" } ], "trackedCompetitors": [] } ``` ## Source Dated public App Store search results and suggestion observations for the US English storefront. Positions are observed result order. Difficulty and demand are directional indicators, not search volume or advertising data. ## Refresh This read returns stored observations. Use the related refresh command when newer evidence is needed. --- # Update an ASO app role Mark a tracked iOS app as your app or a competitor, or change which datasets it follows. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: PATCH /v1/aso/apps/{appId} - Method: `PATCH` - Path: `/v1/aso/apps/{appId}` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `manage_aso` - MCP action: `update` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `appId` | path | uuid | yes | Tracked ASO app id. | | `role` | body | mine | competitor | yes | The explicit ownership role. | | `follows` | body | { chart, ads, organic, keywords } | yes | Optional datasets to follow. At least one must stay true. Turning every stamp off is rejected; remove the app instead. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request Changes the stored role and/or follows. It preserves tracked keywords and observations and does not call Apple. Follow-only patches do not start an ASO refresh. ``` curl --request PATCH 'https://api.peekpanda.com/v1/aso/apps/11111111-1111-4111-8111-111111111111' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --header 'content-type: application/json' \ --data '{"role":"mine"}' ``` ## MCP request Call manage_aso with action update. Path fields go in params, filters in query, and a write body in body. ``` { "name": "manage_aso", "arguments": { "request": { "action": "update", "params": { "appId": "" } } } } ``` ## Response The updated tracked-app object. ``` { "storeId": "0000000000", "name": "Example Calorie Tracker", "iconUrl": null, "developerName": "Example Labs", "primaryGenre": "Health & Fitness", "rating": 4.7, "ratingCount": 12450, "inCatalogue": true, "id": "11111111-1111-4111-8111-111111111111", "kind": "apple", "folderId": null, "role": "mine", "createdAt": "2026-09-06T00:00:00.000Z", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "keywordCount": 1, "follows": { "chart": false, "ads": false, "organic": false, "keywords": true } } ``` ## Source Dated public App Store search results and suggestion observations for the US English storefront. Positions are observed result order. Difficulty and demand are directional indicators, not search volume or advertising data. ## Refresh This read returns stored observations. Use the related refresh command when newer evidence is needed. --- # Remove an ASO app Stop tracking an iOS app. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: DELETE /v1/aso/apps/{appId} - Method: `DELETE` - Path: `/v1/aso/apps/{appId}` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `manage_aso` - MCP action: `untrack` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `appId` | path | uuid | yes | Tracked ASO app id. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request Deletes the tracked app. This write does not call Apple. ``` curl --request DELETE 'https://api.peekpanda.com/v1/aso/apps/11111111-1111-4111-8111-111111111111' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call manage_aso with action untrack. Path fields go in params, filters in query, and a write body in body. ``` { "name": "manage_aso", "arguments": { "request": { "action": "untrack", "params": { "appId": "" } } } } ``` ## Response Empty JSON object. The public Nest handler returns an empty 200. ``` {} ``` ## Source Dated public App Store search results and suggestion observations for the US English storefront. Positions are observed result order. Difficulty and demand are directional indicators, not search volume or advertising data. ## Refresh Stored observations are reused while current. Explicit refresh commands run asynchronously and report freshness, progress and any applicable limits. --- # Add ASO keywords Track keywords on an ASO app and return the updated workstation. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: POST /v1/aso/apps/{appId}/keywords - Method: `POST` - Path: `/v1/aso/apps/{appId}/keywords` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `manage_aso` - MCP action: `add_keywords` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `appId` | path | uuid | yes | Tracked ASO app id. | | `queries` | body | string[] | yes | Keywords to track; max 100 per app. | | `source` | body | manual | suggested | extracted | hint | yes | How the keywords were chosen. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request Adds queries (max 100 per app). Source is `manual`, `suggested`, `extracted`, or `hint`. The response returns the updated workstation while any needed collection continues asynchronously. ``` curl --request POST 'https://api.peekpanda.com/v1/aso/apps/11111111-1111-4111-8111-111111111111/keywords' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --header 'content-type: application/json' \ --data '{"queries":["calorie tracker"],"source":"manual"}' ``` ## MCP request Call manage_aso with action add_keywords. Path fields go in params, filters in query, and a write body in body. ``` { "name": "manage_aso", "arguments": { "request": { "action": "add_keywords", "params": { "appId": "" } } } } ``` ## Response Returns the same workstation response shape as GET /v1/aso/apps/{appId}. ``` { "app": { "storeId": "0000000000", "name": "Example Calorie Tracker", "iconUrl": null, "developerName": "Example Labs", "primaryGenre": "Health & Fitness", "rating": 4.7, "ratingCount": 12450, "inCatalogue": true, "id": "11111111-1111-4111-8111-111111111111", "kind": "apple", "folderId": null, "role": "mine", "createdAt": "2026-09-06T00:00:00.000Z", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "keywordCount": 1, "follows": { "chart": false, "ads": false, "organic": false, "keywords": true } }, "refresh": { "state": "fresh", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "nextAllowedAt": "2026-09-06T01:00:00.000Z", "staleQueryCount": 0, "activeJobId": null, "budget": { "userRefreshesToday": 1, "userDailyLimit": 40 } }, "profile": { "positionKind": "observed_search_api_position", "storeId": "0000000000", "coverage": { "queriesMatched": 0, "country": "us", "language": "en_us", "source": "itunes_search", "latestObservedAt": null, "observationCount": 0 }, "summary": { "top3": 0, "top10": 0, "top50": 0, "bestPosition": null, "medianPosition": null }, "keywords": [], "rivals": [], "keywordIdeas": [], "insights": [ { "kind": "coverage_gap", "title": "Not in the collected query set yet", "detail": "This app did not appear in any collected App Store search. That says nothing about its real rankings.", "keywords": [], "catalogueStoreId": null } ] }, "keywords": [ { "query": "calorie tracker", "position": 4, "trackedKeywordId": "22222222-2222-4222-8222-222222222222", "source": "manual", "note": null, "tags": [ { "id": "55555555-5555-4555-8555-555555555555", "name": "Potential", "color": "#0f9d58", "createdAt": "2026-09-06T00:00:00.000Z", "updatedAt": "2026-09-06T00:00:00.000Z" } ], "observedAt": "2026-09-06T00:00:00.000Z", "isStale": false, "resultCount": 1, "apps": [ { "storeId": "0000000000", "position": 4, "name": "Example Calorie Tracker", "isSubject": true, "inCatalogue": true } ], "history": [ { "observedAt": "2026-09-06T00:00:00.000Z", "position": 4 } ], "difficulty": { "score": 40 }, "demand": { "score": 90, "tier": 5, "country": "us", "periodStart": "2026-08-30", "periodEnd": "2026-09-05", "observedAt": "2026-09-06T00:00:00.000Z" } } ], "tags": [ { "id": "55555555-5555-4555-8555-555555555555", "name": "Potential", "color": "#0f9d58", "createdAt": "2026-09-06T00:00:00.000Z", "updatedAt": "2026-09-06T00:00:00.000Z" } ], "trackedCompetitors": [] } ``` ## Source Dated public App Store search results and suggestion observations for the US English storefront. Positions are observed result order. Difficulty and demand are directional indicators, not search volume or advertising data. ## Refresh Stored observations are reused while current. Explicit refresh commands run asynchronously and report freshness, progress and any applicable limits. --- # Update an ASO keyword Save a private note on one tracked keyword. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: PATCH /v1/aso/apps/{appId}/keywords/{keywordId} - Method: `PATCH` - Path: `/v1/aso/apps/{appId}/keywords/{keywordId}` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `manage_aso` - MCP action: `update_keyword` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `appId` | path | uuid | yes | Tracked ASO app id. | | `keywordId` | path | uuid | yes | Tracked keyword id. | | `note` | body | string | null | yes | Private note. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request Updates the private note. Keyword text and notes are never sent to analytics. ``` curl --request PATCH 'https://api.peekpanda.com/v1/aso/apps/11111111-1111-4111-8111-111111111111/keywords/22222222-2222-4222-8222-222222222222' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --header 'content-type: application/json' \ --data '{"note":"Test the subtitle."}' ``` ## MCP request Call manage_aso with action update_keyword. Path fields go in params, filters in query, and a write body in body. ``` { "name": "manage_aso", "arguments": { "request": { "action": "update_keyword", "params": { "appId": "", "keywordId": "" } } } } ``` ## Response The updated workstation. ``` { "app": { "storeId": "0000000000", "name": "Example Calorie Tracker", "iconUrl": null, "developerName": "Example Labs", "primaryGenre": "Health & Fitness", "rating": 4.7, "ratingCount": 12450, "inCatalogue": true, "id": "11111111-1111-4111-8111-111111111111", "kind": "apple", "folderId": null, "role": "mine", "createdAt": "2026-09-06T00:00:00.000Z", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "keywordCount": 1, "follows": { "chart": false, "ads": false, "organic": false, "keywords": true } }, "refresh": { "state": "fresh", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "nextAllowedAt": "2026-09-06T01:00:00.000Z", "staleQueryCount": 0, "activeJobId": null, "budget": { "userRefreshesToday": 1, "userDailyLimit": 40 } }, "profile": { "positionKind": "observed_search_api_position", "storeId": "0000000000", "coverage": { "queriesMatched": 0, "country": "us", "language": "en_us", "source": "itunes_search", "latestObservedAt": null, "observationCount": 0 }, "summary": { "top3": 0, "top10": 0, "top50": 0, "bestPosition": null, "medianPosition": null }, "keywords": [], "rivals": [], "keywordIdeas": [], "insights": [ { "kind": "coverage_gap", "title": "Not in the collected query set yet", "detail": "This app did not appear in any collected App Store search. That says nothing about its real rankings.", "keywords": [], "catalogueStoreId": null } ] }, "keywords": [ { "query": "calorie tracker", "position": 4, "trackedKeywordId": "22222222-2222-4222-8222-222222222222", "source": "manual", "note": null, "tags": [ { "id": "55555555-5555-4555-8555-555555555555", "name": "Potential", "color": "#0f9d58", "createdAt": "2026-09-06T00:00:00.000Z", "updatedAt": "2026-09-06T00:00:00.000Z" } ], "observedAt": "2026-09-06T00:00:00.000Z", "isStale": false, "resultCount": 1, "apps": [ { "storeId": "0000000000", "position": 4, "name": "Example Calorie Tracker", "isSubject": true, "inCatalogue": true } ], "history": [ { "observedAt": "2026-09-06T00:00:00.000Z", "position": 4 } ], "difficulty": { "score": 40 }, "demand": { "score": 90, "tier": 5, "country": "us", "periodStart": "2026-08-30", "periodEnd": "2026-09-05", "observedAt": "2026-09-06T00:00:00.000Z" } } ], "tags": [ { "id": "55555555-5555-4555-8555-555555555555", "name": "Potential", "color": "#0f9d58", "createdAt": "2026-09-06T00:00:00.000Z", "updatedAt": "2026-09-06T00:00:00.000Z" } ], "trackedCompetitors": [] } ``` ## Source Dated public App Store search results and suggestion observations for the US English storefront. Positions are observed result order. Difficulty and demand are directional indicators, not search volume or advertising data. ## Refresh This read returns stored observations. Use the related refresh command when newer evidence is needed. --- # Remove an ASO keyword Remove one tracked keyword from an ASO app. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: DELETE /v1/aso/apps/{appId}/keywords/{keywordId} - Method: `DELETE` - Path: `/v1/aso/apps/{appId}/keywords/{keywordId}` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `manage_aso` - MCP action: `remove_keyword` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `appId` | path | uuid | yes | Tracked ASO app id. | | `keywordId` | path | uuid | yes | Tracked keyword id. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request Removes one tracked keyword. This write does not call Apple. ``` curl --request DELETE 'https://api.peekpanda.com/v1/aso/apps/11111111-1111-4111-8111-111111111111/keywords/22222222-2222-4222-8222-222222222222' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call manage_aso with action remove_keyword. Path fields go in params, filters in query, and a write body in body. ``` { "name": "manage_aso", "arguments": { "request": { "action": "remove_keyword", "params": { "appId": "", "keywordId": "" } } } } ``` ## Response Empty JSON object. The public Nest handler returns an empty 200. ``` {} ``` ## Source Dated public App Store search results and suggestion observations for the US English storefront. Positions are observed result order. Difficulty and demand are directional indicators, not search volume or advertising data. ## Refresh Stored observations are reused while current. Explicit refresh commands run asynchronously and report freshness, progress and any applicable limits. --- # Refresh an ASO app Queue stale queries for one tracked app. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: POST /v1/aso/apps/{appId}/refresh - Method: `POST` - Path: `/v1/aso/apps/{appId}/refresh` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `refresh` - MCP action: `aso_app` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `appId` | path | uuid | yes | Tracked ASO app id. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request Requests newer observations for stale or never-collected queries. The response reports whether work was accepted, already current or temporarily limited. ``` curl --request POST 'https://api.peekpanda.com/v1/aso/apps/11111111-1111-4111-8111-111111111111/refresh' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call refresh with action aso_app. Path fields go in params, filters in query, and a write body in body. ``` { "name": "refresh", "arguments": { "request": { "action": "aso_app", "params": { "appId": "" } } } } ``` ## Response `queries` lists the searches accepted for refresh and is empty when everything was already current. ``` { "jobId": "33333333-3333-4333-8333-333333333333", "queries": [ "calorie tracker" ], "refresh": { "state": "fresh", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "nextAllowedAt": "2026-09-06T01:00:00.000Z", "staleQueryCount": 0, "activeJobId": null, "budget": { "userRefreshesToday": 1, "userDailyLimit": 40 } } } ``` ## Source Dated public App Store search results and suggestion observations for the US English storefront. Positions are observed result order. Difficulty and demand are directional indicators, not search volume or advertising data. ## Refresh Stored observations are reused while current. Explicit refresh commands run asynchronously and report freshness, progress and any applicable limits. --- # Get ASO keyword gap Compare stored keyword positions between two apps. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/aso/apps/{appId}/gap/{rivalStoreId} - Method: `GET` - Path: `/v1/aso/apps/{appId}/gap/{rivalStoreId}` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `aso` - MCP action: `gap` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `appId` | path | uuid | yes | Tracked ASO app id. | | `rivalStoreId` | path | numeric string | yes | Rival Apple App Store identifier. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Compares stored positions for the subject app and a rival store id. This GET never calls Apple. ``` curl --get 'https://api.peekpanda.com/v1/aso/apps/11111111-1111-4111-8111-111111111111/gap/0000000001' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call aso with action gap. Path fields go in params, filters in query, and a write body in body. ``` { "name": "aso", "arguments": { "request": { "action": "gap", "params": { "appId": "", "rivalStoreId": "" } } } } ``` ## Response Only-rival, only-subject and shared rows contain stored observed positions, not estimates. ``` { "subjectStoreId": "0000000000", "rivalStoreId": "0000000001", "rivalName": "Rival Tracker", "onlyRival": [ { "query": "calorie counter", "rivalPosition": 2 } ], "onlySubject": [ { "query": "calorie tracker", "subjectPosition": 4 } ], "shared": [ { "query": "food diary", "subjectPosition": 8, "rivalPosition": 3 } ] } ``` ## Source Dated public App Store search results and suggestion observations for the US English storefront. Positions are observed result order. Difficulty and demand are directional indicators, not search volume or advertising data. ## Refresh This read returns stored observations. Use the related refresh command when newer evidence is needed. --- # Search iOS apps Search the live App Store by name. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: POST /v1/aso/app-search - Method: `POST` - Path: `/v1/aso/app-search` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `manage_aso` - MCP action: `search_ios` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `term` | body | string | yes | App Store search text. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request This explicit command searches Apple's public App Store by name. Standard API rate limits apply. ``` curl --request POST 'https://api.peekpanda.com/v1/aso/app-search' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --header 'content-type: application/json' \ --data '{"term":"calorie"}' ``` ## MCP request Call manage_aso with action search_ios. Path fields go in params, filters in query, and a write body in body. ``` { "name": "manage_aso", "arguments": { "request": { "action": "search_ios" } } } ``` ## Response The response contains public App Store matches for the supplied term. ``` { "data": [ { "storeId": "0000000000", "name": "Example Calorie Tracker", "iconUrl": null, "developerName": "Example Labs", "primaryGenre": "Health & Fitness", "rating": 4.7, "ratingCount": 12450, "inCatalogue": true } ], "term": "calorie" } ``` ## Source Dated public App Store search results and suggestion observations for the US English storefront. Positions are observed result order. Difficulty and demand are directional indicators, not search volume or advertising data. ## Refresh This explicit search command reads the public App Store. Standard API rate limits apply. --- # Explore an ASO keyword Read the stored explore view for one keyword. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/aso/keywords/{query} - Method: `GET` - Path: `/v1/aso/keywords/{query}` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `aso` - MCP action: `explore` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `query` | path | string | yes | Keyword to explore. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Reads the stored keyword observation and its freshness state. ``` curl --get 'https://api.peekpanda.com/v1/aso/keywords/calorie%20tracker' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call aso with action explore. Path fields go in params, filters in query, and a write body in body. ``` { "name": "aso", "arguments": { "request": { "action": "explore", "params": { "query": "" } } } } ``` ## Response The response includes observed apps, directional difficulty and demand indicators, related suggestions and refresh state. ``` { "query": "calorie tracker", "country": "us", "observedAt": "2026-09-06T00:00:00.000Z", "isStale": false, "apps": [ { "storeId": "0000000000", "position": 4, "name": "Example Calorie Tracker", "isSubject": false, "inCatalogue": true } ], "difficulty": { "score": 40 }, "demand": { "score": 90, "tier": 5, "country": "us", "periodStart": "2026-08-30", "periodEnd": "2026-09-05", "observedAt": "2026-09-06T00:00:00.000Z" }, "relatedHints": [ "calorie tracker app" ], "refresh": { "state": "fresh", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "nextAllowedAt": "2026-09-06T01:00:00.000Z", "staleQueryCount": 0, "activeJobId": null, "budget": { "userRefreshesToday": 1, "userDailyLimit": 40 } } } ``` ## Source Dated public App Store search results and suggestion observations for the US English storefront. Positions are observed result order. Difficulty and demand are directional indicators, not search volume or advertising data. ## Refresh This read returns stored observations. Use the related refresh command when newer evidence is needed. --- # Refresh an ASO keyword Queue a refresh for one keyword snapshot. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: POST /v1/aso/keywords/{query}/refresh - Method: `POST` - Path: `/v1/aso/keywords/{query}/refresh` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `refresh` - MCP action: `aso_keyword` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `query` | path | string | yes | Keyword to refresh. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request Requests a newer observation when the stored keyword evidence is missing or stale. ``` curl --request POST 'https://api.peekpanda.com/v1/aso/keywords/calorie%20tracker/refresh' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call refresh with action aso_keyword. Path fields go in params, filters in query, and a write body in body. ``` { "name": "refresh", "arguments": { "request": { "action": "aso_keyword", "params": { "query": "" } } } } ``` ## Response `queries` lists the searches accepted for refresh and is empty when the observation was already current. ``` { "jobId": "33333333-3333-4333-8333-333333333333", "queries": [ "calorie tracker" ], "refresh": { "state": "fresh", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "nextAllowedAt": "2026-09-06T01:00:00.000Z", "staleQueryCount": 0, "activeJobId": null, "budget": { "userRefreshesToday": 1, "userDailyLimit": 40 } } } ``` ## Source Dated public App Store search results and suggestion observations for the US English storefront. Positions are observed result order. Difficulty and demand are directional indicators, not search volume or advertising data. ## Refresh Stored observations are reused while current. Explicit refresh commands run asynchronously and report freshness, progress and any applicable limits. --- # Get ASO case evidence Read the compact ASO profile and keyword evidence shown on Home for one to six tracked apps. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/aso/case-evidence - Method: `GET` - Path: `/v1/aso/case-evidence` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `aso` - MCP action: `evidence` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `appIds` | query | uuid, 1 to 6 | yes | Tracked app ids, comma-separated. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Pass one to six tracked app ids. This read does not call Apple. ``` curl --get 'https://api.peekpanda.com/v1/aso/case-evidence' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --data-urlencode 'appIds=11111111-1111-4111-8111-111111111111' ``` ## MCP request Call aso with action evidence. Path fields go in params, filters in query, and a write body in body. ``` { "name": "aso", "arguments": { "request": { "action": "evidence", "query": { "appIds": "" } } } } ``` ## Response data holds one row per requested app, at most six. profile is null until ASO evidence is stored. keywords is empty when none are stored. ``` { "data": [ { "appId": "11111111-1111-4111-8111-111111111111", "profile": null, "keywords": [] } ] } ``` ## Source Stored ASO profile and keyword evidence for apps the caller tracks. ## Refresh This GET never starts collection. --- # Browse ASO keyword discovery Page unsaved observed rankings or derived keyword ideas for one tracked app. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/aso/apps/{appId}/keyword-discovery - Method: `GET` - Path: `/v1/aso/apps/{appId}/keyword-discovery` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `aso` - MCP action: `discover` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `appId` | path | uuid | yes | Tracked app id. | | `sourceGroup` | query | string | no | observed or derived. Defaults to observed. | | `search` | query | string | no | Optional keyword text, up to 200 characters. | | `cursor` | query | string | no | Page cursor from the previous response. | | `limit` | query | integer | no | Page size, 1 to 100. Defaults to 50. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request appId is the tracked app. sourceGroup selects observed rankings or derived ideas. Pass the returned cursor to read the next page. ``` curl --get 'https://api.peekpanda.com/v1/aso/apps/11111111-1111-4111-8111-111111111111/keyword-discovery' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --data-urlencode 'sourceGroup=observed' ``` ## MCP request Call aso with action discover. Path fields go in params, filters in query, and a write body in body. ``` { "name": "aso", "arguments": { "request": { "action": "discover", "params": { "appId": "" } } } } ``` ## Response phase matches the source group you asked for. An empty data array means this phase has no rows yet. ``` { "phase": "observed", "data": [], "nextCursor": null } ``` ## Source Stored observations and derived ideas for apps you track. ## Refresh This read does not collect. Refresh the app when you want newer observations. --- # Search organic content Search the copied public organic corpus with bounded filters. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/organic - Method: `GET` - Path: `/v1/organic` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `search_organic` - MCP action: `search` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `q` | query | string, 1–200 | no | Free-text search. | | `category` | query | Health & Wellness | Personal Development | Relationships & Lifestyle | Arts, Hobbies & Entertainment | Education & Knowledge | Fashion & Beauty | Spirituality & Beliefs | Business & Finance | Travel | Food & Cooking | Home & Design | Fitness | Technology | Gaming & Esports | Productivity & Organization | no | Exact organic corpus category. | | `business_type` | query | app | ecommerce | saas | service | affiliate | other | creator | game | book | unclassified | no | Exact business-type filter. | | `platform` | query | tiktok | instagram | threads | no | Organic platform filter. | | `content_type` | query | slideshow | video | mixed | no | Content-type filter. | | `account` | query | string, 1–200 | no | Creator account filter. | | `product` | query | string, 1–200 | no | Product-name filter. | | `max_slides` | query | integer, 1–100 | no | Maximum slide count. | | `published_since` | query | date, YYYY-MM-DD | no | Inclusive calendar start date. | | `published_until` | query | date, YYYY-MM-DD | no | Inclusive calendar end date; must be at or after published_since. | | `sort` | query | relevance | newest | latest_posted | most_views | most_likes | most_comments | most_slides | no | Defaults to relevance when q is set and newest otherwise. | | `limit` | query | integer, 1–40 | no | Page size; defaults to 12. offset + limit cannot exceed 1000. | | `offset` | query | integer, 0–960 | no | Result offset; defaults to 0. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Send filters as query parameters. This read serves stored corpus rows and does not scrape TikTok. ``` curl --get 'https://api.peekpanda.com/v1/organic' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --data-urlencode 'q=meal planner' \ --data-urlencode 'platform=tiktok' \ --data-urlencode 'limit=12' ``` ## MCP request Call search_organic with action search. Path fields go in params, filters in query, and a write body in body. ``` { "name": "search_organic", "arguments": { "request": { "action": "search" } } } ``` ## Response posts is a page of organic corpus rows. product_app is null when no catalogue app was matched; product_app.matchedBy = product_name means a conservative inferred association, not verified ownership. slide_texts is null on list results. relevance is always null. next_offset is null on the last page or when the offset cap is reached. ``` { "posts": [ { "id": "organic-post-example", "platform": "tiktok", "content_type": "slideshow", "link": "https://www.tiktok.com/@example/video/1", "category": "Food & Cooking", "business_type": "app", "product_name": "Example Meal Planner", "product_website": "https://example.com", "product_app": { "platform": "ios", "storeId": "6480417616", "appName": "Example Meal Planner", "iconUrl": "https://is1-ssl.mzstatic.com/example-meal-planner.jpg", "matchedBy": "product_name" }, "account": "example", "caption": "five dinners in ten minutes", "hashtags": [ "#cooking", "#mealprep" ], "date_published": "2026-04-02", "metrics": { "views": 412000, "likes": 31000, "comments": 480, "shares": 210, "bookmarks": 9400 }, "slide_count": 7, "preview_image_url": "https://cdn.example.com/preview.jpg", "image_urls": [ "https://cdn.example.com/preview.jpg" ], "video_url": null, "hook_text": "I stopped meal planning and did this instead", "hook_analysis": { "content_class": "listicle_hook", "format_template": "I stopped X and did Y", "psychology_tags": [ "curiosity", "relief" ] }, "search_summary": "A slideshow about weeknight cooking for a meal-planning app.", "niche": "meal planning", "topics": [ "cooking", "meal prep" ], "search_keywords": [ "dinner ideas", "meal planner app" ], "slide_texts": null, "relevance": null } ], "total": 1, "has_more": false, "next_offset": null } ``` ## Source Stored public organic content with its original source identity and observation date. ## Refresh This read returns stored content and does not begin collection. --- # Get organic content Read one stored organic corpus post by its original source id. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/organic/{postId} - Method: `GET` - Path: `/v1/organic/{postId}` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `search_organic` - MCP action: `get` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `postId` | path | string, 1–300 | yes | Organic corpus post id. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request The path id is the original corpus source id. This read does not scrape TikTok. ``` curl --get 'https://api.peekpanda.com/v1/organic/organic-post-example' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call search_organic with action get. Path fields go in params, filters in query, and a write body in body. ``` { "name": "search_organic", "arguments": { "request": { "action": "get", "params": { "postId": "" } } } } ``` ## Response The same post object as search. product_app.matchedBy = product_name means a conservative inferred association, not verified ownership. get may include slide_texts as an array. relevance is always null. ``` { "id": "organic-post-example", "platform": "tiktok", "content_type": "slideshow", "link": "https://www.tiktok.com/@example/video/1", "category": "Food & Cooking", "business_type": "app", "product_name": "Example Meal Planner", "product_website": "https://example.com", "product_app": { "platform": "ios", "storeId": "6480417616", "appName": "Example Meal Planner", "iconUrl": "https://is1-ssl.mzstatic.com/example-meal-planner.jpg", "matchedBy": "product_name" }, "account": "example", "caption": "five dinners in ten minutes", "hashtags": [ "#cooking", "#mealprep" ], "date_published": "2026-04-02", "metrics": { "views": 412000, "likes": 31000, "comments": 480, "shares": 210, "bookmarks": 9400 }, "slide_count": 7, "preview_image_url": "https://cdn.example.com/preview.jpg", "image_urls": [ "https://cdn.example.com/preview.jpg" ], "video_url": null, "hook_text": "I stopped meal planning and did this instead", "hook_analysis": { "content_class": "listicle_hook", "format_template": "I stopped X and did Y", "psychology_tags": [ "curiosity", "relief" ] }, "search_summary": "A slideshow about weeknight cooking for a meal-planning app.", "niche": "meal planning", "topics": [ "cooking", "meal prep" ], "search_keywords": [ "dinner ideas", "meal planner app" ], "slide_texts": [ "I stopped meal planning and did this instead", "Shop once. Cook five nights." ], "relevance": null } ``` ## Source Stored public organic content with its original source identity and observation date. ## Refresh This read returns stored content and does not begin collection. --- # List saved organic content List saved organic corpus ids for the authenticated caller. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/organic/saved - Method: `GET` - Path: `/v1/organic/saved` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `search_organic` - MCP action: `saved` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `cursor` | query | opaque string, 1–512 | no | Cursor returned by the previous page. | | `limit` | query | integer, 1–1000 | no | Page size; defaults to 1000. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Returns saved ids, not full post cards. Follow nextCursor with the same limit. ``` curl --get 'https://api.peekpanda.com/v1/organic/saved' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call search_organic with action saved. Path fields go in params, filters in query, and a write body in body. ``` { "name": "search_organic", "arguments": { "request": { "action": "saved" } } } ``` ## Response bookmarkedIds are actor-owned organic corpus ids. nextCursor is null on the last page. ``` { "bookmarkedIds": [ "organic-post-example" ], "nextCursor": null, "hasMore": false } ``` ## Source Actor-owned bookmarks on copied public organic corpus IDs. The post identity is the original source ID. ## Refresh Save and remove update account bookmark state only. They do not refresh or recopy the organic corpus. --- # Save organic content Save one organic corpus post to the authenticated caller account bookmarks. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: POST /v1/organic/{postId}/saved - Method: `POST` - Path: `/v1/organic/{postId}/saved` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `save` - MCP action: `organic` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `postId` | path | string, 1–300 | yes | Organic corpus post id. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request The command has no request body. Repeating it is naturally idempotent. ``` curl --request POST 'https://api.peekpanda.com/v1/organic/organic-post-example/saved' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call save with action organic. Path fields go in params, filters in query, and a write body in body. ``` { "name": "save", "arguments": { "request": { "action": "organic", "params": { "postId": "" } } } } ``` ## Response Returns Bookmark added, or Already bookmarked when the id is already saved. ``` { "message": "Bookmark added" } ``` ## Source Actor-owned bookmarks on copied public organic corpus IDs. The post identity is the original source ID. ## Refresh Save and remove update account bookmark state only. They do not refresh or recopy the organic corpus. --- # Remove saved organic content Remove one organic corpus post from the authenticated caller account bookmarks. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: DELETE /v1/organic/{postId}/saved - Method: `DELETE` - Path: `/v1/organic/{postId}/saved` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `save` - MCP action: `unsave_organic` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `postId` | path | string, 1–300 | yes | Organic corpus post id. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request The command has no request body. Repeating it is naturally idempotent. ``` curl --request DELETE 'https://api.peekpanda.com/v1/organic/organic-post-example/saved' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call save with action unsave_organic. Path fields go in params, filters in query, and a write body in body. ``` { "name": "save", "arguments": { "request": { "action": "unsave_organic", "params": { "postId": "" } } } } ``` ## Response Returns Bookmark removed after the actor-owned bookmark is cleared. ``` { "message": "Bookmark removed" } ``` ## Source Actor-owned bookmarks on copied public organic corpus IDs. The post identity is the original source ID. ## Refresh Save and remove update account bookmark state only. They do not refresh or recopy the organic corpus. --- # Search paid ads Search stored Meta and TikTok ad-library observations. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/paid-ads - Method: `GET` - Path: `/v1/paid-ads` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_ads` - MCP action: `search` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `cursor` | query | opaque string | no | Cursor returned by the previous page. | | `limit` | query | integer, 1–40 | no | Page size; defaults to 18. | | `search` | query | string, max 120 | no | Advertiser or creative search text. Recognized category terms also include ads linked to that canonical category. | | `platform` | query | meta | tiktok | no | Advertising platform filter. | | `status` | query | active | inactive | unknown | no | Observed ad status. | | `advertiser` | query | string, max 120 | no | Advertiser name filter. | | `category` | query | Health & Wellness | Personal Development | Relationships & Lifestyle | Arts, Hobbies & Entertainment | Education & Knowledge | Fashion & Beauty | Spirituality & Beliefs | Business & Finance | Travel | Food & Cooking | Home & Design | Fitness | Technology | Gaming & Esports | Productivity & Organization | no | Canonical English category of the verified linked iOS app; known source aliases are grouped automatically. | | `format` | query | image | video | carousel | dynamic | unknown | no | Creative format filter. | | `startDate` | query | YYYY-MM-DD | no | Earliest disclosed ad start date, inclusive. | | `endDate` | query | YYYY-MM-DD | no | Latest disclosed ad start date, inclusive. | | `minRunDays` | query | 7 | 30 | 90 | 180 | 365 | no | Minimum observed run length. | | `cta` | query | present | missing | no | Whether the creative has a call to action. | | `copyLength` | query | short | medium | long | no | Primary-text length bucket. | | `reach` | query | reported | 10000 | 100000 | 1000000 | no | Require reported reach, optionally above a threshold. | | `appLink` | query | linked | unlinked | no | Whether the ad has a verified App Store match. | | `saved` | query | boolean | true | false | no | When true, only account-saved ads. | | `sort` | query | balanced | recently_discovered | newest_started | longest_running | most_tested | highest_reported_reach | no | Defaults to balanced. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Send the documented filters as query parameters. This read does not collect new ads. ``` curl --get 'https://api.peekpanda.com/v1/paid-ads' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --data-urlencode 'platform=meta' \ --data-urlencode 'limit=18' \ --data-urlencode 'sort=balanced' ``` ## MCP request Call get_ads with action search. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_ads", "arguments": { "request": { "action": "search" } } } ``` ## Response items are stored paid-ad observations. canonicalKey matches paid:(meta|tiktok):{archiveId}. performance is unavailable or a reported_reach disclosure. There is no spend field. Use nextCursor to load the next page; this response does not compute collection totals. ``` { "items": [ { "id": "33333333-3333-4333-8333-333333333333", "canonicalKey": "paid:meta:111222333", "source": "paid", "platform": "meta", "archiveId": "111222333", "advertiser": { "externalId": "444555666", "name": "Example Advertiser" }, "status": "active", "startedAt": "2026-08-01T00:00:00.000Z", "endedAt": null, "discoveredAt": "2026-09-06T00:00:00.000Z", "sourceUrl": "https://www.facebook.com/ads/library?id=111222333", "creative": { "format": "video", "hook": "Turn saved recipes into a grocery list", "body": "Turn saved recipes into a grocery list.", "title": "Example Meal Planner", "description": null, "ctaText": "Download", "imageUrls": [ "https://cdn.example.com/ad.jpg" ], "videoUrls": [ "https://cdn.example.com/ad.mp4" ], "units": [] }, "productLink": { "kind": "official_domain", "name": "Example Meal Planner", "url": "https://example.com", "externalId": null, "confidence": "unresolved" }, "destination": { "kind": "web", "url": "https://example.com", "host": "example.com", "page": "example.com", "appStoreId": null }, "productIconUrl": null, "performance": { "kind": "unavailable" }, "saved": false } ], "nextCursor": null, "hasMore": false } ``` ## Source Stored public advertising-library observations with source dates. Spend is unavailable unless the public record reports it. ## Refresh This read returns stored observations. Save and remove update account bookmarks only. --- # Get paid ad Read one stored paid-ad observation with its matched listing and the ads around it. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/paid-ads/{platform}/{archiveId} - Method: `GET` - Path: `/v1/paid-ads/{platform}/{archiveId}` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_ads` - MCP action: `get` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | meta | tiktok | yes | Advertising platform. | | `archiveId` | path | string, 1–255 | yes | Provider archive id. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Identify the ad with the platform and archive id carried in its canonicalKey. This read returns stored observations and never collects new ads. ``` curl --get 'https://api.peekpanda.com/v1/paid-ads/meta/111222333' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call get_ads with action get. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_ads", "arguments": { "request": { "action": "get", "params": { "platform": "", "archiveId": "" } } } } ``` ## Response item is the stored observation. product is the matched iOS listing, or null when the ad carries no verified store link. sameAppAds holds up to six further stored ads for that listing and otherAppAds up to six for other apps. transcript and analysis are null until they have been collected. The route answers 404 when no stored ad matches the identity. ``` { "item": { "id": "33333333-3333-4333-8333-333333333333", "canonicalKey": "paid:meta:111222333", "source": "paid", "platform": "meta", "archiveId": "111222333", "advertiser": { "externalId": "444555666", "name": "Example Advertiser" }, "status": "active", "startedAt": "2026-08-01T00:00:00.000Z", "endedAt": null, "discoveredAt": "2026-09-06T00:00:00.000Z", "sourceUrl": "https://www.facebook.com/ads/library?id=111222333", "creative": { "format": "video", "hook": "Turn saved recipes into a grocery list", "body": "Turn saved recipes into a grocery list.", "title": "Example Meal Planner", "description": null, "ctaText": "Download", "imageUrls": [ "https://cdn.example.com/ad.jpg" ], "videoUrls": [ "https://cdn.example.com/ad.mp4" ], "units": [] }, "productLink": { "kind": "official_domain", "name": "Example Meal Planner", "url": "https://example.com", "externalId": null, "confidence": "unresolved" }, "destination": { "kind": "web", "url": "https://example.com", "host": "example.com", "page": "example.com", "appStoreId": null }, "productIconUrl": null, "performance": { "kind": "unavailable" }, "saved": false }, "product": null, "sameAppAds": [], "otherAppAds": [], "transcript": null, "analysis": null } ``` ## Source Stored public advertising-library observations with source dates. Spend is unavailable unless the public record reports it. ## Refresh This read returns stored observations. Save and remove update account bookmarks only. --- # Generate paid-ad transcript Generate and persist a transcript from one stored ad’s mirrored video. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: POST /v1/paid-ads/{platform}/{archiveId}/transcript - Method: `POST` - Path: `/v1/paid-ads/{platform}/{archiveId}/transcript` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `refresh` - MCP action: `ad_transcript` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | meta | tiktok | yes | Advertising platform. | | `archiveId` | path | string, 1–255 | yes | Provider archive id. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request The command has no request body. It uses only a stored, mirrored video; image-only ads return 422. A concurrent generation returns 409, while a ready transcript is returned from storage. ``` curl --request POST 'https://api.peekpanda.com/v1/paid-ads/meta/111222333/transcript' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call refresh with action ad_transcript. Path fields go in params, filters in query, and a write body in body. ``` { "name": "refresh", "arguments": { "request": { "action": "ad_transcript", "params": { "platform": "", "archiveId": "" } } } } ``` ## Response Returns the persisted transcript with word timing, language, and generation-time provenance when available. ``` { "text": "Turn saved recipes into a grocery list.", "words": null, "language": "en", "model": "transcription-model", "generatedAt": "2026-09-06T00:00:00.000Z" } ``` ## Source Stored public advertising-library observations with source dates. Spend is unavailable unless the public record reports it. ## Refresh An explicit generation uses the owned mirror and persists its result. Repeating after settlement returns the stored result without generating it again. --- # Analyze paid-ad creative Generate and persist creative analysis from one stored ad’s owned media. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: POST /v1/paid-ads/{platform}/{archiveId}/analysis - Method: `POST` - Path: `/v1/paid-ads/{platform}/{archiveId}/analysis` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `refresh` - MCP action: `ad_analysis` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | meta | tiktok | yes | Advertising platform. | | `archiveId` | path | string, 1–255 | yes | Provider archive id. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request The command has no request body. It accepts mirrored image or video media up to 25 MB. A concurrent generation returns 409, while a ready analysis is returned from storage. ``` curl --request POST 'https://api.peekpanda.com/v1/paid-ads/meta/111222333/analysis' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call refresh with action ad_analysis. Path fields go in params, filters in query, and a write body in body. ``` { "name": "refresh", "arguments": { "request": { "action": "ad_analysis", "params": { "platform": "", "archiveId": "" } } } } ``` ## Response Returns the persisted creative analysis and its generation-time provenance. Treat creative observations as analysis, not a performance or policy claim. ``` { "summary": "A concise product demonstration with a utility-first opening.", "visibleText": [ "Turn saved recipes into a grocery list" ], "hookNotes": [ "Opens with the customer outcome before showing the product." ], "timestampNotes": [ { "timestampSeconds": 0, "note": "The app result appears immediately." } ], "editingPattern": "Fast product demonstration with direct onscreen text.", "reusablePromptIngredients": [ "utility-first hook", "product demonstration" ], "model": "analysis-model", "generatedAt": "2026-09-06T00:00:00.000Z" } ``` ## Source Stored public advertising-library observations with source dates. Spend is unavailable unless the public record reports it. ## Refresh An explicit generation uses the owned mirror and persists its result. Repeating after settlement returns the stored result without generating it again. --- # Save paid ad Save one stored paid-ad observation to the authenticated caller account bookmarks. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: POST /v1/paid-ads/{platform}/{archiveId}/saved - Method: `POST` - Path: `/v1/paid-ads/{platform}/{archiveId}/saved` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `save` - MCP action: `ad` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | meta | tiktok | yes | Advertising platform. | | `archiveId` | path | string, 1–255 | yes | Provider archive id. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request The command has no request body. Repeating it is naturally idempotent. ``` curl --request POST 'https://api.peekpanda.com/v1/paid-ads/meta/111222333/saved' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call save with action ad. Path fields go in params, filters in query, and a write body in body. ``` { "name": "save", "arguments": { "request": { "action": "ad", "params": { "platform": "", "archiveId": "" } } } } ``` ## Response Confirms the actor-owned paid-ad bookmark was saved. ``` { "message": "Saved paid ad" } ``` ## Source Account bookmarks on stored Meta/TikTok ad-library observations. Not Apple. ## Refresh Save and remove update account bookmark state only. --- # Remove saved paid ad Remove one stored paid-ad observation from the authenticated caller account bookmarks. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: DELETE /v1/paid-ads/{platform}/{archiveId}/saved - Method: `DELETE` - Path: `/v1/paid-ads/{platform}/{archiveId}/saved` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `save` - MCP action: `unsave_ad` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | meta | tiktok | yes | Advertising platform. | | `archiveId` | path | string, 1–255 | yes | Provider archive id. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 409 conflict when the write would duplicate an exclusive row. ## Request The command has no request body. Repeating it is naturally idempotent. ``` curl --request DELETE 'https://api.peekpanda.com/v1/paid-ads/meta/111222333/saved' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call save with action unsave_ad. Path fields go in params, filters in query, and a write body in body. ``` { "name": "save", "arguments": { "request": { "action": "unsave_ad", "params": { "platform": "", "archiveId": "" } } } } ``` ## Response Confirms the actor-owned paid-ad bookmark was removed. ``` { "message": "Removed saved paid ad" } ``` ## Source Account bookmarks on stored Meta/TikTok ad-library observations. Not Apple. ## Refresh Save and remove update account bookmark state only. --- # Read paid-ad rankings Rank advertisers by how many creatives they ship and how long those creatives stay live. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/paid-ads/intelligence - Method: `GET` - Path: `/v1/paid-ads/intelligence` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_ads` - MCP action: `intelligence` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `advertiserExternalId` | query | string | no | Limit the ranking to one advertiser. | | `category` | query | category | no | Limit the ranking to one category. | | `platform` | query | meta | tiktok | no | Advertising platform. | | `advertiserLimit` | query | integer, 1-50 | no | Advertisers to return. Defaults to 25. | | `cadenceWeeks` | query | integer, 4-52 | no | ISO weeks of new-creative cadence. Defaults to 12. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Every filter is optional. This read never starts collection and never ranks on money. ``` curl --get 'https://api.peekpanda.com/v1/paid-ads/intelligence' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call get_ads with action intelligence. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_ads", "arguments": { "request": { "action": "intelligence" } } } ``` ## Response totals cover the stored library in scope. advertisers, longevity, formats, ctas, destinations, categories and cadence are empty when nothing is stored. scope is null for the unfiltered library. ``` { "generatedAt": "2026-09-06T00:00:00.000Z", "scope": null, "totals": { "ads": 0, "liveAds": 0, "advertisers": 0, "liveAdvertisers": 0, "newAdsLast7Days": 0, "advertisersShippingLast7Days": 0, "medianRunDays": null, "longestRunDays": null, "liveBeyond60Days": 0, "reachDisclosedAds": 0, "unlinkedAds": 0 }, "advertisers": [], "longevity": [], "formats": [], "ctas": [], "destinations": [], "categories": [], "cadence": [] } ``` ## Source Stored paid-ad observations. Rankings count creatives and how long they stay live. There is no spend or impression field. ## Refresh These reads do not collect new ads. --- # Browse paid-ad shelves Read paid ads as per-advertiser shelves, plus shelves for newly launched and long-running creatives. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/paid-ads/rails - Method: `GET` - Path: `/v1/paid-ads/rails` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_ads` - MCP action: `rails` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `category` | query | category | no | Limit shelves to one category. | | `platform` | query | meta | tiktok | no | Advertising platform. | | `search` | query | string, max 120 | no | Advertiser or creative text. | | `sort` | query | most_tested | longest_running | most_live | recently_tracked | no | Order of the advertiser shelves. Defaults to most_tested. | | `railSize` | query | integer, 4-20 | no | Creatives on each shelf. Defaults to 10. | | `advertiserLimit` | query | integer, 1-20 | no | Advertiser shelves to return. Defaults to 8. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Every filter is optional. This read serves stored ads and does not collect new ones. ``` curl --get 'https://api.peekpanda.com/v1/paid-ads/rails' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call get_ads with action rails. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_ads", "arguments": { "request": { "action": "rails" } } } ``` ## Response rails is empty when nothing is stored. totals uses the same counts as the ranking read. ``` { "generatedAt": "2026-09-06T00:00:00.000Z", "rails": [], "totals": { "ads": 0, "liveAds": 0, "advertisers": 0, "liveAdvertisers": 0, "newAdsLast7Days": 0, "advertisersShippingLast7Days": 0, "medianRunDays": null, "longestRunDays": null, "liveBeyond60Days": 0, "reachDisclosedAds": 0, "unlinkedAds": 0 } } ``` ## Source Stored paid-ad observations. Rankings count creatives and how long they stay live. There is no spend or impression field. ## Refresh These reads do not collect new ads. --- # Get Apple charts Read the latest stored Apple free, paid or grossing chart for one storefront and category. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/charts - Method: `GET` - Path: `/v1/charts` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_market` - MCP action: `charts` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `country` | query | ^[a-z]{2}$ | no | Apple storefront. Defaults to us. Coverage is us, gb, fr, de, br, be. | | `categoryId` | query | all or digits | no | Overall (all) or an Apple category id. Defaults to all. | | `chartType` | query | free | paid | grossing | no | Apple chart type. Defaults to free. Grossing is a position, not dollars. | | `view` | query | rankings | gainers | losers | new-entrants | new-releases | no | Leaderboard slice. Defaults to rankings. | | `limit` | query | integer, 1–100 | no | Page size. Defaults to 100. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Send country, categoryId, chartType, view and limit as query parameters. This GET never calls Apple. ``` curl --get 'https://api.peekpanda.com/v1/charts' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --data-urlencode 'country=us' \ --data-urlencode 'categoryId=all' \ --data-urlencode 'chartType=free' \ --data-urlencode 'view=rankings' \ --data-urlencode 'limit=100' ``` ## MCP request Call get_market with action charts. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_market", "arguments": { "request": { "action": "charts" } } } ``` ## Response The response includes query, coverage, snapshot, summary, insights and data. Dates use ISO format, URLs are included when present, and estimates are optional. 0000000000 is a synthetic store ID. ``` { "query": { "country": "us", "categoryId": "all", "chartType": "free", "view": "rankings", "limit": 100 }, "coverage": { "countries": [ { "code": "us", "name": "United States" }, { "code": "gb", "name": "United Kingdom" }, { "code": "fr", "name": "France" }, { "code": "de", "name": "Germany" }, { "code": "br", "name": "Brazil" }, { "code": "be", "name": "Belgium" } ], "categories": [ { "id": "all", "name": "Overall" } ], "chartTypes": [ "free", "paid", "grossing" ] }, "snapshot": { "id": "chart-snapshot-example", "day": "2026-09-06", "sourceUpdatedAt": "2026-09-06T00:00:00.000Z", "fetchedAt": "2026-09-06T00:15:00.000Z", "depth": 100, "previousDay": "2026-09-05", "stale": false }, "summary": { "trackedApps": 1, "rankedApps": 1, "gainers": 0, "losers": 0, "newEntrants": 0, "newReleases": 0 }, "insights": { "categoryMix": [ { "categoryId": "6013", "name": "Health & Fitness", "count": 1 } ], "releaseAges": [ { "label": "1-3years", "count": 1 } ], "categoryLeaders": [ { "categoryId": "6013", "name": "Health & Fitness", "storeId": "0000000000", "appName": "Example Calorie Tracker", "iconUrl": "https://example.com/icon.png", "day": "2026-09-06", "depth": 100 } ], "storefronts": [ { "country": "us", "day": "2026-09-06", "depth": 100 }, { "country": "gb", "day": "2026-09-06", "depth": 100 }, { "country": "fr", "day": "2026-09-06", "depth": 100 }, { "country": "de", "day": "2026-09-06", "depth": 100 }, { "country": "br", "day": "2026-09-06", "depth": 100 }, { "country": "be", "day": "2026-09-06", "depth": 100 } ] }, "data": [ { "storeId": "0000000000", "name": "Example Calorie Tracker", "developer": "Example Labs", "iconUrl": "https://example.com/icon.png", "appUrl": "https://apps.apple.com/us/app/id0000000000", "categoryId": "6013", "categoryName": "Health & Fitness", "releaseDate": "2024-05-01", "price": 0, "currency": "USD", "rating": 4.7, "ratingCount": 12450, "metadataUpdatedAt": "2026-09-06T00:15:00.000Z", "history": [ { "day": "2026-09-06", "rank": 1 } ], "rank": 1, "previousRank": 2, "movement": 1, "status": "ranked", "inCatalogue": true, "estimates": { "monthlyInstalls": { "range": { "low": 34000, "mid": 84000, "high": 207000 }, "confidence": { "group": "finance", "sampleSize": 149, "errorFactor": 2.589 }, "observedAt": "2026-09-06T00:00:00.000Z", "previous": { "mid": 72000, "observedAt": "2026-09-05T00:00:00.000Z" } }, "monthlyRevenue": null, "currency": "USD" } } ], "updatedAt": "2026-09-06T00:15:00.000Z" } ``` ## Source Dated public App Store chart observations. Grossing is a position, not a revenue amount, and this read returns a stored snapshot. ## Refresh New snapshots are collected asynchronously. Completed historical snapshots remain unchanged. --- # Get Apple chart history Read dated rank points for one numeric store ID on a stored Apple chart series. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/charts/apps/{storeId}/history - Method: `GET` - Path: `/v1/charts/apps/{storeId}/history` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_app` - MCP action: `chart_history` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `storeId` | path | numeric string | yes | Apple App Store identifier. | | `country` | query | ^[a-z]{2}$ | no | Apple storefront. Defaults to us. Coverage is us, gb, fr, de, br, be. | | `categoryId` | query | all or digits | no | Overall (all) or an Apple category id. Defaults to all. | | `chartType` | query | free | paid | grossing | no | Apple chart type. Defaults to free. Grossing is a position, not dollars. | | `limit` | query | integer, 1–100 | no | History length. Defaults to 30. There is no view filter. | ## Known errors - 400 invalid_request for an invalid filter, identity or limit. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing scope or unavailable preview access. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. ## Request Path storeId is numeric. Query accepts country, categoryId, chartType and limit. There is no view parameter. This GET never calls Apple. ``` curl --get 'https://api.peekpanda.com/v1/charts/apps/0000000000/history' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --data-urlencode 'country=us' \ --data-urlencode 'categoryId=all' \ --data-urlencode 'chartType=free' \ --data-urlencode 'limit=30' ``` ## MCP request Call get_app with action chart_history. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_app", "arguments": { "request": { "action": "chart_history", "params": { "storeId": "" } } } } ``` ## Response The response includes the store ID, chart query, dated rank history, optional app context and observed storefront positions. The example uses store ID 0000000000. ``` { "storeId": "0000000000", "query": { "country": "us", "categoryId": "all", "chartType": "free", "limit": 30 }, "history": [ { "day": "2026-09-05", "rank": 2 }, { "day": "2026-09-06", "rank": 1 } ], "app": { "storeId": "0000000000", "name": "Example Calorie Tracker", "iconUrl": "https://example.com/icon.png", "developer": "Example Labs", "categoryName": "Health & Fitness" }, "countries": [ { "country": "us", "day": "2026-09-06", "rank": 1, "depth": 100 } ] } ``` ## Source The same dated public App Store chart observations as GET /v1/charts. Grossing remains a position. ## Refresh History is assembled from stored daily snapshots. New collection does not rewrite completed history. --- # Get market overview Read who is winning one App Store storefront: the market pulse, the advertiser, viral and search-owner boards, the hottest and widest-open searches, and typed findings. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/market/overview - Method: `GET` - Path: `/v1/market/overview` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_market` - MCP action: `overview` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `country` | query | ^[a-z]{2}$ | no | Apple storefront as two lowercase letters. Defaults to us. | ## Known errors - 400 invalid_request for an unknown query parameter or a country that is not two lowercase letters. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing context:read scope or unavailable preview access. - 429 rate_limited; wait for the Retry-After seconds, then send the same request again. ## Request Send country as a query parameter. Unknown parameters are rejected. This GET never starts collection. ``` curl --get 'https://api.peekpanda.com/v1/market/overview' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --data-urlencode 'country=us' ``` ## MCP request Call get_market with action overview. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_market", "arguments": { "request": { "action": "overview" } } } ``` ## Response The response includes the country, the chart day with how many days of chart history exist, eight weekly dates, the market pulse, three boards, two search lists, typed findings and the suspects. The example uses observations from the US storefront on 10 September 2026, trimmed to one or two rows per list. ``` { "country": "us", "chart": { "day": "2026-09-10", "previousDay": "2026-09-09", "historyDays": 6 }, "weeks": [ "2026-07-20", "2026-07-27", "2026-08-03", "2026-08-10", "2026-08-17", "2026-08-24", "2026-08-31", "2026-09-07" ], "pulse": { "paid": { "totalAds": 905, "liveAds": 830, "advertisers": 61, "startedLast7Days": 122, "longestDaysLive": 332, "weeklyStarts": [ 26, 55, 73, 38, 72, 118, 126, 67 ], "weeklyAdvertisers": [ 2, 3, 5, 1, 2, 7, 3, 1 ] }, "organic": { "posts": 4879, "matchedPosts": 1397, "matchedApps": 699, "weeklyFound": [ 0, 4, 1, 106, 156, 98, 103, 51 ], "viewsCountedAt": "2026-05-29T00:00:00.000Z" }, "keywords": { "rankedKeywords": 2285, "popularityTerms": 33039, "storefronts": 10 } }, "boards": { "advertisers": [ { "storeId": "6480417616", "name": "Cal AI - Calorie Tracker", "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/a1/74/2f/a1742f0e-6092-1e6c-38a9-135fd674f189/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/128x128bb.png", "categoryName": "Health & Fitness", "liveAds": 95, "totalAds": 95, "startedLast7Days": 30, "medianDaysLive": 23, "longestDaysLive": 149, "videoShare": 0.905, "topCallToAction": "Learn more", "placement": { "rank": 6, "previousRank": 7, "chartType": "free", "categoryId": "6013", "categoryName": "Health & Fitness", "day": "2026-09-10" } } ], "viral": [ { "storeId": "6759000863", "name": "Cheater Buster : AI Checker", "iconUrl": null, "categoryName": "Utilities", "matchedPosts": 6, "views": 275617771, "medianViews": 41073742, "bestPostViews": 82900000, "placement": null }, { "storeId": "529479190", "name": "Clash of Clans", "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/d7/af/ca/d7afca1f-0b51-4cc1-7e7f-b0fba87e3739/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/128x128bb.png", "categoryName": "Games", "matchedPosts": 1, "views": 146800000, "medianViews": 146800000, "bestPostViews": 146800000, "placement": { "rank": 25, "previousRank": 33, "chartType": "grossing", "categoryId": "6014", "categoryName": "Games", "day": "2026-09-10" } } ], "searchOwners": [ { "storeId": "389801252", "name": "Instagram", "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/5a/18/99/5a189914-fa88-fab3-7ce2-506a624d2fbf/Prod-0-0-1x_U007epad-0-1-0-sRGB-85-220.png/128x128bb.png", "categoryName": "Photo & Video", "ranked": 328, "top10": 197, "top3": 86, "placement": { "rank": 2, "previousRank": 2, "chartType": "free", "categoryId": "6008", "categoryName": "Photo & Video", "day": "2026-09-10" } } ] }, "searches": { "hottest": [ { "query": "instagram", "popularity": 100, "previousPopularity": 100, "difficulty": 100, "rankingApps": 148, "leader": { "storeId": "389801252", "name": "Instagram", "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/5a/18/99/5a189914-fa88-fab3-7ce2-506a624d2fbf/Prod-0-0-1x_U007epad-0-1-0-sRGB-85-220.png/128x128bb.png" } } ], "wideOpen": [ { "query": "empower", "popularity": 69, "difficulty": 0, "opportunity": 69, "rankingApps": 184, "leader": { "storeId": "1454849875", "name": "Empower - Your ride, your way", "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/02/f6/b9/02f6b9f2-104b-e30c-5b04-2e004077a347/AppIconPassenger-0-0-1x_U007emarketing-0-8-0-85-220.png/128x128bb.png" } } ] }, "findings": { "climbingAndBuying": [ { "storeId": "6480417616", "name": "Cal AI - Calorie Tracker", "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/a1/74/2f/a1742f0e-6092-1e6c-38a9-135fd674f189/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/128x128bb.png", "categoryName": "Health & Fitness", "placement": { "rank": 6, "previousRank": 7, "chartType": "free", "categoryId": "6013", "categoryName": "Health & Fitness", "day": "2026-09-10" }, "startedLast7Days": 30, "liveAds": 95 } ], "topTenUncollected": [ { "storeId": "555376968", "name": "ESPN Fantasy Sports & More", "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/fc/c8/f1/fcc8f1f5-9a4d-1a40-59e7-fae3fc7db2c8/AppIcon-0-0-1x_U007epad-0-11-0-85-220.png/128x128bb.png", "categoryName": "Sports", "rank": 2, "chartType": "free" } ], "newEntrants": [ { "storeId": "6760173601", "name": "Muse from Meta", "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/bf/06/f7/bf06f70f-8230-7065-5096-3d89145e18cf/HatchAppIconPublic-0-0-1x_U007ephone-0-1-0-sRGB-0-85-220.png/128x128bb.png", "categoryName": "Productivity", "rank": 4, "hasAds": false, "hasOrganic": false, "hasKeywords": false } ], "viralNotBuying": [ { "storeId": "6759000863", "name": "Cheater Buster : AI Checker", "iconUrl": null, "categoryName": "Utilities", "matchedPosts": 6, "views": 275617771 } ], "buyingNotCharting": [ { "storeId": "1018368216", "name": "Cantina: AI Video & Characters", "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/2a/78/80/2a78801f-f10f-78d8-d99d-9701b44edfa2/AppIcon-0-0-1x_U007ephone-0-1-0-sRGB-85-220.png/128x128bb.png", "categoryName": "Photo & Video", "liveAds": 30, "placement": { "rank": 13, "previousRank": 10, "chartType": "free", "categoryId": "6008", "categoryName": "Photo & Video", "day": "2026-09-10" } } ] }, "suspects": [ { "storeId": "341232718", "name": "MyFitnessPal: Calorie Counter", "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/3b/97/aa/3b97aa91-aa9d-d66d-e531-8cdef051e137/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/128x128bb.png", "categoryName": "Health & Fitness", "boards": [ "chart", "paid", "organic", "search" ], "placement": { "rank": 1, "previousRank": 1, "chartType": "grossing", "categoryId": "6013", "categoryName": "Health & Fitness", "day": "2026-09-10" }, "paid": { "liveAds": 19, "startedLast7Days": 2 }, "organic": { "matchedPosts": 1, "views": 342500 }, "keywords": { "top10": 45 } } ] } ``` ## Source Chart placements come from dated public App Store chart observations. chart.historyDays says how much chart history exists, so movement is day over day, not a trend. Paid ads are collected per chosen app, so an app without ads has not been collected. An ad counts for the app its own verified App Store link names, and the paid pulse counts only such ads. "Last 7 days" and run lengths are UTC calendar days. Organic posts are matched by product name, not account ownership, and their views are as last counted. pulse.organic.viewsCountedAt says when. Keyword positions are observed App Store search result positions, not search volume. ## Refresh This GET reads stored evidence and never calls Apple or an advertising library. Keyword and organic aggregates are precomputed off the request path, and the dates in the payload say how fresh each stream is. ## Rate limits Market reads share the standard read budget of 60 requests per minute with every other PeekPanda read. During the preview the count is kept per client network address, not per plan, and it resets at the end of each one-minute window. A 429 response carries Retry-After when the server can say how long to wait: wait that many seconds, then send the same request again. Peek and Panda quotas for these reads are not enforced yet, and stored reads do not use your refresh allowance. ## Bounded lists This read has no cursor. Each board holds at most 15 apps and each search list at most 20 queries. Every weekly series has exactly 8 Monday-start weeks, oldest first, and the last one is the current, partial week. Findings and suspects list only the apps that meet their rule, so any of them can be empty. A suspect sits on at least 3 of the four boards: chart, paid, organic and search. --- # Get market leaderboard Read the complete bounded market page in one request: three overall charts, the selected chart, market overview, and signals for the apps shown. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/market/leaderboard - Method: `GET` - Path: `/v1/market/leaderboard` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_market` - MCP action: `leaderboard` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `country` | query | ^[a-z]{2}$ | no | Apple storefront as two lowercase letters. Defaults to us. | | `categoryId` | query | all or digits | no | Overall or the Apple category for the selected chart. Defaults to all. | | `chartType` | query | free | paid | grossing | no | Selected chart type. Defaults to free. | ## Known errors - 400 invalid_request for an unknown query parameter, invalid storefront, category or chart type. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing context:read scope or unavailable preview access. - 429 rate_limited; wait for the Retry-After seconds, then send the same request again. ## Request Send country, categoryId and chartType as query parameters. This GET reads stored evidence and never starts collection. ``` curl --get 'https://api.peekpanda.com/v1/market/leaderboard' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --data-urlencode 'country=us' \ --data-urlencode 'categoryId=6013' \ --data-urlencode 'chartType=paid' ``` ## MCP request Call get_market with action leaderboard. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_market", "arguments": { "request": { "action": "leaderboard" } } } ``` ## Response The response keeps the full chart-page fields, including history, coverage and freshness, for free, paid, grossing and a category-selected chart. For categoryId=all, viewChart is null because charts[chartType] is the selected chart already. It also includes the market overview and one signals batch for every app rendered. Each chart has at most 100 rows, so the response has at most 400 chart rows before duplicates; signals remain capped at 600 unique apps. ``` { "projection": { "computedAt": "2026-09-16T12:00:00.000Z", "staleAt": "2026-09-16T15:00:00.000Z", "stale": false }, "overview": { "country": "us", "chart": { "day": "2026-09-10", "previousDay": "2026-09-09", "historyDays": 6 }, "weeks": [ "2026-07-20", "2026-07-27", "2026-08-03", "2026-08-10", "2026-08-17", "2026-08-24", "2026-08-31", "2026-09-07" ], "pulse": { "paid": { "totalAds": 905, "liveAds": 830, "advertisers": 61, "startedLast7Days": 122, "longestDaysLive": 332, "weeklyStarts": [ 26, 55, 73, 38, 72, 118, 126, 67 ], "weeklyAdvertisers": [ 2, 3, 5, 1, 2, 7, 3, 1 ] }, "organic": { "posts": 4879, "matchedPosts": 1397, "matchedApps": 699, "weeklyFound": [ 0, 4, 1, 106, 156, 98, 103, 51 ], "viewsCountedAt": "2026-05-29T00:00:00.000Z" }, "keywords": { "rankedKeywords": 2285, "popularityTerms": 33039, "storefronts": 10 } }, "boards": { "advertisers": [ { "storeId": "6480417616", "name": "Cal AI - Calorie Tracker", "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/a1/74/2f/a1742f0e-6092-1e6c-38a9-135fd674f189/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/128x128bb.png", "categoryName": "Health & Fitness", "liveAds": 95, "totalAds": 95, "startedLast7Days": 30, "medianDaysLive": 23, "longestDaysLive": 149, "videoShare": 0.905, "topCallToAction": "Learn more", "placement": { "rank": 6, "previousRank": 7, "chartType": "free", "categoryId": "6013", "categoryName": "Health & Fitness", "day": "2026-09-10" } } ], "viral": [ { "storeId": "6759000863", "name": "Cheater Buster : AI Checker", "iconUrl": null, "categoryName": "Utilities", "matchedPosts": 6, "views": 275617771, "medianViews": 41073742, "bestPostViews": 82900000, "placement": null }, { "storeId": "529479190", "name": "Clash of Clans", "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/d7/af/ca/d7afca1f-0b51-4cc1-7e7f-b0fba87e3739/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/128x128bb.png", "categoryName": "Games", "matchedPosts": 1, "views": 146800000, "medianViews": 146800000, "bestPostViews": 146800000, "placement": { "rank": 25, "previousRank": 33, "chartType": "grossing", "categoryId": "6014", "categoryName": "Games", "day": "2026-09-10" } } ], "searchOwners": [ { "storeId": "389801252", "name": "Instagram", "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/5a/18/99/5a189914-fa88-fab3-7ce2-506a624d2fbf/Prod-0-0-1x_U007epad-0-1-0-sRGB-85-220.png/128x128bb.png", "categoryName": "Photo & Video", "ranked": 328, "top10": 197, "top3": 86, "placement": { "rank": 2, "previousRank": 2, "chartType": "free", "categoryId": "6008", "categoryName": "Photo & Video", "day": "2026-09-10" } } ] }, "searches": { "hottest": [ { "query": "instagram", "popularity": 100, "previousPopularity": 100, "difficulty": 100, "rankingApps": 148, "leader": { "storeId": "389801252", "name": "Instagram", "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/5a/18/99/5a189914-fa88-fab3-7ce2-506a624d2fbf/Prod-0-0-1x_U007epad-0-1-0-sRGB-85-220.png/128x128bb.png" } } ], "wideOpen": [ { "query": "empower", "popularity": 69, "difficulty": 0, "opportunity": 69, "rankingApps": 184, "leader": { "storeId": "1454849875", "name": "Empower - Your ride, your way", "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/02/f6/b9/02f6b9f2-104b-e30c-5b04-2e004077a347/AppIconPassenger-0-0-1x_U007emarketing-0-8-0-85-220.png/128x128bb.png" } } ] }, "findings": { "climbingAndBuying": [ { "storeId": "6480417616", "name": "Cal AI - Calorie Tracker", "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/a1/74/2f/a1742f0e-6092-1e6c-38a9-135fd674f189/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/128x128bb.png", "categoryName": "Health & Fitness", "placement": { "rank": 6, "previousRank": 7, "chartType": "free", "categoryId": "6013", "categoryName": "Health & Fitness", "day": "2026-09-10" }, "startedLast7Days": 30, "liveAds": 95 } ], "topTenUncollected": [ { "storeId": "555376968", "name": "ESPN Fantasy Sports & More", "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/fc/c8/f1/fcc8f1f5-9a4d-1a40-59e7-fae3fc7db2c8/AppIcon-0-0-1x_U007epad-0-11-0-85-220.png/128x128bb.png", "categoryName": "Sports", "rank": 2, "chartType": "free" } ], "newEntrants": [ { "storeId": "6760173601", "name": "Muse from Meta", "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/bf/06/f7/bf06f70f-8230-7065-5096-3d89145e18cf/HatchAppIconPublic-0-0-1x_U007ephone-0-1-0-sRGB-0-85-220.png/128x128bb.png", "categoryName": "Productivity", "rank": 4, "hasAds": false, "hasOrganic": false, "hasKeywords": false } ], "viralNotBuying": [ { "storeId": "6759000863", "name": "Cheater Buster : AI Checker", "iconUrl": null, "categoryName": "Utilities", "matchedPosts": 6, "views": 275617771 } ], "buyingNotCharting": [ { "storeId": "1018368216", "name": "Cantina: AI Video & Characters", "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/2a/78/80/2a78801f-f10f-78d8-d99d-9701b44edfa2/AppIcon-0-0-1x_U007ephone-0-1-0-sRGB-85-220.png/128x128bb.png", "categoryName": "Photo & Video", "liveAds": 30, "placement": { "rank": 13, "previousRank": 10, "chartType": "free", "categoryId": "6008", "categoryName": "Photo & Video", "day": "2026-09-10" } } ] }, "suspects": [ { "storeId": "341232718", "name": "MyFitnessPal: Calorie Counter", "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/3b/97/aa/3b97aa91-aa9d-d66d-e531-8cdef051e137/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/128x128bb.png", "categoryName": "Health & Fitness", "boards": [ "chart", "paid", "organic", "search" ], "placement": { "rank": 1, "previousRank": 1, "chartType": "grossing", "categoryId": "6013", "categoryName": "Health & Fitness", "day": "2026-09-10" }, "paid": { "liveAds": 19, "startedLast7Days": 2 }, "organic": { "matchedPosts": 1, "views": 342500 }, "keywords": { "top10": 45 } } ] }, "charts": { "free": { "query": { "country": "us", "categoryId": "all", "chartType": "free", "view": "rankings", "limit": 100 }, "coverage": { "countries": [ { "code": "us", "name": "United States" }, { "code": "gb", "name": "United Kingdom" }, { "code": "fr", "name": "France" }, { "code": "de", "name": "Germany" }, { "code": "br", "name": "Brazil" }, { "code": "be", "name": "Belgium" } ], "categories": [ { "id": "all", "name": "Overall" } ], "chartTypes": [ "free", "paid", "grossing" ] }, "snapshot": { "id": "chart-snapshot-example", "day": "2026-09-06", "sourceUpdatedAt": "2026-09-06T00:00:00.000Z", "fetchedAt": "2026-09-06T00:15:00.000Z", "depth": 100, "previousDay": "2026-09-05", "stale": false }, "summary": { "trackedApps": 1, "rankedApps": 1, "gainers": 0, "losers": 0, "newEntrants": 0, "newReleases": 0 }, "insights": { "categoryMix": [ { "categoryId": "6013", "name": "Health & Fitness", "count": 1 } ], "releaseAges": [ { "label": "1-3years", "count": 1 } ], "categoryLeaders": [ { "categoryId": "6013", "name": "Health & Fitness", "storeId": "0000000000", "appName": "Example Calorie Tracker", "iconUrl": "https://example.com/icon.png", "day": "2026-09-06", "depth": 100 } ], "storefronts": [ { "country": "us", "day": "2026-09-06", "depth": 100 }, { "country": "gb", "day": "2026-09-06", "depth": 100 }, { "country": "fr", "day": "2026-09-06", "depth": 100 }, { "country": "de", "day": "2026-09-06", "depth": 100 }, { "country": "br", "day": "2026-09-06", "depth": 100 }, { "country": "be", "day": "2026-09-06", "depth": 100 } ] }, "data": [ { "storeId": "0000000000", "name": "Example Calorie Tracker", "developer": "Example Labs", "iconUrl": "https://example.com/icon.png", "appUrl": "https://apps.apple.com/us/app/id0000000000", "categoryId": "6013", "categoryName": "Health & Fitness", "releaseDate": "2024-05-01", "price": 0, "currency": "USD", "rating": 4.7, "ratingCount": 12450, "metadataUpdatedAt": "2026-09-06T00:15:00.000Z", "history": [ { "day": "2026-09-06", "rank": 1 } ], "rank": 1, "previousRank": 2, "movement": 1, "status": "ranked", "inCatalogue": true, "estimates": { "monthlyInstalls": { "range": { "low": 34000, "mid": 84000, "high": 207000 }, "confidence": { "group": "finance", "sampleSize": 149, "errorFactor": 2.589 }, "observedAt": "2026-09-06T00:00:00.000Z", "previous": { "mid": 72000, "observedAt": "2026-09-05T00:00:00.000Z" } }, "monthlyRevenue": null, "currency": "USD" } } ], "updatedAt": "2026-09-06T00:15:00.000Z" }, "paid": { "query": { "country": "us", "categoryId": "all", "chartType": "paid", "view": "rankings", "limit": 100 }, "coverage": { "countries": [ { "code": "us", "name": "United States" }, { "code": "gb", "name": "United Kingdom" }, { "code": "fr", "name": "France" }, { "code": "de", "name": "Germany" }, { "code": "br", "name": "Brazil" }, { "code": "be", "name": "Belgium" } ], "categories": [ { "id": "all", "name": "Overall" } ], "chartTypes": [ "free", "paid", "grossing" ] }, "snapshot": { "id": "chart-snapshot-example", "day": "2026-09-06", "sourceUpdatedAt": "2026-09-06T00:00:00.000Z", "fetchedAt": "2026-09-06T00:15:00.000Z", "depth": 100, "previousDay": "2026-09-05", "stale": false }, "summary": { "trackedApps": 1, "rankedApps": 1, "gainers": 0, "losers": 0, "newEntrants": 0, "newReleases": 0 }, "insights": { "categoryMix": [ { "categoryId": "6013", "name": "Health & Fitness", "count": 1 } ], "releaseAges": [ { "label": "1-3years", "count": 1 } ], "categoryLeaders": [ { "categoryId": "6013", "name": "Health & Fitness", "storeId": "0000000000", "appName": "Example Calorie Tracker", "iconUrl": "https://example.com/icon.png", "day": "2026-09-06", "depth": 100 } ], "storefronts": [ { "country": "us", "day": "2026-09-06", "depth": 100 }, { "country": "gb", "day": "2026-09-06", "depth": 100 }, { "country": "fr", "day": "2026-09-06", "depth": 100 }, { "country": "de", "day": "2026-09-06", "depth": 100 }, { "country": "br", "day": "2026-09-06", "depth": 100 }, { "country": "be", "day": "2026-09-06", "depth": 100 } ] }, "data": [ { "storeId": "0000000000", "name": "Example Calorie Tracker", "developer": "Example Labs", "iconUrl": "https://example.com/icon.png", "appUrl": "https://apps.apple.com/us/app/id0000000000", "categoryId": "6013", "categoryName": "Health & Fitness", "releaseDate": "2024-05-01", "price": 0, "currency": "USD", "rating": 4.7, "ratingCount": 12450, "metadataUpdatedAt": "2026-09-06T00:15:00.000Z", "history": [ { "day": "2026-09-06", "rank": 1 } ], "rank": 1, "previousRank": 2, "movement": 1, "status": "ranked", "inCatalogue": true, "estimates": { "monthlyInstalls": { "range": { "low": 34000, "mid": 84000, "high": 207000 }, "confidence": { "group": "finance", "sampleSize": 149, "errorFactor": 2.589 }, "observedAt": "2026-09-06T00:00:00.000Z", "previous": { "mid": 72000, "observedAt": "2026-09-05T00:00:00.000Z" } }, "monthlyRevenue": null, "currency": "USD" } } ], "updatedAt": "2026-09-06T00:15:00.000Z" }, "grossing": { "query": { "country": "us", "categoryId": "all", "chartType": "grossing", "view": "rankings", "limit": 100 }, "coverage": { "countries": [ { "code": "us", "name": "United States" }, { "code": "gb", "name": "United Kingdom" }, { "code": "fr", "name": "France" }, { "code": "de", "name": "Germany" }, { "code": "br", "name": "Brazil" }, { "code": "be", "name": "Belgium" } ], "categories": [ { "id": "all", "name": "Overall" } ], "chartTypes": [ "free", "paid", "grossing" ] }, "snapshot": { "id": "chart-snapshot-example", "day": "2026-09-06", "sourceUpdatedAt": "2026-09-06T00:00:00.000Z", "fetchedAt": "2026-09-06T00:15:00.000Z", "depth": 100, "previousDay": "2026-09-05", "stale": false }, "summary": { "trackedApps": 1, "rankedApps": 1, "gainers": 0, "losers": 0, "newEntrants": 0, "newReleases": 0 }, "insights": { "categoryMix": [ { "categoryId": "6013", "name": "Health & Fitness", "count": 1 } ], "releaseAges": [ { "label": "1-3years", "count": 1 } ], "categoryLeaders": [ { "categoryId": "6013", "name": "Health & Fitness", "storeId": "0000000000", "appName": "Example Calorie Tracker", "iconUrl": "https://example.com/icon.png", "day": "2026-09-06", "depth": 100 } ], "storefronts": [ { "country": "us", "day": "2026-09-06", "depth": 100 }, { "country": "gb", "day": "2026-09-06", "depth": 100 }, { "country": "fr", "day": "2026-09-06", "depth": 100 }, { "country": "de", "day": "2026-09-06", "depth": 100 }, { "country": "br", "day": "2026-09-06", "depth": 100 }, { "country": "be", "day": "2026-09-06", "depth": 100 } ] }, "data": [ { "storeId": "0000000000", "name": "Example Calorie Tracker", "developer": "Example Labs", "iconUrl": "https://example.com/icon.png", "appUrl": "https://apps.apple.com/us/app/id0000000000", "categoryId": "6013", "categoryName": "Health & Fitness", "releaseDate": "2024-05-01", "price": 0, "currency": "USD", "rating": 4.7, "ratingCount": 12450, "metadataUpdatedAt": "2026-09-06T00:15:00.000Z", "history": [ { "day": "2026-09-06", "rank": 1 } ], "rank": 1, "previousRank": 2, "movement": 1, "status": "ranked", "inCatalogue": true, "estimates": { "monthlyInstalls": { "range": { "low": 34000, "mid": 84000, "high": 207000 }, "confidence": { "group": "finance", "sampleSize": 149, "errorFactor": 2.589 }, "observedAt": "2026-09-06T00:00:00.000Z", "previous": { "mid": 72000, "observedAt": "2026-09-05T00:00:00.000Z" } }, "monthlyRevenue": null, "currency": "USD" } } ], "updatedAt": "2026-09-06T00:15:00.000Z" } }, "viewChart": { "query": { "country": "us", "categoryId": "6013", "chartType": "free", "view": "rankings", "limit": 100 }, "coverage": { "countries": [ { "code": "us", "name": "United States" }, { "code": "gb", "name": "United Kingdom" }, { "code": "fr", "name": "France" }, { "code": "de", "name": "Germany" }, { "code": "br", "name": "Brazil" }, { "code": "be", "name": "Belgium" } ], "categories": [ { "id": "all", "name": "Overall" } ], "chartTypes": [ "free", "paid", "grossing" ] }, "snapshot": { "id": "chart-snapshot-example", "day": "2026-09-06", "sourceUpdatedAt": "2026-09-06T00:00:00.000Z", "fetchedAt": "2026-09-06T00:15:00.000Z", "depth": 100, "previousDay": "2026-09-05", "stale": false }, "summary": { "trackedApps": 1, "rankedApps": 1, "gainers": 0, "losers": 0, "newEntrants": 0, "newReleases": 0 }, "insights": { "categoryMix": [ { "categoryId": "6013", "name": "Health & Fitness", "count": 1 } ], "releaseAges": [ { "label": "1-3years", "count": 1 } ], "categoryLeaders": [ { "categoryId": "6013", "name": "Health & Fitness", "storeId": "0000000000", "appName": "Example Calorie Tracker", "iconUrl": "https://example.com/icon.png", "day": "2026-09-06", "depth": 100 } ], "storefronts": [ { "country": "us", "day": "2026-09-06", "depth": 100 }, { "country": "gb", "day": "2026-09-06", "depth": 100 }, { "country": "fr", "day": "2026-09-06", "depth": 100 }, { "country": "de", "day": "2026-09-06", "depth": 100 }, { "country": "br", "day": "2026-09-06", "depth": 100 }, { "country": "be", "day": "2026-09-06", "depth": 100 } ] }, "data": [ { "storeId": "0000000000", "name": "Example Calorie Tracker", "developer": "Example Labs", "iconUrl": "https://example.com/icon.png", "appUrl": "https://apps.apple.com/us/app/id0000000000", "categoryId": "6013", "categoryName": "Health & Fitness", "releaseDate": "2024-05-01", "price": 0, "currency": "USD", "rating": 4.7, "ratingCount": 12450, "metadataUpdatedAt": "2026-09-06T00:15:00.000Z", "history": [ { "day": "2026-09-06", "rank": 1 } ], "rank": 1, "previousRank": 2, "movement": 1, "status": "ranked", "inCatalogue": true, "estimates": { "monthlyInstalls": { "range": { "low": 34000, "mid": 84000, "high": 207000 }, "confidence": { "group": "finance", "sampleSize": 149, "errorFactor": 2.589 }, "observedAt": "2026-09-06T00:00:00.000Z", "previous": { "mid": 72000, "observedAt": "2026-09-05T00:00:00.000Z" } }, "monthlyRevenue": null, "currency": "USD" } } ], "updatedAt": "2026-09-06T00:15:00.000Z" }, "signals": { "country": "us", "weeks": [ "2026-07-20", "2026-07-27", "2026-08-03", "2026-08-10", "2026-08-17", "2026-08-24", "2026-08-31", "2026-09-07" ], "keywordsObservedAt": "2026-09-10T08:40:00.000Z", "organicViewsCountedAt": "2026-05-29T00:00:00.000Z", "apps": [ { "storeId": "1018368216", "placement": { "rank": 13, "previousRank": 10, "chartType": "free", "categoryId": "6008", "categoryName": "Photo & Video", "day": "2026-09-10" }, "paid": { "totalAds": 30, "liveAds": 30, "startedLast7Days": 5, "medianDaysLive": 29, "longestDaysLive": 38, "formats": { "video": 29, "dynamic": 0, "image": 1 }, "topCallToAction": "Install now", "destinations": { "appStore": 29, "web": 1 }, "weeklyStarts": [ 0, 0, 11, 13, 0, 0, 3, 3 ] }, "organic": { "matchedPosts": 10, "views": 29765621, "medianViews": 1991019, "bestPostViews": 7700000, "postsOverMillion": 7, "newestPublishedAt": "2026-07-29T00:00:00.000Z", "platforms": { "TikTok": 8, "Threads": 2 }, "matchedBy": "product_name" }, "keywords": { "ranked": 20, "top1": 1, "top3": 1, "top10": 4, "top50": 9, "best": { "query": "chai", "position": 63, "popularity": 78 }, "top": [ { "query": "chai", "position": 63, "popularity": 78 }, { "query": "character ai", "position": 47, "popularity": 75 }, { "query": "talkie", "position": 68, "popularity": 74 } ] } }, { "storeId": "555376968", "placement": { "rank": 1, "previousRank": 1, "chartType": "free", "categoryId": "6004", "categoryName": "Sports", "day": "2026-09-10" }, "paid": null, "organic": null, "keywords": { "ranked": 29, "top1": 8, "top3": 15, "top10": 17, "top50": 20, "best": { "query": "espn fantasy", "position": 1, "popularity": 83 }, "top": [ { "query": "espn fantasy", "position": 1, "popularity": 83 }, { "query": "espn", "position": 2, "popularity": 78 }, { "query": "yahoo fantasy", "position": 2, "popularity": 78 } ] } } ] } } ``` ## Source Chart placements come from dated public App Store chart observations. Paid ads are collected per chosen app, so an app without ads has not been collected. An ad counts for the app its own verified App Store link names, and the paid pulse counts only such ads. "Last 7 days" and run lengths are UTC calendar days. Organic posts are matched by product name, not account ownership, and their views are as last counted. Keyword positions are observed App Store search result positions, not search volume. ## Refresh This GET reads stored evidence only. It combines existing chart, overview and bounded signal reads under one access check; it never calls Apple or an advertising library. ## Rate limits Market reads share the standard read budget of 60 requests per minute with every other PeekPanda read. During the preview the count is kept per client network address, not per plan, and it resets at the end of each one-minute window. A 429 response carries Retry-After when the server can say how long to wait: wait that many seconds, then send the same request again. Peek and Panda quotas for these reads are not enforced yet, and stored reads do not use your refresh allowance. ## Bounded response Three overall charts and a category-selected chart carry at most 100 rows each. For categoryId=all, viewChart is null and the selected chart is charts[chartType], so it is sent once. Signals are deduplicated and capped at 600 app IDs. The endpoint has no cursor. --- # Get market signals Read chart, paid-ad, organic and keyword signals for up to 600 iOS apps in one batch. ## Contract status Preview contract: these routes and tools are implemented in the shared contract, but the public host, customer API access and OAuth flow have not been deployment verified. ## Endpoint: GET /v1/market/signals - Method: `GET` - Path: `/v1/market/signals` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_market` - MCP action: `signals` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `country` | query | ^[a-z]{2}$ | no | Apple storefront as two lowercase letters. Defaults to us. | | `storeIds` | query | comma-separated numeric IDs, 1–600 unique | yes | Apple store IDs to annotate, such as the rows of one chart page. Duplicates are removed before the 600 limit applies. | ## Known errors - 400 invalid_request for an unknown query parameter, an invalid country, or storeIds that are missing, non-numeric or more than 600 unique IDs. - 401 authentication_required for a missing or invalid credential. - 402 subscription_required when access is not entitled. - 403 authorization_required for a missing context:read scope or unavailable preview access. - 429 rate_limited; wait for the Retry-After seconds, then send the same request again. ## Request Send country and storeIds as query parameters. storeIds is one comma-separated value of numeric Apple store IDs; MCP clients may send an array instead. This GET never starts collection. ``` curl --get 'https://api.peekpanda.com/v1/market/signals' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --data-urlencode 'country=us' \ --data-urlencode 'storeIds=1018368216,555376968' ``` ## MCP request Call get_market with action signals. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_market", "arguments": { "request": { "action": "signals", "query": { "storeIds": "" } } } } ``` ## Response The response echoes the country and the eight weekly dates, says when keyword positions were observed and when organic views were counted, and lists apps keyed by storeId. Look entries up by storeId, not by position. placement, paid, organic and keywords are each null when that stream was never collected for the app, never zero. The second app in the example has no collected ads or organic posts. ``` { "country": "us", "weeks": [ "2026-07-20", "2026-07-27", "2026-08-03", "2026-08-10", "2026-08-17", "2026-08-24", "2026-08-31", "2026-09-07" ], "keywordsObservedAt": "2026-09-10T08:40:00.000Z", "organicViewsCountedAt": "2026-05-29T00:00:00.000Z", "apps": [ { "storeId": "1018368216", "placement": { "rank": 13, "previousRank": 10, "chartType": "free", "categoryId": "6008", "categoryName": "Photo & Video", "day": "2026-09-10" }, "paid": { "totalAds": 30, "liveAds": 30, "startedLast7Days": 5, "medianDaysLive": 29, "longestDaysLive": 38, "formats": { "video": 29, "dynamic": 0, "image": 1 }, "topCallToAction": "Install now", "destinations": { "appStore": 29, "web": 1 }, "weeklyStarts": [ 0, 0, 11, 13, 0, 0, 3, 3 ] }, "organic": { "matchedPosts": 10, "views": 29765621, "medianViews": 1991019, "bestPostViews": 7700000, "postsOverMillion": 7, "newestPublishedAt": "2026-07-29T00:00:00.000Z", "platforms": { "TikTok": 8, "Threads": 2 }, "matchedBy": "product_name" }, "keywords": { "ranked": 20, "top1": 1, "top3": 1, "top10": 4, "top50": 9, "best": { "query": "chai", "position": 63, "popularity": 78 }, "top": [ { "query": "chai", "position": 63, "popularity": 78 }, { "query": "character ai", "position": 47, "popularity": 75 }, { "query": "talkie", "position": 68, "popularity": 74 } ] } }, { "storeId": "555376968", "placement": { "rank": 1, "previousRank": 1, "chartType": "free", "categoryId": "6004", "categoryName": "Sports", "day": "2026-09-10" }, "paid": null, "organic": null, "keywords": { "ranked": 29, "top1": 8, "top3": 15, "top10": 17, "top50": 20, "best": { "query": "espn fantasy", "position": 1, "popularity": 83 }, "top": [ { "query": "espn fantasy", "position": 1, "popularity": 83 }, { "query": "espn", "position": 2, "popularity": 78 }, { "query": "yahoo fantasy", "position": 2, "popularity": 78 } ] } } ] } ``` ## Source placement is the best rank across every stored App Store chart of the storefront on the latest chart day, with the previous day’s rank. Paid ads are collected per chosen app, so an app without ads has not been collected. An ad counts for the app its own verified App Store link names, and the paid pulse counts only such ads. "Last 7 days" and run lengths are UTC calendar days. Its paid signal is null. Organic posts are matched by product name, not account ownership, and their views are as last counted. organicViewsCountedAt says when. Keyword positions are observed App Store search result positions, not search volume. ## Refresh This GET reads the same stored evidence as GET /v1/market/overview and never starts collection. To ask for paid-ad or organic evidence on one app, record a data request for that competitor. ## Rate limits Market reads share the standard read budget of 60 requests per minute with every other PeekPanda read. During the preview the count is kept per client network address, not per plan, and it resets at the end of each one-minute window. A 429 response carries Retry-After when the server can say how long to wait: wait that many seconds, then send the same request again. Peek and Panda quotas for these reads are not enforced yet, and stored reads do not use your refresh allowance. ## Batches This read has no cursor. Send at most 600 unique store IDs per request and split a longer list into batches of 600. Each app carries at most 5 top keywords, and every weekly series has exactly 8 Monday-start weeks, oldest first, ending with the current, partial week.