# Plans and tracked apps Peek and Panda plans, how many apps each one tracks, and the API and MCP call allowance. PeekPanda offers two plans: Peek and Panda. Both open the same iOS catalogue and case files. Each plan sells one number: how many apps it tracks. Put an app on a board and PeekPanda tracks everything about it. Peek is PeekPanda-only. Panda adds your whole team in a shared workspace and includes the whole Genviral Creator plan. ## Peek and Panda | | Peek | Panda | | --- | --- | --- | | Monthly | $59 | $99 | | Yearly | $590 | $990 | | Tracked apps | 1 | 100 | | API and MCP calls | 30 a day | 1,000 a day | | Who it is for | Just you | Your whole team | | Shared workspace | No | Yes, members inherit the owner plan | | Genviral | Not included | Personal AI assistant, images, video, slideshows, posting and scheduling | A tracked app is any app on one of your boards. Each app counts once, however many boards it is on. The number covers your own space and every workspace you own, and workspace members track on the owner's plan. See [boards](/docs/boards). Browsing stored evidence is never limited by your plan. 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. ## API and MCP calls The public API and MCP share one daily call allowance per plan: 30 a day on Peek, 1,000 a day on Panda, resetting at 00:00 UTC. It is one shared pool per plan holder: extra API keys, MCP connections, and workspace members do not add to it, and a workspace member draws on the workspace owner's pool. In-app browsing in the product is never counted against it. ## Requested refreshes are automatic PeekPanda keeps evidence current on its own; opening a case file, a chart, or a category never spends anything from your plan. A quiet, automatic safeguard paces how often the same public source is re-collected, so this never appears as a plan number. See [refreshing keywords](/docs/refreshing-keywords) for how ASO keyword collection reports its own status. ## 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. 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 put it on a board 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. ## 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 newer evidence 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**. They are not measured downloads or reported revenue, 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 Put the app on a [board](/docs/boards). On its case file, use **Add to board**. On a board, use **Track an app**, search for the app and pick it. With no boards yet, Home offers **Track an app** and puts that app on a board called **My apps**. Put an app on a board and PeekPanda tracks everything about it: its paid ads, organic posts, keywords and chart position, collected from the day you add it. Home opens your boards, so what changed is waiting there. Each app counts once toward your plan, however many boards it is on. > Tracking a competitor is separate from saving content. In Organic Content, **Save** keeps an individual post. In Paid Ads, the bookmark saves an ad to one or more [boards](/docs/boards), 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 tracked apps](/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. Each point is a 30-day estimate, not a calendar-month total. When newer evidence cannot support a new estimate, the last valid value remains with its original 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. Then **Monetization**: download price, whether the listing declares in-app purchases, whether it declares ads, and an installs figure labelled `Installs` and marked `Estimate`. It is a PeekPanda estimate, not measured installs. Below that, the stored screenshots and the store observation behind them. ## 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 see 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 PeekPanda estimates, not measured downloads or reported revenue. ![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 are directional PeekPanda estimates, not measured downloads. Where there is not yet enough evidence to support an estimate, it remains unavailable. That is not a zero. After a valid estimate exists, a missed collection or newer evidence that cannot support a new reading keeps that estimate visible with its original **As of** date until newer evidence can support another one. ## Revenue estimates Revenue estimates are directional PeekPanda estimates, not reported revenue. Apps without enough 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, put it on one of your [boards](/docs/boards). Use **Add to board** on its case file, or **Track an app** on a board. PeekPanda then tracks everything about it: its paid ads, organic posts, keywords and chart position, collected from the day you add it. Open the board to see what changed. ## For agents The API and MCP profile expose a stored similar-listings read, `get_similar_competitors`, at `GET /v1/competitors/{platform}/{storeId}/similar` (in MCP, `search_apps` with `similar`). It returns the competitor list's paginated envelope, strongest evidence first, and every app says why it is there: | `matchReason` | What it means | |---|---| | `shared_searches` | It ranks in the top 100 for at least two US App Store searches this app ranks for, and at least one of them does not name this app. | | `similar_listing` | Its name, in the same category, holds a job from this app's searches or current name, such as "recipe keeper". | | `similar_description` | It is one of the 1,000 most-rated apps in the same category, and its description names at least two of this app's jobs word for word, such as "meal planner". | | `same_category` | Fallback only: same category, and its name shares a word with this app. | `sharedSearches` is how many US searches both apps rank for; it is null for `same_category`. Jobs are two-word phrases taken from the app's current App Store name and from the searches it ranks for; a search that copies another app's title ("Samsung Food: Meal Planner") adds that app's job, never its brand. An app that shares searches and does the same jobs ranks above one that only shares searches or only shares words. Evidence counts more when the other app ranks higher, and among equally similar apps the one with more ratings comes first. Searches that only name this app never make a match on their own. Send `fallback=none` to get evidence-backed apps only, or leave the default `fallback=category` to fill the rest of the list with same-category apps. 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, 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 count. | | Installs | A PeekPanda download estimate, 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 more public traction. **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 PeekPanda estimate, when available. | | Revenue est. | A PeekPanda estimate, when available. | | Rank trend | Recently observed chart positions. | | Rating | Public star rating and rating count. | | 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 | 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) --- # Boards Put apps on boards to track them, save single ads, then look at Paid Ads, Organic Content and ASO through one board. A board is a named, coloured collection of apps and single paid ads. Make one for the top apps in your niche, one for hooks you want to test, one for pricing pages you keep checking. An app or an ad can sit on as many boards as you like. Your boards live in the sidebar under **Boards**. Open one to see its apps, the ads you saved on it, and what changed recently. ## Tracking an app Put an app on a board and PeekPanda tracks everything about it. That means its paid ads, organic posts, keywords and chart position, collected from the day you add it. On a board, use **Track an app**, search for the app and pick it. You can also add an app from its case file with **Add to board**. ## Editing or deleting a board From Home, click **Edit** on a board, or open it and click **Edit board**. Change its name or colour and press **Save**. **Delete board** opens a separate confirmation showing how many apps will stop tracking, what happens to tracked keywords and saved ads, and that the board is permanently deleted. **Keep board** returns to your settings draft. Use **Manage apps** in settings to add or remove apps inside the popup. **Add an app** adds to this board; **Remove** takes an app off it. An app you already track shows **Add to board** and still counts once, even when your plan is full. The app count follows the apps on the board. Removing an app from its last board stops tracking it and removes its tracked keywords, so PeekPanda asks first. Deleting a board removes its saved ads from that board; ads saved only there lose their bookmark, while ads saved on other boards stay bookmarked. Ads remain in the library. ## What counts as a tracked app Any app on one of your boards. Each app counts once, however many boards it is on. Peek tracks 1 app and Panda tracks 100. The board shows how many of your apps are tracked, for example `1 of 1 app tracked` on Peek or `50 of 100 apps tracked` on Panda. When you are at your plan's number, take an app off its boards to track another, or move up to Panda. Taking an app off its last board stops tracking it. Deleting a board does the same for every app that is on no other board, and the delete confirmation shows how many apps this affects. Saving a single ad never tracks its app. ## Reading an app card Each app card counts the ads PeekPanda has collected for that app: live ads, ads that started running recently, and ads saved on this board. An ad counts for an app only when its App Store link is verified to that app. Live ads are the ones you can open, the same number the API and MCP `app_intelligence` report. It also says when its ads were last collected. If that was more than two days ago, the card says so plainly. Zero means none collected yet, never that the app runs no ads. The board's pulse shows new ads per week for the last 8 weeks. Switch it to months to see the last 6 months. ## Saving an ad Click the bookmark on any paid ad card. A small panel opens with your boards: - Tick a board to save the ad there, untick it to take the ad off. - Type a new name and press Enter to create a board and save the ad to it in one go. - Add a note, for example the hook you want to borrow. The bookmark is filled when the ad sits on at least one of your boards. The **Saved** toggle in Paid Ads shows every ad on any of your boards. ## Seeing the library through a board Paid Ads and Organic Content have a lens next to the search: **Everything**, **Tracked apps**, or one of your boards. Pick a board and Paid Ads shows ads from that board's apps. Organic Content asks you to pick one of the board's apps, because organic posts are matched by app name. In ASO, the research rail groups your tracked apps by board, shows how many apps your plan tracks, and a tracked app's keyword gaps compare it with the tracked rivals that share a board with it. ## Personal and shared boards Boards belong to where you are working. With no workspace selected, boards are yours alone. Inside a workspace, boards are shared with everyone in it, and the apps on them are tracked on the workspace owner's plan. ## Limits - Tracked apps: 1 on Peek, 100 on Panda, counted across your own space and every workspace you own. - 100 boards per personal space or workspace. - 500 apps and ads per board. - Notes up to 500 characters. ## For developers Boards have the same public API and MCP tools as the app: - **Public API:** `GET /v1/boards`, `GET /v1/boards/memberships`, `POST /v1/boards`, `GET|PATCH|DELETE /v1/boards/{boardId}`, `POST /v1/boards/{boardId}/items` and `PATCH|DELETE /v1/boards/{boardId}/items/{itemId}`. - **Memberships:** ask for one app (`storeId`), one ad (`platform` and `archiveId`), or up to 100 apps at once (`storeIds=1,2,3`). Each entry names the board, the item, the app's store id and the item's note. - **Quick save:** `POST /v1/paid-ads/{platform}/{archiveId}/saved` puts an ad on your board named **Saved ads**, and creates that board on the first save. `DELETE` on the same route takes the ad off every board you have. - **Board ads in search:** `GET /v1/paid-ads?savedToBoardId=` lists a board's saved ads, and `?boardId=` lists ads from its apps. - **MCP:** the `boards` tool reads (`list`, `get`, `memberships`), `manage_boards` writes (`create`, `update`, `remove`, `add_item`, `update_item`, `remove_item`), and the `save` tool's `ad` and `unsave_ad` actions quick save. ## Related - [Saving creatives](/docs/saving-creatives) - [Paid ads library](/docs/paid-ads-library) - [ASO workstation](/docs/aso-workstation) - [Track your app](/docs/track-your-app) --- # 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. Ask for one post's creative analysis with `POST /v1/organic/{postId}/analysis` or the `refresh` MCP tool with action `organic_analysis`, then read it with `GET /v1/organic/{postId}/analysis` or `search_organic` with action `analysis`. ## 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. ## AI analysis Any Peek or Panda member can ask for a post's creative analysis. It runs in the background and is usually ready within a minute. A video post is read from its stored video and comes back with a timeline of scenes, cuts and on-screen text, and its transcript is stored when the post had none. A slideshow is read from its first 10 slides, in order, without a timeline. A failed run keeps any analysis the post already had, and you can ask again. The analysis describes what is visible or audible in the post. It is not a performance claim. ## 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, how long it has run, or by reported reach where available. Run length counts a live ad up to today and a stopped ad up to its disclosed end. Through the API and MCP, `GET /v1/paid-ads` (MCP `get_ads` with action `search`) also takes `storeIds` for ads verified to link to up to 25 App Store apps, `advertiserExternalId` for one advertiser's exact page id, and `discoveredAfter` to read only ads recorded after an instant. A recurring check passes `sort=recently_discovered` and the newest `discoveredAt` it has already read. The `dynamic` format is Meta dynamic creative, which is often a static image. 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 an ad to one or more [boards](/docs/boards), 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 A paid card prints how long the ad has been paid for 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 posts to your account and ads to your boards, and use the Saved views to get back to them. Click the bookmark on any creative card to save it. An organic post is saved to your account. A paid ad is saved to one or more [boards](/docs/boards). ![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 posts belong to your account and nobody else sees them. Saved ads live on boards: personal boards are yours alone, and boards inside a workspace are shared with its members. ## 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 a paid ad card, in the grid or in the inspector, opens your boards. Tick the boards the ad belongs on, or type a new board name and press Enter. You can add a note while you are there. The bookmark stays filled while the ad is on at least one board. ## 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, or a board's **Saved ads** tab | The paid list narrowed to ads on any of your boards, or on that one board | 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 ads that sit on one of your boards, and the count changes from `N ads` to `N saved ads`. Taking an ad off its last board while the toggle is on removes it from 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 or the board tick returns to its previous state and a short line says the change did not go through. Saving something twice is harmless. Saving an ad to a board it is already on keeps the one item rather than a duplicate. ## What removing does Removing a bookmark, or taking an ad off a board, removes only that save. It does not delete the ad or the post. The creative stays in the library and you can save it again. ## Related - [Boards](/docs/boards) - [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 tracked apps](/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. Every App Store app you track here is on at least one of your [boards](/docs/boards): an app you add from ASO lands on your **My apps** board, and you can add it to any other board. Put an app on a board and PeekPanda tracks everything about it. You can also track a pre-launch app 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. API and MCP clients can use `get_aso_case_evidence` for a compact stored summary of up to six tracked apps: 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 marked as *Mine*. - **Competitors**: everything you added as *Competitor*. - How many apps your plan tracks, for example `1 of 1 app tracked` on Peek or `3 of 100 apps tracked` on Panda. Peek tracks 1 app and Panda tracks 100. Each app counts once, however many boards it is on, and each pre-launch app counts as one. 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.** Next to the title sit the refresh pill, **Mark as mine** or **Mark as competitor**, and **Stop tracking**. If the app also has a catalogue profile, a **Case file** 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 track 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 - Tracked apps: 1 on Peek, 100 on Panda, counted across your own space and every workspace you own. - 100 tracked keywords per app. - Stopping tracking takes the app off every board in the space and 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. The keyword gap compares a tracked app with one rival: the searches only the rival ranks for, the searches you both rank for, and the searches only your app ranks for. Ask for it through the [API](/docs/get-aso-keyword-gap) or through the `aso` tool in MCP. ## Who counts as a rival Put your app and its rivals on the same board, and mark your own app as *Mine* in ASO. The apps it shares a board with are the rivals the comparison uses. You can also name any App Store id as the rival. Both sides come from stored search snapshots, so an app needs at least one collected keyword refresh before it has anything to compare. ## 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 ask. 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 tracked 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 tracked apps](/docs/plans-and-refreshes) --- # Track your app Add an iOS app to the ASO workspace, identify it as yours or a competitor’s, and track 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. Put an app on a board and PeekPanda tracks everything about it, so an app you add here lands on your **My apps** board. ![Tracked apps in the ASO workstation, split between your apps and competitors.](/docs-media/aso.png?w=1728&h=1084) ## Adding an app 1. Choose **Track an app** on Home, a board, or the **Tracked apps** rail in `/aso`. 2. Type an app name or paste its App Store link, then choose **Track** on the match. It goes on the board you opened from, or on **My apps**. 3. Choose **Open case file** to watch it fill up. A tracked app counts toward your plan once, however many boards it is on. Peek tracks 1 app and Panda tracks 100. If adding an app would go over that number, PeekPanda says so and changes nothing. 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. ## Mine or competitor The role is an account label used to organize your ASO board and case files. Both roles receive the same public observations. Your first tracked app starts as **Mine** and later ones as **Competitor**. You can change the label from the app's case file in ASO with **Mark as mine** or **Mark as competitor**. ## Tracking keywords Track a keyword from the keyword table, an idea, or **Add keywords**. You can add one phrase or a short list. Tracked keywords are kept for your app’s future ASO refreshes, including phrases the app does not yet appear for. In a workspace, everyone works on the same keyword list for an app. PeekPanda standardizes keyword input before saving it so duplicate spellings do not create separate entries. Removing a keyword takes it off the app’s list; it does not erase historical public observations. If a request would go over the keyword limit, PeekPanda explains the limit and keeps the existing list. ## Removing an app Use **Stop tracking** in the app header and confirm the choice. The app comes off every board in this space, its tracked keywords go with it, and it no longer counts toward your plan. 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 | | Peek | Panda | | --- | --- | --- | | Monthly | $59 | $99 | | Yearly | $590 | $990 | | Tracked apps | 1 | 100 | | API and MCP calls | 30 a day | 1,000 a day | | Who it is for | Just you | Your whole team | Yearly billing includes the equivalent of two months free compared with monthly billing. These are launch prices and may change before general availability. A tracked app is any app on one of your boards. Each app counts once, however many boards it is on. 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 tracked apps](/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: tracked apps and active keys.](/docs-media/account.png?w=1728&h=1084) | Item | What it means | | --- | --- | | Tracked apps | How many apps your plan tracks: 1 on Peek, 100 on Panda. Each app on your boards counts once. | | 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 Your plan sells one number: how many apps it tracks. Put an app on a board and PeekPanda tracks everything about it. Ordinary browsing is never a billable action: case files, charts, and stored observations remain readable without limiting your plan. ## API and MCP calls The public API and MCP share one daily call allowance per plan holder: 30 a day on Peek, 1,000 a day on Panda, resetting at 00:00 UTC. It is one shared pool: extra API keys, MCP connections, and workspace members do not add to it, and a workspace member draws on the workspace owner's pool. In-app usage in the product itself is never counted against it. Going over the limit returns a clear error naming the limit, your plan, and when it resets. ## The public starter preview The public starter preview of the curated starter boards can be read without an account or an API key. For each app in a niche it shows its two longest-running ads that are still live, how many days they have run, and when they were last seen running, plus a few keywords and organic posts, with counts for everything it leaves out. It never shows ad copy, advertiser names, videos or links. Without a key, each network may send 20 requests a minute and 200 a UTC day, and every visitor together shares one minute budget; a limit names its window and when to retry. Read through an API key or MCP connection, the same calls count toward your plan's daily call allowance. See [read a public starter preview](/docs/public-preview). Catalogue, keyword, and ASO collection also have their own quiet operational safeguards so public-source collection stays fair and reliable. These are not part of your plan's numbers; where relevant, their status is shown where you request the data. 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 tracked apps](/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. Paid ads are collected per chosen app, so boards and the market signals API and MCP read say when each app’s ads were last collected: **never** (no collection has finished, so no ads says nothing about the app), **stale** (the last collection is more than two days old) or **fresh**. Zero ads only means an app runs no ads PeekPanda could find when its collection is fresh. ## 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 tracked apps](/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 PeekPanda estimates. 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, how to read them, and when an estimate is unavailable. As of 2026-09-15, PeekPanda provides directional app revenue and download estimates. They are PeekPanda estimates, 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 and the date of the observation. Revenue means estimated App Store revenue only. It does not include web subscriptions, advertising income, or the company's total revenue. These are PeekPanda estimates. They are directional, not measured downloads, reported revenue, an audited country total, or a developer’s reported result. ## Why every result is a range An estimate is never a single exact number. The low and high values show the span a reading could reasonably fall in, and the middle value is the one PeekPanda plots and compares. Read the whole range, not only the middle. ## 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: - there is not enough evidence yet to support an estimate; - PeekPanda does not yet estimate that storefront or category; 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. Profile growth percentages compare the latest monthly estimate with the estimate in effect 30 days earlier. No percentage is shown when either value is unavailable or below the reporting floor, the earlier value is zero, or the change rounds to zero. A change can reflect a real change in the app's business or a change in the timing and coverage of collection. An estimate is not measured downloads or reported 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 PeekPanda estimates, not measured downloads. Developers’ measured download data lives in their own analytics tools. ### Are app revenue estimates the same as reported revenue? No. They are directional PeekPanda estimates, not reported revenue. 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. Download and revenue figures are PeekPanda estimates, not 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 ads, posts, queries and listing to collect for your space. It does not connect to the app, install software, access private analytics, or collect information about its users. Your tracked apps and tracked keywords stay private to your space. ## 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) --- # Pull new competitor ads on a schedule Check your competitors for newly found paid ads on a timer, read only what is new since the last run, and transcribe the video ads. A scheduled job, a cron script or an agent on a timer can ask PeekPanda one question every run: which ads did you find for these apps since I last looked? The loop below reads stored observations only. It never asks PeekPanda to collect new ads, so it is cheap and safe to repeat. ## What you need - An API key from `/account`, or an MCP client that is signed in. See [API keys](/docs/api-keys). - The App Store ids of the apps you follow, up to 25 per request. - One place to remember a single value between runs: the newest `discoveredAt` you have read. ## The loop 1. Read the value you stored last time. On the very first run there is none, so leave `discoveredAfter` out. 2. Call `GET /v1/paid-ads` with `storeIds`, `sort=recently_discovered` and, when you have one, `discoveredAfter` set to the stored value. 3. Follow `nextCursor` while `hasMore` is true. Keep every other filter and the sort unchanged between pages. 4. Process the new `items`. Each one is an ad PeekPanda first recorded after your stored instant. 5. Store the newest `discoveredAt` from this run. Only replace the stored value once the run has finished, so a failed run is simply read again next time. ```bash curl --get https://api.peekpanda.com/v1/paid-ads \ -H "Authorization: Bearer $PEEKPANDA_API_KEY" \ --data-urlencode 'storeIds=6480417616,1234567890' \ --data-urlencode 'sort=recently_discovered' \ --data-urlencode 'discoveredAfter=2026-10-05T06:00:00Z' \ --data-urlencode 'limit=40' ``` `discoveredAfter` is strict: an ad recorded at exactly the stored instant is not returned again. `discoveredAt` is when PeekPanda first recorded the ad, not when the advertiser started running it. Use `startedAt` for that, where the source discloses it. `storeIds` returns only ads verified to link to those apps. To follow one advertiser instead, pass `advertiserExternalId` with its exact page id. ## The same loop through MCP An agent makes the same read with the `get_ads` tool and the `search` action. The query takes the same names as the HTTP request. ```json { "name": "get_ads", "arguments": { "request": { "action": "search", "query": { "storeIds": "6480417616,1234567890", "sort": "recently_discovered", "discoveredAfter": "2026-10-05T06:00:00Z", "limit": 40 } } } } ``` ## Transcribing the video ads Only video ads have speech to transcribe. For each new item whose `creative.format` is `video`, call `POST /v1/paid-ads/{platform}/{archiveId}/transcript`, taking the platform and archive id from the item. Through MCP, use the `refresh` tool with the `ad_transcript` action and pass `platform` and `archiveId` in `params`. ```bash curl -X POST https://api.peekpanda.com/v1/paid-ads/meta/1234567890123456/transcript \ -H "Authorization: Bearer $PEEKPANDA_API_KEY" ``` - A transcript that is already ready comes back from storage, so asking twice is safe. - `409` means a transcript for that ad is being generated right now. Leave it and try again on the next run. - `422` means the ad has no stored video, for example an image-only ad. Do not retry it. Transcribing uses the `content:write` scope. The search itself only needs `context:read`. ## Fitting it in your daily allowance Every API request and every MCP call counts toward your plan's daily allowance, which resets at 00:00 UTC. Your plan's figure is on `/account` and in [Usage and limits](/docs/usage-and-limits). Budget one run like this: calls per run = pages read + video ads transcribed calls per day = runs per day × calls per run Keep calls per day under your allowance with room for the rest of your work. Two ways to stay inside it: run less often, since the loop never misses an ad between runs, and transcribe only the video ads you will actually read. If you do run out, the API answers `429` with `daily_call_limit` until the reset. --- # Get started Connect an assistant to PeekPanda so it can ask about competitors, creatives, keywords, and charts. You need a PeekPanda account. ## 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 | | --- | --- | --- | --- | | `get_context` | `workspaces` | `GET /v1/workspaces` | context:read | | `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` | `compare` | `GET /v1/competitors/compare` | context:read | | `search_apps` | `developer` | `GET /v1/developers/{platform}/{developerId}` | 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` | `ad_refresh_status` | `GET /v1/competitors/{platform}/{storeId}/paid-ads/refresh` | 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` | `analysis` | `GET /v1/paid-ads/{platform}/{archiveId}/analysis` | context:read | | `get_ads` | `intelligence` | `GET /v1/paid-ads/intelligence` | context:read | | `get_ads` | `app_intelligence` | `GET /v1/competitors/{platform}/{storeId}/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 | | `search_organic` | `analysis` | `GET /v1/organic/{postId}/analysis` | 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 | | `boards` | `list` | `GET /v1/boards` | context:read | | `boards` | `get` | `GET /v1/boards/{boardId}` | context:read | | `boards` | `memberships` | `GET /v1/boards/memberships` | context:read | | `starter_boards` | `list` | `GET /v1/starter-boards` | context:read | | `starter_boards` | `search` | `GET /v1/starter-boards/search` | context:read | | `starter_boards` | `detail` | `GET /v1/starter-boards/{nicheKey}` | context:read | | `starter_boards` | `public_niches` | `GET /v1/public/starter-boards` | context:read | | `starter_boards` | `public_preview` | `GET /v1/public/starter-boards/{nicheKey}` | context:read | | `adopt_starter_board` | `adopt` | `POST /v1/starter-boards/{nicheKey}/adopt` | content:write | | `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` | `ad_refresh` | `POST /v1/competitors/{platform}/{storeId}/paid-ads/refresh` | 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 | | `refresh` | `organic_analysis` | `POST /v1/organic/{postId}/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` | `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 | | `manage_boards` | `create` | `POST /v1/boards` | content:write | | `manage_boards` | `update` | `PATCH /v1/boards/{boardId}` | content:write | | `manage_boards` | `remove` | `DELETE /v1/boards/{boardId}` | content:write | | `manage_boards` | `add_item` | `POST /v1/boards/{boardId}/items` | content:write | | `manage_boards` | `update_item` | `PATCH /v1/boards/{boardId}/items/{itemId}` | content:write | | `manage_boards` | `remove_item` | `DELETE /v1/boards/{boardId}/items/{itemId}` | 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. ## 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. ## 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. A 429 with daily_call_limit means the plan's API and MCP calls for the day are used: 30 calls a day on Peek and 1,000 on Panda, reset at 00:00 UTC. [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. ## API keys Create an API key on /account under API keys. The secret is shown once, so store it in a credential manager, and revoke a key there as soon as you stop trusting it. Every request made with a key, and every MCP call, counts toward the plan's daily allowance: 30 calls a day on Peek and 1,000 on Panda, reset at 00:00 UTC. Extra keys share that one allowance. [Manage API keys](/docs/api-keys) --- # 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. GET /v1/workspaces (MCP get_context, action workspaces) lists the selectors this credential may use. This selects the owner context; it does not create membership or turn public catalogue evidence into private customer data. Naming a workspace the credential cannot reach returns 403 workspace_not_accessible. ## 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. --- # List workspaces List where this credential may work, with the tenancy selector for each. ## Endpoint: GET /v1/workspaces - Method: `GET` - Path: `/v1/workspaces` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_context` - MCP action: `workspaces` ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## Request This read has no query parameters. Call it before naming a workspace so you never guess an id. ``` curl --get 'https://api.peekpanda.com/v1/workspaces' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call get_context with action workspaces. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_context", "arguments": { "request": { "action": "workspaces" } } } ``` ## Response Each row carries the selector to send back as x-genviral-tenancy, or as the MCP workspace field. An API key and a connection pinned to one workspace list only that tenancy. An all-access connection lists personal plus every workspace its owner can currently reach. ``` { "data": [ { "selector": "personal", "kind": "personal", "name": "Personal space" }, { "selector": "33333333-3333-4333-8333-333333333333", "kind": "workspace", "name": "Growth team" } ] } ``` ## Source The credential itself and current workspace membership. Nothing is collected. ## Refresh Membership is read on every request, so a workspace the owner leaves drops out immediately. --- # 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 /competitors/compare reads up to 10 apps with their current download and revenue estimates in one call. GET /developers/{platform}/{developerId} reads one App Store developer and up to 400 of its catalogued apps, with download and revenue totals. 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 and starts nothing on the spot; a paid_ads request puts the app first in the daily paid-ad collection run. ## Creative library GET /organic and /organic/{postId} search the copied public organic corpus. POST /organic/{postId}/analysis asks for one post’s creative analysis, which a background worker runs, and GET on the same path reads where it stands. 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/workspaces` | `context:read` | `get_context workspaces` | | 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/compare` | `context:read` | `search_apps compare` | | GET | `/v1/developers/{platform}/{developerId}` | `context:read` | `search_apps developer` | | 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}/paid-ads/intelligence` | `context:read` | `get_ads app_intelligence` | | 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}/paid-ads/refresh` | `context:read` | `get_app ad_refresh_status` | | POST | `/v1/competitors/{platform}/{storeId}/paid-ads/refresh` | `content:write` | `refresh ad_refresh` | | 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` | | POST | `/v1/organic/{postId}/analysis` | `content:write` | `refresh organic_analysis` | | GET | `/v1/organic/{postId}/analysis` | `context:read` | `search_organic analysis` | | 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` | | GET | `/v1/paid-ads/{platform}/{archiveId}/analysis` | `context:read` | `get_ads 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/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` | | GET | `/v1/boards` | `context:read` | `boards list` | | GET | `/v1/boards/memberships` | `context:read` | `boards memberships` | | POST | `/v1/boards` | `content:write` | `manage_boards create` | | GET | `/v1/boards/{boardId}` | `context:read` | `boards get` | | PATCH | `/v1/boards/{boardId}` | `content:write` | `manage_boards update` | | DELETE | `/v1/boards/{boardId}` | `content:write` | `manage_boards remove` | | POST | `/v1/boards/{boardId}/items` | `content:write` | `manage_boards add_item` | | PATCH | `/v1/boards/{boardId}/items/{itemId}` | `content:write` | `manage_boards update_item` | | DELETE | `/v1/boards/{boardId}/items/{itemId}` | `content:write` | `manage_boards remove_item` | | GET | `/v1/starter-boards` | `context:read` | `starter_boards list` | | GET | `/v1/starter-boards/search` | `context:read` | `starter_boards search` | | GET | `/v1/starter-boards/{nicheKey}` | `context:read` | `starter_boards detail` | | POST | `/v1/starter-boards/{nicheKey}/adopt` | `content:write` | `adopt_starter_board adopt` | | GET | `/v1/public/starter-boards` | `context:read` | `starter_boards public_niches` | | GET | `/v1/public/starter-boards/{nicheKey}` | `context:read` | `starter_boards public_preview` | --- # 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. A read that runs out of its time budget answers 503 timeout with Retry-After; send the same read again after it. A 403 is authorization_required for a missing scope and workspace_not_accessible for a tenancy the credential cannot reach; switch workspace for the second rather than asking for a new scope. 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. ## 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 | Names starting with the text come first, then names holding every word, then descriptions holding every word. Words match whole, singular or plural. | | `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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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. Show currentFacts: each value names its provenance, observed from an App Store storefront or the catalogue snapshot, and its own asOf date. The top-level appName, rating, totalRatings and price keep the dated snapshot values. ``` { "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", "currentFacts": { "appName": { "provenance": "observed", "value": "Example Calorie Tracker", "asOf": "2026-09-06T00:00:00.000Z", "country": "us" }, "rating": { "provenance": "observed", "value": 4.8, "asOf": "2026-09-06T00:00:00.000Z", "country": "us" }, "totalRatings": { "provenance": "observed", "value": 13210, "asOf": "2026-09-06T00:00:00.000Z", "country": "us" }, "price": { "provenance": "snapshot", "value": 0, "asOf": "2026-09-06" } } } ], "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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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. Show currentFacts: each value names its provenance, observed from an App Store storefront or the catalogue snapshot, and its own asOf date. The top-level appName, rating, totalRatings and price keep the dated snapshot values. 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", "currentFacts": { "appName": { "provenance": "observed", "value": "Example Calorie Tracker", "asOf": "2026-09-06T00:00:00.000Z", "country": "us" }, "rating": { "provenance": "observed", "value": 4.8, "asOf": "2026-09-06T00:00:00.000Z", "country": "us" }, "totalRatings": { "provenance": "observed", "value": 13210, "asOf": "2026-09-06T00:00:00.000Z", "country": "us" }, "price": { "provenance": "snapshot", "value": 0, "asOf": "2026-09-06" } }, "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, strongest evidence first, each with why it matched. ## 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. | | `fallback` | query | category | none | no | category (default) adds same-category apps whose name shares a word with this app after the evidence; none returns evidence-backed apps only. | ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## Request Send an optional opaque cursor, limit and fallback. Defaults match the competitor list page, with fallback=category. ``` 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' \ --data-urlencode 'fallback=none' ``` ## 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 paginated listing envelope of GET /v1/competitors. Each listing adds matchReason (shared_searches, similar_listing, similar_description or same_category) and sharedSearches, the number of US App Store searches both apps rank for (null for same_category). Show currentFacts: each value names its provenance, observed from an App Store storefront or the catalogue snapshot, and its own asOf date. The top-level appName, rating, totalRatings and price keep the dated snapshot values. ``` { "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", "currentFacts": { "appName": { "provenance": "observed", "value": "Example Calorie Tracker", "asOf": "2026-09-06T00:00:00.000Z", "country": "us" }, "rating": { "provenance": "observed", "value": 4.8, "asOf": "2026-09-06T00:00:00.000Z", "country": "us" }, "totalRatings": { "provenance": "observed", "value": 13210, "asOf": "2026-09-06T00:00:00.000Z", "country": "us" }, "price": { "provenance": "snapshot", "value": 0, "asOf": "2026-09-06" } }, "matchReason": "shared_searches", "sharedSearches": 7 } ], "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. --- # Compare competitors Read up to 10 iOS apps side by side: identity, rating and current monthly download and revenue estimates in one call. ## Endpoint: GET /v1/competitors/compare - Method: `GET` - Path: `/v1/competitors/compare` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `search_apps` - MCP action: `compare` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `storeIds` | query | comma-separated numeric strings, 2–10 distinct | no | Apple App Store identifiers. Put the app in question first; the response keeps this order. | ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## Request Send every store ID in one storeIds value. Duplicates are removed before the 2–10 bound applies. ``` curl --get 'https://api.peekpanda.com/v1/competitors/compare' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --data-urlencode 'storeIds=0000000000,1145935738' ``` ## MCP request Call search_apps with action compare. Path fields go in params, filters in query, and a write body in body. ``` { "name": "search_apps", "arguments": { "request": { "action": "compare" } } } ``` ## Response subjectStoreId is the first requested app, the one in question, even when PeekPanda does not hold it. data keeps the request order. Each estimate is the current worldwide monthly range (or below_floor under 5,000 a month) with the night it was scored, and previous is the estimate that held a month earlier. An estimate is null when no nightly score covers the app: unknown, not zero. Store IDs PeekPanda does not hold are listed in missingStoreIds. ``` { "subjectStoreId": "0000000000", "data": [ { "app": { "storeId": "0000000000", "name": "Example Calorie Tracker", "iconUrl": null, "genre": "Health & Fitness", "category": "health_and_fitness", "developerName": "Example Labs", "rating": 4.7, "totalRatings": 12450 }, "monthlyInstalls": { "kind": "range", "scope": "worldwide", "range": { "low": 62000, "mid": 130000, "high": 270000 }, "observedAt": "2026-09-27T00:00:00.000Z", "previous": { "kind": "range", "scope": "worldwide", "range": { "low": 51000, "mid": 110000, "high": 220000 }, "observedAt": "2026-08-28T00:00:00.000Z" } }, "monthlyRevenue": { "kind": "below_floor", "scope": "worldwide", "ceiling": 5000, "observedAt": "2026-09-27T00:00:00.000Z", "previous": null }, "currency": "USD" } ], "missingStoreIds": [ "1145935738" ] } ``` ## Source PeekPanda estimates, refreshed nightly, beside dated public App Store listing observations. Figures are worldwide and monthly ranges, never store-reported numbers. ## Refresh This read returns stored estimates and does not start collection. --- # Get an app developer Read one iOS App Store developer and every app PeekPanda catalogues for it, with store facts, monthly download and revenue estimates and paid-ad counts, in one call. ## Endpoint: GET /v1/developers/{platform}/{developerId} - Method: `GET` - Path: `/v1/developers/{platform}/{developerId}` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `search_apps` - MCP action: `developer` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | ios | yes | The preview supports iOS only. | | `developerId` | path | numeric string, up to 20 digits | yes | Apple's numeric developer id: the developerId on a catalogue listing, or the number after id in an apps.apple.com/developer link. | ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## Request Pass Apple's numeric developer id in the path. Take it from the developerId on a catalogue listing, or from an App Store developer link: apps.apple.com/developer/example-labs/id1000000000 has developerId 1000000000. A developer is identified by this id, never matched by name. There is no query or body. ``` curl --get 'https://api.peekpanda.com/v1/developers/ios/1000000000' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call search_apps with action developer. Path fields go in params, filters in query, and a write body in body. ``` { "name": "search_apps", "arguments": { "request": { "action": "developer", "params": { "platform": "", "developerId": "" } } } } ``` ## Response name is the seller name the App Store shows, or null until a storefront lookup has named the developer. apps come ordered by estimated revenue, then estimated downloads, then ratings. Each app has its store facts; storeStatus is listed, not_found when every observed storefront reported it missing, or unchecked before any lookup. monthlyInstalls and monthlyRevenue are the current worldwide monthly range (or below_floor under 5,000 a month) with the night it was scored, and previous is the estimate that held a month earlier. paidAds holds the stored paid-ad counts {liveAds, totalAds, builtAt}. An estimate or paidAds is null when PeekPanda holds none for the app: unknown, not zero. categories lists the served apps’ categories, most apps first, and topRevenueStoreId and topInstallsStoreId name the leading apps. history holds the developer’s daily totals for up to the last 90 scored days, oldest first and ending on the newest scored night. Each point applies the totals rule to the estimates in effect that day and carries the same counts, so when rangedApps rises from one day to the next, more apps were scored, which is not growth; a range is null on a day no app had one, and the list is empty until any app has been scored. A developer with no catalogued app answers 404 not_found. ``` { "platform": "ios", "developerId": "1000000000", "name": "Example Labs", "appCount": 2, "appsTruncated": false, "categories": [ { "category": "health_and_fitness", "appCount": 2 } ], "totals": { "monthlyInstalls": { "range": { "low": 62000, "mid": 130000, "high": 270000 }, "rangedApps": 1, "belowFloorApps": 1, "belowFloorCeiling": 5000, "unknownApps": 0 }, "monthlyRevenue": { "range": { "low": 140000, "mid": 290000, "high": 600000 }, "rangedApps": 1, "belowFloorApps": 0, "belowFloorCeiling": null, "unknownApps": 1 }, "liveAds": 18, "appsWithPaidAds": 1 }, "topInstallsStoreId": "0000000000", "topRevenueStoreId": "0000000000", "estimatesAsOf": "2026-10-15T00:00:00.000Z", "currency": "USD", "history": [ { "day": "2026-10-13", "monthlyInstalls": { "range": { "low": 60000, "mid": 120000, "high": 250000 }, "rangedApps": 1, "belowFloorApps": 0, "belowFloorCeiling": null, "unknownApps": 1 }, "monthlyRevenue": { "range": { "low": 130000, "mid": 270000, "high": 560000 }, "rangedApps": 1, "belowFloorApps": 0, "belowFloorCeiling": null, "unknownApps": 1 } }, { "day": "2026-10-14", "monthlyInstalls": { "range": { "low": 60000, "mid": 120000, "high": 250000 }, "rangedApps": 1, "belowFloorApps": 1, "belowFloorCeiling": 5000, "unknownApps": 0 }, "monthlyRevenue": { "range": { "low": 130000, "mid": 270000, "high": 560000 }, "rangedApps": 1, "belowFloorApps": 0, "belowFloorCeiling": null, "unknownApps": 1 } }, { "day": "2026-10-15", "monthlyInstalls": { "range": { "low": 62000, "mid": 130000, "high": 270000 }, "rangedApps": 1, "belowFloorApps": 1, "belowFloorCeiling": 5000, "unknownApps": 0 }, "monthlyRevenue": { "range": { "low": 140000, "mid": 290000, "high": 600000 }, "rangedApps": 1, "belowFloorApps": 0, "belowFloorCeiling": null, "unknownApps": 1 } } ], "apps": [ { "storeId": "0000000000", "name": "Example Calorie Tracker", "iconUrl": null, "category": "health_and_fitness", "genre": "Health & Fitness", "price": 0, "formattedPrice": "Free", "inAppPurchases": true, "storeStatus": "listed", "rating": 4.7, "totalRatings": 12450, "releaseDate": "2024-05-01", "storeUpdatedAt": "2026-09-30", "monthlyInstalls": { "kind": "range", "scope": "worldwide", "range": { "low": 62000, "mid": 130000, "high": 270000 }, "observedAt": "2026-10-15T00:00:00.000Z", "previous": { "kind": "range", "scope": "worldwide", "range": { "low": 51000, "mid": 110000, "high": 220000 }, "observedAt": "2026-09-15T00:00:00.000Z" } }, "monthlyRevenue": { "kind": "range", "scope": "worldwide", "range": { "low": 140000, "mid": 290000, "high": 600000 }, "observedAt": "2026-10-15T00:00:00.000Z", "previous": null }, "paidAds": { "liveAds": 18, "totalAds": 64, "builtAt": "2026-10-15T03:00:00.000Z" } }, { "storeId": "0000000001", "name": "Example Water Reminder", "iconUrl": null, "category": "health_and_fitness", "genre": "Health & Fitness", "price": 0, "formattedPrice": "Free", "inAppPurchases": true, "storeStatus": "listed", "rating": 4.5, "totalRatings": 860, "releaseDate": "2025-02-11", "storeUpdatedAt": "2026-08-19", "monthlyInstalls": { "kind": "below_floor", "scope": "worldwide", "ceiling": 5000, "observedAt": "2026-10-15T00:00:00.000Z", "previous": null }, "monthlyRevenue": null, "paidAds": null } ] } ``` ## Source Dated public App Store listing observations from the PeekPanda catalogue, with PeekPanda estimates refreshed nightly and stored paid-ad counts. An app belongs to the developer its storefront listing names, so an app transferred to another developer moves with it. ## Refresh This read returns stored observations and estimates and does not start collection. ## App limit One read serves at most 400 apps. A developer with more catalogued apps serves its 400 most-rated apps, appCount says how many apps the catalogue holds for it, and appsTruncated is true. There is no cursor. ## Totals totals are computed over the served apps. monthlyInstalls.range and monthlyRevenue.range add up the ranges of the apps with a ranged estimate (rangedApps) and are null when none has one. Apps under the floor (belowFloorApps) and apps with no estimate (unknownApps) are counted apart, never folded into the sum, so a total covers only the scored apps it names. belowFloorCeiling is the level the below-floor apps sit under (5,000 a month today) and is null when belowFloorApps is 0. liveAds adds up the apps’ stored live-ad counts and appsWithPaidAds counts the apps with stored counts, even when none of their ads is live; an app without stored counts is unknown, not zero. ## Revenue scope Estimates are App Store revenue only: worldwide monthly App Store figures in USD. Revenue earned outside the App Store, such as web checkout, is not included. ## Rate limits This read uses the standard read budget; honor Retry-After on a 429 rate_limited response. Every call also counts toward your plan's daily API and MCP allowance: 30 calls a day on Peek and 1,000 on Panda, shared by every key and MCP connection on the plan and reset at 00:00 UTC. Past it, calls answer 429 daily_call_limit until the reset. --- # Get competitor case file Read one competitor app’s whole overview in one request. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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 listing.currentFacts holds the name, rating, rating count and price to show, each with its provenance (observed from an App Store storefront, or the catalogue snapshot) and its own asOf date; the top-level listing fields keep the dated snapshot. paid is ready, updating (a rebuild is pending and nothing is stored yet), none (no paid evidence, with the reason) or unavailable (its read failed). A ready summary is read at request time over the last 7 days and 12 weeks, and its attribution splits the ads by basis. startedLast7Days counts ads that started on the reading day or the six UTC days before it; recentDays lays out those same seven days one by one, newest first, with quiet days as zero: newAds is how many ads started that day and liveAds how many of those are still live, and the seven newAds add up to startedLast7Days. weeklyStarts counts new ads per Monday week, oldest first, ending with the reading day’s own partial week. latestAds (newest first) and longestRunningAds (live ads with a start date, earliest start first, empty when none) use 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. listingHistory.storefronts lists every App Store storefront with an earlier iPhone screenshot set, the most recently changed first; each carries its currentScreenshotUrls and its earlier sets, most recently replaced first, with up to 12 earlier sets across all storefronts. PeekPanda keeps a set when a store lookup returns different iPhone screenshots: replacedAt is the lookup that first saw the new set and lastSeenAt the last lookup that still saw this one, so the developer changed the page between them. History starts at the first change PeekPanda saw. 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", "currentFacts": { "appName": { "provenance": "observed", "value": "Example Calorie Tracker", "asOf": "2026-09-06T00:00:00.000Z", "country": "us" }, "rating": { "provenance": "observed", "value": 4.8, "asOf": "2026-09-06T00:00:00.000Z", "country": "us" }, "totalRatings": { "provenance": "observed", "value": 13210, "asOf": "2026-09-06T00:00:00.000Z", "country": "us" }, "price": { "provenance": "snapshot", "value": 0, "asOf": "2026-09-06" } } }, "paid": { "kind": "ready", "summary": { "totalAds": 14, "liveAds": 6, "startedLast7Days": 2, "recentDays": [ { "day": "2026-09-06", "newAds": 0, "liveAds": 0 }, { "day": "2026-09-05", "newAds": 0, "liveAds": 0 }, { "day": "2026-09-04", "newAds": 1, "liveAds": 1 }, { "day": "2026-09-03", "newAds": 0, "liveAds": 0 }, { "day": "2026-09-02", "newAds": 0, "liveAds": 0 }, { "day": "2026-09-01", "newAds": 1, "liveAds": 0 }, { "day": "2026-08-31", "newAds": 0, "liveAds": 0 } ], "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, "attribution": { "appStoreLink": { "total": 12, "live": 5 }, "sellerDomain": { "total": 2, "live": 1 }, "operator": { "total": 0, "live": 0 } } }, "advertisers": [ { "advertiserId": "33333333-3333-4333-8333-333333333333", "externalId": "example-page", "name": "Example Calorie Tracker", "totalAds": 14, "liveAds": 6 } ], "latestAds": [], "longestRunningAds": [], "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 } ] }, "listingHistory": { "storefronts": [ { "country": "us", "currentScreenshotUrls": [ "https://is1-ssl.mzstatic.com/image/thumb/example-now/392x696bb.jpg" ], "versions": [ { "replacedAt": "2026-08-14T09:30:00.000Z", "lastSeenAt": "2026-08-13T09:30:00.000Z", "version": "4.1.3", "screenshotUrls": [ "https://is1-ssl.mzstatic.com/image/thumb/example-old/392x696bb.jpg" ] } ] } ] }, "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, or to the app whose proven advertiser page ran it), current US App Store search observations, PeekPanda estimates, stored Apple chart snapshots and the public organic corpus. Missing evidence is null or none, never zero. ## Refresh This read returns stored observations and does not start collection. ## How paid ads reach an app Every paid count covers all of the app’s ads, and paid.summary.attribution splits total and live ads by basis. appStoreLink: the ad itself links to the app on the App Store. sellerDomain: the ad ran on the app’s own advertiser page, matched by the website its App Store listing publishes and the page name, and lands on that website. operator: the page was confirmed by PeekPanda review. The three add up to totalAds and liveAds. An ad belongs to one app at most: an ad that links another app counts for that app. A name alone never attributes a page. ## Why paid can be none paid.kind none says why PeekPanda shows no paid counts, read from stored collection state. reason is not_searched (PeekPanda has not looked for the app’s advertiser yet), searching (a search runs now), no_proven_advertiser (the latest search proved no advertiser page; lastSearchedAt says when), collecting (the app’s page is proven and its ads are on the way), advertiser_has_no_ads (the proven page’s latest walk found no Meta ads) or advertiser_under_review (the page is held back pending review). advertiser names the proven page, or is null. none never means the app does not advertise. ``` { "kind": "none", "reason": "advertiser_has_no_ads", "lastSearchedAt": "2026-09-06T00:00:00.000Z", "advertiser": { "externalId": "100000000000001", "name": "Example Calorie Tracker" } } ``` ## Section states Each section says why it holds no evidence. null means the section does not apply to this listing: aso, estimates, rankStanding and listingHistory are null outside iOS. none means PeekPanda holds no paid evidence for the app, with the reason. 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, listingHistory, 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", "currentFacts": { "appName": { "provenance": "observed", "value": "Example Calorie Tracker", "asOf": "2026-09-06T00:00:00.000Z", "country": "us" }, "rating": { "provenance": "observed", "value": 4.8, "asOf": "2026-09-06T00:00:00.000Z", "country": "us" }, "totalRatings": { "provenance": "observed", "value": 13210, "asOf": "2026-09-06T00:00:00.000Z", "country": "us" }, "price": { "provenance": "snapshot", "value": 0, "asOf": "2026-09-06" } } }, "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 } ] }, "listingHistory": { "storefronts": [ { "country": "us", "currentScreenshotUrls": [ "https://is1-ssl.mzstatic.com/image/thumb/example-now/392x696bb.jpg" ], "versions": [ { "replacedAt": "2026-08-14T09:30:00.000Z", "lastSeenAt": "2026-08-13T09:30:00.000Z", "version": "4.1.3", "screenshotUrls": [ "https://is1-ssl.mzstatic.com/image/thumb/example-old/392x696bb.jpg" ] } ] } ] }, "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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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 one listing's organic posts and where PeekPanda's search for more stands. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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 data holds the matched posts, newest first, as suggestions rather than verified ownership. search.state is idle when PeekPanda has no search for this app and none is on the way: nobody tracks it or requested its organic posts, or PeekPanda cannot read its App Store listing yet, queued when it is tracked or requested and PeekPanda will set up its searches, stopped when PeekPanda set up a search for the app but none of its searches ever ran and none can (they are paused or retired, or there was nothing to search for), so a request changes nothing, searching while an active search has not run yet or found posts still wait for review (searches and searchesRun count the active searches, waitingForReview the posts), and searched once no active search waits for its first run and nothing waits for review (searches counts the searches that ran, activeSearches those still checking). When the search status could not be read, search is kind unavailable and data still holds the posts. An empty data list is never proof the app has no organic content. ``` { "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, "search": { "state": "searching", "startedAt": "2026-09-06T00:00:00.000Z", "searches": 22, "searchesRun": 14, "waitingForReview": 6 } } ``` ## Source The public organic corpus, tied to apps by App Store link, website or name; suggestions, not verified ownership. ## Refresh This read starts no collection. PeekPanda searches for the organic posts of apps someone tracks or requested organic posts for; search says where that stands. --- # Get competitor ASO intelligence Read stored App Store search observations for one iOS listing. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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. `demand` is Apple's weekly search popularity for the phrase (1 to 100, with Apple's 1 to 5 tier). Where Apple reports nothing for a US phrase, `popularityEstimate` carries PeekPanda's own estimate: a tier (`very_low`, `low`, `medium` or `high`), never a number on Apple's scale, with `estimatedAt` and the `modelTrainedAt` of the weekly model behind it. The two never appear together: when `demand` is set, `popularityEstimate` is null. Neither is a search count. ``` { "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" }, "popularityEstimate": null, "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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 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. --- # Get competitor paid-ad refresh status Read an app's latest paid-ad refresh and your remaining daily refreshes. ## Endpoint: GET /v1/competitors/{platform}/{storeId}/paid-ads/refresh - Method: `GET` - Path: `/v1/competitors/{platform}/{storeId}/paid-ads/refresh` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_app` - MCP action: `ad_refresh_status` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | ios | yes | Paid-ad refresh supports 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## Request No request body. This read never starts a refresh; poll it while latest.state is running. ``` curl --get 'https://api.peekpanda.com/v1/competitors/ios/0000000000/paid-ads/refresh' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call get_app with action ad_refresh_status. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_app", "arguments": { "request": { "action": "ad_refresh_status", "params": { "platform": "", "storeId": "" } } } } ``` ## Response latest is null until the app has been refreshed. latest.state is running, completed, not_matched or failed. newAdCount is null until the walk has started. quota is daily with limit, used, remaining and resetAt, or unlimited. ``` { "appStoreId": "0000000000", "latest": { "state": "running", "requestedAt": "2026-09-24T08:00:00.000Z", "settledAt": null, "newAdCount": null }, "quota": { "kind": "daily", "limit": 3, "used": 1, "remaining": 2, "resetAt": "2026-09-25T08:00:00.000Z" } } ``` ## Source Public ads from the app's own advertiser page, matched by its exact app name. Ads are attributed only when the advertiser matches; no looser name matching is used. ## Refresh A refresh walks the advertiser from its newest ad. This endpoint has its own quiet daily limit, separate from your plan's tracked-app number: 1 on Peek and 3 on Panda, in a rolling 24 hours, shared by a workspace. A refresh always walks the advertiser again; while an app's walk is still running, a refresh by anyone joins that walk and is not counted. A failed refresh is not counted at all. --- # Refresh competitor paid ads Look for an iOS app's newest paid ads now instead of waiting for the next scheduled pass. ## Endpoint: POST /v1/competitors/{platform}/{storeId}/paid-ads/refresh - Method: `POST` - Path: `/v1/competitors/{platform}/{storeId}/paid-ads/refresh` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `refresh` - MCP action: `ad_refresh` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | ios | yes | Paid-ad refresh supports 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 409 conflict when the write would duplicate an exclusive row. ## Request No request body. Send an Idempotency-Key when a client may retry. ``` curl --request POST 'https://api.peekpanda.com/v1/competitors/ios/0000000000/paid-ads/refresh' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call refresh with action ad_refresh. Path fields go in params, filters in query, and a write body in body. ``` { "name": "refresh", "arguments": { "request": { "action": "ad_refresh", "params": { "platform": "", "storeId": "" } } } } ``` ## Response state is running, completed, limited, unavailable or failed. Always inspect state, even after HTTP success. A refresh answers running: charged is true when it started a new walk, and false when the app's walk was already running and the request joined it. limited and unavailable are never charged, and a walk that ends failed is refunded and can be retried. retryAt says when a new request can succeed. Read the status for the outcome; its latest.state not_matched means no advertiser has the exact app name. ``` { "appStoreId": "0000000000", "state": "running", "charged": true, "retryAt": null, "latest": { "state": "running", "requestedAt": "2026-09-24T08:00:00.000Z", "settledAt": null, "newAdCount": null }, "quota": { "kind": "daily", "limit": 3, "used": 1, "remaining": 2, "resetAt": "2026-09-25T08:00:00.000Z" } } ``` ## Source Public ads from the app's own advertiser page, matched by its exact app name. Ads are attributed only when the advertiser matches; no looser name matching is used. ## Refresh A refresh walks the advertiser from its newest ad. This endpoint has its own quiet daily limit, separate from your plan's tracked-app number: 1 on Peek and 3 on Panda, in a rolling 24 hours, shared by a workspace. A refresh always walks the advertiser again; while an app's walk is still running, a refresh by anyone joins that walk and is not counted. A failed refresh is not counted at all. --- # List competitor data requests List recorded intelligence-request rows for one listing. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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. A paid_ads request gives the app top priority in the daily paid-ad collection run; an organic request asks PeekPanda to search for an iOS app's organic posts; ASO requests may begin an asynchronous collection. --- # Request competitor data Record deduplicated demand for one missing competitor-intelligence kind. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 409 conflict when the write would duplicate an exclusive row. ## Request Send one kind. paid_ads gives the app top priority in the daily paid-ad collection run while the request stands; nothing is collected on the spot, so read new ads later with GET /v1/paid-ads and storeIds. organic asks PeekPanda to search for an iOS app's organic posts, unless PeekPanda already set up a search for the app (even a paused one): read the progress in the search field of GET /v1/competitors/{platform}/{storeId}/organic, and the posts there once reviewed. You can file 20 new organic requests per UTC day; past that the request answers 429 organic_request_daily_limit with limit and retryAfterSeconds (the wait until 00:00 UTC) and nothing is recorded. Repeating a request you already filed is always accepted and never counts. Other kinds record demand only; use the ASO refresh command to start ASO 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. A paid_ads request gives the app top priority in the daily paid-ad collection run; an organic request asks PeekPanda to search for an iOS app's organic posts; ASO requests may begin an asynchronous collection. --- # Request an iOS competitor Add one explicitly requested iOS app without requiring established-app status. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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: the listing's live ads, most recently discovered first. Continue it with GET /v1/paid-ads?storeIds={storeId}&status=active&sort=recently_discovered&cursor={nextCursor}; a cursor only continues the query and sort that produced it. Both read the app's stored paid projection, about a minute behind new ads; landing pages not built yet read as an empty list. ``` { "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 The public organic corpus, tied to apps by App Store link, website or name; 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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 app names the listing the series describes. Estimate fields are null when the available public evidence cannot support them. ``` { "app": { "storeId": "0000000000", "name": "Example Calorie Tracker", "iconUrl": null, "genre": "Health & Fitness", "category": "health_and_fitness", "developerName": null, "rating": 4.7, "totalRatings": 12450 }, "data": [ { "storeId": "0000000000", "platform": "ios", "source": "peekpanda", "observedAt": "2026-09-06T00:00:00.000Z", "country": "us", "ratingVelocityPerDay": 31.5, "monthlyInstalls": { "kind": "range", "scope": "worldwide", "range": { "low": 62000, "mid": 130000, "high": 270000 } }, "previousMonthlyInstalls": { "kind": "below_floor", "scope": "worldwide", "ceiling": 5000 }, "monthlyRevenue": { "kind": "range", "scope": "worldwide", "range": { "low": 970000, "mid": 2000000, "high": 4100000 } }, "previousMonthlyRevenue": null, "currency": "USD" } ] } ``` ## Source PeekPanda estimates, refreshed nightly. Figures are worldwide and monthly: a low, mid and high range, or below_floor under 5,000 a month. A day before an app was first scored 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 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, refreshed nightly. Figures are worldwide and monthly: a low, mid and high range, or below_floor under 5,000 a month. A day before an app was first scored 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 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 tracked iOS apps of the credential's space, with the plan's tracked-app allowance. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## Request Returns the tracked apps of the tenancy the credential names: personal, or one workspace. This GET never calls Apple. ``` 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 `trackedAppAllowance` is the plan holder allowance: apps tracked across every space the holder pays for (each app once), the plan limit (Peek 1, Panda 100) and the plan. 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", "role": "mine", "createdAt": "2026-09-06T00:00:00.000Z", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "keywordCount": 1 } ], "trackedAppAllowance": { "used": 1, "limit": 1, "plan": "peek" } } ``` ## 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 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", "role": "mine", "createdAt": "2026-09-06T00:00:00.000Z", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "keywordCount": 1 }, "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" }, "popularityEstimate": null }, { "query": "calorie tracker for keto", "position": null, "trackedKeywordId": "66666666-6666-4666-8666-666666666666", "source": "manual", "note": null, "tags": [], "observedAt": "2026-09-06T00:00:00.000Z", "isStale": false, "resultCount": 0, "apps": [], "history": [], "difficulty": null, "demand": null, "popularityEstimate": { "tier": "medium", "method": "peekpanda_popularity_estimate", "country": "us", "estimatedAt": "2026-09-06T00:00:00.000Z", "modelTrainedAt": "2026-09-05T00:30: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, or swap it in for a tracked app. ## 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. | | `boardId` | body | uuid (optional) | yes | A board in the tenancy the app goes on. Omitted, an app on no board goes on `My apps`. | | `replacesAppId` | body | uuid (optional) | yes | A tracked app of the same tenancy to stop tracking in the same step, so this app takes its place in the allowance. | ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 409 conflict when the write would duplicate an exclusive row. ## Request Body is `storeId` and `role` (`mine` or `competitor`), with an optional `boardId`. Tracking collects the app's paid ads, organic posts, keywords and chart position from that day; it never backfills. An app on no board in the space is put on the space's `My apps` board. Each app counts once toward the plan holder allowance; over it the API answers 409 `tracked_app_limit` with `limit` and `plan`. 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", "role": "mine", "createdAt": "2026-09-06T00:00:00.000Z", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "keywordCount": 1 } ``` ## 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. ## Swap a tracked app at the limit Add `replacesAppId` to track this app in place of another one. The App Store lookup for the new app runs first; then one transaction stops tracking the replaced app exactly as `DELETE /v1/aso/apps/{appId}` does (it leaves every board in the tenancy and its tracked keywords are removed) and tracks the new app. Either both happen or neither does: when the call is refused, the replaced app, its boards and its keywords are unchanged. The replaced app frees its place only when the plan holder does not also track it in another of their spaces. Errors: 404 `not_found` when `replacesAppId` is not a tracked app of the tenancy (or `boardId` is not a board there, or the App Store has no such app), 400 `invalid_request` when it names the app being added, 409 `conflict` when the tenancy already tracks the new app while the replaced app is still tracked, 409 `tracked_app_limit` when the swap still leaves no room, 503 `provider_unavailable` when the App Store did not answer. A timeout or 5xx proves nothing either way. Retrying is safe: when the replaced app is already gone and the tenancy tracks the new app, the call returns that tracked app and changes nothing; when the replaced app is gone and the new app is not tracked, it answers 404. The first keyword refresh of a newly tracked app is best effort and never fails the call. ``` 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":"competitor","replacesAppId":"11111111-1111-4111-8111-111111111111"}' ``` --- # Add a temporary ASO app Add an app idea before it has an App Store listing. ## 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 | Pre-launch app name. | ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 409 conflict when the write would duplicate an exclusive row. ## Request Creates a pre-launch 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"}' ``` ## 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", "role": "mine", "createdAt": "2026-09-06T00:00:00.000Z", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "keywordCount": 1 } ``` ## 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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. `demand` is Apple's weekly search popularity for the phrase (1 to 100, with Apple's 1 to 5 tier). Where Apple reports nothing for a US phrase, `popularityEstimate` carries PeekPanda's own estimate: a tier (`very_low`, `low`, `medium` or `high`), never a number on Apple's scale, with `estimatedAt` and the `modelTrainedAt` of the weekly model behind it. The two never appear together: when `demand` is set, `popularityEstimate` is null. Neither is a search count. ``` { "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", "role": "mine", "createdAt": "2026-09-06T00:00:00.000Z", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "keywordCount": 1 }, "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" }, "popularityEstimate": null }, { "query": "calorie tracker for keto", "position": null, "trackedKeywordId": "66666666-6666-4666-8666-666666666666", "source": "manual", "note": null, "tags": [], "observedAt": "2026-09-06T00:00:00.000Z", "isStale": false, "resultCount": 0, "apps": [], "history": [], "difficulty": null, "demand": null, "popularityEstimate": { "tier": "medium", "method": "peekpanda_popularity_estimate", "country": "us", "estimatedAt": "2026-09-06T00:00:00.000Z", "modelTrainedAt": "2026-09-05T00:30: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. ## 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. | ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 409 conflict when the write would duplicate an exclusive row. ## Request Changes the stored role. It preserves tracked keywords and observations, does not call Apple and does 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", "role": "mine", "createdAt": "2026-09-06T00:00:00.000Z", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "keywordCount": 1 } ``` ## 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 409 conflict when the write would duplicate an exclusive row. ## Request Stops tracking the app in the tenancy: it leaves every board there and its tracked keywords are removed. 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 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", "role": "mine", "createdAt": "2026-09-06T00:00:00.000Z", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "keywordCount": 1 }, "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" }, "popularityEstimate": null }, { "query": "calorie tracker for keto", "position": null, "trackedKeywordId": "66666666-6666-4666-8666-666666666666", "source": "manual", "note": null, "tags": [], "observedAt": "2026-09-06T00:00:00.000Z", "isStale": false, "resultCount": 0, "apps": [], "history": [], "difficulty": null, "demand": null, "popularityEstimate": { "tier": "medium", "method": "peekpanda_popularity_estimate", "country": "us", "estimatedAt": "2026-09-06T00:00:00.000Z", "modelTrainedAt": "2026-09-05T00:30: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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 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", "role": "mine", "createdAt": "2026-09-06T00:00:00.000Z", "lastRefreshedAt": "2026-09-06T00:00:00.000Z", "keywordCount": 1 }, "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" }, "popularityEstimate": null }, { "query": "calorie tracker for keto", "position": null, "trackedKeywordId": "66666666-6666-4666-8666-666666666666", "source": "manual", "note": null, "tags": [], "observedAt": "2026-09-06T00:00:00.000Z", "isStale": false, "resultCount": 0, "apps": [], "history": [], "difficulty": null, "demand": null, "popularityEstimate": { "tier": "medium", "method": "peekpanda_popularity_estimate", "country": "us", "estimatedAt": "2026-09-06T00:00:00.000Z", "modelTrainedAt": "2026-09-05T00:30: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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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. `demand` is Apple's weekly search popularity for the phrase (1 to 100, with Apple's 1 to 5 tier). Where Apple reports nothing for a US phrase, `popularityEstimate` carries PeekPanda's own estimate: a tier (`very_low`, `low`, `medium` or `high`), never a number on Apple's scale, with `estimatedAt` and the `modelTrainedAt` of the weekly model behind it. The two never appear together: when `demand` is set, `popularityEstimate` is null. Neither is a search count. ``` { "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" }, "popularityEstimate": null, "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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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. `demand` is Apple's weekly search popularity for the phrase (1 to 100, with Apple's 1 to 5 tier). Where Apple reports nothing for a US phrase, `popularityEstimate` carries PeekPanda's own estimate: a tier (`very_low`, `low`, `medium` or `high`), never a number on Apple's scale, with `estimatedAt` and the `modelTrainedAt` of the weekly model behind it. The two never appear together: when `demand` is set, `popularityEstimate` is null. Neither is a search count. ``` { "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. --- # List boards List the boards in the credential tenancy with their app and saved-ad counts and the tracked-app allowance. ## Endpoint: GET /v1/boards - Method: `GET` - Path: `/v1/boards` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `boards` - 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## Request Boards belong to the tenancy the credential names: personal, or one workspace. Workspace membership is re-read on every call. ``` curl --get 'https://api.peekpanda.com/v1/boards' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call boards with action list. Path fields go in params, filters in query, and a write body in body. ``` { "name": "boards", "arguments": { "request": { "action": "list" } } } ``` ## Response `limit` is the board cap per personal space or workspace (100), not a page size. `trackedAppAllowance` is the plan holder allowance: apps tracked across every space the holder pays for (each app once), the plan limit (Peek 1, Panda 100) and the plan. ``` { "data": [ { "id": "66666666-6666-4666-8666-666666666666", "name": "Top food apps", "color": "#5cb427", "scope": "personal", "workspaceId": null, "ownerUserId": "99999999-9999-4999-8999-999999999999", "appCount": 1, "adCount": 1, "createdAt": "2026-09-06T00:00:00.000Z", "updatedAt": "2026-09-06T00:00:00.000Z" } ], "limit": 100, "trackedAppAllowance": { "used": 1, "limit": 1, "plan": "peek" } } ``` ## Source Boards are the caller's own organization, stored in PeekPanda. Apps carry the listing name and icon as added; evidence counts are stored paid-ad observations. ## Refresh Board reads never call a provider or start collection. An app on any board in a space is tracked there, and tracking collects its paid ads, organic posts, keywords and chart position. --- # Find board memberships Find which boards hold an app, up to 100 apps, or one paid ad, with each item id and note. ## Endpoint: GET /v1/boards/memberships - Method: `GET` - Path: `/v1/boards/memberships` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `boards` - MCP action: `memberships` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `storeId` | query | string | no | Numeric App Store id. Send one target form only. | | `storeIds` | query | string | no | Up to 100 comma-separated App Store ids, for every tracked app in one read. | | `platform` | query | 'meta' | 'tiktok' | no | Advertising platform of the ad. | | `archiveId` | query | string | no | Archive id of the ad. | ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## Request Name exactly one target form. Use the item ids to remove the app or ad from a board. ``` curl --get 'https://api.peekpanda.com/v1/boards/memberships?storeId=0000000000' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call boards with action memberships. Path fields go in params, filters in query, and a write body in body. ``` { "name": "boards", "arguments": { "request": { "action": "memberships" } } } ``` ## Response Every board in the tenancy that holds the target. ``` { "data": [ { "boardId": "66666666-6666-4666-8666-666666666666", "itemId": "77777777-7777-4777-8777-777777777777", "storeId": "0000000000", "note": null } ] } ``` ## Source Boards are the caller's own organization, stored in PeekPanda. Apps carry the listing name and icon as added; evidence counts are stored paid-ad observations. ## Refresh Board reads never call a provider or start collection. An app on any board in a space is tracked there, and tracking collects its paid ads, organic posts, keywords and chart position. --- # Create a board Create a named, coloured board in the credential tenancy. ## Endpoint: POST /v1/boards - Method: `POST` - Path: `/v1/boards` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `manage_boards` - MCP action: `create` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `name` | body | string | yes | Board name, unique within the tenancy. | | `color` | body | #rrggbb | yes | Optional colour; defaults to bamboo green. | ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 409 conflict when the write would duplicate an exclusive row. ## Request A duplicate name returns 409. At most 100 boards per personal space or workspace. ``` curl --request POST 'https://api.peekpanda.com/v1/boards' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --header 'content-type: application/json' \ --data '{"name":"Top food apps","color":"#5cb427"}' ``` ## MCP request Call manage_boards with action create. Path fields go in params, filters in query, and a write body in body. ``` { "name": "manage_boards", "arguments": { "request": { "action": "create" } } } ``` ## Response The new board. ``` { "id": "66666666-6666-4666-8666-666666666666", "name": "Top food apps", "color": "#5cb427", "scope": "personal", "workspaceId": null, "ownerUserId": "99999999-9999-4999-8999-999999999999", "appCount": 0, "adCount": 0, "createdAt": "2026-09-06T00:00:00.000Z", "updatedAt": "2026-09-06T00:00:00.000Z" } ``` ## Source Boards are the caller's own organization, stored in PeekPanda. Apps carry the listing name and icon as added; evidence counts are stored paid-ad observations. ## Refresh Board reads never call a provider or start collection. An app on any board in a space is tracked there, and tracking collects its paid ads, organic posts, keywords and chart position. --- # Get a board Read one board: tracked apps with stored evidence, saved ads, the weekly and monthly pulse, activity and similar advertisers. ## Endpoint: GET /v1/boards/{boardId} - Method: `GET` - Path: `/v1/boards/{boardId}` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `boards` - MCP action: `get` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `boardId` | path | uuid | yes | Board 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## Request Render saved ad creatives with `GET /v1/paid-ads?savedToBoardId=`. `trackedAppId` is the app in the tenancy ASO workstation; `stopsTrackingOnRemove` is true when no other board in the tenancy holds the app. ``` curl --get 'https://api.peekpanda.com/v1/boards/66666666-6666-4666-8666-666666666666' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call boards with action get. Path fields go in params, filters in query, and a write body in body. ``` { "name": "boards", "arguments": { "request": { "action": "get", "params": { "boardId": "" } } } } ``` ## Response `adsFreshness` is `never` until a collection has finished for the app and `stale` once `adsLastCollectedAt` is older than two days, so zero ads never means an app runs no ads. The pulse counts ads whose run started in each UTC week (Monday first, last 8) and month (last 6); the last bucket is the current one. `similarAdvertisers` lists up to 6 apps not on the board that are similar to its apps and have collected paid ads, most live ads first, each with the board app it is most similar to (`similarTo`) and when its paid counts were built. Counts are collected ads, never spend. The list is empty until similar apps have been computed for the board apps. ``` { "id": "66666666-6666-4666-8666-666666666666", "name": "Top food apps", "color": "#5cb427", "scope": "personal", "workspaceId": null, "ownerUserId": "99999999-9999-4999-8999-999999999999", "appCount": 1, "adCount": 1, "createdAt": "2026-09-06T00:00:00.000Z", "updatedAt": "2026-09-06T00:00:00.000Z", "apps": [ { "id": "77777777-7777-4777-8777-777777777777", "kind": "app", "storeId": "0000000000", "name": "Example Recipe Planner", "iconUrl": null, "note": null, "addedByUserId": "99999999-9999-4999-8999-999999999999", "createdAt": "2026-09-06T00:00:00.000Z", "trackedAppId": "55555555-5555-4555-8555-555555555555", "stopsTrackingOnRemove": true, "evidence": { "liveAds": 4, "newAdsThisWeek": 1, "savedAdsOnBoard": 1, "inCatalogue": true, "adsLastCollectedAt": "2026-09-06T00:00:00.000Z", "adsFreshness": "fresh" } } ], "ads": [ { "id": "88888888-8888-4888-8888-888888888888", "kind": "ad", "platform": "meta", "archiveId": "example-archive-id", "storeId": "0000000000", "advertiserName": "Example Labs", "note": "Hook: \"dinner in 10 minutes\"", "addedByUserId": "99999999-9999-4999-8999-999999999999", "createdAt": "2026-09-06T00:00:00.000Z" } ], "pulse": { "apps": 1, "savedAds": 1, "liveAds": 4, "newAdsThisWeek": 1, "newAdsByWeek": [ { "weekStart": "2026-07-13", "newAds": 0 }, { "weekStart": "2026-07-20", "newAds": 0 }, { "weekStart": "2026-07-27", "newAds": 0 }, { "weekStart": "2026-08-03", "newAds": 0 }, { "weekStart": "2026-08-10", "newAds": 0 }, { "weekStart": "2026-08-17", "newAds": 0 }, { "weekStart": "2026-08-24", "newAds": 0 }, { "weekStart": "2026-08-31", "newAds": 1 } ], "newAdsByMonth": [ { "month": "2026-04", "newAds": 0 }, { "month": "2026-05", "newAds": 0 }, { "month": "2026-06", "newAds": 0 }, { "month": "2026-07", "newAds": 0 }, { "month": "2026-08", "newAds": 0 }, { "month": "2026-09", "newAds": 1 } ] }, "activity": [ { "kind": "new_ads", "date": "2026-09-06", "storeId": "0000000000", "appName": "Example Recipe Planner", "count": 1 }, { "kind": "ad_saved", "at": "2026-09-06T00:00:00.000Z", "platform": "meta", "archiveId": "example-archive-id", "advertiserName": "Example Labs", "byUserId": "99999999-9999-4999-8999-999999999999" } ], "trackedAppAllowance": { "used": 1, "limit": 1, "plan": "peek" }, "similarAdvertisers": [ { "storeId": "1065397474", "name": "Example Meal Kit", "iconUrl": null, "matchReason": "shared_searches", "sharedSearches": 12, "similarTo": { "storeId": "0000000000", "name": "Example Recipe Planner" }, "liveAds": 38, "totalAds": 120, "paidEvidenceBuiltAt": "2026-09-06T00:00:00.000Z" } ] } ``` ## Source Boards are the caller's own organization, stored in PeekPanda. Apps carry the listing name and icon as added; evidence counts are stored paid-ad observations. ## Refresh Board reads never call a provider or start collection. An app on any board in a space is tracked there, and tracking collects its paid ads, organic posts, keywords and chart position. --- # Update a board Rename or recolour a board. ## Endpoint: PATCH /v1/boards/{boardId} - Method: `PATCH` - Path: `/v1/boards/{boardId}` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `manage_boards` - MCP action: `update` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `boardId` | path | uuid | yes | Board id. | | `name` | body | string | yes | Optional new name. | | `color` | body | #rrggbb | yes | Optional new 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 409 conflict when the write would duplicate an exclusive row. ## Request Send at least one field. A duplicate name returns 409. ``` curl --request PATCH 'https://api.peekpanda.com/v1/boards/66666666-6666-4666-8666-666666666666' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --header 'content-type: application/json' \ --data '{"name":"Food apps to beat"}' ``` ## MCP request Call manage_boards with action update. Path fields go in params, filters in query, and a write body in body. ``` { "name": "manage_boards", "arguments": { "request": { "action": "update", "params": { "boardId": "" } } } } ``` ## Response The updated board. ``` { "id": "66666666-6666-4666-8666-666666666666", "name": "Food apps to beat", "color": "#5cb427", "scope": "personal", "workspaceId": null, "ownerUserId": "99999999-9999-4999-8999-999999999999", "appCount": 1, "adCount": 1, "createdAt": "2026-09-06T00:00:00.000Z", "updatedAt": "2026-09-06T00:00:00.000Z" } ``` ## Source Boards are the caller's own organization, stored in PeekPanda. Apps carry the listing name and icon as added; evidence counts are stored paid-ad observations. ## Refresh Board reads never call a provider or start collection. An app on any board in a space is tracked there, and tracking collects its paid ads, organic posts, keywords and chart position. --- # Delete a board Delete a board and its items. ## Endpoint: DELETE /v1/boards/{boardId} - Method: `DELETE` - Path: `/v1/boards/{boardId}` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `manage_boards` - MCP action: `remove` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `boardId` | path | uuid | yes | Board 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 409 conflict when the write would duplicate an exclusive row. ## Request Apps on no other board in the tenancy stop being tracked (see `stopsTrackingOnRemove` on the board detail). Ads stay in the library. ``` curl --request DELETE 'https://api.peekpanda.com/v1/boards/66666666-6666-4666-8666-666666666666' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call manage_boards with action remove. Path fields go in params, filters in query, and a write body in body. ``` { "name": "manage_boards", "arguments": { "request": { "action": "remove", "params": { "boardId": "" } } } } ``` ## Response Empty JSON object. ``` {} ``` ## Source Boards are the caller's own organization, stored in PeekPanda. Apps carry the listing name and icon as added; evidence counts are stored paid-ad observations. ## Refresh Board reads never call a provider or start collection. An app on any board in a space is tracked there, and tracking collects its paid ads, organic posts, keywords and chart position. --- # Add a board item Put an App Store app on a board, which tracks it, or save one paid ad. ## Endpoint: POST /v1/boards/{boardId}/items - Method: `POST` - Path: `/v1/boards/{boardId}/items` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `manage_boards` - MCP action: `add_item` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `boardId` | path | uuid | yes | Board id. | | `kind` | body | 'app' | 'ad' | yes | What to add. | | `storeId` | body | string | yes | For an app: numeric App Store id of a catalogue app. | | `platform` | body | 'meta' | 'tiktok' | yes | For an ad: advertising platform. | | `archiveId` | body | string | yes | For an ad: archive id. | | `note` | body | string | null | yes | Optional note, up to 500 characters. | ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 409 conflict when the write would duplicate an exclusive row. ## Request Repeating the same app or ad returns the existing item. An app on a board is tracked in the tenancy and counts once toward the plan holder allowance; over it the API answers 409 `tracked_app_limit` with `limit` and `plan`. At most 500 items per board. ``` curl --request POST 'https://api.peekpanda.com/v1/boards/66666666-6666-4666-8666-666666666666/items' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --header 'content-type: application/json' \ --data '{"kind":"app","storeId":"0000000000"}' ``` ## MCP request Call manage_boards with action add_item. Path fields go in params, filters in query, and a write body in body. ``` { "name": "manage_boards", "arguments": { "request": { "action": "add_item", "params": { "boardId": "" } } } } ``` ## Response The board item. An app item carries its tracked app id and stored evidence. ``` { "id": "77777777-7777-4777-8777-777777777777", "kind": "app", "storeId": "0000000000", "name": "Example Recipe Planner", "iconUrl": null, "note": null, "addedByUserId": "99999999-9999-4999-8999-999999999999", "createdAt": "2026-09-06T00:00:00.000Z", "trackedAppId": "55555555-5555-4555-8555-555555555555", "stopsTrackingOnRemove": true, "evidence": { "liveAds": 4, "newAdsThisWeek": 1, "savedAdsOnBoard": 1, "inCatalogue": true, "adsLastCollectedAt": "2026-09-06T00:00:00.000Z", "adsFreshness": "fresh" } } ``` ## Source Boards are the caller's own organization, stored in PeekPanda. Apps carry the listing name and icon as added; evidence counts are stored paid-ad observations. ## Refresh Board reads never call a provider or start collection. An app on any board in a space is tracked there, and tracking collects its paid ads, organic posts, keywords and chart position. --- # Update a board item Set or clear an item's note. ## Endpoint: PATCH /v1/boards/{boardId}/items/{itemId} - Method: `PATCH` - Path: `/v1/boards/{boardId}/items/{itemId}` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `manage_boards` - MCP action: `update_item` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `boardId` | path | uuid | yes | Board id. | | `itemId` | path | uuid | yes | Board item id. | | `note` | body | string | null | yes | New note, or null to clear 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 409 conflict when the write would duplicate an exclusive row. ## Request Notes are private board content; they never enter analytics. ``` curl --request PATCH 'https://api.peekpanda.com/v1/boards/66666666-6666-4666-8666-666666666666/items/88888888-8888-4888-8888-888888888888' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --header 'content-type: application/json' \ --data '{"note":"Hook: \"dinner in 10 minutes\""}' ``` ## MCP request Call manage_boards with action update_item. Path fields go in params, filters in query, and a write body in body. ``` { "name": "manage_boards", "arguments": { "request": { "action": "update_item", "params": { "boardId": "", "itemId": "" } } } } ``` ## Response The updated item. ``` { "id": "88888888-8888-4888-8888-888888888888", "kind": "ad", "platform": "meta", "archiveId": "example-archive-id", "storeId": "0000000000", "advertiserName": "Example Labs", "note": "Hook: \"dinner in 10 minutes\"", "addedByUserId": "99999999-9999-4999-8999-999999999999", "createdAt": "2026-09-06T00:00:00.000Z" } ``` ## Source Boards are the caller's own organization, stored in PeekPanda. Apps carry the listing name and icon as added; evidence counts are stored paid-ad observations. ## Refresh Board reads never call a provider or start collection. An app on any board in a space is tracked there, and tracking collects its paid ads, organic posts, keywords and chart position. --- # Remove a board item Take one app or ad off a board. ## Endpoint: DELETE /v1/boards/{boardId}/items/{itemId} - Method: `DELETE` - Path: `/v1/boards/{boardId}/items/{itemId}` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `manage_boards` - MCP action: `remove_item` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `boardId` | path | uuid | yes | Board id. | | `itemId` | path | uuid | yes | Board item 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 409 conflict when the write would duplicate an exclusive row. ## Request Repeating the request is naturally idempotent. An app on no other board in the tenancy stops being tracked; the ad stays in the library. ``` curl --request DELETE 'https://api.peekpanda.com/v1/boards/66666666-6666-4666-8666-666666666666/items/77777777-7777-4777-8777-777777777777' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call manage_boards with action remove_item. Path fields go in params, filters in query, and a write body in body. ``` { "name": "manage_boards", "arguments": { "request": { "action": "remove_item", "params": { "boardId": "", "itemId": "" } } } } ``` ## Response Empty JSON object. ``` {} ``` ## Source Boards are the caller's own organization, stored in PeekPanda. Apps carry the listing name and icon as added; evidence counts are stored paid-ad observations. ## Refresh Board reads never call a provider or start collection. An app on any board in a space is tracked there, and tracking collects its paid ads, organic posts, keywords and chart position. --- # Search organic content Search the copied public organic corpus with bounded filters. ## 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 | Exact product-name filter. | | `store_ids` | query | comma-separated numeric App Store ids, 1–100 | no | Posts PeekPanda tied to any of these apps, such as every app on a board. | | `max_slides` | query | integer, 1–100 | no | Maximum slide count. | | `ad` | query | ads | likely_or_ads | not_ads | no | TikTok paid reach: posts TikTok served as ads, also likely ads (Spark Ads authorized or paid-reach engagement), or neither. Unchecked posts count as not ads. | | `creator_sponsored` | query | boolean | no | true for posts a brand paid a creator to make, false to hide them. Unchecked posts count as not sponsored. | | `language` | query | comma-separated ISO 639-1 codes | no | Posts in any of these languages, such as en,es. mul is bilingual, zxx has no spoken or written words, und is undetermined. | | `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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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" ], "ad_signal": null, "creator_sponsored": false, "language": "en", "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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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" ], "ad_signal": null, "creator_sponsored": false, "language": "en", "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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 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. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 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. --- # Analyze organic post Ask for creative analysis of one stored organic post; it runs in the background. ## Endpoint: POST /v1/organic/{postId}/analysis - Method: `POST` - Path: `/v1/organic/{postId}/analysis` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `refresh` - MCP action: `organic_analysis` ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 409 conflict when the write would duplicate an exclusive row. ## Request The command has no request body and answers 202 at once: it stores the request and a background worker runs it, usually within a minute. Read where it stands with GET /v1/organic/{postId}/analysis every 15 to 30 seconds until status is ready or failed; each read counts toward the daily API and MCP call allowance. Asking again while it runs returns the same pending request, and a ready analysis is returned from storage without another run. A video post is read from its stored video, with a timeline of scenes, cuts and on-screen text; a slideshow is read from up to 10 slides in order, without a timeline. For a video, the same run also stores the transcript when the post has none; a transcript that fails, or a video with no speech, does not fail the analysis. A post with no stored video or slides answers 422. ``` curl --request POST 'https://api.peekpanda.com/v1/organic/organic-post-example/analysis' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call refresh with action organic_analysis. Path fields go in params, filters in query, and a write body in body. ``` { "name": "refresh", "arguments": { "request": { "action": "organic_analysis", "params": { "postId": "" } } } } ``` ## Response Returns the analysis state: pending with requestedAt while the worker runs it, or ready with the stored analysis when one already exists. ``` { "status": "pending", "requestedAt": "2026-11-02T10:00:00.000Z" } ``` ## Source The post's stored copy in the organic corpus: its mirrored video, or its stored slides, with its caption, hook and creator handle as context. ## Refresh An explicit request reads the stored copy and persists its result. Repeating after settlement returns the stored result without generating it again. A failed run keeps any analysis the post already had. --- # Get organic post analysis Read where one post’s creative analysis stands, and the analysis once it is ready. ## Endpoint: GET /v1/organic/{postId}/analysis - Method: `GET` - Path: `/v1/organic/{postId}/analysis` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `search_organic` - MCP action: `analysis` ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## Request This read never starts an analysis. status is not_requested when nobody has asked, pending while the worker runs it, ready with the persisted analysis and its generation-time provenance, or failed with failedAt when the last run produced nothing; ask again with POST to retry. Treat creative observations as analysis, not a performance claim. ``` curl --get 'https://api.peekpanda.com/v1/organic/organic-post-example/analysis' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call search_organic with action analysis. Path fields go in params, filters in query, and a write body in body. ``` { "name": "search_organic", "arguments": { "request": { "action": "analysis", "params": { "postId": "" } } } } ``` ## Response Returns the analysis state. A slideshow analysis has no timeline. ``` { "status": "ready", "analysis": { "summary": "Three slides name one fridge app each and end on the one that stopped the waste.", "visibleText": [ "3 apps that fixed my fridge", "This one plans meals from what you have" ], "hookNotes": [ "Opens on a crowded fridge with a direct question." ], "editingPattern": "One app per slide with large overlay text.", "reusablePromptIngredients": [ "problem-first hook", "numbered list of apps" ], "model": "analysis-model", "generatedAt": "2026-11-02T10:00:40.000Z" } } ``` ## Source The post's stored copy in the organic corpus: its mirrored video, or its stored slides, with its caption, hook and creator handle as context. ## Refresh This read returns the stored state and does not begin analysis. --- # Search paid ads Search stored Meta and TikTok ad-library observations. ## 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 | Search stored lexical evidence across creative copy, transcripts, creative analysis, advertiser names and linked apps. A recognized category term may include related-category matches when includeRelated=true. | | `searchScope` | query | all | creative | apps | advertisers | no | Limit the search fields; defaults to all. | | `includeRelated` | query | boolean | true | false | no | Include related-category matches. Disabled by default. | | `excludeAdvertiser` | query | string, 1–120 | no | Exclude one exact advertiser name from the returned page. | | `groupCreatives` | query | boolean | true | false | no | Group ads that share an exact media asset and return their SHA-256 group keys. Requires a ranked sort: Best match with a nonblank search or a ranked browse order. | | `creativeGroupKey` | query | 64 lowercase hexadecimal characters | no | Filter to one exact creative asset group; pass groupCreatives=false to expand its ads. Requires a ranked sort; balanced browsing is not supported. | | `platform` | query | meta | tiktok | no | Advertising platform filter. | | `status` | query | comma-separated list of active | inactive | unknown | no | Observed ad status; matches any listed value, e.g. status=active,inactive. A single value works as before. | | `advertiser` | query | string, max 120 | no | Exact, case-sensitive advertiser name. | | `advertiserExternalId` | query | string, 1–255 | no | One advertiser by its exact platform page id (a Meta page id). Prefer it over advertiser. | | `archiveId` | query | string, 1–255 | no | Exact platform archive id filter. | | `productExternalId` | query | string, max 255 | no | Include ads linked to this exact app store id. | | `excludeProductExternalId` | query | string, max 255 | no | Exclude ads linked to this exact app store id. | | `storeIds` | query | comma-separated numeric App Store IDs, max 25 | no | Only ads verified to link to these apps: the same ads the app’s paid intelligence counts. | | `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 | comma-separated list of image | video | carousel | dynamic | unknown | no | Creative format filter; matches any listed value, e.g. format=video,carousel. dynamic is Meta dynamic creative (DCO), often a static image. | | `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 run length. Live ads count to now; stopped ads to their disclosed end. | | `discoveredAfter` | query | ISO 8601 date-time | no | Only ads PeekPanda first recorded strictly after this instant. | | `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 | relevance | balanced | recently_discovered | newest_started | longest_running | most_tested | highest_reported_reach | no | Defaults to relevance for nonblank search text, recently_discovered for a storeIds browse, and balanced otherwise. Explicit relevance without search text becomes that browse default. Relevance ranks lexical evidence with advertiser diversity. longest_running is the closest performance proxy: no spend is published. | ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## Request Send the documented filters as query parameters. To follow an app or advertiser over time, pass storeIds or advertiserExternalId with sort=recently_discovered and set discoveredAfter to the newest discoveredAt you already read. 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 'search=hydration' \ --data-urlencode 'platform=meta' \ --data-urlencode 'storeIds=6480417616' \ --data-urlencode 'discoveredAfter=2026-09-28T06:00:00Z' \ --data-urlencode 'limit=18' \ --data-urlencode 'sort=relevance' ``` ## 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. Search uses lexical evidence already stored in the corpus; it does not fetch new provider data. Relevance ranks matching evidence with advertiser diversity. Creative grouping and creativeGroupKey require a ranked sort; balanced browsing returns HTTP 400 for these options. Expand one group by passing its key with groupCreatives=false. Each item may include searchMatch.sources and an exact-asset creativeGroup (SHA-256 key, count and representative archive id). Group counts describe ads sharing identical media, not identical copy. Use creativeGroupKey to filter one exact group. searchSummary counts only the returned page, never the matching corpus. Keep filters and sort unchanged while following nextCursor so pages continue the same ordered search. A storeIds read is served from the stored ad list of those apps, about a minute behind new ads; without sort or search it lists them by recently_discovered (sort=balanced still mixes advertisers). With sort=recently_discovered or newest_started it reads that list in order and checks each other filter on the ads it scans, at most 4,096 per request, so a rare filter can return a short or empty page with hasMore true: keep following nextCursor until hasMore is false. Other sorts, balanced included, rank the listed ads of those apps. canonicalKey matches paid:(meta|tiktok):{archiveId}. performance is unavailable or a reported_reach disclosure. There is no spend field. ``` { "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, "searchSummary": { "returnedAds": 18, "returnedAdvertisers": 7, "returnedApps": 5, "grouped": true } } ``` ## Source Stored public advertising-library observations with source dates. Spend is unavailable unless the public record reports it. ## Refresh This read returns stored observations. Saving an ad changes boards only. --- # Get paid ad Read one stored paid-ad observation with its matched listing and the ads around it. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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 of the app the ad belongs to, latest start first with undated ads last, and otherAppAds up to six of the newest ads of 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. Saving an ad changes boards only. --- # Generate paid-ad transcript Generate and persist a transcript from one stored ad’s mirrored video. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 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 Ask for creative analysis of one stored ad’s owned media; it runs in the background. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 409 conflict when the write would duplicate an exclusive row. ## Request The command has no request body and answers 202 at once: it stores the request and a background worker runs it, usually within a minute. Read where it stands with GET /v1/paid-ads/{platform}/{archiveId}/analysis every 15 to 30 seconds until status is ready or failed; each read counts toward the daily API and MCP call allowance. Asking again while it runs returns the same pending request, and a ready analysis is returned from storage without another run. A video analysis stored before timelines is regenerated to add one, and the stored analysis stays if that fails. It accepts mirrored image or video media up to 25 MB. For a video, the same run also generates and stores the transcript when it is missing; read it from GET /v1/paid-ads/{platform}/{archiveId}. A transcript that fails, or a video with no speech, does not fail the analysis. ``` 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 analysis state: pending with requestedAt while the worker runs it, or ready with the stored analysis when one already exists. ``` { "status": "pending", "requestedAt": "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 request uses the owned mirror and persists its result. Repeating after settlement returns the stored result without generating it again. --- # Get paid-ad analysis Read where one ad’s creative analysis stands, and the analysis once it is ready. ## Endpoint: GET /v1/paid-ads/{platform}/{archiveId}/analysis - Method: `GET` - Path: `/v1/paid-ads/{platform}/{archiveId}/analysis` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_ads` - MCP action: `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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## Request This read never starts an analysis. status is not_requested when nobody has asked, pending while the worker runs it, ready with the persisted analysis and its generation-time provenance, or failed with failedAt when the last run produced nothing; ask again with POST to retry. Treat creative observations as analysis, not a performance or policy claim. ``` curl --get 'https://api.peekpanda.com/v1/paid-ads/meta/111222333/analysis' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call get_ads with action analysis. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_ads", "arguments": { "request": { "action": "analysis", "params": { "platform": "", "archiveId": "" } } } } ``` ## Response Returns the analysis state. ``` { "status": "ready", "analysis": { "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." } ], "timeline": { "durationSeconds": 17, "format": "product demo", "pace": "medium", "cutCount": 1, "productAppearsAtSeconds": 0, "scenes": [ { "startSeconds": 0, "endSeconds": 4, "description": "The finished grocery list fills the screen." }, { "startSeconds": 4, "endSeconds": 17, "description": "A saved recipe becomes a grocery list." } ], "onScreenText": [ { "startSeconds": 0, "endSeconds": 4, "text": "Turn saved recipes into a grocery list" } ] }, "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 This read returns the stored state and does not begin analysis. --- # Save paid ad Quick save: put one paid ad on the Saved ads board of the credential tenancy. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 409 conflict when the write would duplicate an exclusive row. ## Request The command has no request body. The Saved ads board is created on first use. Repeating it returns the same item. To choose boards, use `POST /v1/boards/{boardId}/items`. ``` 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 The board item on the Saved ads board. ``` { "id": "88888888-8888-4888-8888-888888888888", "kind": "ad", "platform": "meta", "archiveId": "111222333", "storeId": null, "advertiserName": "Example Labs", "note": null, "addedByUserId": "99999999-9999-4999-8999-999999999999", "createdAt": "2026-09-06T00:00:00.000Z" } ``` ## Source Board items on stored Meta/TikTok ad-library observations. Not Apple. ## Refresh Save and unsave change board items only and never start collection. --- # Remove saved paid ad Unsave: take one paid ad off every board in the credential tenancy. ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 409 conflict when the write would duplicate an exclusive row. ## Request The command has no request body. Repeating it is naturally idempotent. The ad stays in the library. ``` 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 Empty JSON object. ``` {} ``` ## Source Board items on stored Meta/TikTok ad-library observations. Not Apple. ## Refresh Save and unsave change board items only and never start collection. --- # Read paid-ad rankings Rank advertisers by how many creatives they ship and how long those creatives stay live. ## 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-400 | no | Advertisers to return. Defaults to 25. | | `cadenceWeeks` | query | integer, 4-52 | no | UTC Monday weeks of creatives started and stopped. 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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; scope is null for the unfiltered library. uncategorizedLiveAds counts live ads whose advertiser resolves to no category, so on the unfiltered library the liveAds of the category rows plus uncategorizedLiveAds equals liveAds; unlinkedAds counts ads, live or stopped, that belong to no app: no verified App Store link of their own and no proven advertiser page. A web-funnel or Google Play ad of a proven advertiser belongs to its app and is not counted. Ad survival is the share of the ads we collected that were still running N days after launch, estimated with Kaplan–Meier: a stopped ad counts on the day it stopped, a live ad only up to its current age, so a young ad never reads as stopped. survival holds one curve per format (all, video, dynamic, image) with stillRunning[0] at launch and stillRunning[90] at 90 days; a format with no ads is left out. halfLifeDays is the first day half the ads have stopped, null when more than half still run at 90 days. stillRunningAt30Days is day 30 of the same curve. lastingFactors compares that day-30 reading across button text (the most-used calls to action) and body-copy length (every band, in order); a band with no ads has a null reading. cadence counts new and stopped ads per week; isPartial marks the week still being counted. Each advertiser carries liveBeyond30Days and weeklyNewAds, 12 weeks oldest first. These figures describe ads we collected. They are not spend or performance. Right after an update the stored figures can still be rebuilding: the read then answers 503 with error code projection_warming and a Retry-After header, never an empty market. ``` { "generatedAt": "2026-09-24T09:00:00.000Z", "scope": null, "totals": { "ads": 1200, "liveAds": 360, "advertisers": 12, "liveAdvertisers": 9, "newAdsLast7Days": 48, "advertisersShippingLast7Days": 4, "stoppedLast7Days": 31, "halfLifeDays": 12, "stillRunningAt30Days": 0.3, "medianRunDays": 9, "longestRunDays": 188, "liveBeyond60Days": 40, "reachDisclosedAds": 0, "unlinkedAds": 25, "uncategorizedLiveAds": 20 }, "advertisers": [ { "advertiserExternalId": "example-page", "name": "Example Fitness", "productExternalId": "0000000000", "productIconUrl": null, "category": "Health & Wellness", "liveAds": 64, "totalAds": 210, "newLast7Days": 9, "liveBeyond30Days": 22, "weeklyNewAds": [ 6, 4, 8, 5, 7, 9, 6, 10, 8, 7, 11, 3 ], "medianRunDays": 14, "longestRunDays": 188, "formatMix": { "video": 150, "image": 10, "carousel": 0, "dynamic": 50, "unknown": 0 }, "topCta": "Install now", "topDestinationKind": "app_store", "storeRating": 4.7, "storeRatingCount": 12000 } ], "longevity": [ { "band": "0-6", "count": 120 }, { "band": "7-14", "count": 70 }, { "band": "15-30", "count": 60 }, { "band": "31-60", "count": 70 }, { "band": "61-90", "count": 25 }, { "band": "90+", "count": 15 } ], "formats": [ { "key": "video", "label": "Video", "count": 780 }, { "key": "dynamic", "label": "Dynamic", "count": 380 }, { "key": "image", "label": "Image", "count": 40 }, { "key": "carousel", "label": "Carousel", "count": 0 }, { "key": "unknown", "label": "Unknown", "count": 0 } ], "ctas": [ { "key": "install now", "label": "Install now", "count": 520 } ], "destinations": [ { "key": "app_store", "label": "App Store", "count": 1100 }, { "key": "web", "label": "Web", "count": 100 }, { "key": "play_store", "label": "Play Store", "count": 0 }, { "key": "unknown", "label": "Unknown", "count": 0 } ], "categories": [ { "category": "Health & Wellness", "advertisers": 5, "liveAds": 160, "newLast7Days": 20, "medianRunDays": 10, "stillRunningAt30Days": 0.33, "halfLifeDays": 14 } ], "cadence": [ { "weekStart": "2026-07-06", "newAds": 40, "advertisers": 6, "stoppedAds": 35, "isPartial": false }, { "weekStart": "2026-07-13", "newAds": 40, "advertisers": 6, "stoppedAds": 35, "isPartial": false }, { "weekStart": "2026-07-20", "newAds": 40, "advertisers": 6, "stoppedAds": 35, "isPartial": false }, { "weekStart": "2026-07-27", "newAds": 40, "advertisers": 6, "stoppedAds": 35, "isPartial": false }, { "weekStart": "2026-08-03", "newAds": 40, "advertisers": 6, "stoppedAds": 35, "isPartial": false }, { "weekStart": "2026-08-10", "newAds": 40, "advertisers": 6, "stoppedAds": 35, "isPartial": false }, { "weekStart": "2026-08-17", "newAds": 40, "advertisers": 6, "stoppedAds": 35, "isPartial": false }, { "weekStart": "2026-08-24", "newAds": 40, "advertisers": 6, "stoppedAds": 35, "isPartial": false }, { "weekStart": "2026-08-31", "newAds": 40, "advertisers": 6, "stoppedAds": 35, "isPartial": false }, { "weekStart": "2026-09-07", "newAds": 40, "advertisers": 6, "stoppedAds": 35, "isPartial": false }, { "weekStart": "2026-09-14", "newAds": 40, "advertisers": 6, "stoppedAds": 35, "isPartial": false }, { "weekStart": "2026-09-21", "newAds": 18, "advertisers": 3, "stoppedAds": 9, "isPartial": true } ], "survival": [ { "format": "all", "ads": 1200, "stillRunning": [ 1, 0.932, 0.87, 0.812, 0.758, 0.709, 0.664, 0.622, 0.583, 0.547, 0.515, 0.484, 0.456, 0.43, 0.406, 0.384, 0.364, 0.345, 0.328, 0.312, 0.297, 0.283, 0.271, 0.259, 0.248, 0.238, 0.229, 0.221, 0.213, 0.206, 0.199, 0.193, 0.188, 0.182, 0.178, 0.173, 0.169, 0.165, 0.162, 0.159, 0.156, 0.153, 0.15, 0.148, 0.146, 0.144, 0.142, 0.14, 0.139, 0.137, 0.136, 0.135, 0.134, 0.133, 0.132, 0.131, 0.13, 0.129, 0.128, 0.128, 0.127, 0.127, 0.126, 0.126, 0.125, 0.125, 0.124, 0.124, 0.124, 0.123, 0.123, 0.123, 0.123, 0.123, 0.122, 0.122, 0.122, 0.122, 0.122, 0.122, 0.121, 0.121, 0.121, 0.121, 0.121, 0.121, 0.121, 0.121, 0.121, 0.121, 0.121 ] }, { "format": "video", "ads": 780, "stillRunning": [ 1, 0.926, 0.859, 0.797, 0.74, 0.688, 0.641, 0.597, 0.557, 0.52, 0.487, 0.456, 0.428, 0.402, 0.378, 0.357, 0.337, 0.319, 0.302, 0.287, 0.273, 0.26, 0.248, 0.238, 0.228, 0.219, 0.21, 0.203, 0.196, 0.19, 0.184, 0.178, 0.173, 0.169, 0.165, 0.161, 0.158, 0.155, 0.152, 0.149, 0.147, 0.144, 0.142, 0.14, 0.139, 0.137, 0.136, 0.134, 0.133, 0.132, 0.131, 0.13, 0.129, 0.129, 0.128, 0.127, 0.127, 0.126, 0.125, 0.125, 0.125, 0.124, 0.124, 0.124, 0.123, 0.123, 0.123, 0.122, 0.122, 0.122, 0.122, 0.122, 0.122, 0.121, 0.121, 0.121, 0.121, 0.121, 0.121, 0.121, 0.121, 0.121, 0.121, 0.121, 0.121, 0.121, 0.12, 0.12, 0.12, 0.12, 0.12 ] }, { "format": "dynamic", "ads": 380, "stillRunning": [ 1, 0.942, 0.887, 0.836, 0.788, 0.744, 0.703, 0.664, 0.628, 0.594, 0.562, 0.533, 0.506, 0.48, 0.456, 0.434, 0.413, 0.393, 0.375, 0.358, 0.342, 0.328, 0.314, 0.301, 0.289, 0.278, 0.267, 0.257, 0.248, 0.24, 0.232, 0.224, 0.217, 0.211, 0.205, 0.199, 0.194, 0.189, 0.185, 0.18, 0.176, 0.172, 0.169, 0.166, 0.163, 0.16, 0.157, 0.155, 0.152, 0.15, 0.148, 0.146, 0.145, 0.143, 0.141, 0.14, 0.139, 0.137, 0.136, 0.135, 0.134, 0.133, 0.132, 0.132, 0.131, 0.13, 0.129, 0.129, 0.128, 0.128, 0.127, 0.127, 0.126, 0.126, 0.125, 0.125, 0.125, 0.124, 0.124, 0.124, 0.124, 0.123, 0.123, 0.123, 0.123, 0.123, 0.122, 0.122, 0.122, 0.122, 0.122 ] }, { "format": "image", "ads": 40, "stillRunning": [ 1, 0.87, 0.758, 0.664, 0.583, 0.515, 0.456, 0.406, 0.364, 0.328, 0.297, 0.271, 0.248, 0.229, 0.213, 0.199, 0.188, 0.178, 0.169, 0.162, 0.156, 0.15, 0.146, 0.142, 0.139, 0.136, 0.134, 0.132, 0.13, 0.128, 0.127, 0.126, 0.125, 0.124, 0.124, 0.123, 0.123, 0.122, 0.122, 0.122, 0.121, 0.121, 0.121, 0.121, 0.121, 0.121, 0.121, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12 ] } ], "lastingFactors": [ { "factor": "cta", "slices": [ { "key": "install now", "label": "Install now", "ads": 520, "stillRunningAt30Days": 0.26 }, { "key": "learn more", "label": "Learn more", "ads": 210, "stillRunningAt30Days": 0.32 } ] }, { "factor": "copy_length", "slices": [ { "key": "none", "label": "No text", "ads": 90, "stillRunningAt30Days": 0.22 }, { "key": "1-50", "label": "1-50", "ads": 540, "stillRunningAt30Days": 0.26 }, { "key": "51-125", "label": "51-125", "ads": 350, "stillRunningAt30Days": 0.4 }, { "key": "126-250", "label": "126-250", "ads": 150, "stillRunningAt30Days": 0.36 }, { "key": "251-500", "label": "251-500", "ads": 70, "stillRunningAt30Days": 0.3 }, { "key": "500+", "label": "500+", "ads": 0, "stillRunningAt30Days": null } ] } ] } ``` ## 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. --- # Chart one app's paid ads Chart one competitor app's paid ads: launches, run lengths and creative mixes, for all of its advertisers or one. ## Endpoint: GET /v1/competitors/{platform}/{storeId}/paid-ads/intelligence - Method: `GET` - Path: `/v1/competitors/{platform}/{storeId}/paid-ads/intelligence` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `get_ads` - MCP action: `app_intelligence` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `platform` | path | ios | yes | The preview supports iOS only. | | `storeId` | path | numeric string | yes | Apple App Store identifier. | | `advertiserExternalId` | query | string | no | Narrow the charts to one of the advertisers the response lists. Omit it for all of them. | ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## Request Identify the listing. Every chart covers all of its advertisers unless you name one. ``` curl --get 'https://api.peekpanda.com/v1/competitors/ios/0000000000/paid-ads/intelligence' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --data-urlencode 'advertiserExternalId=example-page' ``` ## MCP request Call get_ads with action app_intelligence. Path fields go in params, filters in query, and a write body in body. ``` { "name": "get_ads", "arguments": { "request": { "action": "app_intelligence", "params": { "platform": "", "storeId": "" } } } } ``` ## Response advertisers lists every advertiser of the app, whichever one the charts narrow to. Days are whole UTC days, the same windows the case file reads. weeks holds 26 Monday weeks, oldest first; liveAds is the part of newAds still live now. longestRunning uses the Search paid ads item shape with short-lived media URLs. medianRunDays and longestRunDays are null when no ad is live. ``` { "generatedAt": "2026-09-24T09:00:00.000Z", "storeId": "0000000000", "advertiserExternalId": null, "advertisers": [], "totals": { "ads": 27, "liveAds": 4, "advertisers": 1, "newAdsLast7Days": 2, "newAdsLast30Days": 5, "medianRunDays": 12, "longestRunDays": 188 }, "weeks": [ { "weekStart": "2026-03-09", "newAds": 1, "liveAds": 0 }, { "weekStart": "2026-03-16", "newAds": 1, "liveAds": 0 }, { "weekStart": "2026-03-23", "newAds": 1, "liveAds": 0 }, { "weekStart": "2026-03-30", "newAds": 1, "liveAds": 0 }, { "weekStart": "2026-04-06", "newAds": 1, "liveAds": 0 }, { "weekStart": "2026-04-13", "newAds": 1, "liveAds": 0 }, { "weekStart": "2026-04-20", "newAds": 1, "liveAds": 0 }, { "weekStart": "2026-04-27", "newAds": 1, "liveAds": 0 }, { "weekStart": "2026-05-04", "newAds": 1, "liveAds": 0 }, { "weekStart": "2026-05-11", "newAds": 1, "liveAds": 0 }, { "weekStart": "2026-05-18", "newAds": 1, "liveAds": 0 }, { "weekStart": "2026-05-25", "newAds": 1, "liveAds": 0 }, { "weekStart": "2026-06-01", "newAds": 1, "liveAds": 0 }, { "weekStart": "2026-06-08", "newAds": 1, "liveAds": 0 }, { "weekStart": "2026-06-15", "newAds": 1, "liveAds": 0 }, { "weekStart": "2026-06-22", "newAds": 1, "liveAds": 0 }, { "weekStart": "2026-06-29", "newAds": 1, "liveAds": 0 }, { "weekStart": "2026-07-06", "newAds": 1, "liveAds": 0 }, { "weekStart": "2026-07-13", "newAds": 1, "liveAds": 0 }, { "weekStart": "2026-07-20", "newAds": 1, "liveAds": 0 }, { "weekStart": "2026-07-27", "newAds": 1, "liveAds": 0 }, { "weekStart": "2026-08-03", "newAds": 1, "liveAds": 0 }, { "weekStart": "2026-08-10", "newAds": 1, "liveAds": 1 }, { "weekStart": "2026-08-17", "newAds": 1, "liveAds": 1 }, { "weekStart": "2026-08-24", "newAds": 1, "liveAds": 1 }, { "weekStart": "2026-08-31", "newAds": 2, "liveAds": 1 } ], "longevity": [ { "band": "0-6", "count": 0 }, { "band": "7-14", "count": 0 }, { "band": "15-30", "count": 0 }, { "band": "31-60", "count": 0 }, { "band": "61-90", "count": 0 }, { "band": "90+", "count": 1 } ], "longestRunning": [], "formats": [ { "key": "video", "label": "Video", "count": 27 } ], "ctas": [ { "key": "install now", "label": "Install now", "count": 20 } ], "destinations": [ { "key": "app_store", "label": "App Store", "count": 27 } ], "landingPages": [ { "page": "apps.apple.com/app/id0000000000", "url": "https://apps.apple.com/app/id0000000000", "kind": "app_store", "adCount": 27, "activeCount": 4, "firstSeenAt": "2026-03-02T00:00:00.000Z", "lastSeenAt": "2026-09-01T00:00:00.000Z" } ] } ``` ## Source Stored paid-ad observations. An ad belongs to the app its own verified App Store link names, or to the app its proven advertiser page runs ads for, web funnels and Google Play links included. There is no spend, impression, reach or country field: Meta does not publish them. ## 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. ## 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. Shelves are grouped from every match, so a search matching more than 30,000 ads answers 422 invalid_request; use a more specific term, or page the matches through Search paid ads. | | `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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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-24T09:00:00.000Z", "rails": [], "totals": { "ads": 1200, "liveAds": 360, "advertisers": 12, "liveAdvertisers": 9, "newAdsLast7Days": 48, "advertisersShippingLast7Days": 4, "stoppedLast7Days": 31, "halfLifeDays": 12, "stillRunningAt30Days": 0.3, "medianRunDays": 9, "longestRunDays": 188, "liveBeyond60Days": 40, "reachDisclosedAds": 0, "unlinkedAds": 25, "uncategorizedLiveAds": 20 } } ``` ## 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. ## 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, jp, kr, cn, ru. | | `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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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. Estimates are worldwide monthly figures, whichever storefront the chart is for: a range (a low, mid and high reading) or below_floor when the app sits under the 5,000 a month the model can resolve. 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" }, { "code": "jp", "name": "Japan" }, { "code": "kr", "name": "South Korea" }, { "code": "cn", "name": "China" }, { "code": "ru", "name": "Russia" } ], "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 }, { "country": "jp", "day": "2026-09-06", "depth": 100 }, { "country": "kr", "day": "2026-09-06", "depth": 100 }, { "country": "cn", "day": "2026-09-06", "depth": 100 }, { "country": "ru", "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": { "kind": "range", "scope": "worldwide", "range": { "low": 40000, "mid": 84000, "high": 170000 }, "observedAt": "2026-09-06T00:00:00.000Z", "previous": { "kind": "range", "scope": "worldwide", "range": { "low": 34000, "mid": 72000, "high": 150000 }, "observedAt": "2026-09-05T00:00:00.000Z" } }, "monthlyRevenue": { "kind": "below_floor", "scope": "worldwide", "ceiling": 5000, "observedAt": "2026-09-06T00:00:00.000Z", "previous": 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. ## 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, jp, kr, cn, ru. | | `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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## 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. ## 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, or for the app its proven advertiser page runs ads for, web funnels and Google Play links included; the paid pulse counts the same ads. destinations splits them into App Store, Play Store and web-funnel ads, and only App Store ads are counted as appStore. "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 rate_limited response carries Retry-After when the server can say how long to wait: wait that many seconds, then send the same request again. Every call also counts toward your plan's daily API and MCP allowance: 30 calls a day on Peek and 1,000 on Panda, shared by every key and MCP connection on the plan and reset at 00:00 UTC. Past it, calls answer 429 daily_call_limit until the reset. 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. ## 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" }, { "code": "jp", "name": "Japan" }, { "code": "kr", "name": "South Korea" }, { "code": "cn", "name": "China" }, { "code": "ru", "name": "Russia" } ], "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 }, { "country": "jp", "day": "2026-09-06", "depth": 100 }, { "country": "kr", "day": "2026-09-06", "depth": 100 }, { "country": "cn", "day": "2026-09-06", "depth": 100 }, { "country": "ru", "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": { "kind": "range", "scope": "worldwide", "range": { "low": 40000, "mid": 84000, "high": 170000 }, "observedAt": "2026-09-06T00:00:00.000Z", "previous": { "kind": "range", "scope": "worldwide", "range": { "low": 34000, "mid": 72000, "high": 150000 }, "observedAt": "2026-09-05T00:00:00.000Z" } }, "monthlyRevenue": { "kind": "below_floor", "scope": "worldwide", "ceiling": 5000, "observedAt": "2026-09-06T00:00:00.000Z", "previous": 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" }, { "code": "jp", "name": "Japan" }, { "code": "kr", "name": "South Korea" }, { "code": "cn", "name": "China" }, { "code": "ru", "name": "Russia" } ], "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 }, { "country": "jp", "day": "2026-09-06", "depth": 100 }, { "country": "kr", "day": "2026-09-06", "depth": 100 }, { "country": "cn", "day": "2026-09-06", "depth": 100 }, { "country": "ru", "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": { "kind": "range", "scope": "worldwide", "range": { "low": 40000, "mid": 84000, "high": 170000 }, "observedAt": "2026-09-06T00:00:00.000Z", "previous": { "kind": "range", "scope": "worldwide", "range": { "low": 34000, "mid": 72000, "high": 150000 }, "observedAt": "2026-09-05T00:00:00.000Z" } }, "monthlyRevenue": { "kind": "below_floor", "scope": "worldwide", "ceiling": 5000, "observedAt": "2026-09-06T00:00:00.000Z", "previous": 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" }, { "code": "jp", "name": "Japan" }, { "code": "kr", "name": "South Korea" }, { "code": "cn", "name": "China" }, { "code": "ru", "name": "Russia" } ], "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 }, { "country": "jp", "day": "2026-09-06", "depth": 100 }, { "country": "kr", "day": "2026-09-06", "depth": 100 }, { "country": "cn", "day": "2026-09-06", "depth": 100 }, { "country": "ru", "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": { "kind": "range", "scope": "worldwide", "range": { "low": 40000, "mid": 84000, "high": 170000 }, "observedAt": "2026-09-06T00:00:00.000Z", "previous": { "kind": "range", "scope": "worldwide", "range": { "low": 34000, "mid": 72000, "high": 150000 }, "observedAt": "2026-09-05T00:00:00.000Z" } }, "monthlyRevenue": { "kind": "below_floor", "scope": "worldwide", "ceiling": 5000, "observedAt": "2026-09-06T00:00:00.000Z", "previous": 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" }, { "code": "jp", "name": "Japan" }, { "code": "kr", "name": "South Korea" }, { "code": "cn", "name": "China" }, { "code": "ru", "name": "Russia" } ], "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 }, { "country": "jp", "day": "2026-09-06", "depth": 100 }, { "country": "kr", "day": "2026-09-06", "depth": 100 }, { "country": "cn", "day": "2026-09-06", "depth": 100 }, { "country": "ru", "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": { "kind": "range", "scope": "worldwide", "range": { "low": 40000, "mid": 84000, "high": 170000 }, "observedAt": "2026-09-06T00:00:00.000Z", "previous": { "kind": "range", "scope": "worldwide", "range": { "low": 34000, "mid": 72000, "high": 150000 }, "observedAt": "2026-09-05T00:00:00.000Z" } }, "monthlyRevenue": { "kind": "below_floor", "scope": "worldwide", "ceiling": 5000, "observedAt": "2026-09-06T00:00:00.000Z", "previous": 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, "playStore": 0, "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, "popularityEstimate": null }, "top": [ { "query": "chai", "position": 63, "popularity": 78, "popularityEstimate": null }, { "query": "character ai", "position": 47, "popularity": 75, "popularityEstimate": null }, { "query": "talkie", "position": 68, "popularity": 74, "popularityEstimate": null } ] } }, { "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, "popularityEstimate": null }, "top": [ { "query": "espn fantasy", "position": 1, "popularity": 83, "popularityEstimate": null }, { "query": "espn", "position": 2, "popularity": 78, "popularityEstimate": null }, { "query": "yahoo fantasy", "position": 2, "popularity": 78, "popularityEstimate": null }, { "query": "fantasy football draft", "position": 3, "popularity": null, "popularityEstimate": { "tier": "medium", "method": "peekpanda_popularity_estimate", "country": "us", "estimatedAt": "2026-09-07T04:00:00.000Z", "modelTrainedAt": "2026-09-07T03:55:00.000Z" } } ] } } ] } } ``` ## 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, or for the app its proven advertiser page runs ads for, web funnels and Google Play links included; the paid pulse counts the same ads. destinations splits them into App Store, Play Store and web-funnel ads, and only App Store ads are counted as appStore. "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. Each signals keyword carries popularity, Apple's weekly search popularity (1 to 100), or null when Apple reports nothing. Where Apple reports nothing for a US keyword, popularityEstimate carries PeekPanda's own tier (very_low, low, medium or high) with estimatedAt and the modelTrainedAt of the weekly model behind it, never a number on Apple's scale. popularityEstimate is null whenever popularity is set and on every non-US storefront. Neither is a search count. ## 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 rate_limited response carries Retry-After when the server can say how long to wait: wait that many seconds, then send the same request again. Every call also counts toward your plan's daily API and MCP allowance: 30 calls a day on Peek and 1,000 on Panda, shared by every key and MCP connection on the plan and reset at 00:00 UTC. Past it, calls answer 429 daily_call_limit until the reset. 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. ## 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. Each app carries the name and 128px icon the market boards print; both are null when no stored fact names the app. placement, paid, organic and keywords are each null when that stream was never collected for the app, never zero. paidCollection says when the app’s paid ads were last collected: freshness is never (no collection has finished, so a null paid signal says nothing about the app’s ads), stale (the last collection is more than two days old) or fresh, the same words boards use, and lastCollectedAt is null until one has finished. A null paid signal with fresh collection means the app ran no ads PeekPanda could find. 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, "playStore": 0, "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, "popularityEstimate": null }, "top": [ { "query": "chai", "position": 63, "popularity": 78, "popularityEstimate": null }, { "query": "character ai", "position": 47, "popularity": 75, "popularityEstimate": null }, { "query": "talkie", "position": 68, "popularity": 74, "popularityEstimate": null } ] }, "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", "paidCollection": { "freshness": "fresh", "lastCollectedAt": "2026-09-10T06:15:00.000Z" } }, { "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, "popularityEstimate": null }, "top": [ { "query": "espn fantasy", "position": 1, "popularity": 83, "popularityEstimate": null }, { "query": "espn", "position": 2, "popularity": 78, "popularityEstimate": null }, { "query": "yahoo fantasy", "position": 2, "popularity": 78, "popularityEstimate": null }, { "query": "fantasy football draft", "position": 3, "popularity": null, "popularityEstimate": { "tier": "medium", "method": "peekpanda_popularity_estimate", "country": "us", "estimatedAt": "2026-09-07T04:00:00.000Z", "modelTrainedAt": "2026-09-07T03:55:00.000Z" } } ] }, "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", "paidCollection": { "freshness": "never", "lastCollectedAt": null } } ] } ``` ## 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, or for the app its proven advertiser page runs ads for, web funnels and Google Play links included; the paid pulse counts the same ads. destinations splits them into App Store, Play Store and web-funnel ads, and only App Store ads are counted as appStore. "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. Each signals keyword carries popularity, Apple's weekly search popularity (1 to 100), or null when Apple reports nothing. Where Apple reports nothing for a US keyword, popularityEstimate carries PeekPanda's own tier (very_low, low, medium or high) with estimatedAt and the modelTrainedAt of the weekly model behind it, never a number on Apple's scale. popularityEstimate is null whenever popularity is set and on every non-US storefront. Neither is a search count. ## 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 rate_limited response carries Retry-After when the server can say how long to wait: wait that many seconds, then send the same request again. Every call also counts toward your plan's daily API and MCP allowance: 30 calls a day on Peek and 1,000 on Panda, shared by every key and MCP connection on the plan and reset at 00:00 UTC. Past it, calls answer 429 daily_call_limit until the reset. 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. --- # List starter boards Browse the curated starter-board niches and their app summaries. ## Endpoint: GET /v1/starter-boards - Method: `GET` - Path: `/v1/starter-boards` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `starter_boards` - 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## Request No query or body. The list is bounded to the curated catalogue. ``` curl --get 'https://api.peekpanda.com/v1/starter-boards' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call starter_boards with action list. Path fields go in params, filters in query, and a write body in body. ``` { "name": "starter_boards", "arguments": { "request": { "action": "list" } } } ``` ## Response Each niche has a stable key, display label and up to 12 iOS app summaries. Listing, rating and paid-ad facts include their observation dates; unavailable observations are null. ``` { "data": [ { "key": "calorie-tracking", "label": "Calorie tracking", "apps": [ { "storeId": "6480417616", "name": "Cal AI - Calorie Tracker", "iconUrl": null, "developer": null, "category": null, "rating": null, "ratingCount": null, "listingObservedAt": null, "liveAds": null, "totalAds": null, "paidObservedAt": null } ] } ] } ``` ## Source Curated PeekPanda starter-board catalogue and public competitor evidence. No private customer board content is returned. ## Refresh Reads stored listing, paid-ad, keyword and organic observations only. These requests never start provider collection. Each observed stream carries its observation date; missing evidence remains null or an empty list. ## Rate limits and daily budget Authenticated API and MCP calls share the plan holder’s daily allowance: Peek 30 or Panda 1,000 calls, reset at 00:00 UTC. The standard per-minute rate limit also applies; honor Retry-After and daily-call reset headers on 429 responses. --- # Search apps for onboarding Find stored iOS catalogue apps by name or App Store identity. ## Endpoint: GET /v1/starter-boards/search - Method: `GET` - Path: `/v1/starter-boards/search` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `starter_boards` - MCP action: `search` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `query` | query | string | yes | At least 2 and at most 200 characters: app name, App Store URL, or store 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## Request Send one query parameter. The response contains at most eight app summaries. ``` curl --get 'https://api.peekpanda.com/v1/starter-boards/search?query=Acorn' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call starter_boards with action search. Path fields go in params, filters in query, and a write body in body. ``` { "name": "starter_boards", "arguments": { "request": { "action": "search", "query": { "query": "" } } } } ``` ## Response Matching stored iOS listings with their App Store IDs, current display names, developers, categories, icons and observed rating counts. This lookup reads the stored catalogue; it does not call providers, enqueue enrichment or write visit records. ``` { "data": [ { "storeId": "1234567890", "name": "Acorn", "iconUrl": null, "developer": null, "category": null, "rating": null, "ratingCount": null, "listingObservedAt": null, "liveAds": null, "totalAds": null, "paidObservedAt": null } ] } ``` ## Source Curated PeekPanda starter-board catalogue and public competitor evidence. No private customer board content is returned. ## Refresh Reads stored listing, paid-ad, keyword and organic observations only. These requests never start provider collection. Each observed stream carries its observation date; missing evidence remains null or an empty list. ## Rate limits and daily budget Authenticated API and MCP calls share the plan holder’s daily allowance: Peek 30 or Panda 1,000 calls, reset at 00:00 UTC. The standard per-minute rate limit also applies; honor Retry-After and daily-call reset headers on 429 responses. --- # Preview a starter board Preview a curated niche or find potential competitors for a selected app. ## Endpoint: GET /v1/starter-boards/{nicheKey} - Method: `GET` - Path: `/v1/starter-boards/{nicheKey}` - Authentication: Bearer API credential or OAuth access token - Required scope: `context:read` - MCP tool: `starter_boards` - MCP action: `detail` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `nicheKey` | path | string | yes | Curated niche key, or app- to seed a preview with that stored iOS app and up to five potential competitors. | ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. ## Request No query or body. Use a curated key such as `calorie-tracking`, or `app-` for an app returned by starter-board search. The app is first, followed by up to five distinct stored iOS candidates ranked from shared keyword searches, listing names, descriptions and category. Sparse evidence produces a smaller preview. Unknown keys or apps return 404 `not_found`. ``` curl --get 'https://api.peekpanda.com/v1/starter-boards/calorie-tracking' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' ``` ## MCP request Call starter_boards with action detail. Path fields go in params, filters in query, and a write body in body. ``` { "name": "starter_boards", "arguments": { "request": { "action": "detail", "params": { "nicheKey": "" } } } } ``` ## Response The preview includes at most two paid ads per app, five keywords per app and three organic posts total, plus total and locked counts. App previews also return `seed.storeId`, ordered `seed.matches` with match reasons and nullable shared-search counts, and `seed.previewToken` for adoption. Curated previews return `seed: null`. The example below is illustrative only, not a live observation. ``` { "key": "calorie-tracking", "label": "Calorie tracking", "seed": null, "apps": [], "organicPosts": [], "organicPostsTotal": 0, "organicPostsLocked": 0 } ``` ## Source Curated PeekPanda starter-board catalogue and public competitor evidence. No private customer board content is returned. ## Refresh Reads stored listing, paid-ad, keyword and organic observations only. These requests never start provider collection. Each observed stream carries its observation date; missing evidence remains null or an empty list. ## Preview bounds The lists are deliberately bounded for onboarding. Locked counts describe additional stored public evidence when available. Null counts mean the source has not been observed; zero means no matching records are available in the stored projection, without claiming complete provider coverage. ## Potential competitors Match reasons are `shared_searches`, `similar_listing`, `similar_description` or `same_category`. Category is a weaker match, not proof of direct competition. Preserve the returned `seed.previewToken` with the chosen IDs: it binds this displayed set for 24 hours without granting subscription access. Refresh an expired preview before adoption. ## Rate limits and daily budget Authenticated API and MCP calls share the plan holder’s daily allowance: Peek 30 or Panda 1,000 calls, reset at 00:00 UTC. The standard per-minute rate limit also applies; honor Retry-After and daily-call reset headers on 429 responses. --- # Adopt a starter board Track selected apps from a starter preview in a real board for the credential tenancy. ## Endpoint: POST /v1/starter-boards/{nicheKey}/adopt - Method: `POST` - Path: `/v1/starter-boards/{nicheKey}/adopt` - Authentication: Bearer API credential or OAuth access token - Required scope: `content:write` - MCP tool: `adopt_starter_board` - MCP action: `adopt` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `nicheKey` | path | string | yes | Curated niche key, or app- to seed a preview with that stored iOS app and up to five potential competitors. | | `selectedStoreIds` | body | string[] | yes | Distinct iOS App Store IDs from this preview: up to 12 for a curated niche, up to six for an app seed. Peek selects one; Panda may select the prepared set within its remaining allowance. | | `previewToken` | body | string | no | Required for app-: copy seed.previewToken from the preview response. Expires after 24 hours; optional for curated niches. | ## 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. - 403 workspace_not_accessible when x-genviral-tenancy names a workspace the credential cannot reach. - 404 not_found when the record is outside the accessible catalogue. - 429 rate_limited; honor Retry-After when present. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 409 conflict when the write would duplicate an exclusive row. ## Request For a curated niche key, selected IDs must belong to that niche. For `app-`, send the preview token and select only IDs from that signed candidate set. Adoption keeps the apps shown in the preview without rerunning similarity. Board ownership, entitlement, tracking allowance and stored app validation remain canonical. ``` curl --request POST 'https://api.peekpanda.com/v1/starter-boards/calorie-tracking/adopt' \ --header "Authorization: Bearer $PEEKPANDA_TOKEN" \ --header 'x-genviral-tenancy: personal' \ --header 'content-type: application/json' \ --data '{"selectedStoreIds":["6480417616"]}' ``` ## MCP request Call adopt_starter_board with action adopt. Path fields go in params, filters in query, and a write body in body. ``` { "name": "adopt_starter_board", "arguments": { "request": { "action": "adopt", "params": { "nicheKey": "" } } } } ``` ## Response The real board ID and the tracked App Store IDs. The caller must have an active PeekPanda entitlement, `content:write`, and room under the plan holder’s tracked-app allowance. Authenticated API and MCP calls share the plan holder’s daily allowance: Peek 30 or Panda 1,000 calls, reset at 00:00 UTC. The standard per-minute rate limit also applies; honor Retry-After and daily-call reset headers on 429 responses. ``` { "boardId": "11111111-1111-4111-8111-111111111111", "trackedStoreIds": [ "6480417616" ] } ``` ## Source Curated PeekPanda starter-board catalogue and public competitor evidence. No private customer board content is returned. ## Refresh Reads stored listing, paid-ad, keyword and organic observations only. These requests never start provider collection. Each observed stream carries its observation date; missing evidence remains null or an empty list. ## Errors and limits 400 `invalid_request` for malformed, duplicate or out-of-preview IDs, a missing/expired/invalid app preview token, or selecting multiple apps on Peek; 402 `subscription_required` without an active plan; 403 `authorization_required` without `content:write`; 404 `not_found` for an unknown niche or selected app missing from the stored catalogue; 409 `tracked_app_limit` when the plan allowance would be exceeded; and 429 for per-minute or daily call limits. On a daily cap, honor `Retry-After` and the daily reset headers. --- # List public starter preview niches The curated niches the public starter preview covers, without an API key. ## Endpoint: GET /v1/public/starter-boards - Method: `GET` - Path: `/v1/public/starter-boards` - Authentication: None required. Optional: a Bearer API credential or OAuth access token, which makes the call a metered credentialed call. - Required scope: `context:read` - MCP tool: `starter_boards` - MCP action: `public_niches` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | ## Known errors - 400 invalid_request for a malformed niche key or an app- key; seeded app previews need the authenticated GET /v1/starter-boards/{nicheKey}. - 401 authentication_required for an invalid credential; a credential that fails is never treated as anonymous. - 403 authorization_required for an MCP credential without context:read. - 404 not_found for a niche key outside the curated starter boards. - 429 rate_limited with window (minute or day) and retryAfterSeconds for an anonymous caller, or the credential’s per-minute read limit; honor Retry-After. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 503 public_preview_unavailable while the public starter preview is switched off; retry later. - 503 timeout when the stored read ran out of its time budget; retry after Retry-After. ## Request No query or body, and no credential needed. The list is bounded to the curated catalogue. ``` curl --get 'https://api.peekpanda.com/v1/public/starter-boards' ``` ## MCP request Call starter_boards with action public_niches. Path fields go in params, filters in query, and a write body in body. ``` { "name": "starter_boards", "arguments": { "request": { "action": "public_niches" } } } ``` ## Response Up to 20 niches, each with a stable key, a label and up to six iOS apps named by store ID, name, icon and category. Apps the catalogue does not hold are left out. ``` { "data": [ { "key": "calorie-tracking", "label": "Calorie tracking", "apps": [ { "storeId": "0000000000", "name": "Example Calories", "iconUrl": null, "category": null } ] } ] } ``` ## Source The curated PeekPanda starter-board niches and stored public competitor evidence: the app paid ad membership, the paid ad library’s last sighting of each ad, stored keyword rankings and the Viral Library. No private customer content is returned. ## Refresh Reads stored observations only and never starts provider collection. A niche is read again at most every five minutes; thumbnail links are signed for each response and stay valid for 15 to 30 minutes. ## Access, limits and caching Without an Authorization header the call is anonymous and free, counted in shared storage only (a 503 public_preview_unavailable answers when it cannot be counted): each client network (an IPv4 address or an IPv6 /64) may send 20 requests a minute and 200 a UTC day, and all anonymous callers together share 1,200 a minute. A 429 says which window was used up (`window`: `minute` or `day`) and when to retry (`retryAfterSeconds` and `Retry-After`); `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` describe the current window. With an Authorization header the call is credentialed: it needs context:read, counts toward the plan holder’s shared daily allowance (Peek 30, Panda 1,000, reset 00:00 UTC) and the per-minute read limit, and an invalid credential is refused rather than treated as anonymous. Responses are `Cache-Control: private, max-age=60`, vary by Authorization and are marked `X-Robots-Tag: noindex`. --- # Read a public starter preview The longest-running live ads of a curated niche, and how long they have run, without an API key. ## Endpoint: GET /v1/public/starter-boards/{nicheKey} - Method: `GET` - Path: `/v1/public/starter-boards/{nicheKey}` - Authentication: None required. Optional: a Bearer API credential or OAuth access token, which makes the call a metered credentialed call. - Required scope: `context:read` - MCP tool: `starter_boards` - MCP action: `public_preview` ## Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `nicheKey` | path | string | yes | A curated niche key from GET /v1/public/starter-boards, such as calorie-tracking. App keys are refused. | ## Known errors - 400 invalid_request for a malformed niche key or an app- key; seeded app previews need the authenticated GET /v1/starter-boards/{nicheKey}. - 401 authentication_required for an invalid credential; a credential that fails is never treated as anonymous. - 403 authorization_required for an MCP credential without context:read. - 404 not_found for a niche key outside the curated starter boards. - 429 rate_limited with window (minute or day) and retryAfterSeconds for an anonymous caller, or the credential’s per-minute read limit; honor Retry-After. - 429 daily_call_limit; the plan's daily API and MCP calls are used until 00:00 UTC. - 503 public_preview_unavailable while the public starter preview is switched off; retry later. - 503 timeout when the stored read ran out of its time budget; retry after Retry-After. ## Request No query or body, and no credential needed. Use a curated niche key. ``` curl --get 'https://api.peekpanda.com/v1/public/starter-boards/calorie-tracking' ``` ## MCP request Call starter_boards with action public_preview. Path fields go in params, filters in query, and a write body in body. ``` { "name": "starter_boards", "arguments": { "request": { "action": "public_preview", "params": { "nicheKey": "" } } } } ``` ## Response For each app: longestRunningAds, its two longest-running live ads with a start date (earliest start first), each with platform, format, a short-lived thumbnail, its start, days live (whole UTC days from its start day to today, as board tiles count them) and when the ad library last saw it running (stillLiveCheckedAt). They are the longest-running ads, ordered by run length, not only ads past a minimum age. Then five top keywords; and how many live ads, keywords and organic posts the preview leaves out. liveAdsTotal is null when the app’s paid ads were never collected. Three organic posts are shown across the board, with organicPostsTotal. The preview returns never ad copy, advertiser names, video, links or ad identifiers. The example below is illustrative only, not a live observation. ``` { "key": "calorie-tracking", "label": "Calorie tracking", "apps": [ { "storeId": "0000000000", "name": "Example Calories", "iconUrl": null, "developer": null, "category": null, "rating": null, "ratingCount": null, "listingObservedAt": null, "paidObservedAt": "2026-10-16T09:00:00.000Z", "longestRunningAds": [ { "platform": "meta", "format": "video", "thumbnailUrl": "https://cdn.peekpanda.com/paid-ad-media/sha256/example?expires=1800000000&signature=example", "startedAt": "2026-05-01T08:00:00.000Z", "daysLive": 168, "stillLiveCheckedAt": "2026-10-16T08:00:00.000Z" } ], "keywords": [ { "keyword": "calorie counter", "rank": 3, "popularity": 61, "observedAt": "2026-10-16T00:00:00.000Z" } ], "liveAdsTotal": 40, "liveAdsLocked": 39, "keywordsLocked": 12, "organicPostsLocked": 5 } ], "organicPosts": [], "organicPostsTotal": 6 } ``` ## Source The curated PeekPanda starter-board niches and stored public competitor evidence: the app paid ad membership, the paid ad library’s last sighting of each ad, stored keyword rankings and the Viral Library. No private customer content is returned. ## Refresh Reads stored observations only and never starts provider collection. A niche is read again at most every five minutes; thumbnail links are signed for each response and stay valid for 15 to 30 minutes. ## Access, limits and caching Without an Authorization header the call is anonymous and free, counted in shared storage only (a 503 public_preview_unavailable answers when it cannot be counted): each client network (an IPv4 address or an IPv6 /64) may send 20 requests a minute and 200 a UTC day, and all anonymous callers together share 1,200 a minute. A 429 says which window was used up (`window`: `minute` or `day`) and when to retry (`retryAfterSeconds` and `Retry-After`); `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` describe the current window. With an Authorization header the call is credentialed: it needs context:read, counts toward the plan holder’s shared daily allowance (Peek 30, Panda 1,000, reset 00:00 UTC) and the per-minute read limit, and an invalid credential is refused rather than treated as anonymous. Responses are `Cache-Control: private, max-age=60`, vary by Authorization and are marked `X-Robots-Tag: noindex`.