Para desenvolvedores

API do LulaHub · v1.1.0

Dados do 1º turno de 2026 por cidade e bairro, a prioridade de cada lugar (onde mais gente não escolheu ninguém) e o perfil estimado de quem não votou. Só dados agregados e públicos (TSE e IBGE); nenhum dado de pessoa.

Endereço: https://squid.sts.lol/api/v1 · Especificação OpenAPI 3.1: openapi.json (serve para gerar cliente ou abrir no Postman/Insomnia).

Autenticação

Todo pedido de dados exige uma chave de API. Sem chave, ou com chave errada ou revogada, a resposta é 401 e nenhum dado sai. Só esta documentação, a especificação (openapi.json) e /saude abrem sem chave, e nenhuma delas traz dados.

Sem chave

$ curl -i https://squid.sts.lol/api/v1/lugares

HTTP/1.1 401 Unauthorized
{"erro":{"codigo":"sem_chave","mensagem":"Envie a chave no cabeçalho Authorization: Bearer <chave> ou X-API-Key."}}

Começar

  1. Peça uma chave à equipe do LulaHub.
  2. Chame a API do seu servidor, com a chave no cabeçalho. A API não libera CORS: chamada direta do navegador exporia a chave.
curl -H "Authorization: Bearer $LULAHUB_CHAVE" \
  "https://squid.sts.lol/api/v1/lugares?tipo=bairro&uf=RJ&prioridade=alta&limite=20"
const resposta = await fetch(
  `https://squid.sts.lol/api/v1/lugares/${encodeURIComponent("RJ-58777|MOSELA")}`,
  { headers: { Authorization: `Bearer ${process.env.LULAHUB_CHAVE}` } },
);
if (resposta.status === 429) {
  // passou do limite: espere o tempo de Retry-After (em segundos)
}
const lugar = await resposta.json();
const idosos = lugar.perfil?.idade.find((g) => g.nome === "70+");

Limites e paginação

Endpoints

GET/api/v1/lugares

Lista cidades e bairros

Cidades (todas as 5.571) e bairros (nas cidades com 50 mil eleitores ou mais), com o resultado do 1º turno de 2026 para presidente. Por padrão vem quem tem mais gente sem votar primeiro.

ParâmetroOndeValoresDescrição
tipoquerycidade | bairroSó cidades ou só bairros. Sem o parâmetro, vêm os dois.
ufquerystringSigla do estado.
cidadequerystringId de uma cidade: traz ela e os bairros dela.
prioridadequeryalta | media | normalPrioridade calculada pelo LulaHub.
ordemqueryfora | taxa | nome, padrão forafora = mais gente sem votar (número) primeiro; taxa = maior proporção primeiro; nome = alfabética.
limitequeryinteger, 1–500, padrão 100Itens por página.
paginaqueryinteger, 1–, padrão 1Página, a partir de 1.

Respostas

  • 200 Página de lugares.
  • 400 Parâmetro inválido.
  • 401 Sem chave, ou chave inválida ou revogada.
  • 429 Passou do limite de pedidos por minuto da chave. Veja o cabeçalho Retry-After.

GET/api/v1/lugares/{id}

Um lugar, com o perfil de quem não votou

Resultado do lugar e o perfil estimado de quem não votou (idade, gênero, escolaridade, cor ou raça, religião e locais de votação). Id de bairro tem "|": codifique como %7C.

ParâmetroOndeValoresDescrição
id *caminhostringId da cidade (RJ-58777) ou do bairro (RJ-58777|MOSELA, na URL RJ-58777%7CMOSELA).

Respostas

  • 200 O lugar. perfil vem null quando ainda não foi calculado.
  • 400 Id em formato inválido.
  • 401 Sem chave, ou chave inválida ou revogada.
  • 404 Lugar não existe.
  • 429 Passou do limite de pedidos por minuto.
curl -H "Authorization: Bearer $LULAHUB_CHAVE" \
  "https://squid.sts.lol/api/v1/lugares/RJ-58777%7CMOSELA"

Exemplo de resposta (Mosela, Petrópolis/RJ; listas encurtadas)

