Vitals API Reference
This guide includes reference information for you to use to integrate the Vitals API.
Use of the Vitals API is subject to the Program Materials License Agreement.
- Prerequisites
- Base URL
- Operations
- Request
- Response
- Implementation examples
- Error handling
- Retries and best practices
- Related topics
Prerequisites
Before making API calls, make sure you meet the following requirements:
| Requirement | Details |
|---|---|
| Amazon Developer Account | Admin-level credentials required |
| Security Profile | Created via Developer Console > My Settings > API Access |
| Security Profile mapped to API | Attach the profile to the Reporting API |
| Client ID and Client Secret | From the security profile's Web Settings tab |
| LWA Scope | adx_reporting::appstore:marketer |
| Package Name | Your app's package name (for example, com.example.myapp) must be owned by your vendor account |
Python 3.6+ with requests library |
Base URL
The base URL of the Vitals API is https://developer.amazon.com/api/appstore.
Operations
The Vitals API includes the following operations and endpoints:
| Operation | Method | Endpoint | Description |
|---|---|---|---|
| Check Freshness | GET | /vitals/apps/{packageName}/{metricSetName} |
Latest available data timestamp |
| Query Metrics | POST | /vitals/apps/{packageName}/{metricSetName}:query |
Time-series metrics with dimensions/filters |
Request
See the following sections for details on the parameters and validation rules.
Request parameters
Every query request must include the following parameters:
| Parameter | Description | Type | Required |
|---|---|---|---|
timelineSpec.aggregationPeriod |
Granularity of the returned data: DAILY or HOURLY |
String | Yes |
timelineSpec.startTime |
Lower bound of the query window, as a calendar date in UTC: {year, month, day} |
Object | Yes |
timelineSpec.endTime |
Upper bound of the query window, as a calendar date in UTC: {year, month, day}. Must be greater than startTime. |
Object | Yes |
metrics[] |
Subset of metric names to return from the metric set (for example, crashRate, crashCount). Omit to return all metrics defined on the set. |
Array | No |
dimensions[] |
Dimensions to group results by (for example, versionCode, countryCode) |
Array | No |
filter |
Filter conditions as a JSON object. Each key is a dimension; its value is a list of allowed values (OR within a field, AND across fields). For example, {"countryCode": ["US","CA"], "deviceType": ["AMAZON_FIRE_TV"]}. |
String | No |
pageSize |
Max rows per page. Default: 1000. Max: 100,000. | Integer | No |
pageToken |
Opaque cursor returned in the previous response's nextPageToken. Omit on the first request. |
String | No |
reportType |
Error type for issuesMetricSet only. One of CRASH or ANR. Not applicable to rate metric sets. | String | No |
Response
See the following sections for details on the response fields.
Response fields
| Field | Always Present | Notes |
|---|---|---|
status |
Yes | HTTP status code (200) |
requestId |
Yes | UUID for debugging; include in support tickets |
freshness |
Yes | Latest data timestamp for the queried period |
resultCount |
Yes | Number of rows in this page |
rows[] |
Yes | May be empty if no data matches |
nextPageToken |
Only if more pages | Pass in next request to get next page |
Successful response (200)
{
"status": 200,
"requestId": "8a3f2c71-e4b9-4012-a8c1-9d5e7f3b2a10",
"freshness": {
"aggregationPeriod": "DAILY",
"latestDataTimestamp": "2026-04-25T00:00:00Z"
},
"resultCount": 3,
"rows": [
{
"startTime": {"year": 2026, "month": 4, "day": 25},
"aggregationPeriod": "DAILY",
"dimensions": [
{"name": "versionCode", "value": "151"},
{"name": "countryCode", "value": "US"}
],
"metrics": [
{"name": "crashRate", "value": "0.0142"},
{"name": "distinctDevices", "value": "54320"}
]
}
],
"nextPageToken": "MjAyNi0wNC0yNXwxNA=="
}
Implementation examples
The following examples show Python code for various tasks using the Vitals API.
Obtain access token
Use your client ID and client secret to obtain an auth token. You must add the auth token to the header of each API request. The following example shows how to obtain an auth token and create the Authorization header using the token.
# Values that you need to provide
client_id = "<your client id>"
client_secret = "<your client secret>"
package_name = "<your app package name>"
BASE_URL = "https://developer.amazon.com/api/appstore"
scope = "adx_reporting::appstore:marketer"
data = {
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret,
"scope": scope
}
auth_response = requests.post("https://api.amazon.com/auth/o2/token", data=data)
auth_token = auth_response.json()["access_token"]
headers = {
"Authorization": f"Bearer {auth_token}",
"Content-Type": "application/json"
}
Check data freshness
The following example shows how to check the latest available data timestamp before querying.
freshness_url = f"{BASE_URL}/vitals/apps/{package_name}/crashMetricSet"
freshness_response = requests.get(freshness_url, headers=headers)
freshness = freshness_response.json()
for info in freshness["freshnessInfo"]:
print(f"{info['aggregationPeriod']}: {info['latestDataTimestamp']}")
GET freshness response example
{
"name": "crashMetricSet",
"metrics": ["crashRate", "crashRate7dUserWeighted", "crashRate28dUserWeighted", "userPerceivedCrashRate", "userPerceivedCrashRate7dUserWeighted", "userPerceivedCrashRate28dUserWeighted",
"distinctDevices", "crashCount"],
"dimensions": ["versionCode", "countryCode", "deviceModel", "deviceType", "deviceOS", "osVersion"],
"freshnessInfo": [
{"aggregationPeriod": "DAILY", "latestDataTimestamp": "2026-04-25"},
{"aggregationPeriod": "HOURLY", "latestDataTimestamp": "2026-04-25T14"}
]
}
Convert local time to UTC
The following example shows how to convert a local datetime to UTC.
from datetime import datetime, timezone, timedelta
def utc_date_components(local_dt=None):
"""Convert a datetime to UTC year/month/day for the API.
A naive local_dt is interpreted as system local time.
Pass a tz-aware datetime to avoid ambiguity.
"""
if local_dt is None:
utc_dt = datetime.now(timezone.utc)
else:
if local_dt.tzinfo is None:
local_dt = local_dt.astimezone() # attach system local tz explicitly
utc_dt = local_dt.astimezone(timezone.utc)
return {"year": utc_dt.year, "month": utc_dt.month, "day": utc_dt.day}
# Example: "yesterday" in UTC (regardless of your local timezone)
yesterday_utc = datetime.now(timezone.utc) - timedelta(days=1)
start = utc_date_components(yesterday_utc)
Query metrics
The following example shows the full shape of a query against a rate metric set (crash, ANR, LMK). Comments call out which fields are required vs. optional.
metric_set = "crashMetricSet" # or "anrMetricSet", "lmkMetricSet"
query_url = f"{BASE_URL}/vitals/apps/{package_name}/{metric_set}:query"
body = {
# Required: time range and granularity
"timelineSpec": {
"aggregationPeriod": "DAILY", # DAILY / HOURLY
"startTime": {"year": 2026, "month": 4, "day": 01},
"endTime": {"year": 2026, "month": 4, "day": 01},
},
# Optional: metrics from the metricset being queried
"metrics": ["crashRate", "crashCount", "distinctDevices"],
# Optional: break down by dimension (max 5)
"dimensions": ["versionCode", "countryCode"],
# Optional: AIP-160 filter expression
"filter": {"countryCode": ["US", "CA"], "deviceType": ["AMAZON_FIRE_TV"]},
# Optional: paging (default 1000, max 100000)
"pageSize": 1000,
"pageToken": "<from previous response>",
}
response = requests.post(query_url, headers=headers, json=body)
result = response.json()
Paginate through results
The following example shows how to fetch all results when the response includes a nextPageToken.
all_rows = []
body.pop("pageToken", None)
while True:
result = requests.post(query_url, headers=headers, json=body).json()
all_rows.extend(result["rows"])
token = result.get("nextPageToken")
if not token:
break
body["pageToken"] = token
Query top crash issues
The issuesMetricSet returns top crash/ANR/LMK issues ranked by frequency. Unlike rate metric sets, it aggregates over the entire time range and returns one row per unique crash signature with its stacktrace. ReportType specifies CRASH or ANR for the shared stacktraces.
issues_url = f"{BASE_URL}/vitals/apps/{package_name}/issuesMetricSet:query"
body = {
"startTime": { "year": 2025, "month": 6, "day": 1 },
"endTime": { "year": 2025, "month": 6, "day": 15 },
"reportType": "CRASH",
"dimensions": ["deviceType", "osVersion"],
"filter": { "countryCode": ["US", "CA"] },
"pageSize": 25,
"pageToken": null
}
response = requests.post(issues_url, headers=headers, json=body)
result = response.json()
# Response shape (different from rate metric sets):
# {
# "rows": [
# {
# "crashDescriptor": "67f35eb2...",
# "reportText": "java.lang.IllegalStateException: ...",
# "metrics": [
# {"name": "errorEventCount", "value": "78"},
# {"name": "affectedDeviceCount", "value": "45"}
# ]
# }
# ],
# "nextPageToken": "..."
# }
Error handling
See the following sections for how to handle errors.
Error response format
All errors return structured JSON:
{
"code": "INVALID_ARGUMENT",
"message": "Unknown metric 'bogus'. Valid metrics for crashMetricSet: crashRate, crashRate7dUserWeighted, ...",
"status": 400
}
Error codes
| HTTP Status | Code | Cause | Action |
|---|---|---|---|
| 400 | INVALID_ARGUMENT | Bad request params (invalid metric, dimension, date range) | Fix the request; do not retry |
| 403 | UNAUTHORIZED | Expired or invalid token | Refresh token and retry |
| 404 | NOT_FOUND | Unknown metric set name | Check spelling of metricSetName |
| 429 | RATE_LIMITED | Exceeded 3 req/sec | Back off and retry with exponential delay |
| 500 | INTERNAL | Server error | Retry with backoff (up to 3 times) |
Common mistakes and fixes
| Mistake | Error You'll See | Fix |
|---|---|---|
Missing timelineSpec |
"timelineSpec is required" | Add the required timelineSpec object |
Using HOURLY with 28d metric |
"crashRate28dUserWeighted not available for HOURLY" | Switch to DAILY or remove the 28d metric |
| Date range too wide (hourly) | "HOURLY queries limited to 15 days" | Narrow to 15 days or fewer |
| Expired token | 403 Forbidden | Refresh the LWA token |
| Too many requests | 429 Too Many Requests | Add time.sleep() between calls |
Retries and best practices
See the following sections for retry strategy and best practices.
Retry strategy example
import time
MAX_RETRIES = 3
RETRIABLE_CODES = {429, 500, 502, 503}
def query_with_retry(client, package, metric_set, body):
for attempt in range(MAX_RETRIES):
resp = client.query(package, metric_set, body)
if resp.status_code == 200:
return resp.json()
if resp.status_code not in RETRIABLE_CODES:
break # Non-retriable error
wait = (2 ** attempt) + 0.5 # 1.5s, 2.5s, 4.5s
print(f"Retry {attempt + 1}/{MAX_RETRIES} after {wait}s (HTTP {resp.status_code})")
time.sleep(wait)
# Final attempt failed
error = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else {}
raise RuntimeError(f"Failed after {MAX_RETRIES} retries: {resp.status_code} {error.get('message', '')}")
Production best practices
| Practice | Recommendation |
|---|---|
| Token caching | Cache the token and refresh 5 min before expiry. Never request a new token per API call. |
| Check freshness first | Always call the freshness endpoint before querying to avoid empty results. |
| Use pagination | Default pageSize is 1000. For large dimension breakdowns, paginate to avoid timeouts. |
| Rate limiting | Stay under 3 req/sec. Add time.sleep(0.4) between sequential calls. |
| Retry only retriable errors | Retry 429, 500, 502, 503. Never retry 400 (fix the request instead). |
| Log requestId | Every response includes requestId. Log it for debugging and support tickets. |
| Handle empty results | A query returning rows: [] is not an error. It means there is no data for that date range. |
| Avoid over-fetching | Request only the metrics you need. Omitting metrics[] returns all, which is slower. |
Related topics
- Vitals API Overview: Available metrics, dimensions, and FAQ
- App Health Insights Dashboard: Visual dashboard for the same metrics
- Reporting API Explorer: Interactive API explorer on the developer portal
Last updated: Jun 29, 2026

