Skip to main content
Limits exist to protect two different things. Request rate protects the API and the database. Store capacity protects the proxy pool and the daily run, because an API caller that triggers a refresh spends exactly what a dashboard user spends by clicking refresh.

Limits are per workspace

Every limit counts against the workspace, never against the token or the client address. Minting a second token buys no extra capacity, and several people behind one office address do not share a bucket. Counters live in Redis, so every API instance sees the same window. Two things are counted per client address instead, because no workspace is known yet when they happen: the authentication endpoints, and rejected credentials. A caller that presents a session cookie or an asob_ token the API refuses may do so 120 times a minute from one address before the next refusal is answered with 429 instead of 401. A credential the API accepts is never counted and never refused because of its address, so a working client never meets this limit, even when it shares an address with a caller that is being refused. Only a credential the API has to look up is counted. A session cookie that does not verify, because it was signed with another secret, has expired or carries no session id, and an Authorization header that carries no asob_ token are answered 401 and never counted, because neither costs a lookup or can be guessed, so a browser signed out by an AUTH_SECRET rotation always reaches the sign in page. A signed cookie whose session has ended, by signing out, by a password change or by a recovery, is counted like any other refused credential. A request that arrives with no client address is not counted. Refusals at the admin surfaces spend the same budget. An IPv4 client address is counted on its own, and an IPv4 address written in IPv4-mapped IPv6 form counts as that IPv4 address. An IPv6 client address is counted by the /64 network it belongs to, because one connection is usually assigned a whole /64 and could otherwise reset the count by moving to the next address in it. Two devices on one IPv6 network therefore share an allowance.

Self hosted instances are unlimited

With BILLING_ENABLED set to false there are no API rate limits at all beyond the existing authentication throttle, which answers a refusal with the same error envelope, retryAfterSeconds and Retry-After as the plan limits. See How are sign in attempts throttled? The only cost of a read is your own machine, and the only real constraint is store collection, bounded by SCRAPE_ITUNES_RPM and SCRAPE_GPLAY_RPM. See The daily pipeline and rate limits. AI calls are the one exception an operator can opt in to, with AI_CALLS_PER_MONTH. Everything below applies to a metered instance.

Request size limits apply everywhere

These ceilings are resource safety rather than plan capacity, so they hold on a self hosted instance too. A keyword is also capped at five words. Each tracked keyword market costs one real store search every day, so a pasted spreadsheet column or a scripting mistake would otherwise multiply a workspace’s daily store volume without anyone choosing to. Exceeding any of these returns 400 and writes nothing, so a refused batch never lands half applied. Split a larger list across several requests. A keyword import is different from a bulk add: a problem with one row, such as a phrase over 100 characters or an unknown storefront, comes back as an invalid row in the 200 answer and the other rows are imported. Only a problem with the request itself, such as more than 500 rows, answers 400 and writes nothing. The keyword field text may run to 1,000 characters, so spacing and duplicates in a pasted field never cost a refusal. The 100 character limit applies to the field once it is normalized, counted in Unicode code points, so a letter such as ą or a Chinese, Japanese or Korean character counts once, whether or not it lies outside the Basic Multilingual Plane. See App Store and Google Play for how a save is measured.

Three request classes

Keyword suggestions reach the store only for the search, similar, developer and competitorSearch strategies, and each of them spends one lookup only when it has something to ask the store. Search needs an active tracked keyword to expand, developer needs the developer id in the newest listing and competitorSearch needs a phrase of two or more words in a competitor listing it reads, the one in the market or the home listing of a competitor with none there; without one they answer an empty list and spend nothing, even when the hour’s allowance is spent. The listing, competitor, seasonal and review strategies answer from stored data and spend no lookup. Reading a year of stored rankings is cheap. Triggering a refresh is not, which is why store endpoints carry a separate on demand allowance on top of the request rate. Three classes do not mean three budgets. There are two per minute budgets, one for reads and one shared by writes and store requests, and a single daily allowance every class spends from. The class names how a request is priced, never how much room it buys. A class is also not a permission. A read only token may call any endpoint that does not change data, including the store touching lookups, and it spends store capacity when it does.

What each plan allows

A chart category is one store, storefront, category and device, and its free, paid and grossing charts count once. See Read top charts. A trial runs at Indie limits. A workspace whose plan has lapsed keeps the same read allowance, because its data stays readable and exportable while its capacity does not.

The hosted MCP endpoint has its own budget

