Install
openclaw skills install @scavio-ai/scavio-app-storeSearch the Apple App Store, read a full app listing by App Store id or bundle id, and pull user reviews as structured JSON. 3 endpoints, 1 credit each, any Apple storefront.
openclaw skills install @scavio-ai/scavio-app-storeSearch the App Store, pull a full app listing by App Store id or bundle id, and read user reviews with the app version each was written against. All three endpoints return structured JSON.
Use this skill when the user asks to:
Get a free API key at https://scavio.dev (50 free credits to get started, no card required):
export SCAVIO_API_KEY=sk_live_your_key
Every request is a POST with a JSON body and:
Authorization: Bearer $SCAVIO_API_KEY
Base URL: https://api.scavio.dev. All paths are under /api/v1/appstore. Every endpoint costs 1 credit.
| Endpoint | Credits | What it returns |
|---|---|---|
POST /api/v1/appstore/search | 1 | Up to 200 fully-shaped apps, the same row as /app. No pagination. |
POST /api/v1/appstore/app | 1 | One app's complete listing, by App Store id or bundle id |
POST /api/v1/appstore/reviews | 1 | A page of reviews: rating, title, text, author, app version |
This runs on Apple's own iTunes JSON API, so the data is first-party and cheap.
/appstore/search with term. Apple matches an app name, a keyword or a publisher name, so searching a developer returns their catalogue./app returns, up to 200 at a time. If you searched for it, you do not need to call /app for it as well./appstore/app with app_id. It takes either a numeric App Store id or a bundle id (notion.id, com.burbn.instagram) and auto-detects which; the payload is identical either way./appstore/reviews with a numeric app_id and a page from 1 to 10.Search does not paginate. limit (1-200, default 25) is the only lever on result volume, and every offset spelling is silently ignored. To get more results, raise limit - there is no second page.
Reviews paginate by page, 50 per page, and hard-stop at page 10. 500 reviews per storefront is Apple's anonymous ceiling. To reach further, ask a different country - each storefront has its own review pool.
/app returns a single object and takes no paging parameter.
country parameter is not cosmeticcountry picks the storefront, and the storefront decides the price, the currency, the localised title and description, and whether the app is sold there at all. It must be a two-letter code: the transport falls back to us for anything else, so "usa" silently buys a US result set that looks correct.
On /search, lang is independent of country: the storefront sets the prices, lang sets the words.
/search)| Parameter | Type | Default | Description |
|---|---|---|---|
term | string | required | What to search for (1-500 chars). Matches app name, keyword, or publisher name. |
limit | integer | 25 | 1-200. The only lever on result volume - there is no pagination. |
country | string | us | Two-letter storefront code. Anything not two letters silently falls back to us. |
entity | string | software | software (iPhone), ipad_software, mac_software |
lang | string | -- | Five-letter locale, e.g. en_us. Independent of country. |
/app)| Parameter | Type | Default | Description |
|---|---|---|---|
app_id | string | required | App Store id or bundle id (1-255 chars, ^[A-Za-z0-9][A-Za-z0-9._-]*$). A pasted apps.apple.com URL is rejected with a free 400 - extract the id first. |
country | string | us | Two-letter storefront code |
/reviews)| Parameter | Type | Default | Description |
|---|---|---|---|
app_id | string | required | Numeric App Store id only. The reviews RSS feed has no bundle-id form. |
country | string | us | Two-letter storefront code. Each storefront has its own review pool. |
page | integer | 1 | 1-10, 50 reviews each. Apple hard-stops at page 10. |
sort | string | most_recent | most_recent or most_helpful |
import os, requests
BASE = "https://api.scavio.dev"
HEADERS = {"Authorization": f"Bearer {os.environ['SCAVIO_API_KEY']}"}
# 1. Search - one call, up to 200 complete app rows. No pagination: raise limit.
apps = requests.post(f"{BASE}/api/v1/appstore/search", headers=HEADERS,
json={"term": "habit tracker", "limit": 200, "country": "us"}).json()
# 2. A publisher's catalogue - the same search endpoint, a developer name as the term
catalogue = requests.post(f"{BASE}/api/v1/appstore/search", headers=HEADERS,
json={"term": "Notion Labs", "limit": 50}).json()
# 3. One app, by numeric id or by bundle id - identical payload
app = requests.post(f"{BASE}/api/v1/appstore/app", headers=HEADERS,
json={"app_id": "1232780281"}).json()
same_app = requests.post(f"{BASE}/api/v1/appstore/app", headers=HEADERS,
json={"app_id": "notion.id"}).json()
# 4. Reviews - numeric id only, page 1-10, 50 per page
reviews = requests.post(f"{BASE}/api/v1/appstore/reviews", headers=HEADERS,
json={"app_id": "1232780281", "page": 1, "sort": "most_helpful"}).json()
Reviews stop at 500 per storefront. To go wider, ask another country rather than another page:
def reviews_across_storefronts(app_id, countries=("us", "gb", "de"), pages=10):
"""1 credit per call. 10 pages x 3 countries = 30 credits, so cap it."""
out = []
for country in countries:
for page in range(1, pages + 1): # page 10 is Apple's hard stop
data = requests.post(f"{BASE}/api/v1/appstore/reviews", headers=HEADERS,
json={"app_id": app_id, "country": country,
"page": page, "sort": "most_recent"}).json()["data"]
if not data:
break # empty feed, stop this storefront
out.append({"country": country, "page": page, "data": data})
return out
Every response uses the envelope { data, response_time, credits_used, credits_remaining }.
/app returns.Two shapes to code for:
entity: mac_software carries no iPad or Apple TV screenshots, no advisories, no features, no supported-device list and no Game Center flag. These come back empty rather than absent, so a Mac app is not a broken response./app calls.limit.sort: most_recent the vote fields come back as zeroes - those reviews are simply too new to have been voted on. Do not report that as "no one found these helpful". Use most_helpful if you need vote data.country changes the price, the currency, the localised text and the availability. If the user cares about price, name the storefront you queried in your answer.400 means an invalid or missing parameter - including a pasted apps.apple.com URL in app_id. Not billed. Extract the numeric id or bundle id and retry.401 means the API key is invalid or missing. Check SCAVIO_API_KEY.404 on /app means Apple could not resolve that id. This one is billed - Apple answers with a 200 carrying an empty result list and charges for it. Verify the id before looping over a list of them./reviews cannot 404. An unknown id and a real app with zero reviews return the same empty feed, so an empty result never proves the app does not exist. Confirm the app with /app first.429 means rate or usage limit exceeded. Wait before retrying. See https://scavio.dev/docs/rate-limits.502 / 503 mean upstream is temporarily unavailable - wait a few seconds and retry.SCAVIO_API_KEY is not set, prompt the user to export it before continuing.langchain-scavio has no App Store tool - use the Scavio SDK directly (it handles the auth header):
pip install scavio
from scavio import ScavioClient
client = ScavioClient() # reads SCAVIO_API_KEY
apps = client.app_store.search("habit tracker", limit=200, country="us")
app = client.app_store.app("notion.id")
reviews = client.app_store.reviews("1232780281", page=1, sort="most_helpful")
JavaScript / TypeScript:
npm install scavio
import { Scavio } from "scavio";
const scavio = new Scavio(); // reads SCAVIO_API_KEY
const apps = await scavio.appStore.search({ term: "habit tracker", limit: 200 });
const reviews = await scavio.appStore.reviews({ app_id: "1232780281", page: 1 });