Voltar ao PoupaLitro

Dados abertos

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çoCORSLimite
/api/stationsDesligado 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/lookupsSem 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/trendsSim, com origem configurável em POUPALITRO_CORS_ORIGIN.Nenhum limite próprio; aceita até 200 ids por pedido.
/api/history/coverageSim, 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.

Os 10 gasóleos mais baratos do distrito de Braga

curl -s "https://poupalitro.com/api/stations?fuelId=2101&district=Braga&sortBy=price&perPage=10"
const resposta = await fetch(
  "https://poupalitro.com/api/stations?fuelId=2101&district=Braga&sortBy=price&perPage=10",
);
const dados = await resposta.json();

for (const posto of dados.stations) {
  console.log(posto.price.toFixed(3), "€/litro —", posto.name, "(", posto.municipality, ")");
}
console.log("média do distrito:", dados.stats.average?.toFixed(3) ?? "(sem preços publicados)");

O que está mais barato num raio de 5 km de um ponto

curl -s "https://poupalitro.com/api/stations?fuelId=3201&lat=41.5503&lng=-8.4200&radiusKm=5&sortBy=distance&perPage=5"

Áreas de serviço de auto-estrada num distrito

curl -s "https://poupalitro.com/api/stations?fuelId=2101&district=Lisboa&stationType=Auto-estrada&perPage=10"

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)

curl -s "https://poupalitro.com/api/lookups"

Quanto histórico existe, e se a recolha está viva

curl -s "https://poupalitro.com/api/history/coverage"

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

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.

Geocodificação: Nominatim/OpenStreetMap e Photon (komoot). Cartografia do mapa: © colaboradores do OpenStreetMap, sob a licença ODbL. A lista completa de licenças e atribuições está em /sobre.

Confirma sempre o preço no placar do posto antes de abastecer. Um preço errado, aqui ou em qualquer outro sítio, é um número; no depósito é dinheiro.