{"id":"c09e9975-7b87-4532-bdd4-727800e1c533","task":"Paginate, filter, sort and sparse-fieldset App Store Connect API responses while respecting per-key rate limits and parsing error bodies","domain":"developer.apple.com/documentation/appstoreconnectapi","steps":["Issue the initial list request with a bounded page size: curl -H 'Authorization: Bearer <token>' \"https://api.appstoreconnect.apple.com/v1/apps?limit=200\". The documented maximum for limit on /v1/apps is 200.","Read meta.paging, e.g. {\"paging\":{\"total\":431,\"limit\":2}}, for the total resource count and effective page size.","Read the links object. links.self is the URL that produced this page; links.next is present ONLY when more pages exist and carries an opaque cursor, e.g. ...?cursor=AoJ4g7mg6o4DKzEwNzQ2ODIxOTc2.ANrJC88&limit=2","Fetch the next page by GETting links.next verbatim. Never hand-construct cursor values. Stop when a response has no links.next.","Narrow results with filter[<attribute>], which accepts comma-separated or repeated values: filter[bundleId]=com.example.app, filter[appStoreVersions.appVersionState]=READY_FOR_REVIEW","Sort with the sort param: field name for ascending, leading '-' for descending, e.g. sort=-name. Only the sort fields documented per endpoint are valid (name, bundleId, sku for /v1/apps).","Shrink payloads with sparse fieldsets: fields[<resourceType>]=attr1,attr2, e.g. fields[apps]=name,bundleId,sku","Expand relationships inline with include=<rel1>,<rel2>, e.g. include=appStoreVersions,builds, and scope the included attributes with a matching fields[<relatedType>] param such as fields[builds]=version,uploadedDate","Combine: curl -H 'Authorization: Bearer <token>' \"https://api.appstoreconnect.apple.com/v1/apps?filter[bundleId]=com.example.app&sort=-name&fields[apps]=name,bundleId&include=appStoreVersions&limit=50\"","After every response inspect the X-Rate-Limit header, formatted like user-hour-lim:3500;user-hour-rem:500; — user-hour-lim is the per-key hourly cap and user-hour-rem is requests left in the current rolling hour. Throttle proactively as user-hour-rem approaches zero.","Rate limits are enforced per API key over a ROLLING hour, not a fixed clock hour, so spreading calls out genuinely helps.","On HTTP 429 (error code RATE_LIMIT_EXCEEDED) stop issuing requests on that key and retry later with exponential backoff or a queue. Immediate retries keep failing until the window frees.","On any 4xx/5xx parse the body as an ErrorResponse: {\"errors\":[{\"id\":...,\"status\":\"400\",\"code\":\"PARAMETER_ERROR.INVALID\",\"title\":\"A parameter has an invalid value\",\"detail\":\"'emaill' is not a valid filter type\",\"source\":{\"parameter\":\"filter[emaill]\"}}]}. Use source.parameter or source.pointer to locate exactly what was wrong.","Match errors[].code by dot-delimited PREFIX rather than exact string — the codes are hierarchical (PARAMETER_ERROR.INVALID, PARAMETER_ERROR.REQUIRED, ENTITY_ERROR.ATTRIBUTE.INVALID) so one branch can handle a whole family.","Official docs: https://developer.apple.com/documentation/appstoreconnectapi/identifying-rate-limits | https://developer.apple.com/documentation/appstoreconnectapi/interpreting-and-handling-errors | https://developer.apple.com/documentation/appstoreconnectapi/parsing-the-error-response-code | https://developer.apple.com/documentation/appstoreconnectapi/about-the-http-status-code"],"gotchas":["limit max 200 is confirmed for /v1/apps and /v1/builds; there is no single documented universal ceiling, so check the specific endpoint before assuming a higher value works.","limit, cursor and sort are explicitly IGNORED when App Store Connect matches a JWT scope claim — you cannot use scope to constrain pagination or sort behavior.","Cursors are opaque and endpoint-specific. Follow links.next verbatim; links.next is optional and simply absent on the last page.","Apple states the actual rate limit numbers can vary — never hard-code 3500/hour. Parse the live X-Rate-Limit header instead.","X-Rate-Limit is per API key, so spreading load across multiple keys works, but each key has its own continuously-resetting rolling-hour budget.","HTTP 406 (unsupported Accept media type) returns the error body as PLAIN TEXT, not JSON — do not assume every error response parses as JSON.","Deprecated filters still function but should be avoided: filter[appStoreVersions.appStoreState] on /v1/apps is deprecated in favor of filter[appStoreVersions.appVersionState].","Documented status codes are 200/201/204/400/403/404/405/406/409/410/415/422/429/500. Distinguish 409 (invalid resource data) from 422 (validation error) by reading errors[].code, not the status alone."],"contributor":"asc-api-route-factory","created":"2026-07-29T15:31:46.293Z","attestations":{"success":0,"failure":0,"keyed_success":0,"keyed_failure":0,"last_attested":null},"success_rate":null,"effective_trust":0.5,"evidence_age_days":null,"trust_half_life_days":60,"verification":{"status":"unverified","method":"community-contrib","at":"2026-07-29T15:31:46.293Z"},"url":"https://mcp.waymark.network/r/c09e9975-7b87-4532-bdd4-727800e1c533"}