Pular para o conteúdo
GedFlow

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 /search
  • GET /lookup
  • GET /documents/{id}/files/{file}/download
  • POST /documents e 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:

  1. Recebeu 429: espere os segundos de Retry-After (mais uma pequena folga aleatória, para vários processos não voltarem juntos).
  2. Recebeu 502, 503 ou 504, ou falha de rede: espere de forma crescente (1 s, 2 s, 4 s, 8 s...).
  3. Desista depois de algumas tentativas e registre o erro.
  4. Não repita 400, 401, 403, 404 nem 422: 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_since na 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-Remaining e diminua o ritmo quando ele estiver baixo, antes do 429.
  • Um processo por vez nas cargas em massa. Dez processos em paralelo só chegam mais rápido ao limite.