O PoupaLitro publica preços de combustíveis lidos da DGEG. Se tens um site, um blog, um projeto de dados ou uma página de câmara municipal, podes ler estes dados diretamente — sem chave, sem registo e sem pedir autorização para uso normal. Esta página diz o que existe, como se pede e o que não podes contar que exista.
Antes de mais, o essencial: isto corre num Raspberry Pi em casa, atrás de um túnel. Não há SLA, não há promessa de disponibilidade e não há escala. Os dados são da DGEG e são publicados pelos próprios operadores, com os atrasos deles. Se pensares em fazer mais do que algumas centenas de pedidos por dia, escreve primeiro para suporte@poupalitro.pt — quase sempre há uma resposta melhor do que um ciclo de pedidos (um ficheiro, um intervalo maior, ou simplesmente não fazer).
O estado da recolha, agora
Estes números são lidos neste momento, com as mesmas funções que respondem ao /api/history/coverage. Não são uma simulação da documentação: são o que o servidor está a devolver.
Alterações de preço
182 733
Postos com histórico
3360
Dias de leitura diária
44
A recolher desde
27 de setembro de 2026
Estado da recolha: fresco — última passagem há 5 h. Os preços estão a ser recolhidos: a última leitura foi há 5 h.
Os endereços
Tudo devolve JSON. Os exemplos usam https://poupalitro.com, que é o endereço deste servidor.
GET /api/stations
A pesquisa. Devolve os postos com o preço do combustível pedido, as estatísticas da área (mínimo, máximo, média, mediana e a poupança por depósito de 50 litros) e a data de atualização de cada preço, tal como a DGEG a publica.
Parâmetros
fuelIdCombustível, pelo id da DGEG (1000–9999). Por omissão 3201 (Gasolina simples 95). Um id que não exista no catálogo devolve erro, não uma lista vazia.
lat, lngCentro da pesquisa, em graus decimais. Têm de vir os dois; se faltar um, são ignorados e a pesquisa passa a ser nacional.
radiusKmRaio à volta de lat/lng, em km (0,5 a 200). Por omissão 10.
sortByprice (por omissão), distance, name ou updated. Um valor desconhecido cai em price — nunca devolve erro.
descdesc=true inverte a ordem. Qualquer outro valor é falso.
district, municipality, brand, stationTypeFiltros de igualdade, comparados sem acentos e sem maiúsculas. stationType aceita «Auto-estrada» e «Área comercial».
qTexto livre sobre o nome, a marca, a morada, a localidade, o concelho, o distrito e o código postal.
maxAgeHoursSó preços atualizados nas últimas N horas (0 a 1440). Sem ele não há filtro de idade.
north, south, east, westCaixa geográfica. Com os quatro cantos e mode=map, a caixa substitui o raio — é o que o mapa usa.
page, perPagePaginação. page de 1 a 500; perPage de 1 a 200 (por omissão 40).
idsLista de ids separados por vírgula. Devolve exatamente esses postos e ignora todos os outros filtros; serve para reabrir uma lista de favoritos.
locationEtiqueta de texto que é devolvida tal e qual no campo locationLabel.
O que devolve
fuel, total, page, perPage, totalPages, stats, cache, generatedAt e stations[], onde cada posto traz id, nome, marca, tipo, distrito, concelho, morada, localidade, código postal, coordenadas, distanceKm, price, reportedAt (a data da DGEG), ageHours, stale, isOpenNow e os outros preços que o mesmo posto publica. O isOpenNow pode ser null: a DGEG não publica horário para todos os postos, e null quer dizer «não se sabe», nunca «fechado».
GET /api/lookups
As tabelas de apoio, para não teres de inventar a lista de concelhos: combustíveis, distritos, concelhos (com o distrito de cada um) e marcas.
scopeall (por omissão) ou fuels — fuels devolve só o catálogo de combustíveis e poupa as outras três listas.
withStatswithStats=true acrescenta, por combustível, a média, o mínimo, o máximo e a contagem nacional. É a parte CARA: obriga a carregar a lista nacional de cada combustível.
statsForstatsFor=3201,2101 calcula as estatísticas só desses combustíveis. É a forma de pedir estatísticas sem pedir as catorze.
Devolve também databaseReady, que diz se o histórico está a funcionar neste servidor. Esta resposta não traz cabeçalho de cache: o que deves fazer é guardá-la tu, porque a lista muda uma vez por dia e withStats custa.
GET /api/stations/<id>
A ficha de um posto, vinda do serviço de detalhe da DGEG: horário de funcionamento (dias úteis, sábado, domingo e feriados), serviços, meios de pagamento, observações do operador, todos os preços publicados e a comparação de cada um com a média nacional.
lat, lngOpcionais. Se vierem, a resposta inclui a distância do posto a esse ponto.
recordrecord=true grava uma observação de preço no histórico deste servidor. NÃO O USES: existe para quando uma pessoa abre a ficha na aplicação, e um ciclo automático de terceiros a chamá-lo encheria o histórico com postos que ninguém abriu. É a única coisa desta página que escreve na base de dados.
GET /api/trends
Por posto: a média, o mínimo e o máximo dos preços que este servidor já observou, e quantas observações existem. Serve para dizer se o preço de hoje está acima ou abaixo do que aquele posto costuma praticar. Isto não é histórico oficial. A DGEG não publica séries temporais — publica o preço atual e mais nada. Tudo o que vês aqui foi recolhido por nós, e é por isso que a contagem de observações vem no resultado: sem ela, uma média de duas leituras parecia uma média de dois anos.
fuelIdObrigatório na prática: sem ele a resposta vem vazia.
stationIdsIds separados por vírgula, no máximo 200. Sem eles a resposta vem vazia.
GET /api/history/coverage
Quanto histórico existe e se a recolha continua a correr. Não tem parâmetros nenhuns. Devolve observations, stations, trackingSince, ultimaRecolhaEm, recolhasRegistadas, leiturasDiarias (dias, linhas, desde, até) e saude (estado, idade das tentativas e uma frase pronta a mostrar). Vem com Cache-Control: no-store, de propósito: é o endereço que responde a «a recolha está a correr?», e uma cópia em cache respondia que sim depois de ela ter parado. Isso também quer dizer que não o deves chamar em ciclo — se o usares num painel, guarda tu o resultado alguns minutos.
CORS: o que o browser de outra pessoa consegue ler
Uma página de outro domínio não pode ler uma resposta sem Access-Control-Allow-Origin. Isto não é um detalhe de implementação: é a diferença entre «posso pôr isto no meu site» e «só posso usar isto do meu servidor». A tabela é o estado real de cada endereço.
Endereço
CORS
Limite
/api/stations
Desligado por omissão. Liga-se com a variável de ambiente POUPALITRO_API_PUBLICA=true.
120 pedidos por minuto por IP, só quando a variável está ligada.
/api/lookups
Sem CORS.
Nenhum limite próprio; guarda a resposta.
/api/stations/<id>
Sem CORS.
Nenhum limite próprio; cada pedido vai à DGEG se não estiver em cache.
/api/trends
Sim, com origem configurável em POUPALITRO_CORS_ORIGIN.
Nenhum limite próprio; aceita até 200 ids por pedido.
/api/history/coverage
Sim, com origem configurável em POUPALITRO_CORS_ORIGIN.
Nenhum limite próprio; não guarda cache (ver acima).
Porque é que o CORS da pesquisa está desligado por omissão. Abrir a porta e não pôr um limitador ao lado era trocar um problema de SEO por uma denúncia de abuso: qualquer página de terceiros podia chamar isto num ciclo, e cada pedido pode custar uma recolha nacional à DGEG — que é um serviço público, e não nosso. Com a variável ligada, o CORS e o limite de 120 pedidos por minuto entram juntos e não em separado; foi uma decisão de operação, e não uma alteração de comportamento escondida numa publicação. Se precisares dele ligado para o teu projeto, escreve para suporte@poupalitro.pt e diz para que site é.
Os pedidos que passam pelo limite recebem 429 com o cabeçalho Retry-After. Respeita-o: é a diferença entre uma visita e um problema.
Exemplos
Todos os exemplos são copiáveis como estão. Troca apenas o endereço se estiveres a correr isto localmente.
Atenção ao usar isto: o mesmo nome aparece duas vezes, uma em cada sentido da via, com preços diferentes. O sentido publicado pela DGEG só existe em /api/stations/<id>, no campo station.sentido — sem o ires buscar, a lista ordenada por preço pode estar a apontar-te para o lado por onde não passas.
As listas de apoio (combustíveis, distritos, concelhos, marcas)
Comparar os preços de hoje com o que já se observou
# 1) os ids dos postos que interessam
curl -s "https://poupalitro.com/api/stations?fuelId=2101&municipality=Braga&perPage=5"
# 2) a média observada de cada um (máximo 200 ids por pedido)
curl -s "https://poupalitro.com/api/trends?fuelId=2101&stationIds=94923,87583"
O que não podes contar que exista
Disponibilidade. Isto corre num Raspberry Pi em casa, atrás de um túnel, sem SLA e sem redundância. Uma atualização do sistema, uma falha de energia ou uma passagem de recolha pesada podem deixá-lo lento ou em baixo durante minutos ou horas. Se o teu projeto depende disto, trata-o como uma fonte que falha — com cópia local e uma mensagem honesta para quem te lê.
Histórico oficial. Não existe. A DGEG publica o preço atual e não tem serviço de séries temporais; tudo o que está no /api/trends foi recolhido por esta aplicação, e a série começa no dia em que ela começou a correr.
Frescura garantida. Os preços são comunicados pelos operadores à DGEG e alguns postos ficam dias sem atualizar. Cada preço traz a data de origem (reportedAt) e um sinal de desatualizado (stale) — usa-os. Se mostrares preços a alguém, mostra também a data e diz para confirmar no placar do posto.
Ilhas. O conjunto de dados público da DGEG cobre apenas os 18 distritos do continente: não há Açores nem Madeira. Uma pesquisa por «Funchal» devolve zero resultados porque não existem dados na fonte, e não porque a pesquisa falhou.
Geocodificação aberta. Endereços e códigos postais são resolvidos com o Nominatim/OpenStreetMap e com o Photon, cujas políticas de utilização são restritivas (um pedido por segundo no Nominatim). Por isso não há aqui um endereço de geocodificação para terceiros usarem: quem tem de respeitar esses limites somos nós, e não passa a ser quem nos chama. Usa os serviços deles diretamente, com a tua identificação.
Apoio. Não há equipa de suporte. Há uma pessoa, e lê o correio.
De onde vêm estes dados
Preços, horários, serviços e meios de pagamento: precoscombustiveis.dgeg.gov.pt — Direção-Geral de Energia e Geologia, www.dgeg.gov.pt. O PoupaLitro não é um serviço oficial e não representa nenhuma entidade pública. Se publicares estes dados, atribui-os à DGEG e diz que passaram pelo PoupaLitro — não os atribuas a nós como se fossem nossos, e não os apresentes como informação oficial da DGEG quando já vêm tratados.