{
  "id": "RJ-58777|MOSELA", "tipo": "bairro", "nome": "Mosela",
  "cidade_id": "RJ-58777", "municipio": "Petrópolis", "uf": "RJ",
  "aptos": 11038, "abstencoes": 2772, "brancos": 207, "nulos": 252,
  "fora": 3231, "taxa_fora": 29.3, "prioridade": "media",
  "lat": -22.494432, "lng": -43.200426,
  "perfil": {
    "v": 1, "aptos": 11038, "abstencoes": 2772, "brancos": 207, "nulos": 252,
    "eleitores": 11046, "previsto": 2879,
    "idade": [
      { "nome": "18-24", "eleitores": 865, "faltou": 157 },
      { "nome": "70+", "eleitores": 2048, "faltou": 1386 }
    ],
    "genero": [{ "nome": "mulheres", "eleitores": 5920, "faltou": 1516 }],
    "escolaridade": [{ "nome": "fundamental", "eleitores": 3736, "faltou": 1166 }],
    "grupos": [{ "nome": "mulheres · 70+ · fundamental", "faltou": 455, "taxa": 75 }],
    "raca": [{ "nome": "Branca", "faltou": 1944 }, { "nome": "Parda", "faltou": 556 }],
    "religiao": [{ "nome": "Católica", "faltou": 1560 }, { "nome": "Evangélica", "faltou": 679 }],
    "locais": [{
      "nome": "ESCOLA MUNICIPAL SAO JUDAS TADEU", "endereco": "R. MOSELA, 1445",
      "lat": -22.4956041, "lon": -43.2002285, "eleitores": 3104,
      "abstencoes": 851, "pct70": 24.9, "faltou70": 523
    }],
    "censo": { "setores": 32, "moradores": 13690, "pct70": 11.9, "razao": 0.81,
               "raca": [{ "nome": "Branca", "pct": 66 }, { "nome": "Parda", "pct": 23.3 }] }
  },
  "perfil_atualizado_em": "2026-10-08T02:49:44.296+00:00"
}

Campos

Lugar

CampoTipoDescrição
idstringCidade: UF-código TSE. Bairro: id da cidade + "|" + nome normalizado.
tipocidade | bairro
nomestring
cidade_idstring
municipiostring
ufstring
aptosintegerEleitores aptos no 1º turno de 2026.
abstencoesintegerNão foram votar.
brancosinteger
nulosinteger
foraintegerNão escolheram ninguém: abstenções + brancos + nulos.
taxa_foranumberfora / aptos, em %.
prioridadealta | media | normalalta = volume de "fora" entre os 5% maiores, ou proporção entre as 25% maiores com volume acima da mediana.
latnumber | nullCentro dos locais de votação (só bairros).
lngnumber | null
tem_perfilbooleanSe já existe perfil calculado (só na lista).

Perfil

Estimativa para grupos, nunca sobre pessoas. Veja o método em /docs/api.

CampoTipoDescrição
vobjetoVersão do formato.
aptosinteger
abstencoesinteger
brancosinteger
nulosinteger
eleitoresintegerEleitores no perfil por seção do TSE (pode diferir um pouco de aptos).
previstointegerAbstenções que as taxas de 2022 previam, antes do ajuste ao real de 2026.
idadeGrupo[]Faixas: 16-17, 18-24, 25-34, 35-44, 45-59, 60-69, 70+ (16-17 e 70+ votam por opção).
generoGrupo[]mulheres, homens, não informado.
escolaridadeGrupo[]sem estudo formal, fundamental, médio incompleto, médio completo, superior, não informado.
gruposarrayOs 6 maiores grupos gênero · idade · escolaridade entre quem faltou.
racaobjetoCor ou raça de quem faltou (Censo 2022 da vizinhança, por faixa etária). null quando a vizinhança não bate com o eleitorado (veja censo.razao).
religiaoobjetoReligião de quem faltou (Censo 2022 do município, por sexo e idade).
locaisLocalVotacao[]Locais de votação (só em bairros).
censoobject | nullMoradores da vizinhança (setores do Censo 2022 mais próximos dos locais de votação do lugar).

Grupo

CampoTipoDescrição
nomestring
eleitoresintegerEleitores do grupo no lugar (TSE).
faltouintegerEstimativa de quantos do grupo não foram votar.

Fatia

CampoTipoDescrição
nomestring
faltouintegerEstimativa de quantos de quem faltou.

LocalVotacao

CampoTipoDescrição
nomestring
enderecostring
latnumber | null
lonnumber | null
eleitoresinteger
abstencoesintegerReal (TSE).
pct70number% dos eleitores do local com 70 anos ou mais (0 a 100).
faltou70integerEstimativa de quantos com 70+ não foram votar.

Erros

Todo erro vem como { "erro": { "codigo": "...", "mensagem": "..." } }.

HTTPcodigoQuando
400parametro_invalidoParâmetro fora do formato (a mensagem diz qual).
401sem_chave / chave_invalidaFaltou a chave, ou ela foi revogada.
404nao_encontradoO id não existe.
429limitePassou do limite de pedidos por minuto da chave. Espere o Retry-After (60 s).
500erro_internoFalha nossa. Tente de novo; se continuar, avise a equipe.

De onde vêm os números

Uso responsável

Versões

Mudanças compatíveis (campo novo, filtro novo) mantêm /api/v1 e sobem a versão no cabeçalho X-LulaHub-API. Mudança que quebra cliente vira /api/v2, com aviso antes. O perfil tem o campo v com a versão do formato.