POST /mcp draws from a per minute budget of its own, the MCP requests per minute in the table above. Every JSON-RPC message in the body costs one unit. An ordinary request carries one message and costs one, and a batch of ten messages costs ten, exactly what the same ten requests would cost sent one at a time. A request the remaining budget cannot cover is refused whole with the 429 described below, and none of its messages runs. A refused request still counts against the window, so waiting for Retry-After is the fix. A request that carries more messages than the whole per minute budget can never be served: it is refused without being counted, and the fix is to split it into smaller requests. A batch of more than 100 messages, or a body nested more than 64 levels deep, is refused with 400 before the budget is read, whatever the plan, so it costs nothing either. POST /mcp reports this budget only on a 429, so its successful responses carry no RateLimit headers for it. The tool calls inside a request also spend the read or write budget of the route they use, so this budget comes on top of the limits above rather than replacing them.

Read the headers

Every response carries the tightest limit the request touched. A client that reads RateLimit-Remaining and honours Retry-After never needs to guess.

A 429 names the window that closed

window is minute, hour, day or concurrent. Waiting for resetSeconds is the whole fix for every window except concurrent, where you reduce how many requests you have open at once.

A spent on demand allowance names its action

Refresh, run daily, score, keyword suggestions, deep search, importing an app, adding a competitor, store lookups and capturing a top chart each carry an allowance of their own, counted per day, or per hour for keyword suggestions and store lookups. A store lookup is a name search or a similar apps lookup. Competitor suggestions never answer 429 for a spent store lookup allowance: they still answer the keyword result apps, with similar set to unavailable. When one is spent the 429 carries the same rateLimit object with rateClass set to store, and action names which allowance closed: refresh, runDaily, score, suggestions, spider, importApp, addCompetitor, storeLookup or chartCapture. Finding localizations (POST /apps/{id}/localizations/detect) is not an allowance of its own. It spends one refresh, and only when it queues at least one store lookup, so a request that finds every language already listed or already asked today costs nothing. When the refresh allowance is spent it answers 429 with action set to refresh.
The allowance resets at 00:00 UTC for a daily window and at the top of the hour for an hourly one, so Retry-After is the exact wait. A deep search spends one allowance for the 27 lookups it queues, and only when it queues something. Starting a term whose lookups are all stored for today, or still waiting in the queue from an earlier start, costs nothing. Importing an app the workspace already tracks, adding a competitor it already has, capturing a top chart already captured today or being captured by another request at that moment, and a request the plan has no room for cost nothing either, because they make no store request.

A rate limit is not a quota

Four refusals look similar and mean different things. Conflating them makes all four unfixable. Waiting does not help a 403, and upgrading a plan does not clear a 429 that is already in flight.

The monthly AI allowance

The four endpoints that call OpenAI (POST /actions/{id}/explain, POST /apps/{id}/metadata/assistant, POST /apps/{id}/audit/ai/runs and the deprecated POST /apps/{id}/audit/ai) also spend a monthly allowance of AI calls. When it is spent they answer 429 with an aiAllowance object and a Retry-After that points at 00:00 UTC on the 1st; see A spent AI allowance. GET /auth/plan reports the calls used in usage.aiCalls. What counts as a call is described in AI features.

When Redis is unreachable

Counters live in Redis, and a counter that cannot be read must never make the rest of the API wait for it. A command to Redis is given half a second. After that the request is decided by what the counter was protecting. Everything that only reads PostgreSQL keeps working, so a signed in browser session keeps its dashboards and exports through a Redis restart. A client that gets the 503 should wait the seconds in Retry-After. The first attempt after Redis returns succeeds within a couple of seconds, with no restart of the API. The API log names the outage in one JSON warning every thirty seconds, for the request path and the queues together, and prints no stack trace for each reconnect attempt. Queued work waits and resumes on its own once Redis is back. The same holds for an API that starts while Redis is already down: it serves requests at once, registers its scheduled jobs when Redis returns, and stops promptly on a shutdown signal either way, within a few seconds, cutting any request still waiting on Redis (see What a restart does to a job in flight). A self hosted instance has no plan limits, so the first row and the row about allowances do not apply to it. The other three rows apply to every instance.

Suspension

An operator can suspend a workspace after sustained abuse. A suspended workspace keeps reading and exporting its data from the dashboard and keeps full access to billing, so it can always pay and always leave with its data. Its API tokens are refused with 403 on every route except GET /account/export, so a script can still take the data out. Every write is refused with 403 too, and the daily run skips the workspace. The web app tells every member of the workspace on every page, with the reason the operator recorded, and GET /auth/me reports suspendedAt and suspendedReason for any client that wants to say the same.

Error responses

The envelope every failure shares.

Personal API tokens

Scope, expiry and revocation.
Last modified on October 29, 2026