App Store and Google Play reviews, as a REST API
Fetch reviews for any iOS or Android app as clean JSON. Cache-first: recently requested apps return instantly and new apps are fetched live in seconds. Built on the same pipeline that powers rivioo.app exports, and the same key works with the MCP server.
Quickstart
First reviews in five minutes.
-
Get a key.
Sign up, choose a plan, and create a key on the API keys tab of your workspace. It is shown once.export KEY=rk_live_your_key_here -
Find your app's id.
Search is unmetered and never counts against your quota.curl "https://api.rivioo.app/v1/apps/search?q=spotify&store=android" \ -H "Authorization: Bearer $KEY"Use{ "data": [ { "app_id": { "android": "com.spotify.music" } } ] }app_idin the next step. -
Fetch reviews.
Fresh data returns instantly as a 200 with your reviews. If we need to fetch live, you get a 202 instead. That is step 4.curl -X POST https://api.rivioo.app/v1/reviews/fetch \ -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{ "store": "android", "app_id": "com.spotify.music", "limit": 500 }'import requests r = requests.post( "https://api.rivioo.app/v1/reviews/fetch", headers={"Authorization": f"Bearer {KEY}"}, json={"store": "android", "app_id": "com.spotify.music", "limit": 500}, ) data = r.json()const res = await fetch("https://api.rivioo.app/v1/reviews/fetch", { method: "POST", headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ store: "android", app_id: "com.spotify.music", limit: 500 }), }); const data = await res.json(); -
Got a 202? (Got a 200? You already have reviews, skip to step 5.)
A 202 means a live fetch is running. Poll themeta.pollURL until status iscompleted(usually under a minute), then GET itsreviews_url.curl "https://api.rivioo.app/v1/jobs/3f6f…" -H "Authorization: Bearer $KEY"{ "job": { "status": "completed", "reviews_url": "/v1/jobs/3f6f…/reviews" } } -
Page through the rest.
Followmeta.next_cursorfrom each page until it isnull. That is the whole API.A review counts toward your monthly allowance the first time we deliver it to your account. Fetching it again within 30 days is free.curl "https://api.rivioo.app/v1/reviews?cursor=eyJ2IjoxLC…" -H "Authorization: Bearer $KEY"
Authentication
Pass your key as a bearer token on every request:
curl https://api.rivioo.app/v1/usage \
-H "Authorization: Bearer rk_live_your_key_here"
import requests
r = requests.get("https://api.rivioo.app/v1/usage",
headers={"Authorization": "Bearer rk_live_your_key_here"})
const res = await fetch("https://api.rivioo.app/v1/usage", {
headers: { Authorization: "Bearer rk_live_your_key_here" },
});
Keys are shown once when you create them. Only a hash is stored. If a key leaks, revoke it on the API keys tab of your workspace and create a new one.
Endpoints
https://api.rivioo.app/v1
Find an app's canonical id. Never counts against your quota.
| Param | Description |
|---|---|
q | Search terms (required) |
store | ios or android |
country | 2-letter storefront code, default us. A code the store does not support returns 400. |
curl "https://api.rivioo.app/v1/apps/search?q=spotify&store=android" \
-H "Authorization: Bearer $KEY"
{
"data": [
{
"name": "Spotify: Music and Podcasts",
"app_id": {
"android": "com.spotify.music"
}
}
]
}
Use app_id.android (package name) or app_id.ios (numeric track id) in the fetch call below.
The core endpoint. If we have fresh data for the app, you get reviews immediately (HTTP 200). Otherwise a live fetch starts and you poll it (HTTP 202).
curl -X POST https://api.rivioo.app/v1/reviews/fetch \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{
"store": "android",
"app_id": "com.spotify.music",
"country": "us",
"limit": 500
}'
import requests
r = requests.post(
"https://api.rivioo.app/v1/reviews/fetch",
headers={"Authorization": f"Bearer {KEY}"},
json={
"store": "android",
"app_id": "com.spotify.music",
"country": "us",
"limit": 500,
},
)
data = r.json()
const res = await fetch("https://api.rivioo.app/v1/reviews/fetch", {
method: "POST",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
store: "android",
app_id: "com.spotify.music",
country: "us",
limit: 500,
}),
});
const data = await res.json();
| Field | Description |
|---|---|
store | ios or android (required) |
app_id | Android package name or iOS numeric track id (required) |
country | 2-letter storefront code, default us. For the App Store it selects that country's reviews. For Google Play it selects the review languages (English and, in most countries where English is not the main language, the country's main language) and returns those reviews from every country, since Google Play reviews are per language, not per country |
limit | Total reviews wanted, clamped to your plan's per-request max |
cache_policy | What to do with reviews we already hold for this app and country. use_cache (default) serves them when they are fresh enough and at least limit deep, otherwise starts a live fetch. ask returns status: "cache_choice_required" with HTTP 200 and no reviews when any are held, so you can ask your user; repeat the request with use_cache or refresh. If nothing is held it behaves like use_cache. refresh skips held reviews and starts a live fetch; allow_stale is ignored. If your monthly quota is used up, refresh returns the held reviews instead, since re-reading them is free. Any other value returns 400. |
freshness_days | How old cached data may be before we refresh it (1 to 30, default 7) |
allow_stale | true serves older cached data instantly instead of refreshing |
filters | Optional object that narrows the reviews before paging. ratings: star ratings to keep, for example [1, 2]. posted_from and posted_before: ISO 8601 timestamps with an offset. app_version: an exact version. Other fields return 400. |
Cache hit (200): first page of reviews plus a next_cursor (same shape as /reviews below).
{
"job": {
"id": "3f6f…",
"status": "queued"
},
"meta": {
"source": "scrape",
"poll": "/v1/jobs/3f6f…",
"estimated_seconds": 45
}
}
Poll a fetch job. Status moves from queued to running to completed (or failed).
{
"job": {
"id": "3f6f…",
"status": "completed",
"reviews_url": "/v1/jobs/3f6f…/reviews"
}
}
First page of a completed job's reviews. Same shape and pagination as /reviews below. Optional query parameters narrow the reviews before paging.
| Parameter | Description |
|---|---|
ratings | Star ratings to keep, comma-separated, for example ratings=1,2 |
posted_from | ISO 8601 timestamp with offset; keeps reviews posted at or after it |
posted_before | ISO 8601 timestamp with offset; keeps reviews posted before it |
app_version | Exact app version, for example 9.0.28.630 |
limit | Total reviews wanted across pages, up to the job's size and your plan's per-request max |
page_size | Reviews per page (1 to 500, default 100) |
Paginate with ?cursor=…. Follow meta.next_cursor from any page until it is null. Pages default to 100 reviews (page_size up to 500). Each review:
{
"id": "gp:AOqpTOH…",
"store": "android",
"app_id": "com.spotify.music",
"country": "us",
"lang": "en",
"rating": 4,
"title": null,
"body": "Offline playlists are great on the subway…",
"reviewer": "Jordan M.",
"app_version": "9.0.28.630",
"developer_reply": null,
"developer_reply_at": null,
"url": "https://play.google.com/store/apps/details?id=…",
"posted_at": "2026-07-21T09:14:00.000Z"
}
For Google Play reviews, country is the country of the collection that first stored the review, not where the reviewer is; use lang instead.
Your plan, quota used and remaining, reset date, and today's refresh count.
{
"plan": "pro",
"quota": {
"monthly_reviews": 50000,
"used": 12480,
"remaining": 37520,
"resets_at": "2026-10-01T00:00:00.000Z"
},
"scrape_jobs_today": 14,
"scrape_jobs_daily_cap": 60
}
Errors
Every error uses one envelope:
{
"error": {
"code": "QUOTA_EXCEEDED",
"message": "Monthly review quota (50,000) exhausted. Resets 2026-10-01T00:00:00Z.",
"status": 429
}
}
| Code | Status | Meaning |
|---|---|---|
UNAUTHORIZED | 401 | Missing, malformed, or unknown key |
KEY_REVOKED / CLIENT_SUSPENDED | 403 | Account inactive. Contact us |
SUBSCRIPTION_EXPIRED | 403 | Paid access has lapsed. Renew on the billing tab |
VALIDATION_ERROR | 400 | Bad parameter. The message says which |
NOT_FOUND | 404 | No job with that id on your account |
RATE_LIMITED | 429 | Over your per-minute request limit |
QUOTA_EXCEEDED | 429 | Monthly review quota used up, and none of the matching reviews are free to fetch again |
SCRAPE_CAP_REACHED | 429 | Daily refresh allowance used. Cached data still available |
Rate limits and headers
Responses carry RateLimit-* headers (per minute) and X-Quota-Limit / X-Quota-Remaining (monthly reviews). Quota counts each review the first time we deliver it to your account, cached or fresh, per calendar month in UTC. Delivering the same review again within 30 days is free, whether through the REST API, MCP or a workspace export. It is a hard cap with no overage charges. Once it is used up, we still return reviews you received in the last 30 days and set meta.quota_limited to true; if none of the matching reviews qualify, the request fails with QUOTA_EXCEEDED. Unused quota does not roll over. Cached responses do not consume the daily refresh allowance.
Versioning and stability
v1 is stable. Breaking changes ship as a new version with 90 days notice and a migration guide. We may add new fields to responses at any time, so build your parser to ignore unknown keys.
Data coverage
Exactly what you get:
| Dimension | Coverage |
|---|---|
| Stores | Apple App Store and Google Play |
| Storefronts | Every public App Store country, via the country parameter. Google Play reviews are per language, not per country: country picks the languages and the reviews come from every country |
| Languages | Each review carries a lang code. iOS storefronts return reviews in every language. Google Play collections return reviews written in English and, in most countries where English is not the main language, in the country's main language too |
| History depth | Up to your plan's per-request maximum of the most recent reviews per app per storefront: 2,500 on Plus, 5,000 on Pro |
| Freshness | Live fetches are current as of the request; cached data is at most freshness_days old (default 7) |
| Fields | Rating, title, body, reviewer, app version, developer reply and timestamp, review URL, posted date |
Plans
Priced by reviews delivered per month. Re-reading a review is free for 30 days. Hard cap, no overage charges. One key covers both the REST API and the MCP server.
For one person who works with reviews every week.
plus tax where applicable
- 10,000 reviews per month
- 2,500 reviews per request
- 30 requests per minute
- 20 live refreshes per day
- 1 active API key
Five API keys for separate tools and projects.
plus tax where applicable
- 50,000 reviews per month
- 5,000 reviews per request
- 60 requests per minute
- 60 live refreshes per day
- 5 active API keys
The allowance counts each review the first time it reaches your account through the REST API, MCP or a workspace export, cached or fresh. Re-reading a review is free for 30 days. The allowance resets on the 1st of each month (UTC), and unused reviews do not roll over.
Get a key
Keys are self-serve. The same key works with both the REST API and the MCP server.
- Sign up with Google or an email link.
- Choose Plus or Pro under Plan & billing. Access opens when the first payment is confirmed.
- Create a key under API keys & setup. Copy it straight away, it is shown once.
- Send it as a bearer token, or paste the connect snippet into Claude, Cursor or another MCP client.
