Rate limits
The API has a per-minute request budget. It belongs to the company, not to the key.
What each plan gets
Section titled “What each plan gets”| Plan | API | Requests per minute | Active keys |
|---|---|---|---|
| Free | No | 0 | 0 |
| Start | Yes | 60 | 3 |
| Pleno | Yes | 180 | 10 |
| Supra | Yes | 600 | 25 |
This table is generated from the code that actually decides: the same function the API calls on every request to know whether the call goes through.
The ceiling in force for your company right now comes in the GET /public/v1/me response, in limits.requests_per_minute. Read it from there instead of hard coding the number: the company can change plan, and the panel also allows adjusting the value of one company in particular.
A plan change takes up to a minute to take effect on the API, because the resolved plan is kept in memory for that long.
The headers
Section titled “The headers”Every response that goes through the counter carries three headers:
| Header | What it is | How to use it |
|---|---|---|
RateLimit-Limit | The per-minute ceiling of the company | Compare it with what you plan to fire |
RateLimit-Remaining | How many requests still fit in the window | Below a margin of your own, slow down before taking a 429 |
RateLimit-Reset | In how many seconds the next slot opens | Use it as a minimum interval once RateLimit-Remaining reaches zero |
On a 429 a fourth one shows up:
| Header | What it is |
|---|---|
Retry-After | How many seconds to wait before trying again |
The window slides: it looks at the last 60 seconds from now, not at the clock minute. That is why RateLimit-Reset drops gradually instead of resetting all at once.
A company with no application ceiling does not go through the counter, and then these headers do not show up. In that case limits.requests_per_minute comes back null in GET /public/v1/me.
What to do on a 429
Section titled “What to do on a 429”{ "error": { "type": "rate_limit_error", "code": "rate_limit_exceeded", "message": "Per-minute request limit reached. Try again shortly.", "request_id": "3d81b6ac-2f45-4a0e-9c7b-1e5f8a2d4c60", "doc_url": "https://docs.fatureihoje.com/en/errors#rate_limit_exceeded" }}- Wait what
Retry-Aftersays. It comes in seconds and it is computed on the real window, it is not a guess. - Back off progressively. If a second 429 comes right after, double the wait on every attempt, up to a ceiling of your own.
- Spread it out a little. Add a few random milliseconds to the wait. Without that, several queues that took a 429 at the same instant come back at the same instant.
- Never retry in a tight loop. Retrying right away does not help, and it delays the good calls of the same company, which share the same counter.
A call refused by permission is different: the 403 happens after the counter and does spend budget, on purpose. Probing a route the key cannot use is not free.
Reading the headers
Section titled “Reading the headers”curl -sS -D - -o /dev/null https://api.fatureihoje.com/public/v1/me \ -H "Authorization: Bearer $FH_API_KEY"-D - prints the headers and -o /dev/null throws the body away.
const res = await fetch('https://api.fatureihoje.com/public/v1/me', { headers: { Authorization: `Bearer ${process.env.FH_API_KEY}` },});
console.log('limit', res.headers.get('RateLimit-Limit'));console.log('remaining', res.headers.get('RateLimit-Remaining'));console.log('resets in', res.headers.get('RateLimit-Reset'), 'seconds');
if (res.status === 429) { const wait = Number(res.headers.get('Retry-After') ?? 1); console.log('wait', wait, 'seconds');}Run it with node limits.mjs.
<?php
$headers = [];$ch = curl_init('https://api.fatureihoje.com/public/v1/me');curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('FH_API_KEY')], CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$headers) { $parts = explode(':', $line, 2); if (count($parts) === 2) { $headers[strtolower(trim($parts[0]))] = trim($parts[1]); } return strlen($line); },]);
// curl_exec returns false when the connection never happened, and throws nothing.if (curl_exec($ch) === false) { fwrite(STDERR, 'network failure: ' . curl_error($ch) . PHP_EOL); exit(1);}
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
echo 'limit ', $headers['ratelimit-limit'] ?? '', PHP_EOL;echo 'remaining ', $headers['ratelimit-remaining'] ?? '', PHP_EOL;echo 'resets in ', $headers['ratelimit-reset'] ?? '', ' seconds', PHP_EOL;
if ($status === 429) { echo 'wait ', $headers['retry-after'] ?? '1', ' seconds', PHP_EOL;}Run it with php limits.php. No curl_close: since PHP 8 the handle is freed on its own, and in 8.5 the function became deprecated.
import osimport urllib.errorimport urllib.request
request = urllib.request.Request( "https://api.fatureihoje.com/public/v1/me", headers={"Authorization": f"Bearer {os.environ['FH_API_KEY']}"},)
try: response = urllib.request.urlopen(request) status, headers = response.status, response.headersexcept urllib.error.HTTPError as failure: status, headers = failure.code, failure.headers
print("limit", headers.get("RateLimit-Limit"))print("remaining", headers.get("RateLimit-Remaining"))print("resets in", headers.get("RateLimit-Reset"), "seconds")
if status == 429: print("wait", headers.get("Retry-After", "1"), "seconds")Run it with python3 limits.py.
Other limits that also answer 429
Section titled “Other limits that also answer 429”The per-minute budget is not the only counter. Authentication has a ceiling of its own for failures per IP: a wrong key repeated many times within the same minute starts getting a 429 even before the key is checked. That counter only counts what went wrong, so normal use never touches it. It is described in Authentication.
Both use the same code, rate_limit_exceeded, and both carry Retry-After. The difference shows in the context: if your calls are being accepted and suddenly a 429 arrives, it is the company budget; if they are being refused with 401 and then turn into 429, it is the failure ceiling.
Key limit
Section titled “Key limit”The table above also shows how many active keys each plan allows. A revoked key and an expired key do not count; a key living through a rotation grace period does count, because it still authenticates.
Going past that number returns plan_limit_reached, with status 403. Rotating an existing key does not hit that limit; creating one more does.
Next step
Section titled “Next step”- Errors: the whole catalog, with
rate_limit_exceededandplan_limit_reached. - Pagination: sweeping a long list is what spends the most budget.