Vitals API V2 Reference
This page gives the reference information that you need to integrate the Vitals API V2. For the metric sets, the metrics, and the dimensions, see the Vitals API V2 Overview.
Amazon continues to support version 1 (V1) of the Vitals API. Use V2 for new integrations. Migrate existing integrations to V2. For V1, see the Vitals API Reference. For the changes that you must make, see Migrate from V1.
Use of the Vitals API is subject to the Program Materials License Agreement.
- Prerequisites
- Base URL
- Operations
- Request rules
- Check Freshness
- Query Metrics
- Query Issues
- Implementation examples
- Error handling
- Rate limits, retries, and best practices
- Migrate from V1
- Related topics
Prerequisites
Before you send a request, make sure that you have the following items:
| Requirement | Details |
|---|---|
| Amazon Developer account | Admin-level credentials |
| Security profile | Create the security profile in the Developer Console, at My Settings > API Access. |
| Security profile attached to the Reporting API | Attach the security profile to the Reporting API. |
| Client ID and client secret | Get these values from the Web Settings tab of the security profile. |
| LWA scope | adx_reporting::appstore:marketer |
| Package name | The package name of your app, for example com.example.myapp. Your developer account must own the app. |
The code examples on this page use Python 3.8 or later and the requests library.
Base URL
The base URL of the Vitals API V2 is https://developer.amazon.com/api/appstore.
Operations
The Vitals API V2 includes the following operations and endpoints:
| Operation | Method | Endpoint | Description |
|---|---|---|---|
| Check Freshness | GET | /v2/applications/{packageName}/vitals/{metricSetName} |
Returns the metadata and the freshness of a metric set |
| Query Metrics | POST | /v2/applications/{packageName}/vitals/{metricSetName}:query |
Returns metrics for each time period, with dimensions and filters |
| Query Issues | POST | /v2/applications/{packageName}/vitals/issuesMetricSet:query |
Returns the top crash issues or the top ANR issues |
The endpoints use the following path parameters:
| Parameter | Description |
|---|---|
packageName |
The package name of your app, for example com.example.myapp |
metricSetName |
The name of a metric set, except issuesMetricSet. For the list, see Available metric sets. |
issuesMetricSetsupports Query Issues only. A Check Freshness request for it returns404 RESOURCE_NOT_FOUND.- Query Metrics and Query Issues paths must end in
:query.
Request headers
| Header | Required | Value |
|---|---|---|
Authorization |
Yes | Bearer ACCESS_TOKEN. ACCESS_TOKEN is the access_token value from Login with Amazon. |
Content-Type |
Yes, for POST |
application/json |
Accept |
No | application/json. The API sends only JSON. |
Request rules
The rules in this section apply to the Query Metrics operation and to the Query Issues operation.
Time values
Use UTC timestamps in the format YYYY-MM-DDThh:mm:ssZ for startTime and endTime.
The time values follow these rules:
- The time range includes the time periods of
startTimeandendTime.endTimemust be the same asstartTimeor later. - For
DAILY, the API uses the UTC date. ForHOURLY, the API ignores minutes and seconds. For example,2026-08-18T14:35:00Zselects the UTC period starting at2026-08-18T14:00:00Z. startTimemust be within the latest 30 UTC dates forDAILYor the latest 15 UTC dates forHOURLY. The API counts these dates back from the freshness value. The date of the freshness value is the first of them.- The UTC date (
DAILY) or UTC hour (HOURLY) ofendTimemust not be later than the freshness value. - For a time range that breaks these rules, the API sends
400 VALIDATION_ERRORwithFIELD_OUT_OF_RANGEon the field that breaks the rule,timelineSpec.startTimeortimelineSpec.endTime. - If no freshness value is available, the query returns no rows.
The API rejects these time values:
- A date without a time, for example
2026-08-18 - A time with fractions of a second, for example
2026-08-18T00:00:00.000Z - A time with a time zone offset, for example
2026-08-18T00:00:00+00:00 - A time that ends with a lowercase
z
Dimensions and filters
- A filter key must name a dimension of the metric set. It need not appear in
dimensions. - Use nonempty, case-sensitive strings for filter values. A
countryCodevalue must be an uppercase ISO 3166-1 alpha-2 code, for exampleUS. The API matches any value for each key and requires every key to match. An empty array has no effect. - A breakdown omits data without a value for any requested dimension.
Pagination
To get the next page, send nextPageToken as pageToken. Keep the request unchanged except for pageToken and, if needed, pageSize. Repeat until nextPageToken is null. Omit pageToken to start a new query.
Check Freshness
The Check Freshness operation returns the metadata of a metric set and the freshness of its data. The request has no body.
Check Freshness response fields
| Field | Type | Always Present | Notes |
|---|---|---|---|
name |
String | Yes | The name of the metric set |
description |
String | Yes | The description of the metric set |
metrics[] |
Array of strings | Yes | All metrics of the metric set, for all aggregation periods. Some metrics support only DAILY. The order is not fixed. |
dimensions[] |
Array of strings | Yes | The dimensions of the metric set. The order is not fixed. |
freshnessInfo[] |
Array of objects | Yes | One entry for each aggregation period that has a freshness value. The array can have zero, one, or two entries. |
freshnessInfo[].aggregationPeriod |
String | Yes | DAILY or HOURLY |
freshnessInfo[].latestDataTimestamp |
String | Yes | The UTC start of the latest processed day (DAILY) or hour (HOURLY), in YYYY-MM-DDThh:mm:ssZ format |
Check Freshness request example
GET /api/appstore/v2/applications/com.example.myapp/vitals/crashMetricSet HTTP/1.1
Host: developer.amazon.com
Authorization: Bearer ACCESS_TOKEN
Check Freshness response example
{
"name": "crashMetricSet",
"description": "App crash metrics",
"metrics": [
"crashRate",
"crashRate7dUserWeighted",
"crashRate28dUserWeighted",
"userPerceivedCrashRate",
"userPerceivedCrashRate7dUserWeighted",
"userPerceivedCrashRate28dUserWeighted",
"distinctDevices",
"crashCount"
],
"dimensions": [
"versionCode",
"countryCode",
"deviceModel",
"deviceType",
"deviceOs",
"osVersion"
],
"freshnessInfo": [
{"aggregationPeriod": "HOURLY", "latestDataTimestamp": "2026-08-18T14:00:00Z"},
{"aggregationPeriod": "DAILY", "latestDataTimestamp": "2026-08-18T00:00:00Z"}
]
}
Query Metrics
The Query Metrics operation returns the metrics of one metric set for each time period in the time range that has data.
Query Metrics request parameters
| Parameter | Description | Type | Required |
|---|---|---|---|
timelineSpec |
The aggregation period and the time range. For the rules, see Time values. | Object | Yes |
timelineSpec.aggregationPeriod |
DAILY or HOURLY |
String | Yes |
timelineSpec.startTime |
The start of the time range, in the format YYYY-MM-DDThh:mm:ssZ |
String | Yes |
timelineSpec.endTime |
The end of the time range, in the same format. The value must be the same as startTime or later. |
String | Yes |
metrics[] |
The metrics to return. If you do not include metrics, the API returns all metrics of the metric set that support the aggregation period. |
Array of strings | No |
dimensions[] |
The dimensions for a breakdown. For slowStartRateMetricSet, dimensions must include startType. |
Array of strings | No |
filter |
The dimension values to include. Each key is a dimension name, and each value is an array of strings. For the rules, see Dimensions and filters. | Object | No |
pageSize |
The maximum number of rows in one page. The default is 1,000. A value of 0 uses the default. A value of more than 100,000 uses 100,000. |
Integer | No |
pageToken |
The nextPageToken value from the previous response. Do not include pageToken in the first request. |
String | No |
Query Metrics request example
The dates in the Query Metrics and Query Issues request examples are illustrative. For a real request, choose dates based on Time values and Check Freshness.
{
"timelineSpec": {
"aggregationPeriod": "DAILY",
"startTime": "2026-08-12T00:00:00Z",
"endTime": "2026-08-18T00:00:00Z"
},
"metrics": ["crashRate", "crashCount", "distinctDevices"],
"dimensions": ["versionCode"],
"filter": {
"countryCode": ["US", "CA"],
"deviceType": ["FIRE_TV"]
},
"pageSize": 1000
}
Query Metrics response fields
| Field | Type | Always Present | Notes |
|---|---|---|---|
freshness |
Object, or null |
Yes | The freshness for the aggregation period of the query. The value is null if no freshness is available. In that case, rows is empty. |
freshness.aggregationPeriod |
String | Yes, if freshness is not null |
DAILY or HOURLY |
freshness.latestDataTimestamp |
String | Yes, if freshness is not null |
The UTC start of the latest processed day (DAILY) or hour (HOURLY), in YYYY-MM-DDThh:mm:ssZ format |
resultCount |
Integer | Yes | The number of rows in this page |
rows[] |
Array of objects | Yes | The rows of this page. The array is empty if no data matches the query. |
nextPageToken |
String, or null |
Yes | See Pagination. |
availability |
Object | Only for readyToUseMetricSet |
The state of the ready-to-use data for the query. For more information, see Ready-to-use availability. |
Each row has the following fields:
| Field | Type | Always Present | Notes |
|---|---|---|---|
startTime |
String | Yes | The start of the time period, in the format YYYY-MM-DDThh:mm:ssZ |
aggregationPeriod |
String | Yes | DAILY or HOURLY |
dimensions[] |
Array of objects, or null |
Yes | One object with the fields name and value for each requested dimension, in the order of dimensions in the request. If the request has no dimensions, the field is null. |
metrics[] |
Array of objects | Yes | One metric object for each requested metric. For the shape, see Metric objects. |
Metric objects
A metric object has one of two shapes:
- Rate or count metric:
{"name": "crashRate", "value": 0.0142} - Duration metric:
{"name": "coldStartAppLaunchTime", "statistics": {"Average": 1234.5678, "P90": 1500.0000, "P95": 1800.0000}}. The four duration metrics of theappLatencyMetricSetand thereadyToUseMetricSetuse this shape. Thestatisticsobject is always present. Each statistic can benull.
Rates are fractions (0.0142 means 1.42%), counts are integers, and duration statistics are in milliseconds. A null metric value or statistic means that the API cannot calculate it. For metric meanings, see Metric values.
name field to find a metric in a row. Do not use the position of the metric in metrics.Additional metric rules
The following rules apply to specific metrics:
- A start is slow when app launch time is at least 5 seconds for a cold start or 2 seconds for a warm start.
- For an
HOURLY7-day rate, the API combines the same UTC hour from each of the 7 days.
Query Metrics response example
In this example, the app has data only for August 17 and August 18.
{
"freshness": {
"aggregationPeriod": "DAILY",
"latestDataTimestamp": "2026-08-18T00:00:00Z"
},
"resultCount": 2,
"rows": [
{
"startTime": "2026-08-17T00:00:00Z",
"aggregationPeriod": "DAILY",
"dimensions": [
{"name": "versionCode", "value": "151"}
],
"metrics": [
{"name": "crashRate", "value": 0.0142},
{"name": "crashCount", "value": 912},
{"name": "distinctDevices", "value": 54320}
]
},
{
"startTime": "2026-08-18T00:00:00Z",
"aggregationPeriod": "DAILY",
"dimensions": [
{"name": "versionCode", "value": "151"}
],
"metrics": [
{"name": "crashRate", "value": 0.0138},
{"name": "crashCount", "value": 887},
{"name": "distinctDevices", "value": 55012}
]
}
],
"nextPageToken": null
}
Duration metric example
The following example shows one row of an appLatencyMetricSet query without dimensions.
{
"startTime": "2026-08-18T00:00:00Z",
"aggregationPeriod": "DAILY",
"dimensions": null,
"metrics": [
{
"name": "coldStartAppLaunchTime",
"statistics": {"Average": 1234.5678, "P90": 1500.0000, "P95": 1800.0000}
},
{
"name": "warmStartAppLaunchTime",
"statistics": {"Average": 412.2500, "P90": 620.0000, "P95": 710.5000}
}
]
}
Ready-to-use availability
Each readyToUseMetricSet response has an availability object. The responses of the other metric sets do not have this object.
| Field | Type | Always Present | Notes |
|---|---|---|---|
availability.state |
String | Yes | AVAILABLE or ACTION_REQUIRED |
availability.reasonCode |
String | Only for ACTION_REQUIRED |
READY_TO_USE_DATA_UNAVAILABLE |
availability.message |
String | Only for ACTION_REQUIRED |
A message that tells you what to do |
availability.documentationUrl |
String | Only for ACTION_REQUIRED |
A link to the instructions for ready-to-use data |
The state is AVAILABLE when one or more of the requested ready-to-use metrics has data in the time range of the query. Otherwise, the state is ACTION_REQUIRED. The API checks all pages of the query, not only the current page. These are the possible causes of ACTION_REQUIRED:
- Your app does not report that it is ready to use. On Fire OS, your app must call
Activity.reportFullyDrawn(). On Vega OS, your app must call the callback fromuseReportFullyDrawn(). For the steps, see Available metric sets. - Amazon has no ready-to-use data for your app yet.
- No freshness value is available for the aggregation period of the query.
With ACTION_REQUIRED, the response can have rows. In these rows, the statistics are null.
The following example shows an ACTION_REQUIRED response without rows.
{
"freshness": {
"aggregationPeriod": "DAILY",
"latestDataTimestamp": "2026-08-18T00:00:00Z"
},
"resultCount": 0,
"rows": [],
"nextPageToken": null,
"availability": {
"state": "ACTION_REQUIRED",
"reasonCode": "READY_TO_USE_DATA_UNAVAILABLE",
"message": "Ready to Use data is not available for this app. This could mean the app has not been instrumented for Ready to Use, or data has not been generated yet. Please refer to the documentation below to verify your instrumentation setup, or reach out to your Amazon contact for assistance.",
"documentationUrl": "https://developer.amazon.com/apps-and-games/blogs/2023/06/improve-app-startup-times"
}
}
Query Issues
The Query Issues operation returns the top crash issues or the top ANR issues for the full time range. The response has one row for each issue. If you request dimensions, the response has one row for each combination of an issue and dimension values.
Query Issues request parameters
| Parameter | Description | Type | Required |
|---|---|---|---|
timelineSpec |
The aggregation period and the time range. For the rules, see Time values. | Object | Yes |
reportType |
CRASH or ANR |
String | Yes |
dimensions[] |
The dimensions for a breakdown. The dimensions are the same as for the crashMetricSet. |
Array of strings | No |
filter |
The dimension values to include. For the rules, see Dimensions and filters. | Object | No |
pageSize |
The maximum number of rows in one page. The default is 25. A value of 0 uses the default. A value of more than 1,000 uses 1,000. |
Integer | No |
pageToken |
The nextPageToken value from the previous response. Do not include pageToken in the first request. |
String | No |
A Query Issues request does not have a metrics field. The API returns errorEventCount and affectedDeviceCount for each issue.
Query Issues request example
{
"timelineSpec": {
"aggregationPeriod": "DAILY",
"startTime": "2026-08-05T00:00:00Z",
"endTime": "2026-08-18T00:00:00Z"
},
"reportType": "CRASH",
"filter": {"versionCode": ["152"]},
"pageSize": 25
}
Query Issues response fields
| Field | Type | Always Present | Notes |
|---|---|---|---|
rows[] |
Array of objects | Yes | The issues of this page. The array is empty if no issue matches the query. |
rows[].crashDescriptor |
String | Yes | The identifier of the issue |
rows[].reportText |
String, or null |
Yes | One example stack trace of the issue. The example can be different in each response. The API can make a long stack trace shorter. The value is null or an empty string if no stack trace is available. |
rows[].dimensions[] |
Array of objects, or null |
Yes | One object with the fields name and value for each requested dimension. If the request has no dimensions, the field is null. |
rows[].metrics[] |
Array of objects | Yes | Two metric objects, for errorEventCount and affectedDeviceCount. Each object has name and an integer value. For their meanings, see Available metric sets. |
nextPageToken |
String, or null |
Yes | The token for the next page. The value is null on the last page. |
The response has no resultCount or freshness. Issues are ordered by descending errorEventCount.
Query Issues response example
{
"rows": [
{
"crashDescriptor": "67f35eb2c1a94d0e",
"reportText": "java.lang.IllegalStateException: Player is not ready\n\tat com.example.myapp.Player.start(Player.java:88)",
"dimensions": null,
"metrics": [
{"name": "errorEventCount", "value": 78},
{"name": "affectedDeviceCount", "value": 45}
]
}
],
"nextPageToken": null
}
Implementation examples
The following examples show Python code for the Vitals API V2. Run the examples in order, in one Python session. Each example uses the functions and the variables of the examples before it.
Obtain an access token
To get an access token, send a POST request to https://api.amazon.com/auth/o2/token, with these form fields:
grant_type:client_credentialsclient_id: your client IDclient_secret: your client secretscope:adx_reporting::appstore:marketer
The response has access_token and expires_in. The value of expires_in is the number of seconds until the token expires.
The following example keeps the token. It gets a new token 5 minutes before the token expires.
import time
import requests
# Values that you must supply
client_id = "<your client ID>"
client_secret = "<your client secret>"
package_name = "<your app package name>"
BASE_URL = "https://developer.amazon.com/api/appstore"
VITALS_URL = f"{BASE_URL}/v2/applications/{package_name}/vitals"
_token = {"value": None, "expires_at": 0.0}
def auth_headers():
"""Return the request headers with a valid LWA access token."""
if time.time() > _token["expires_at"] - 300:
response = requests.post(
"https://api.amazon.com/auth/o2/token",
data={
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret,
"scope": "adx_reporting::appstore:marketer",
},
timeout=30,
)
response.raise_for_status()
token = response.json()
_token["value"] = token["access_token"]
_token["expires_at"] = time.time() + token["expires_in"]
return {
"Authorization": f"Bearer {_token['value']}",
"Content-Type": "application/json",
"Accept": "application/json",
}
Send a request with retries
The following helper retries 429 and temporary errors. On the first 401, it refreshes the token and retries if an attempt remains. For the retry rules, see Retry strategy.
import random
import time
import requests
# Uses auth_headers() and _token from "Obtain an access token".
MAX_ATTEMPTS = 4
RETRYABLE_STATUS_CODES = {429, 500, 503, 504}
def send(method, url, body=None):
"""Send a request. Retry a throttled request or a temporary error."""
token_renewed = False
for attempt in range(1, MAX_ATTEMPTS + 1):
headers = auth_headers()
try:
response = requests.request(
method, url, headers=headers, json=body, timeout=60
)
except (requests.ConnectionError, requests.Timeout):
if attempt == MAX_ATTEMPTS:
raise
time.sleep(2 ** attempt + random.uniform(0, 1))
continue
renew = response.status_code == 401 and not token_renewed
if renew and attempt < MAX_ATTEMPTS:
token_renewed = True
_token["expires_at"] = 0.0 # Get a new token for the next attempt.
continue
if response.status_code not in RETRYABLE_STATUS_CODES:
response.raise_for_status()
return response.json()
if attempt == MAX_ATTEMPTS:
response.raise_for_status()
retry_after = response.headers.get("Retry-After")
if retry_after is not None:
delay = float(retry_after)
else:
delay = 2 ** attempt + random.uniform(0, 1)
time.sleep(delay)
Choose a time range
The following helper builds a time range that ends at the metric set's freshness value. Choose a range within the limits in Time values.
from datetime import datetime, timedelta
TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
def time_range(metric_set_name, aggregation_period, periods):
"""Return (startTime, endTime) for the latest time periods of a metric set."""
info = send("GET", f"{VITALS_URL}/{metric_set_name}")
latest = {item["aggregationPeriod"]: item["latestDataTimestamp"] for item in info["freshnessInfo"]}
end = datetime.strptime(latest[aggregation_period], TIME_FORMAT)
step = timedelta(days=1) if aggregation_period == "DAILY" else timedelta(hours=1)
start = end - step * (periods - 1)
return start.strftime(TIME_FORMAT), end.strftime(TIME_FORMAT)
Query crash metrics
The following example gets the daily crash rate, the crash count, and the device count for each app version. The filter includes only Fire TV devices in the US and Canada.
start_time, end_time = time_range("crashMetricSet", "DAILY", 7)
crash_body = {
"timelineSpec": {"aggregationPeriod": "DAILY", "startTime": start_time, "endTime": end_time},
"metrics": ["crashRate", "crashCount", "distinctDevices"],
"dimensions": ["versionCode"],
"filter": {"countryCode": ["US", "CA"], "deviceType": ["FIRE_TV"]},
}
result = send("POST", f"{VITALS_URL}/crashMetricSet:query", crash_body)
for row in result["rows"]:
values = {metric["name"]: metric["value"] for metric in row["metrics"]}
print(row["startTime"], row["dimensions"], values)
Query app launch time
The duration metrics have statistics, not value. The following example gets the app launch times for cold starts and warm starts, for each device type.
start_time, end_time = time_range("appLatencyMetricSet", "DAILY", 7)
result = send("POST", f"{VITALS_URL}/appLatencyMetricSet:query", {
"timelineSpec": {"aggregationPeriod": "DAILY", "startTime": start_time, "endTime": end_time},
"metrics": ["coldStartAppLaunchTime", "warmStartAppLaunchTime"],
"dimensions": ["deviceType"],
})
Query slow start rate
The slowStartRateMetricSet requires startType in dimensions. The following example gets the hourly slow start rate for the latest 24 hours, for cold starts only.
start_time, end_time = time_range("slowStartRateMetricSet", "HOURLY", 24)
slow_start_body = {
"timelineSpec": {"aggregationPeriod": "HOURLY", "startTime": start_time, "endTime": end_time},
"metrics": ["slowStartRate", "slowStartRate7dDeviceWeighted", "distinctDevices"],
"dimensions": ["startType"],
"filter": {"startType": ["COLD"]},
}
result = send("POST", f"{VITALS_URL}/slowStartRateMetricSet:query", slow_start_body)
for row in result["rows"]:
values = {metric["name"]: metric["value"] for metric in row["metrics"]}
print(row["startTime"], values["slowStartRate"], values["distinctDevices"])
Query ready-to-use time
The following example gets the ready-to-use time and checks the availability object.
from datetime import timezone
try:
start_time, end_time = time_range("readyToUseMetricSet", "DAILY", 7)
except KeyError:
# No freshness yet; query recent UTC dates to get the availability state.
end = datetime.now(timezone.utc)
start_time = (end - timedelta(days=6)).strftime(TIME_FORMAT)
end_time = end.strftime(TIME_FORMAT)
ready_to_use_body = {
"timelineSpec": {"aggregationPeriod": "DAILY", "startTime": start_time, "endTime": end_time},
"metrics": ["coldStartReadyToUseTime", "warmStartReadyToUseTime"],
}
result = send("POST", f"{VITALS_URL}/readyToUseMetricSet:query", ready_to_use_body)
availability = result["availability"]
if availability["state"] == "ACTION_REQUIRED":
print(availability["message"])
print(availability["documentationUrl"])
for row in result["rows"]:
for metric in row["metrics"]:
print(row["startTime"], metric["name"], metric["statistics"]["Average"])
Paginate through results
The following function gets all pages of a query. It waits 0.4 seconds between two pages to stay under the rate limit. It returns the rows and the freshness object of the last page. For Query Issues, the second value is None, because the response has no freshness.
def query_all_rows(url, body):
"""Send a query. Return the rows of all pages and the freshness of the last page."""
rows = []
page_body = dict(body)
page_body.pop("pageToken", None)
while True:
result = send("POST", url, page_body)
rows.extend(result["rows"])
if result["nextPageToken"] is None:
return rows, result.get("freshness")
page_body["pageToken"] = result["nextPageToken"]
time.sleep(0.4)
Query top crash issues
The following example gets the first page of the top crash issues of version code 152 in the latest 14 days. The filter selects the version.
start_time, end_time = time_range("crashMetricSet", "DAILY", 14)
issues_body = {
"timelineSpec": {"aggregationPeriod": "DAILY", "startTime": start_time, "endTime": end_time},
"reportType": "CRASH",
"filter": {"versionCode": ["152"]},
"pageSize": 25,
}
result = send("POST", f"{VITALS_URL}/issuesMetricSet:query", issues_body)
for issue in result["rows"]:
counts = {metric["name"]: metric["value"] for metric in issue["metrics"]}
print(issue["crashDescriptor"], counts["errorEventCount"], counts["affectedDeviceCount"])
print(issue["reportText"])
Error handling
See the following sections for how to handle errors.
Error response format
The API sends a JSON body with each error response. The HTTP status code is in the status line of the response. The body does not contain the status code or the request ID.
| Field | Type | Always Present | Notes |
|---|---|---|---|
code |
String | Yes | The error code, for example VALIDATION_ERROR |
message |
String | Yes | A description of the error |
errors[] |
Array of objects | Only for 400 VALIDATION_ERROR |
One entry for each problem |
errors[].field |
String | Yes | The path of the field, for example timelineSpec.endTime, metrics[1], or filter.countryCode[0] |
errors[].code |
String | Yes | The field error code. For the values, see Field error codes. |
errors[].message |
String | Yes | A description of the problem |
Each success response and each error response has an x-amzn-RequestId header. Record this value. If you contact Amazon Developer Support about a request, include this value.
Single-error example
The following response is for an HOURLY query that requests crashRate28dUserWeighted.
{
"code": "VALIDATION_ERROR",
"message": "Metric 'crashRate28dUserWeighted' is not available for HOURLY aggregation",
"errors": [
{
"field": "metrics[0]",
"code": "VALUE_NOT_ALLOWED",
"message": "Metric 'crashRate28dUserWeighted' is not available for HOURLY aggregation"
}
]
}
Multi-field validation example
The following response is for a query with "startTime": "2026-08-18" and "pageSize": -5.
{
"code": "VALIDATION_ERROR",
"message": "The request contains 2 invalid fields.",
"errors": [
{
"field": "timelineSpec.startTime",
"code": "FIELD_INVALID",
"message": "timelineSpec.startTime must be a valid canonical UTC timestamp in YYYY-MM-DDThh:mm:ssZ format"
},
{
"field": "pageSize",
"code": "FIELD_OUT_OF_RANGE",
"message": "pageSize must be non-negative, got: -5"
}
]
}
Error codes
| HTTP Status | Code | Cause | Action |
|---|---|---|---|
| 400 | VALIDATION_ERROR |
The request has a problem, for example an unknown metric or an unknown metricSetName, or a Content-Type header that is not application/json. |
If the response has errors[], use it to find the problem. Correct the request, and then send the corrected request. |
| 401 | AUTHENTICATION_FAILED |
The access token is missing, expired, or not valid, or the security profile is not attached to the Reporting API. | Get a new access token. Make sure that you attached the security profile to the Reporting API. |
| 403 | ACCESS_DENIED |
Your developer account does not own the app, or the Vitals API V2 is not available to your developer account. | Make sure that your developer account owns the app. If your developer account owns the app, wait 1 minute and send the request again one time. If you get the error again, contact Amazon Developer Support. Include the x-amzn-RequestId value. |
| 404 | RESOURCE_NOT_FOUND |
No app has this package name, the path is not correct, or the HTTP method is not correct for the path. | Make sure that the package name and the path are correct. Use GET for Check Freshness. Use POST for Query Metrics and Query Issues, with :query at the end of the path. |
| 429 | RATE_LIMIT_EXCEEDED |
You sent more requests than the rate limit permits. | Wait for the number of seconds in the Retry-After header. Then send the request again. |
| 500 | INTERNAL_ERROR |
An unexpected error occurred in the service, or a service dependency failed during the query. | Send the request again with exponential backoff, up to 3 times. If the response has a Retry-After header, wait for that number of seconds. For the wait times, see Retry strategy. |
| 503 | SERVICE_UNAVAILABLE |
A service dependency is temporarily unavailable, or a gateway cannot reach the service. | Send the request again with exponential backoff, up to 3 times. If the response has a Retry-After header, wait for that number of seconds. For the wait times, see Retry strategy. |
| 504 | GATEWAY_TIMEOUT |
A gateway did not get a response from the service in time. | Send the request again with exponential backoff, up to 3 times. If the response has a Retry-After header, wait for that number of seconds. For the wait times, see Retry strategy. |
Field error codes
| Field Error Code | Meaning |
|---|---|
FIELD_REQUIRED |
The request does not have a required field, or an array has an empty element. |
FIELD_INVALID |
The value has the wrong format or the wrong type, or the field is not a supported field. |
FIELD_TOO_LONG |
A versionCode value is longer than 32 characters. |
FIELD_OUT_OF_RANGE |
The value is out of the permitted range. For example, the time range is too long, or pageSize is negative. |
FIELD_NOT_UNIQUE |
A value or a JSON key occurs more than one time. |
FIELD_IMMUTABLE |
The pageToken is not valid. Use the nextPageToken from the previous response, or omit pageToken to start a new query. |
VALUE_NOT_ALLOWED |
The value is not in the set of permitted values. For example, the metric or the dimension is unknown. A null value in filter, or a null element in metrics or dimensions, also causes this code. |
Common mistakes and fixes
| Mistake | Result | Fix |
|---|---|---|
| Using a V1 time object | 400 VALIDATION_ERROR |
Use a UTC timestamp string, such as 2026-08-18T00:00:00Z. |
Using a V1 device type, such as AMAZON_FIRE_TV |
400 VALIDATION_ERROR |
Use FIRE_TV, FIRE_TABLET, or OTHER. |
Requesting a 28d metric with HOURLY |
400 VALIDATION_ERROR |
Use DAILY. |
Omitting startType from a slowStartRateMetricSet query |
400 VALIDATION_ERROR |
Add startType to dimensions. |
Reading value from a duration metric |
No value field |
Read statistics.Average, statistics.P90, or statistics.P95. |
Rate limits, retries, and best practices
As a guideline, the API permits about 3 requests per second for each developer account, with short bursts of up to 10 requests. All Vitals API V2 operations share this limit, for all your apps. The limit can change, and the API can send a 429 response before you reach it. V1 of the Vitals API has a separate limit.
When you exceed the limit, the API sends a 429 RATE_LIMIT_EXCEEDED response with a Retry-After header. Wait for the number of seconds in the header before you send the request again.
Retry strategy
The send function sends a request again after 429, 500, 503, 504, a connection error, or a timeout, up to 3 times. If the response has a Retry-After header, the function waits for that number of seconds. Otherwise, it waits about 2, 4, and 8 seconds, plus up to 1 second of random jitter. After the first 401, the function gets a new access token and sends the request again if an attempt remains. The function does not retry other 4xx errors or failed token requests.
Production best practices
| Practice | Recommendation |
|---|---|
| Re-query recent periods | Re-query recent periods and replace stored values; late data can change them. |
| Use error codes | Use code, errors[].code, and errors[].field instead of parsing error messages. |
| Ignore unknown values | Ignore response fields, metrics, and dimension values that you do not know; Amazon can add them during the beta. |
Migrate from V1
Version 1 (V1) of the Vitals API continues to operate. The following table shows the changes that you must make to move to version 2 (V2).
| Item | V1 | V2 |
|---|---|---|
| Endpoint path | /vitals/apps/{packageName}/{metricSetName} |
/v2/applications/{packageName}/vitals/{metricSetName} |
| Time values in requests | An object, for example {"year": 2026, "month": 8, "day": 18, "hours": 14} |
A string, for example "2026-08-18T14:00:00Z" |
| Time range of Query Issues | startTime and endTime at the top level of the body |
timelineSpec, with aggregationPeriod, startTime, and endTime |
startTime in rows |
An object | A string in the format YYYY-MM-DDThh:mm:ssZ |
| Freshness time values | For example 2026-08-18 and 2026-08-18T14 |
For example 2026-08-18T00:00:00Z and 2026-08-18T14:00:00Z |
| Metric values | Strings, for example "0.0142" |
Numbers, for example 0.0142 |
| Duration metrics (app launch time and ready-to-use time) | One value |
statistics, with Average, P90, and P95 |
status and requestId in the Query Metrics response |
Present | Removed. The request ID is in the x-amzn-RequestId header. |
deviceType values |
AMAZON_FIRE_TV or AMAZON_TABLETS or AMAZON_FIRE_PHONE or AUCC or CORO |
FIRE_TV or FIRE_TABLET or OTHER |
| Operating system dimension | deviceOS |
deviceOs |
| App that your developer account does not own | 404 NOT_FOUND |
403 ACCESS_DENIED |
| Error body | code, message, status |
code, message, and errors[] for validation errors |
Related topics
- Vitals API V2 Overview: Available metrics and dimensions
- App Health Insights Dashboard: Visual dashboard for the same metrics
- Reporting API Explorer: Interactive API explorer on the developer portal
- Explore the API: Link to the Reporting API Explorer in the Developer Console
Last updated: Oct 13, 2026

