Calendário de Preços
Visualizar os melhores preços por data em uma rota específica
Calendário de Preços
Consulte os preços mínimos de voos para uma rota específica ao longo de um período. Ideal para criar calendários de preços e ajudar usuários a encontrar as melhores datas para viajar.
Planejamento de Viagem
O calendário mostra os preços mínimos por data, ajudando usuários a escolher as datas mais econômicas para viajar.
GET /api/calendario-precos
Busca preços mínimos para uma rota específica ao longo de um período.
Request
GET /api/calendario-precos?origem=GRU&destino=MIA
Authorization: Bearer sua_chave_aquiParâmetros
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
origem | string | Sim | Código IATA do aeroporto de origem (3 letras) |
destino | string | Sim | Código IATA do aeroporto de destino (3 letras) |
Origem e destino devem ser códigos IATA válidos de 3 caracteres e não podem ser iguais.
Response
{
"success": true,
"data": {
"origem": "GRU",
"destino": "MIA",
"moeda": "BRL",
"precos": [
{
"data": "2026-03-01",
"precoMinimo": 1299.00,
"disponivel": true,
"companhiaAerea": "LATAM"
},
{
"data": "2026-03-02",
"precoMinimo": 1450.00,
"disponivel": true,
"companhiaAerea": "GOL"
},
{
"data": "2026-03-03",
"precoMinimo": 1199.00,
"disponivel": true,
"companhiaAerea": "LATAM"
},
{
"data": "2026-03-04",
"precoMinimo": null,
"disponivel": false,
"companhiaAerea": null
}
],
"precoMedio": 1316.00,
"precoMenor": 1199.00,
"precoMaior": 1450.00,
"dataPrecoMenor": "2026-03-03",
"totalDias": 30
},
"message": "Calendário de preços obtido com sucesso"
}Campos da Resposta
| Campo | Tipo | Descrição |
|---|---|---|
origem | string | Código IATA do aeroporto de origem |
destino | string | Código IATA do aeroporto de destino |
moeda | string | Moeda dos preços (ex: BRL, USD) |
precos | array | Lista de preços por data |
precoMedio | number | Preço médio no período |
precoMenor | number | Menor preço encontrado |
precoMaior | number | Maior preço encontrado |
dataPrecoMenor | string | Data com o menor preço |
totalDias | number | Total de dias no período |
Objeto Preco
| Campo | Tipo | Descrição |
|---|---|---|
data | string | Data no formato YYYY-MM-DD |
precoMinimo | number | null | Menor preço encontrado (null se indisponível) |
disponivel | boolean | Se há voos disponíveis nesta data |
companhiaAerea | string | null | Companhia com o menor preço |
Exemplos de Uso
Busca Simples
curl "https://api.seudominio.com/api/calendario-precos?origem=GRU&destino=MIA" \
-H "Authorization: Bearer sua_chave_aqui"Múltiplas Rotas
async function buscarCalendariosDePrecos(rotas) {
const promessas = rotas.map(rota =>
fetch(`/api/calendario-precos?origem=${rota.origem}&destino=${rota.destino}`, {
headers: {
'Authorization': `Bearer ${apiKey}`
}
}).then(r => r.json())
);
return await Promise.all(promessas);
}
// Uso
const calendarios = await buscarCalendariosDePrecos([
{ origem: 'GRU', destino: 'MIA' },
{ origem: 'GRU', destino: 'JFK' },
{ origem: 'GRU', destino: 'CDG' }
]);Implementação em React
import { useState, useEffect } from 'react';
interface PrecoPorData {
data: string;
precoMinimo: number | null;
disponivel: boolean;
companhiaAerea: string | null;
}
interface CalendarioPrecos {
origem: string;
destino: string;
moeda: string;
precos: PrecoPorData[];
precoMedio: number;
precoMenor: number;
precoMaior: number;
dataPrecoMenor: string;
}
interface Props {
origem: string;
destino: string;
}
function CalendarioPrecos({ origem, destino }: Props) {
const [calendario, setCalendario] = useState<CalendarioPrecos | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function carregarCalendario() {
try {
setLoading(true);
setError(null);
const response = await fetch(
`/api/calendario-precos?origem=${origem}&destino=${destino}`,
{
headers: {
'Authorization': `Bearer ${process.env.NEXT_PUBLIC_API_KEY}`
}
}
);
const data = await response.json();
if (!data.success) {
throw new Error(data.message || 'Erro ao carregar calendário');
}
setCalendario(data.data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Erro desconhecido');
} finally {
setLoading(false);
}
}
if (origem && destino) {
carregarCalendario();
}
}, [origem, destino]);
if (loading) {
return <div className="text-center p-4">Carregando calendário...</div>;
}
if (error) {
return <div className="text-red-600 p-4">Erro: {error}</div>;
}
if (!calendario) {
return null;
}
return (
<div className="space-y-6">
{/* Resumo */}
<div className="bg-gray-50 p-6 rounded-lg">
<h3 className="text-lg font-semibold mb-4">
{calendario.origem} → {calendario.destino}
</h3>
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<div className="text-sm text-gray-600">Menor Preço</div>
<div className="text-2xl font-bold text-green-600">
{calendario.moeda} {calendario.precoMenor.toFixed(2)}
</div>
<div className="text-xs text-gray-500">
{new Date(calendario.dataPrecoMenor).toLocaleDateString('pt-BR')}
</div>
</div>
<div>
<div className="text-sm text-gray-600">Preço Médio</div>
<div className="text-2xl font-bold">
{calendario.moeda} {calendario.precoMedio.toFixed(2)}
</div>
</div>
<div>
<div className="text-sm text-gray-600">Maior Preço</div>
<div className="text-2xl font-bold text-red-600">
{calendario.moeda} {calendario.precoMaior.toFixed(2)}
</div>
</div>
</div>
</div>
{/* Calendário */}
<div className="grid grid-cols-7 gap-2">
{calendario.precos.map((preco, index) => {
const date = new Date(preco.data);
const isLowestPrice = preco.precoMinimo === calendario.precoMenor;
return (
<div
key={index}
className={`
p-3 rounded border text-center cursor-pointer transition-all
${!preco.disponivel ? 'bg-gray-100 opacity-50 cursor-not-allowed' : ''}
${isLowestPrice ? 'border-green-500 bg-green-50' : 'border-gray-200'}
${preco.disponivel ? 'hover:shadow-lg hover:scale-105' : ''}
`}
onClick={() => preco.disponivel && handleDateClick(preco)}
>
<div className="text-xs text-gray-600 mb-1">
{date.toLocaleDateString('pt-BR', { day: '2-digit', month: 'short' })}
</div>
{preco.disponivel && preco.precoMinimo ? (
<>
<div className="font-bold text-sm">
{calendario.moeda} {preco.precoMinimo.toFixed(0)}
</div>
<div className="text-xs text-gray-500 truncate">
{preco.companhiaAerea}
</div>
</>
) : (
<div className="text-xs text-gray-400">
Indisponível
</div>
)}
</div>
);
})}
</div>
</div>
);
function handleDateClick(preco: PrecoPorData) {
// Redirecionar para busca de voos nesta data
console.log('Buscar voos para:', preco.data);
}
}import { useState, useEffect } from 'react';
function CalendarioPrecos({ origem, destino }) {
const [calendario, setCalendario] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function carregarCalendario() {
try {
setLoading(true);
setError(null);
const response = await fetch(
`/api/calendario-precos?origem=${origem}&destino=${destino}`,
{
headers: {
'Authorization': `Bearer ${process.env.NEXT_PUBLIC_API_KEY}`
}
}
);
const data = await response.json();
if (!data.success) {
throw new Error(data.message || 'Erro ao carregar calendário');
}
setCalendario(data.data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
if (origem && destino) {
carregarCalendario();
}
}, [origem, destino]);
if (loading) return <div>Carregando...</div>;
if (error) return <div>Erro: {error}</div>;
if (!calendario) return null;
return (
<div>
<h3>{calendario.origem} → {calendario.destino}</h3>
<div className="resumo">
<div>
<span>Menor: </span>
<strong>{calendario.moeda} {calendario.precoMenor}</strong>
</div>
<div>
<span>Médio: </span>
<strong>{calendario.moeda} {calendario.precoMedio}</strong>
</div>
</div>
<div className="calendario-grid">
{calendario.precos.map((preco, i) => (
<div
key={i}
className={preco.disponivel ? 'disponivel' : 'indisponivel'}
>
<div>{new Date(preco.data).toLocaleDateString()}</div>
{preco.precoMinimo && (
<div>{calendario.moeda} {preco.precoMinimo}</div>
)}
</div>
))}
</div>
</div>
);
}Visualizações Comuns
1. Calendário de Mês
Exiba os preços em um calendário mensal tradicional:
Março 2026
Dom Seg Ter Qua Qui Sex Sáb
1 2 3 4
1299 1450 1199 -
5 6 7 8 9 10 11
1350 1275 1399 1499 1350 1199 1550
12 13 14 15 16 17 18
1650 1599 1450 1399 1299 1250 17992. Gráfico de Linha
import { LineChart, Line, XAxis, YAxis, Tooltip } from 'recharts';
function GraficoPrecos({ precos }) {
const data = precos
.filter(p => p.disponivel)
.map(p => ({
data: new Date(p.data).toLocaleDateString('pt-BR', { day: '2-digit', month: 'short' }),
preco: p.precoMinimo
}));
return (
<LineChart width={800} height={400} data={data}>
<XAxis dataKey="data" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="preco" stroke="#2563eb" strokeWidth={2} />
</LineChart>
);
}3. Heatmap de Preços
Use cores para indicar preços relativos:
function getColorForPrice(price, minPrice, maxPrice) {
const range = maxPrice - minPrice;
const normalized = (price - minPrice) / range;
if (normalized < 0.33) return 'bg-green-100 text-green-800'; // Barato
if (normalized < 0.66) return 'bg-yellow-100 text-yellow-800'; // Médio
return 'bg-red-100 text-red-800'; // Caro
}Integração com Busca de Voos
Ao clicar em uma data, inicie uma busca de voos:
function handleDateSelect(preco: PrecoPorData, calendario: CalendarioPrecos) {
// Calcular data de retorno (ex: 7 dias depois)
const departureDate = new Date(preco.data);
const returnDate = new Date(departureDate);
returnDate.setDate(returnDate.getDate() + 7);
// Navegar para busca de voos
const params = new URLSearchParams({
type: 'round_trip',
origin: calendario.origem,
destination: calendario.destino,
departureDate: preco.data,
returnDate: returnDate.toISOString().split('T')[0],
passengers: '1',
cabinClass: 'economy'
});
router.push(`/voos/buscar?${params.toString()}`);
}Dicas de UX
Melhores Práticas
- Destaque a data com menor preço visualmente
- Use cores para indicar preços relativos (verde = barato, vermelho = caro)
- Mostre informação sobre cada data ao passar o mouse (hover)
- Permita seleção de data para buscar voos específicos
- Exiba resumo com menor, médio e maior preço
- Cache os resultados por 12-24 horas
Exemplo de Tooltip
Ao passar o mouse sobre uma data:
┌─────────────────────────────┐
│ 15 de Março, 2026 │
│ │
│ Menor preço: R$ 1.299,00 │
│ Companhia: LATAM │
│ │
│ [Buscar Voos] │
└─────────────────────────────┘Cache e Performance
Recomendações de Cache
// Cache de 12 horas (preços não mudam muito durante o dia)
const cacheTime = 12 * 60 * 60 * 1000;
// Next.js
export const revalidate = 43200; // 12 horas
// React Query
const { data } = useQuery(
['calendario', origem, destino],
() => fetchCalendario(origem, destino),
{
staleTime: cacheTime,
cacheTime: cacheTime
}
);Performance
- Latência típica: 300-800ms
- Cache recomendado: 12-24 horas
- Refresh: Uma vez por dia (madrugada)
Casos de Uso
1. Widget de "Melhor Data para Viajar"
function MelhorDataParaViajar({ origem, destino }) {
const { data } = useCalendarioPrecos(origem, destino);
if (!data) return null;
return (
<div className="bg-blue-50 p-4 rounded">
<h4>💰 Melhor data para viajar</h4>
<p className="text-2xl font-bold">
{new Date(data.dataPrecoMenor).toLocaleDateString('pt-BR')}
</p>
<p>
Por apenas {data.moeda} {data.precoMenor.toFixed(2)}
</p>
<button>Buscar Voos</button>
</div>
);
}2. Comparador de Datas
Permita que o usuário compare preços lado a lado:
function ComparadorDatas({ calendario }) {
const [datasSelecionadas, setDatasSelecionadas] = useState([]);
return (
<div>
{datasSelecionadas.map(data => (
<PrecoPorData key={data} data={data} preco={...} />
))}
</div>
);
}Erros Comuns
| Código | Erro | Solução |
|---|---|---|
400 | Origem e destino são obrigatórios | Passe ambos os parâmetros |
400 | Códigos IATA devem ter 3 caracteres | Use códigos de 3 letras |
400 | Origem e destino não podem ser iguais | Use aeroportos diferentes |
401 | Unauthorized | Verifique sua API key |
404 | Route not found | Verifique se a rota existe |
429 | Rate limit exceeded | Implemente cache |
Próximos Passos
- Busca de Voos - Buscar voos para uma data específica
- Ofertas - Ver ofertas destacadas
- Autocomplete - Buscar aeroportos