Concepts
The handful of rules every endpoint follows. Learn them once and the whole API reads the same way.
App identifiers: {app}
Every app-scoped endpoint takes one path segment that accepts anything that identifies an app. URL-encode it.
- App Store id:
284882215orid284882215 - Google Play package:
com.spotify.music - Apple bundle id:
com.spotify.client - A store URL (apps.apple.com, itunes.apple.com, play.google.com)
- A prefixed id:
apple:284882215,play:com.spotify.music - A name:
spotifyresolves to the best search match and says so inmeta.warnings
Add store=apple or store=play when an identifier could belong to either store. data.linked is the same app in the other store when we know it (same bundle id, or same developer and name).
Freshness
There is no separate fetch endpoint. App-scoped reads refresh data from the store when ours is older than max_age, and tell you how old it is in meta.freshness.
max_age: seconds. Defaults to 7 days for app data and 1 day for reviews;0always refreshes. A refresh costs 1 credit per country fetched (at most 10).wait:true(default) blocks up to about 25 seconds for the refresh. On timeout, or withwait=false, you get the cached data withrefreshing: trueand ajob_id. PollGET /v1/jobs/{id}(free) until it is done, then repeat the call.country:usby default. Paid plans can pass a comma list (us,de,jp); Studio can passall.
An app we have never seen is fetched on the spot. If that takes longer than the wait, the answer is HTTP 202 with data: null and a job_id.
Response envelope
Every success is { data, meta }. data is an object or a list; meta carries what the call cost and how fresh the answer is.
{
"data": { "store": "apple", "app_id": "324684580", "title": "Spotify: Music and Podcasts", … },
"meta": {
"credits_used": 1,
"credits_remaining": 9981,
"request_id": "req_123",
"store": "apple",
"freshness": {
"fetched_at": "2026-10-22T15:03:07Z",
"age_seconds": 3600,
"stale": false,
"refreshing": false,
"job_id": null
},
"next_cursor": null,
"warnings": []
}
}credits_used: what this call cost.credits_remaining: monthly plus pack credits left.freshness: when the oldest requested country was fetched from the store, whether it is stale, and a job to poll if a refresh is still running.next_cursor: the next page, ornull.warnings: things worth telling the user (for example that an app was resolved by name).request_id: quote it when you contact support.
Credits
Plans come with monthly credits; every call reports its cost. Failed requests are free. Tracked keywords are a plan allowance, not credits. The cost of each endpoint, from the OpenAPI document:
| Plan | Price | Credits / month | Tracked keywords | Watched apps | Requests / second | Credit packs |
|---|---|---|---|---|---|---|
| Free | Free | 250 | 25 | 3 | 1 | No |
| Indie | $27/mo | 10,000 | 2,500 | 25 | 10 | Yes |
| Studio | $147/mo | 75,000 | 10,000 | 250 | 25 | Yes |
Credit packs (2,000 for $17, 7,000 for $47, 17,000 for $97) are available on Indie and Studio, never expire and are used after the monthly credits. See pricing.
Pagination and fields
- Lists page with
limitandcursor: passmeta.next_cursorback unchanged until it isnull. Each page of reviews is charged separately. include=adds optional sections (for an app:markets,iap,versions,privacy,estimates,charts).fields=keeps only the listed top-level fields (identifiers are always kept), which keeps agent context small.
Errors
Errors use the HTTP status and one body shape, whoever answered. Read hint: it says what to do next.
{
"error": {
"code": "insufficient_credits",
"message": "Out of credits: monthly plan credits and credit packs are used up.",
"hint": "Upgrade your plan, or on Indie / Studio buy a credit pack …",
"docs": "https://appgoat.ai/docs/errors#insufficient_credits"
}
}15 codes, from invalid_param to upstream_unavailable, are listed with causes and fixes on Errors.
Rate limits
Requests per second are per organization: Free 1, Indie 10, Studio 25. Over the limit you get 429 rate_limited with a Retry-After header.