- Google Play
- Alerts
- Mobile apps
Google Play Developer Reporting API: monitor crash rate and ANR rate before Play flags your app
Use the Google Play Developer Reporting API to pull crash rate, ANR rate and memory vitals into your own alerts, with Node.js code and Play's thresholds.
- Author
- Extenify Team
- Published
- Reading time
- 11 min read

On this page
The Google Play Developer Reporting API is how you get Android vitals out of Play Console and into your own monitoring. Crash rate, ANR rate and excessive wake locks already decide how visible your app is on Google Play, memory usage joins them in February 2027, and Play Console only shows these numbers when someone remembers to open the Android vitals page. The API lets a script check them every day and tell you when a release starts drifting toward Google's thresholds, before Play reduces your visibility or puts a warning on your store listing.
This guide covers what the API returns, the thresholds Play judges you against, how to set up a service account, Node.js code that queries crash and ANR rates by version and device, and the rules that turn the numbers into useful alerts. Every endpoint and threshold here was checked against Google's documentation on October 14, 2026.
Key takeaways
- The API is
playdeveloperreporting.googleapis.com/v1beta1. It exposes Android vitals as metric sets, such ascrashRateMetricSetandanrRateMetricSet, plus an anomalies endpoint.- Play's bad behavior thresholds are a 1.09% user-perceived crash rate and a 0.47% user-perceived ANR rate overall, and 8% for either on a single phone model, judged on a 28-day average.
- The service account needs only the View app information (read-only) permission in Play Console, and the scope
https://www.googleapis.com/auth/playdeveloperreporting.- Daily data uses the
America/Los_Angelestime zone and nothing else. Ask each metric set for its freshness instead of guessing the latest complete day.- Memory and bitmap memory thresholds start to affect visibility in February 2027. The API already returns both.
What the Google Play Developer Reporting API gives you
Google's introduction to metric sets lists the vitals you can query. Each metric set has a get method that reports data freshness and a query method that returns rows.
| Metric set | What it measures | Resource name |
|---|---|---|
vitals.crashrate |
Share of daily users with at least one crash | apps/{app}/crashRateMetricSet |
vitals.anrrate |
Share of daily users with at least one ANR | apps/{app}/anrRateMetricSet |
vitals.anonrssandswapmemoryusage |
Memory usage percentiles | apps/{app}/anonRssAndSwapMemoryUsageMetricSet |
vitals.bitmapmemoryusage |
Bitmap memory usage | See the reference |
vitals.slowstartrate |
Slow app starts | See the reference |
vitals.excessivewakeuprate |
Excessive wakeups | See the reference |
vitals.stuckbackgroundwakelockrate |
Stuck background wake locks | See the reference |
vitals.lmkrate |
Low memory kills | See the reference |
The anomalies endpoint lists metric movements Google itself considers unusual, with the metric set, the dimensions where it happened and the anomalous value.
What the API does not give you is install and user statistics. Those come from Play Console's monthly report files in Cloud Storage, which our guide to Google Play Console statistics explains, and public numbers such as your rating and review count come from the store listing itself.
The thresholds Play judges your app against
The Android vitals documentation, last updated October 9, 2026, lists the bad behavior thresholds:
| Metric | Overall (average across devices) | Per phone model | Per watch model |
|---|---|---|---|
| User-perceived crash rate | 1.09% | 8% | 4% |
| User-perceived ANR rate | 0.47% | 8% | 5% |
| Excessive battery usage | 1% | - | 1% |
| Excessive partial wake locks | 5% | - | - |
Google is direct about the consequence: "If your app or game exceeds a bad behavior threshold, Play may reduce the visibility of your title. Play may also show users a warning on your store listing." It also explains the window: "Play checks your app's key performance indicators daily, using a 28-day average." A bad release therefore does not trip the threshold on day one. It drags the 28-day average up over the following weeks, and fixing it takes weeks to wash out too. That delay is exactly why an automated daily check pays off.
Two counting details matter when you compare numbers. Android vitals counts per daily user: the documentation's example is a user with one crash across three sessions in a day, which vitals shows as a 100% crash rate while Crashlytics would show 33%. And "user-perceived" means the crash or ANR happened while the user was actively using the app, with an activity on screen or a foreground service running. The user-perceived rates are the ones the thresholds use.
On August 26, 2026, Google Play announced new technical quality requirements: "Starting in February 2027, apps and games must meet their respective bad behavior thresholds" for memory usage, bitmap usage and DEX code optimization. The memory thresholds vary by device RAM tier and app state, so look up the tiers that matter to you in the vitals documentation.
Set up access to the Play Developer Reporting API
The overview page describes the setup:
- Create a Google Cloud project, or reuse the one you use for other Play APIs.
- Enable the Google Play Developer Reporting API for that project.
- Create a service account in the project and download a JSON key, or use workload identity if your server runs on Google Cloud.
- In Play Console, open Users and permissions, invite the service account's email address, and grant it access to the apps you want to monitor.
For the vitals metric sets, the reference says "the calling user needs the View app information (read-only) permission for the app." Grant nothing more. Google also notes that you no longer need to link your developer account to a Cloud project to use this API.
Every request needs the OAuth scope https://www.googleapis.com/auth/playdeveloperreporting. Two Stack Overflow questions show where people get stuck: "How to set scope for token in Google Play Developer Reporting API", answered with that single scope, and "Can't connect to Google playdeveloperreporting API", where the answer was a service account from a different project than the one the developer had set up. If you get permission errors, check the key's project_id and the account's permissions in Play Console before anything else.
Query crash rate and ANR rate with Node.js
The code below uses google-auth-library, which reads the key file named in the GOOGLE_APPLICATION_CREDENTIALS environment variable and handles token refresh. Install it with npm install google-auth-library.
// play-vitals.mjs
import { GoogleAuth } from 'google-auth-library';
const BASE = 'https://playdeveloperreporting.googleapis.com/v1beta1';
const auth = new GoogleAuth({ scopes: ['https://www.googleapis.com/auth/playdeveloperreporting'] });
const LA = { id: 'America/Los_Angeles' }; // the only time zone daily data supports
async function call(path, data) {
const client = await auth.getClient();
const res = await client.request({ url: `${BASE}/${path}`, method: data ? 'POST' : 'GET', data });
return res.data;
}
function shiftDay({ year, month, day }, delta) {
const d = new Date(Date.UTC(year, month - 1, day + delta));
return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, day: d.getUTCDate() };
}
// The latest day with complete data, straight from the metric set.
async function latestDailyEnd(pkg, metricSet) {
const info = await call(`apps/${pkg}/${metricSet}`);
const daily = info.freshnessInfo?.freshnesses?.find((f) => f.aggregationPeriod === 'DAILY');
if (!daily) throw new Error(`${metricSet} has no daily data for ${pkg}`);
const { year, month, day } = daily.latestEndTime;
return { year, month, day };
}
export async function queryDaily(pkg, metricSet, { metrics, dimensions = [], days = 7, filter } = {}) {
const end = await latestDailyEnd(pkg, metricSet);
const request = {
timelineSpec: {
aggregationPeriod: 'DAILY',
startTime: { ...shiftDay(end, -days), timeZone: LA },
endTime: { ...end, timeZone: LA }, // exclusive
},
metrics,
dimensions,
pageSize: 1000,
...(filter ? { filter } : {}),
};
const rows = [];
let pageToken;
do {
const page = await call(`apps/${pkg}/${metricSet}:query`, { ...request, ...(pageToken ? { pageToken } : {}) });
rows.push(...(page.rows ?? []));
pageToken = page.nextPageToken;
} while (pageToken);
return rows.map(flattenRow);
}
function flattenRow(row) {
const { year, month, day } = row.startTime;
const out = { date: `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}` };
for (const d of row.dimensions ?? []) out[d.dimension] = d.stringValue ?? d.int64Value;
for (const m of row.metrics ?? []) out[m.metric] = m.decimalValue ? Number(m.decimalValue.value) : null;
return out;
}
A few rules from the crash rate query reference are baked into that code. For daily data, "the default and only supported timezone is America/Los_Angeles," and the start and end must not set hours or minutes. The end time is exclusive, and the freshness value from the metric set's get method is meant to be used as that end. Pages default to 1,000 rows and go up to 100,000, and a pageToken only works if every other parameter matches the first request.
Overall rates for the last week
import { queryDaily } from './play-vitals.mjs';
const pkg = 'com.example.app';
const crash = await queryDaily(pkg, 'crashRateMetricSet', {
metrics: ['userPerceivedCrashRate', 'userPerceivedCrashRate28dUserWeighted', 'distinctUsers'],
});
const anr = await queryDaily(pkg, 'anrRateMetricSet', {
metrics: ['userPerceivedAnrRate', 'userPerceivedAnrRate28dUserWeighted', 'distinctUsers'],
});
console.table(crash);
console.table(anr);
The 28dUserWeighted metrics are rolling averages over 28 days, weighted by daily users, which is the closest match to how Play evaluates the threshold. The plain daily rate is your early warning. Rates come back as decimals; before you wire thresholds, compare one day's value with the percentage Android vitals shows in Play Console for the same day, so you know the scale you are comparing against.
Break it down by version and device
Add dimensions to see where a problem lives:
const byVersion = await queryDaily(pkg, 'crashRateMetricSet', {
metrics: ['userPerceivedCrashRate', 'distinctUsers'],
dimensions: ['versionCode'],
days: 3,
});
const byDevice = await queryDaily(pkg, 'anrRateMetricSet', {
metrics: ['userPerceivedAnrRate28dUserWeighted', 'distinctUsers'],
dimensions: ['deviceModel'],
days: 1,
});
Other useful dimensions include apiLevel, countryCode, deviceType and deviceRamBucket. To narrow a query, pass a filter with an equality expression on a dimension, such as versionCode = 4210. The distinctUsers column is rounded, and the reference warns against summing it across periods, so use it to decide whether a row has enough users to matter, not as a user count.
Turn vitals into alerts
Raw numbers in a terminal are not monitoring. These rules catch the problems that hurt visibility while keeping the noise low:
- Approaching the overall threshold. Alert when
userPerceivedCrashRate28dUserWeightedpasses 75% of 1.09%, or the ANR equivalent passes 75% of 0.47%. That leaves time to ship a fix before the average crosses the line. - A release that is worse than the last one. Compare the daily rate of the newest
versionCodewith the previous version once the new one has a meaningful share ofdistinctUsers. A doubling on day two is a rollback conversation, long before it shows in the 28-day average. - A single device model over 8%. Query with the
deviceModeldimension and alert on any model above the per-device threshold with enough users. Exceeding it can make you less discoverable on that device and show a warning on its listing. - Anomalies Google already found. Poll the anomalies endpoint with
filter=activeBetween("2026-10-07T00:00:00Z", UNBOUNDED)and forward anything new.
Send the result as one daily message, not one per metric. If your team uses Teams, the sender in our guide to the Microsoft Teams webhook in Node.js takes a title and a list of facts, which fits a vitals digest exactly; the same digest also works over Discord webhooks or a Telegram bot. Run the check once a day after the freshness date moves, since daily data does not change more often than that.
Watch memory before February 2027
The memory metric set returns percentiles, not a single rate. The memory query reference lists anonRssAndSwapMemoryUsageP50 through anonRssAndSwapMemoryUsageP99, with dimensions that include deviceRamBucket and appState. It supports daily data only.
const memory = await queryDaily(pkg, 'anonRssAndSwapMemoryUsageMetricSet', {
metrics: ['anonRssAndSwapMemoryUsageP90', 'distinctUsers'],
dimensions: ['deviceRamBucket', 'appState'],
days: 7,
});
Compare each RAM bucket and app state with the matching row in the vitals documentation. Its example for apps on 8 GB devices is 2.25 GB in the foreground and 1.50 GB in the background. A weekly check is enough here: memory changes with releases, not from one day to the next.
Gotchas that waste an afternoon
- Hourly data is different. Hourly queries use UTC only, and the 7-day and 28-day metrics are "Not supported in HOURLY granularity."
- Small apps get empty rows. Low-traffic apps can get empty results, and breakdowns by device or country thin the data further. Start with an overall query before adding dimensions.
- v1beta1 is a beta. Read the reference before upgrading, and keep the code that parses rows in one place so a field change is a one-line fix.
- Do not guess the latest day. Use the freshness from each metric set's
getmethod; it is reported in the metric set's own time zone. - Cohorts. The default
userCohortisOS_PUBLIC, public Android releases. UseAPP_TESTERSto watch a testing track before a rollout.
Where vitals fit in your store monitoring
Crashes rarely stay inside Play Console. They turn into one-star reviews within days, and those reviews pull your rating down long after the fix ships. Watching vitals and reviews together shows you the whole chain: a release, a jump in the crash rate on one device, then the reviews that name it. Our guides to App Store review alerts and app review analysis with an LLM cover the review side, and the Chrome extension metrics scorecard applies the same weekly routine to browser extensions.
For the public side of that chain, Extenify's app store rating alerts track your Google Play and App Store ratings, reviews and versions daily next to your browser extensions, and send what changed to email, Slack, Discord or Telegram. Every plan starts with a 7-day free trial; see Extenify pricing for the details.
Frequently asked questions
Is there an API for Android vitals?
Yes. The Google Play Developer Reporting API exposes Android vitals, including crash rate, ANR rate, slow starts, wake locks and memory usage, as metric sets under playdeveloperreporting.googleapis.com/v1beta1, plus an anomalies endpoint.
What permission does the service account need?
For the vitals metric sets, the View app information (read-only) permission for each app in Play Console, plus the OAuth scope https://www.googleapis.com/auth/playdeveloperreporting. You no longer need to link the developer account to a Cloud project.
Why does the API return no rows for my app?
Usually because there is not enough data. Low-traffic apps can get empty results, and dimension breakdowns split the data further. Query without dimensions first, check the metric set's freshness, and make sure the date range ends no later than the freshness date.
Which metric matches the bad behavior threshold in Play Console?
Use userPerceivedCrashRate28dUserWeighted and userPerceivedAnrRate28dUserWeighted. Play evaluates user-perceived rates on a 28-day average, and these are 28-day rolling averages weighted by daily users.
Why don't the API numbers match Crashlytics?
They count differently. Android vitals reports the share of daily users affected, so one user with one crash in three sessions counts as a 100% crash rate for that user, while Crashlytics counts per session and would show 33%.
Can I get crash stack traces from the Reporting API?
The vitals metric sets return rates and counts, not stack traces. Use the Crashes and ANRs pages in Play Console or a crash reporting SDK for the traces, and the API to decide when to look.
Summary
The Google Play Developer Reporting API turns Android vitals into data you can check every day. Give a service account read-only access, query crashRateMetricSet and anrRateMetricSet in the Los Angeles time zone using each metric set's freshness date, and alert on the 28-day user-perceived rates long before they reach 1.09% and 0.47%. Add per-version and per-device breakdowns to find the cause, check memory weekly ahead of February 2027, and watch reviews alongside vitals, because that is where crashes show up next.
Image credits
- Cover photo: Thermometer at 40 degrees Celsius by Javier Casares on the WordPress Photo Directory, CC0.


