Errors & request_id
Every API error uses one consistent JSON shape, so you can handle them all the same way:
{
"error": { "code": "NOT_FOUND", "message": "App not found" },
"request_id": "req_..."
}error.code— a stable, uppercase machine code. Branch on this, not on the HTTP status or the message text.error.message— a short human-readable explanation. Safe to log; don't parse.error.details— optional, present only when there is something machine-readable to add:retry_after(seconds) on a429,required(the call's cost in credits) on a402. New keys may appear; ignore the ones you don't use.request_id— the id of this request. Include it whenever you report a problem.
Request ids
Every response — success or error — carries an X-Request-Id header with our id for that request. Log it. You can also send your own X-Request-Id (up to 80 letters, digits, ., _, : or -): we record it next to ours, so a request can be traced by either. It has no effect on metering — every call is charged on its own.
Status codes
| Status | code | Meaning |
|---|---|---|
| 400 | BAD_REQUEST | Missing or invalid parameters. |
| 401 | UNAUTHORIZED | Missing, malformed, or revoked API key. |
| 402 | INSUFFICIENT_CREDITS | Your balance can't cover the call's cost. |
| 403 | FORBIDDEN | The key isn't allowed to do this. |
| 403 | ACCOUNT_SUSPENDED | The account is suspended, so API and MCP access are paused. Contact support. |
| 404 | NOT_FOUND | No such resource, or no data for these parameters. |
| 410 | PAGE_EXPIRED | A reviews page was requested out of order, more than about an hour after the previous page, or with a different sort_type. Not charged; read again from page 1. The App Store has only page 1. |
| 422 | INVALID_PARAMETERS | A parameter is outside what the endpoint accepts — its type, allowed values or limits in the API reference — or the data engine rejected it. The message names the parameter. |
| 429 | RATE_LIMIT_EXCEEDED | Too many requests — see rate limits. |
| 500 | INTERNAL_ERROR | Something broke on our side. Retry, then report with the request_id. |
| 502 | UPSTREAM_ERROR | The data engine returned an error. |
| 503 | UPSTREAM_UNAVAILABLE | The data engine is temporarily unavailable. Retry with backoff. |
| 503 | MAINTENANCE | Scheduled maintenance. Nothing is charged; Retry-After and details.ends_at say when it ends. |
| 504 | UPSTREAM_TIMEOUT | The data engine took too long. Retry with backoff. |
400, 404, 410 and 422 are about your request and will fail identically on every retry — fix the parameters instead. 502, 503 and 504 are about us; those are the ones worth retrying with backoff. A 503 also answers a live check (cache=false / force_update=true) the store could not complete — retry it, or drop the flag to get the stored answer. A 503 with MAINTENANCE is planned downtime: wait out Retry-After instead of backing off. 401, 402 and 403 will not change on retry either: they need a key, credits or support.
Repeatable parameters
Parameters documented as repeatable (packages, keywords) are lists even when you send one value — ?packages=com.foo and ?packages=com.foo&packages=com.bar are both valid, and a single value is not a special case.
Insufficient credits
When your balance can't cover a call, the request is rejected before any work is done — you're never charged for a 402:
{
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Out of credits. Buy a credit pack at https://appmanai.com/app/billing#packs or wait for your monthly refill of 5,000 on 2026-10-19.",
"details": { "required": 3 }
},
"request_id": "req_..."
}The message names the way out, links to the page that resolves it, and says when your credits next refresh. All three differ by plan — Free is pointed at the Base upgrade, Base at the credit packs, because a subscriber cannot upgrade any further. There is no overdraft: calls stop rather than running up a bill. An agent seeing a 402 should stop and surface it rather than retrying; retries won't succeed until credits are added.