FluentaAPI

Rate limits

How many requests a minute the API takes per credential and per plan, the X-RateLimit headers, the 429 and how to retry correctly.

The API counts each credential's requests in windows of 60 seconds. Past a budget it answers 429 RATE_LIMITED and tells you how long to wait.

Budgets

A request is counted against every budget it falls under.

BudgetWhat it countsLightPro
Per organizationOne credential's requests in one organization, on any route60120
Heavy routesThat same pair, on the heavy routes2020
Credential-wideOne credential's requests across all its organizations, added up300300
Account routesgetMe, getMyUsage and listOrganizations, which name no organization6060
  • The numbers are requests per 60-second window.
  • Light or Pro is the plan of the organization in the path. A trial of Pro and an exempt organization count as Pro.
  • A heavy request spends one of the heavy budget and one of the per-organization budget.
  • The account routes are counted apart from the other three budgets.
  • The limit is per credential: each API token has its own budgets.

getPlan answers an organization's limits in rateLimits: windowSeconds, organization, heavy, credential and account.

Heavy routes

They are the ones marked x-rate-limit-heavy: true in the OpenAPI document:

OperationWhat it reads
getFloorThe whole floor in one answer
getEfficiency, getProduction, getBillingA figure over a range of days
getOperatorReportThe report of one «operario» (operator) over a range
getModuleEfficiency, getOperatorEfficiencyThe «Eficiencia» (efficiency) of one «módulo» (production line) or one «operario» over a range
getMonthThe month
getPausesThe «Pausas» (pauses) report

Except for getFloor, they are report routes: they answer by the organization's plan. See Plans.

The window

The window is fixed. It opens with a budget's first request and ends 60 seconds later, whatever came in between. When it ends, the count starts over.

Headers

Every answer carries them, an error included:

HeaderValue
X-RateLimit-LimitRequests per window of the budget you are closest to spending
X-RateLimit-RemainingRequests left in that budget's window. 0 on a 429
X-RateLimit-ResetWhen that window ends, in epoch seconds (UTC)
Retry-AfterOn a 429 only: seconds you must wait

getMe answers the same in rateLimit (limit, remaining, reset) for the budget of the account routes.

The 429

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1794499260
Retry-After: 23
{
  "statusCode": 429,
  "code": "RATE_LIMITED",
  "retryAfterSeconds": 23,
  "message": "Demasiadas solicitudes. Espera 23 segundos antes de volver a intentar."
}

retryAfterSeconds repeats the value of Retry-After. The same call before then is refused again.

What counts and what does not

  • Errors count too. A request with a valid credential spends its budget even when the answer is a 4xx.
  • A 403 INSUFFICIENT_SCOPE and a 404 ORGANIZATION_NOT_FOUND are counted against the budget of the account routes, whatever the route.
  • A 429 counts against nothing. Asking again does not push your window out.
  • A 401 INVALID_API_TOKEN does not count either: there is no verified credential to charge it to.

Retrying correctly

Wait what Retry-After says and cap the attempts. Read the token from an environment variable, not from the code.

const BASE = 'https://api.fluenta.io/v1/external';

async function fluenta(path, maxRetries = 5) {
  for (let attempt = 0; ; attempt += 1) {
    const response = await fetch(`${BASE}${path}`, {
      headers: { Authorization: `Bearer ${process.env.FLUENTA_TOKEN}` },
    });

    if (response.status !== 429) return response;
    if (attempt >= maxRetries) throw new Error('Rate limited: retries spent');

    const seconds = Number(response.headers.get('Retry-After')) || 1;
    await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
  }
}

const response = await fluenta('/organizations/42/floor');
import os
import time

import requests

BASE = "https://api.fluenta.io/v1/external"


def fluenta(path, max_retries=5):
    headers = {"Authorization": f"Bearer {os.environ['FLUENTA_TOKEN']}"}

    for attempt in range(max_retries + 1):
        response = requests.get(f"{BASE}{path}", headers=headers, timeout=30)

        if response.status_code != 429:
            return response
        if attempt < max_retries:
            time.sleep(int(response.headers.get("Retry-After", "1")))

    raise RuntimeError("Rate limited: retries spent")


response = fluenta("/organizations/42/floor")

To stay clear of the 429, watch X-RateLimit-Remaining: when it reaches 0, wait until X-RateLimit-Reset before the next request.

Seeing your own usage

getMyUsage answers what your credential asked of the API:

curl 'https://api.fluenta.io/v1/external/me/usage?days=7' \
  -H 'Authorization: Bearer fluenta_live_xxxxxxxxxxxxxxxx'
  • days goes from 1 to 90 and is 30 when you leave it out. Usage is kept 90 days.
  • totals adds up the period, endpoints splits it by method and route, and days by day.
  • Each carries requests (refusals included), errors (the answers 400 or above), averageDurationMs, maxDurationMs and lastAt.
  • Days are UTC days, not the plant's.
  • It counts this credential only, in all its organizations. The figures may trail by a few seconds.

On this page