API de Voos
API

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_aqui

Parâmetros

ParâmetroTipoObrigatórioDescrição
origemstringSimCódigo IATA do aeroporto de origem (3 letras)
destinostringSimCó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

CampoTipoDescrição
origemstringCódigo IATA do aeroporto de origem
destinostringCódigo IATA do aeroporto de destino
moedastringMoeda dos preços (ex: BRL, USD)
precosarrayLista de preços por data
precoMedionumberPreço médio no período
precoMenornumberMenor preço encontrado
precoMaiornumberMaior preço encontrado
dataPrecoMenorstringData com o menor preço
totalDiasnumberTotal de dias no período

Objeto Preco

CampoTipoDescrição
datastringData no formato YYYY-MM-DD
precoMinimonumber | nullMenor preço encontrado (null se indisponível)
disponivelbooleanSe há voos disponíveis nesta data
companhiaAereastring | nullCompanhia 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 1799

2. 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ódigoErroSolução
400Origem e destino são obrigatóriosPasse ambos os parâmetros
400Códigos IATA devem ter 3 caracteresUse códigos de 3 letras
400Origem e destino não podem ser iguaisUse aeroportos diferentes
401UnauthorizedVerifique sua API key
404Route not foundVerifique se a rota existe
429Rate limit exceededImplemente cache

Próximos Passos

On this page