App Store & 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; new apps are fetched live in seconds. Built on the same pipeline that powers rivioo.app exports.
Request accessKeys are issued personally during early access and usually arrive within a day.
Quickstart
First reviews in five minutes.
-
Get a key.
Request access and your key arrives by email within a day.Terminalexport KEY=rk_live_your_key_here
-
Find your app's id.
Search is unmetered and never counts against your quota.Requestcurl "https://api.rivioo.app/v1/apps/search?q=spotify&store=android" \ -H "Authorization: Bearer $KEY"
UseResponse · 200{ "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's 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.Requestcurl "https://api.rivioo.app/v1/jobs/3f6f…" -H "Authorization: Bearer $KEY"
Response · 200{ "job": { "status": "completed", "reviews_url": "/v1/jobs/3f6f…/reviews" } } -
Page through the rest.
Followmeta.next_cursorfrom each page until it'snull. That's the whole API.Reviews are only ever charged once. Re-fetching a page you already received is free.Requestcurl "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 at issue time — only a hash is stored. If a key leaks, email us and we'll rotate it immediately.
Endpoints
https://api.rivioo.app/v1
All paths below are relative to the base URL. OpenAPI spec ↓ · Postman collection ↓
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 |
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 |
limit | Total reviews wanted, clamped to your plan's per-request max |
freshness_days | How old cached data may be before we refresh it (1–30, default 7) |
allow_stale | true = serve older cached data instantly instead of refreshing |
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 is queued → running → 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.
Paginate with ?cursor=… — follow meta.next_cursor from any page until it's 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"
}
Your plan, quota used/remaining, reset date, and today's refresh count.
{
"plan": "pro",
"quota": {
"monthly_reviews": 75000,
"used": 12480,
"remaining": 62520,
"resets_at": "2026-08-01T00:00:00.000Z"
},
"scrape_jobs_today": 14,
"scrape_jobs_daily_cap": 60
}
Errors
Every error uses one envelope:
| Code | Status | Meaning |
|---|---|---|
UNAUTHORIZED | 401 | Missing, malformed, or unknown key |
KEY_REVOKED / CLIENT_SUSPENDED | 403 | Account inactive — contact us |
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 exhausted |
SCRAPE_CAP_REACHED | 429 | Daily refresh allowance used — cached data still available |
Rate limits & headers
Responses carry RateLimit-* headers (per-minute) and X-Quota-Limit / X-Quota-Remaining (monthly reviews). Quota counts reviews delivered to you (cached or fresh) per calendar month, UTC. Hard cap — no surprise overage charges. Unused quota doesn't roll over. Cached responses don't consume the daily refresh allowance.
Versioning & 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 country storefront, via the country parameter |
| Languages | Each review carries a lang code. iOS storefronts return reviews in every language; Android fetches return English reviews per storefront in v1 |
| History depth | Up to the most recent 10,000 reviews per app per storefront |
| 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 + timestamp, review URL, posted date |
Plans
Priced by reviews delivered per month. Hard cap — no surprise overage charges.
Side projects & prototypes
- Reviews / month 15,000
- Max per request 2,500
- Rate limit 30 req/min
- Live refreshes / day 20
Products tracking a few apps
- Reviews / month 75,000
- Max per request 5,000
- Rate limit 60 req/min
- Live refreshes / day 60
Portfolios & market research
- Reviews / month 300,000
- Max per request 10,000
- Rate limit 120 req/min
- Live refreshes / day 200
Quota counts reviews delivered to you (cached or fresh) per calendar month, UTC. Unused quota doesn't roll over. Cached responses don't consume the daily refresh allowance.
Get a key
Request access and your key arrives by email, usually within a day. No card on file, cancel anytime.
Questions? admin@rivioo.app