Rate limits and CORS#
Rate limiting#
Three layers apply: application limits, per-caller limits under them and special endpoint limits on selected sensitive endpoints.
Limits per application#
Every application key has its own limits: a short window per second and a longer one per minute. The defaults are 20 per second and 600 per minute; the operator can set different values for a particular client.
The application limit is shared by all its users
When the application exceeds its limit, all its users are refused until the window ends. One overloaded user (or a faulty loop) can therefore stop the whole application. That is why per-caller limits exist.
Per-caller limits#
Under the application ceiling there is a limit per caller, so that one caller cannot use up the whole capacity:
| Caller | Counted by | Default limit |
|---|---|---|
| signed-in user | the account from the token | 120 per minute |
| anonymous (no token) | IP address | 120 per minute |
Public actions that do not read the token (sign-in, token refresh, forgotten password) are also counted by address — even if you send a token. The website administrator can change both limits per application.
An invalid token never reaches the per-caller limit
An invalid or expired token ends with a 401 before the per-caller limit is counted. The application limit is counted all the same.
Windows and exceeding a limit#
The windows are fixed: a new second, or a new minute, starts a new window and the counter
resets. An exceeded limit (application or caller) returns HTTP 429, a Retry-After
header with the number of seconds until the window ends and this body:
code 404 is the result code RATE_LIMIT_EXCEEDED, not an HTTP status. data.retryAfter
carries the same number as the header. The client should wait Retry-After seconds and only
then retry.
Special endpoint limits#
These apply on top of the limits above — sign-in, token refresh, password reset, phone reveal, content creation and others. The limit is not global; these apply only to selected sensitive endpoints.
| Endpoint | Key | Limit | When exceeded |
|---|---|---|---|
auth/login |
username and IP, failed attempts only | 5 per 5 minutes | 429 with Retry-After, code 404 |
auth/login |
account from all addresses, failed attempts only | 20 per hour | 429 with Retry-After, code 404 |
auth/refresh |
IP, every attempt | 30 per 5 minutes | 429 with Retry-After, code 404 |
user/update-password |
account, wrong current password only | 5 per 5 minutes | HTTP 200, code 404 |
user/show-phone |
IP | 20 per hour | HTTP 200, empty phone |
user/send-new-password |
IP / user | 10 per hour / 3 per hour | HTTP 200, the same response as on success |
user/reset-password |
IP | 10 per hour | HTTP 200, code 112 |
user/send-activation-email |
IP / user | 10 per hour / 3 per hour | HTTP 200, the same response as on success |
The sign-in and token refresh limits return HTTP 429, a Retry-After header and a body with
the same keys; only the message also carries the number of seconds:
Decide by code and data.retryAfter, not by the message text.
Sign-in from an unknown device
The limit of 20 per hour per account and the limits on pairs (name or account,
address) apply in full only to an unknown device: above the limit the password is
not checked at all and the client gets the same 429 as before. A browser that has
signed in to the account before is recognised by the website's signed cookie, and an
address with a successful sign-in in the last 30 days gets past the account limit.
An external auth/login call sends no cookie, so it is always an unknown device;
only an address from which the account recently signed in successfully can help it. An
integration should therefore not wait for a lift from another address when the account
is blocked: it should follow the Retry-After header.
A throttled response must not reveal more than a passing one
That is why "forgotten password" returns the same success with or without the limit, and the password reset the same code as an invalid key. Only sign-in and token refresh report the limit with status 429.
Content creation quota#
Creating selected content has an hourly cap. It applies to the website and to an external
save with a token alike.
| What | Key | Limit |
|---|---|---|
| a response to an advert or a company profile, a discussion post or report, a comment, an advert report, a contact message — signed-in user | user / IP, separately for each kind of content | 30 per hour / 60 per hour |
| a contact message — anonymous visitor | e-mail from the form / IP | 5 per hour / 20 per hour |
| a newsletter subscription activation e-mail | IP / e-mail | 10 per hour / 3 per hour |
Only inserts count, updates do not. Above the limit save returns HTTP 200,
success: false and code 404 with the message Too many new records, try again later. —
without a Retry-After header. An administrator is exempt from the quota.
Ordinary reads and writes have only the application and per-caller limits
They have no special limit. That does not mean anything goes — large includes and
frequent queries load the server just the same.
Failed sign-ins count, successful ones do not
A correctly authenticated client does not hit the limit.
CORS#
This concerns browser calls only. A server-side request is not restricted by CORS.
| Setting | Value |
|---|---|
| Allowed origins | a list; by default only local development addresses |
| Methods | GET, POST, PUT, PATCH, DELETE, OPTIONS |
| Headers | Authorization, X-API-Key, Content-Type, X-Requested-With, Accept |
| Exposed headers | none |
| Credentials | allowed |
| Preflight lifetime | 1 hour |
X-API-Key is an allowed header
The browser preflight permits it. A key embedded in a browser script is not secret, though — see Authentication.
Your domain must be on the list
If it is not, the server responds and the browser discards the response. It looks like an API outage. Ask the operator to add your domain.
You cannot read Retry-After from a browser
The header is not exposed. When rate limited in a browser you cannot tell how
long to wait — apply your own backoff. The response body does carry data.retryAfter.
Preflight#
An OPTIONS request is handled before authentication and returns 204. For a
disallowed origin it returns 204 without the allow header.
Client recommendations#
On receiving a 429, wait Retry-After seconds
Retrying immediately only extends the limit. If you cannot read the header (a browser), back off progressively.
Refresh the token ahead of time, not on a 401
An access token lasts 15 minutes. Refreshing a minute early saves pointless failed requests.
Do not use includes=*
The fastest way to slow your own integration down.