Limites de uso
Quantas chamadas por minuto cada cliente pode fazer, os cabeçalhos X-RateLimit e como tratar o erro 429 com espera e nova tentativa.
Para: Desenvolvedores dos sistemas da prefeitura
A API é compartilhada por todos os sistemas e aplicativos das prefeituras. Para nenhum deles atrapalhar os outros (nem o painel que os servidores estão usando), cada chamada passa por limites por minuto, em camadas.
Na prática: um sistema bem comportado, que pagina com calma e usa webhooks em vez de perguntar "mudou?" a cada segundo, nunca vê um 429.
As camadas#
| Camada | Vale para | Padrão por minuto |
|---|---|---|
| Por integração | Cada integração | 120 (a prefeitura pode ajustar de 1 até o teto de 600) |
| Por usuário dos aplicativos | Cada usuário logado nos aplicativos | 60 |
| Por prefeitura | Soma de todas as integrações e aplicativos da prefeitura | 600 |
| Rotas pesadas | Cada cliente, só nas rotas pesadas | 30 |
| Login dos aplicativos | Cada combinação de IP e e-mail | 5 |
Os valores acima são os padrões da plataforma e podem ser revistos pela administração do GedFlow. O limite próprio de cada integração aparece para o administrador da prefeitura no campo Requisições por minuto.
Rotas pesadas são as que mais exigem do servidor:
GET /searchGET /lookupGET /documents/{id}/files/{file}/downloadPOST /documentse todo o envio de arquivos (/documents/{id}/files,/files/{file}/uploads,/uploads/...)
Uma chamada a rota pesada conta ao mesmo tempo no limite de rota pesada, no do cliente e no da prefeitura. Ou seja: uma integração com 120 por minuto pode fazer até 30 buscas nesse minuto, e o restante em chamadas leves.
Cabeçalhos de cada resposta#
Toda resposta autenticada traz:
| Cabeçalho | Significado |
|---|---|
X-RateLimit-Limit |
O limite, por minuto, da camada mais perto de estourar. |
X-RateLimit-Remaining |
Quantas chamadas ainda cabem nessa camada no minuto atual. |
Como existem várias camadas, os cabeçalhos mostram sempre a mais apertada naquele momento (a de menor Remaining).
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 87
Quando o limite estoura: 429#
Acima do limite, a resposta é 429 Too Many Requests, com dois cabeçalhos a mais:
| Cabeçalho | Significado |
|---|---|
Retry-After |
Quantos segundos esperar antes de tentar de novo. |
X-RateLimit-Reset |
O momento em que a camada libera de novo, em segundos desde 01/01/1970 (Unix). |
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 23
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1790000000
A chamada recusada não foi executada. Pode repeti-la com segurança depois da espera.
Como tratar: espera e nova tentativa#
A receita é a mesma em qualquer linguagem:
- Recebeu
429: espere os segundos deRetry-After(mais uma pequena folga aleatória, para vários processos não voltarem juntos). - Recebeu
502,503ou504, ou falha de rede: espere de forma crescente (1 s, 2 s, 4 s, 8 s...). - Desista depois de algumas tentativas e registre o erro.
- Não repita
400,401,403,404nem422: o resultado não muda sozinho.
PHP (Guzzle)#
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use Psr\Http\Message\ResponseInterface;
final class GedApi
{
private Client $http;
public function __construct(string $token)
{
$this->http = new Client([
'base_uri' => 'https://gedflow.com.br/api/v1/',
'headers' => ['Authorization' => "Bearer {$token}", 'Accept' => 'application/json'],
'http_errors' => false,
'timeout' => 30,
]);
}
/** @return array<string, mixed> */
public function get(string $path, array $query = [], int $maxAttempts = 5): array
{
for ($attempt = 1; ; $attempt++) {
try {
$response = $this->http->get($path, ['query' => $query]);
} catch (ConnectException $e) {
if ($attempt >= $maxAttempts) {
throw $e;
}
sleep(2 ** ($attempt - 1));
continue;
}
$status = $response->getStatusCode();
$retryable = $status === 429 || in_array($status, [502, 503, 504], true);
if (! $retryable || $attempt >= $maxAttempts) {
return $this->decode($response);
}
$wait = $status === 429
? max(1, (int) $response->getHeaderLine('Retry-After'))
: 2 ** ($attempt - 1);
sleep($wait + random_int(0, 2));
}
}
/** @return array<string, mixed> */
private function decode(ResponseInterface $response): array
{
$body = json_decode((string) $response->getBody(), true) ?? [];
if ($response->getStatusCode() >= 400) {
throw new RuntimeException("API respondeu {$response->getStatusCode()}: ".($body['message'] ?? 'sem mensagem'));
}
return $body;
}
}
$api = new GedApi(getenv('GEDFLOW_TOKEN'));
$page = $api->get('search', ['q' => 'habite-se', 'per_page' => 50]);
Python (requests)#
import os
import random
import time
import requests
BASE_URL = "https://gedflow.com.br/api/v1"
RETRYABLE = {429, 502, 503, 504}
session = requests.Session()
session.headers.update({
"Authorization": f"Bearer {os.environ['GEDFLOW_TOKEN']}",
"Accept": "application/json",
})
def get(path: str, params: dict | None = None, max_attempts: int = 5) -> dict:
for attempt in range(1, max_attempts + 1):
try:
response = session.get(f"{BASE_URL}/{path}", params=params, timeout=30)
except requests.ConnectionError:
if attempt == max_attempts:
raise
time.sleep(2 ** (attempt - 1))
continue
if response.status_code not in RETRYABLE or attempt == max_attempts:
response.raise_for_status()
return response.json()
if response.status_code == 429:
wait = max(1, int(response.headers.get("Retry-After", "1")))
else:
wait = 2 ** (attempt - 1)
time.sleep(wait + random.uniform(0, 2))
raise RuntimeError("não deveria chegar aqui")
page = get("search", {"q": "habite-se", "per_page": 50})
JavaScript (Node 18+ com fetch)#
const BASE_URL = 'https://gedflow.com.br/api/v1';
const RETRYABLE = new Set([429, 502, 503, 504]);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export async function get(path, params = {}, maxAttempts = 5) {
const url = `${BASE_URL}/${path}?${new URLSearchParams(params)}`;
for (let attempt = 1; ; attempt++) {
let response;
try {
response = await fetch(url, {
headers: {
Authorization: `Bearer ${process.env.GEDFLOW_TOKEN}`,
Accept: 'application/json',
},
signal: AbortSignal.timeout(30_000),
});
} catch (error) {
if (attempt >= maxAttempts) throw error;
await sleep(2 ** (attempt - 1) * 1000);
continue;
}
if (!RETRYABLE.has(response.status) || attempt >= maxAttempts) {
const body = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(`API respondeu ${response.status}: ${body.message ?? 'sem mensagem'}`);
}
return body;
}
const wait = response.status === 429
? Math.max(1, Number(response.headers.get('Retry-After') ?? 1))
: 2 ** (attempt - 1);
await sleep((wait + Math.random() * 2) * 1000);
}
}
const page = await get('search', { q: 'habite-se', per_page: 50 });
Boas práticas para não chegar ao limite#
- Use webhooks em vez de consultar de tempos em tempos para ver se algo mudou. Veja Webhooks.
- Sincronize por diferença com
updated_sincena lista de documentos, em vez de baixar o acervo inteiro toda noite. - Peça 100 por página (
per_page=100) nas cargas grandes: menos chamadas para o mesmo resultado. - Guarde o catálogo (
/sectors,/document-types,/fields) em cache por algumas horas. Ele muda pouco. - Leia
X-RateLimit-Remaininge diminua o ritmo quando ele estiver baixo, antes do429. - Um processo por vez nas cargas em massa. Dez processos em paralelo só chegam mais rápido ao limite.