Ofertas de Voos
Acessar ofertas destacadas e promocionais de voos
Ofertas de Voos
Acesse ofertas pré-selecionadas de voos com preços especiais. Ideal para exibir em home pages, landing pages e seções de destaques.
Ofertas Curadas
As ofertas são pré-selecionadas e otimizadas para conversão, com preços competitivos e rotas populares.
POST /api/ofertas/buscar
Busca ofertas de voos com filtros opcionais.
Request
{
"internacional": true,
"quantidade": 10,
"shuffle": true,
"page": 0,
"pageSize": 20,
"origem": "GRU",
"destino": "LIS",
"apenasMilhas": false
}Parâmetros
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
internacional | boolean | Não | Filtrar por voos internacionais (padrão: false) |
quantidade | number | Não | Quantidade de ofertas a retornar (padrão: 50, 0 = todas) |
shuffle | boolean | Não | Embaralhar resultados (padrão: true) |
page | number | Não | Número da página para paginação (padrão: 0) |
pageSize | number | Não | Tamanho da página (padrão: 0 = sem paginação) |
origem | string | Não | Código IATA do aeroporto de origem (ex: "GRU", "SDU", "JFK") |
destino | string | Não | Código IATA do aeroporto de destino (ex: "LIS", "MAD", "CDG") |
apenasMilhas | boolean | Não | Se true, retorna apenas ofertas com milhas (padrão: false) |
Response
{
"success": true,
"data": [
{
"id": "oferta_123",
"origem": {
"iata": "GRU",
"cidade": "São Paulo",
"estado": "SP",
"pais": "Brasil",
"aeroporto": "Aeroporto Internacional de São Paulo/Guarulhos",
"urlImagens": [
"repository/places/são paulo - guarulhos/0"
]
},
"destino": {
"iata": "MIA",
"cidade": "Miami",
"estado": "FL",
"pais": "Estados Unidos",
"aeroporto": "Miami International Airport",
"urlImagens": [
"repository/places/miami/2",
"repository/places/miami/3"
]
},
"precoMinimo": 1299.00,
"moeda": "BRL",
"companhiaAerea": {
"codigo": "LA",
"nome": "LATAM Airlines"
},
"tipo": "round_trip",
"internacional": true,
"pontosAdulto": 1,
"valorOriginalMilhas": 35000,
"dataInicio": "2026-03-01",
"dataFim": "2026-06-30",
"restricoes": [
"Antecedência mínima de 7 dias",
"Permanência mínima de 3 dias"
],
"disponivel": true
}
],
"totalItens": 150,
"page": 0,
"pageSize": 10,
"message": "Ofertas encontradas com sucesso"
}Campos da Resposta
| Campo | Tipo | Descrição |
|---|---|---|
id | string | ID único da oferta |
origem | object | Informações do aeroporto de origem (contém iata, cidade, estado, pais, urlImagens[]) |
destino | object | Informações do aeroporto de destino (contém iata, cidade, estado, pais, urlImagens[]) |
precoMinimo | number | Menor preço encontrado para esta rota |
moeda | string | Moeda do preço (ex: BRL, USD) |
companhiaAerea | object | Companhia aérea principal (código e nome) |
tipo | string | Tipo de viagem: one_way ou round_trip |
internacional | boolean | Se é um voo internacional |
pontosAdulto | number | Indica se aceita milhas: 1 = aceita milhas, 0 = apenas pagante |
valorOriginalMilhas | number | Valor em milhas necessário (disponível quando pontosAdulto > 0) |
dataInicio | string | Data de início da validade da oferta |
dataFim | string | Data de fim da validade da oferta |
restricoes | array | Lista de restrições da oferta |
disponivel | boolean | Se a oferta está disponível |
Campos do Objeto Origem/Destino
| Campo | Tipo | Descrição |
|---|---|---|
iata | string | Código IATA do aeroporto (ex: "GRU", "MIA") |
cidade | string | Nome da cidade |
estado | string | Estado/província (opcional) |
pais | string | Nome do país |
urlImagens | string[] | Array de paths das imagens (pode conter 0 a 5 imagens, geralmente 1-3). Recomenda-se seleção aleatória para variedade visual. |
Como Obter Imagens dos Destinos
As imagens são retornadas no campo urlImagens dentro dos objetos origem e destino. Para construir a URL completa da imagem:
Construção da URL da Imagem
A API retorna apenas o path da imagem. Você precisa construir a URL completa usando o CDN base:
CDN Base: https://mbxrepo.azureedge.net/prod
Formato do path retornado:
"repository/places/nome-da-cidade/0"
"repository/places/nome-da-cidade/1"
"repository/places/nome-da-cidade/2"Nota: O array urlImagens pode conter 0 a 5 imagens (geralmente 1-3). Os números no final do path (0, 1, 2, etc.) indicam diferentes imagens do mesmo destino.
URL completa construída:
https://mbxrepo.azureedge.net/prod/repository/places/nome-da-cidade/0_298x160.webpExemplo de Implementação
const MOBLIX_CDN_BASE_URL = 'https://mbxrepo.azureedge.net/prod';
const IMAGE_SIZE = '_298x160.webp';
function buildImageUrl(imagePath) {
if (!imagePath) return null;
// Remove "repository/" se estiver no início
let cleanPath = imagePath.replace(/^repository\//, '');
// Converte para minúsculas (CDN é case-sensitive)
cleanPath = cleanPath.toLowerCase();
// Adiciona tamanho se não tiver extensão
if (!cleanPath.includes('.webp') && !cleanPath.includes('.jpg')) {
cleanPath = `${cleanPath}${IMAGE_SIZE}`;
}
// Encode para URL
const pathParts = cleanPath.split('/');
const encodedPath = pathParts.map(part => encodeURIComponent(part)).join('/');
return `${MOBLIX_CDN_BASE_URL}/${encodedPath}`;
}
// Função para obter primeira imagem válida
function getFirstValidImage(imagePaths) {
if (!imagePaths || !Array.isArray(imagePaths) || imagePaths.length === 0) {
return null;
}
// Retorna a primeira imagem válida do array
for (const path of imagePaths) {
const url = buildImageUrl(path);
if (url) return url;
}
return null;
}
// Função para obter imagem aleatória (recomendado para variedade visual)
// Usa seed para garantir que mesma oferta sempre mostre mesma imagem
// mas diferentes ofertas mostrem imagens diferentes
function getRandomValidImage(imagePaths, seed) {
if (!imagePaths || !Array.isArray(imagePaths) || imagePaths.length === 0) {
return null;
}
// Filtrar apenas imagens válidas
const validImages = [];
for (const path of imagePaths) {
const url = buildImageUrl(path);
if (url) validImages.push(url);
}
if (validImages.length === 0) return null;
if (validImages.length === 1) return validImages[0];
// Usar seed para seleção determinística mas variada
let randomIndex;
if (seed !== undefined) {
// Gerar número pseudo-aleatório baseado no seed
const seedStr = String(seed);
let hash = 0;
for (let i = 0; i < seedStr.length; i++) {
const char = seedStr.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
randomIndex = Math.abs(hash) % validImages.length;
} else {
randomIndex = Math.floor(Math.random() * validImages.length);
}
return validImages[randomIndex];
}
// Uso: Seleção aleatória com seed (recomendado)
// O seed garante que cards com mesmo origem/destino mostrem imagens diferentes
const seed = oferta.idGeral || oferta.id || `${oferta.origem.iata}-${oferta.destino.iata}`;
const imagemDestino = getRandomValidImage(oferta.destino.urlImagens, seed);
const imagemOrigem = getRandomValidImage(oferta.origem.urlImagens, seed);
const imagemPrincipal = imagemDestino || imagemOrigem;
// Alternativa: Usar primeira imagem (mais simples, menos variedade)
// const imagemDestino = getFirstValidImage(oferta.destino.urlImagens);
// const imagemOrigem = getFirstValidImage(oferta.origem.urlImagens);
// const imagemPrincipal = imagemDestino || imagemOrigem;Prioridade de Imagens
- Destino primeiro: Use imagens de
destino.urlImagensse disponível - Origem como fallback: Use imagens de
origem.urlImagensse destino não tiver imagem - Seleção aleatória: Recomenda-se usar
getRandomValidImagecom seed para que cards com mesmo origem/destino mostrem imagens diferentes, criando variedade visual - Placeholder: Se nenhuma imagem estiver disponível, exiba um placeholder ou ícone
Múltiplas Imagens
A API retorna urlImagens como um array que pode conter 0 a 5 imagens (geralmente 1-3). Cada imagem representa uma foto diferente do mesmo destino. Use seleção aleatória para garantir que diferentes ofertas com mesmo origem/destino exibam imagens variadas.
Exemplo no React
function OfertaCard({ oferta }) {
// Obter primeira imagem válida (destino tem prioridade)
const imagemDestino = getFirstValidImage(oferta.destino.urlImagens);
const imagemOrigem = getFirstValidImage(oferta.origem.urlImagens);
const imagemPrincipal = imagemDestino || imagemOrigem;
return (
<div className="oferta-card">
{imagemPrincipal ? (
<img
src={imagemPrincipal}
alt={`${oferta.destino.cidade}, ${oferta.destino.pais}`}
className="w-full h-48 object-cover"
/>
) : (
<div className="w-full h-48 bg-gray-200 flex items-center justify-center">
<PlaneIcon />
</div>
)}
{/* resto do card */}
</div>
);
}Exemplos de Uso
Ofertas Internacionais
curl -X POST https://app.apidevoos.dev/api/ofertas/buscar \
-H "Authorization: Bearer sua_chave_aqui" \
-H "Content-Type: application/json" \
-d '{
"internacional": true,
"quantidade": 20,
"shuffle": true
}'Ofertas Nacionais
curl -X POST https://app.apidevoos.dev/api/ofertas/buscar \
-H "Authorization: Bearer sua_chave_aqui" \
-H "Content-Type: application/json" \
-d '{
"internacional": false,
"quantidade": 15,
"shuffle": false
}'Com Paginação
curl -X POST https://app.apidevoos.dev/api/ofertas/buscar \
-H "Authorization: Bearer sua_chave_aqui" \
-H "Content-Type: application/json" \
-d '{
"internacional": true,
"page": 1,
"pageSize": 10
}'Com Filtros de Origem e Destino
curl -X POST https://app.apidevoos.dev/api/ofertas/buscar \
-H "Authorization: Bearer sua_chave_aqui" \
-H "Content-Type: application/json" \
-d '{
"internacional": true,
"origem": "GRU",
"destino": "LIS",
"quantidade": 20,
"shuffle": true
}'Nota: Os filtros de origem e destino são aplicados diretamente na API, resultando em melhor performance ao filtrar grandes volumes de ofertas.
Apenas Ofertas com Milhas
curl -X POST https://app.apidevoos.dev/api/ofertas/buscar \
-H "Authorization: Bearer sua_chave_aqui" \
-H "Content-Type: application/json" \
-d '{
"internacional": true,
"apenasMilhas": true,
"quantidade": 20,
"shuffle": true
}'Nota: Quando apenasMilhas é true, a API retorna apenas ofertas que podem ser pagas com milhas (pontos de programas de fidelidade como Smiles, LATAM Pass, Tudo Azul).
Implementação em React
import { useState, useEffect } from 'react';
const MOBLIX_CDN_BASE_URL = 'https://mbxrepo.azureedge.net/prod';
const IMAGE_SIZE = '_298x160.webp';
// Função para construir URL completa da imagem
function buildImageUrl(imagePath: string | undefined | null): string | null {
if (!imagePath) return null;
try {
let cleanPath = imagePath.replace(/^repository\//, '');
cleanPath = cleanPath.toLowerCase();
if (!cleanPath.includes('.webp') && !cleanPath.includes('.jpg')) {
cleanPath = `${cleanPath}${IMAGE_SIZE}`;
}
const pathParts = cleanPath.split('/');
const encodedPath = pathParts.map(part => encodeURIComponent(part)).join('/');
const fullUrl = `${MOBLIX_CDN_BASE_URL}/${encodedPath}`;
new URL(fullUrl);
return fullUrl;
} catch {
return null;
}
}
// Função para obter primeira imagem válida
function getFirstValidImage(imagePaths: string[] | undefined | null): string | null {
if (!imagePaths || !Array.isArray(imagePaths) || imagePaths.length === 0) {
return null;
}
for (const path of imagePaths) {
const url = buildImageUrl(path);
if (url) return url;
}
return null;
}
// Função para obter imagem aleatória (recomendado)
function getRandomValidImage(
imagePaths: string[] | undefined | null,
seed?: string | number
): string | null {
if (!imagePaths || !Array.isArray(imagePaths) || imagePaths.length === 0) {
return null;
}
const validImages: string[] = [];
for (const path of imagePaths) {
const url = buildImageUrl(path);
if (url) validImages.push(url);
}
if (validImages.length === 0) return null;
if (validImages.length === 1) return validImages[0];
let randomIndex: number;
if (seed !== undefined) {
const seedStr = String(seed);
let hash = 0;
for (let i = 0; i < seedStr.length; i++) {
const char = seedStr.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
randomIndex = Math.abs(hash) % validImages.length;
} else {
randomIndex = Math.floor(Math.random() * validImages.length);
}
return validImages[randomIndex];
}
interface Oferta {
id: string;
origem: {
iata: string;
cidade: string;
pais: string;
urlImagens?: string[];
};
destino: {
iata: string;
cidade: string;
pais: string;
urlImagens?: string[];
};
precoMinimo: number;
moeda: string;
companhiaAerea: {
codigo: string;
nome: string;
};
}
function OfertasVoos() {
const [ofertas, setOfertas] = useState<Oferta[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function carregarOfertas() {
try {
const response = await fetch('/api/ofertas/buscar', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.NEXT_PUBLIC_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
internacional: true,
quantidade: 12,
shuffle: true
})
});
const data = await response.json();
if (data.success) {
setOfertas(data.data);
}
} catch (error) {
console.error('Erro ao carregar ofertas:', error);
} finally {
setLoading(false);
}
}
carregarOfertas();
}, []);
if (loading) {
return <div>Carregando ofertas...</div>;
}
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{ofertas.map((oferta) => {
// Obter imagem aleatória (destino tem prioridade)
// Usa seed para garantir variedade visual entre cards com mesmo origem/destino
const seed = oferta.idGeral || oferta.id || `${oferta.origem.iata}-${oferta.destino.iata}`;
const imagemDestino = getRandomValidImage(oferta.destino.urlImagens, seed);
const imagemOrigem = getRandomValidImage(oferta.origem.urlImagens, seed);
const imagemPrincipal = imagemDestino || imagemOrigem;
return (
<div key={oferta.id} className="border rounded-lg overflow-hidden shadow-lg">
{imagemPrincipal ? (
<img
src={imagemPrincipal}
alt={`${oferta.destino.cidade}, ${oferta.destino.pais}`}
className="w-full h-48 object-cover"
/>
) : (
<div className="w-full h-48 bg-gray-200 flex items-center justify-center">
<Plane className="h-12 w-12 text-gray-400" />
</div>
)}
<div className="p-4">
<div className="text-sm text-gray-600">
{oferta.origem.cidade} → {oferta.destino.cidade}
</div>
<div className="text-2xl font-bold mt-2">
{oferta.moeda} {oferta.precoMinimo.toFixed(2)}
</div>
<div className="text-sm text-gray-500 mt-1">
{oferta.companhiaAerea.nome}
</div>
<button className="mt-4 w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700">
Ver Detalhes
</button>
</div>
</div>
))}
</div>
);
}import { useState, useEffect } from 'react';
const MOBLIX_CDN_BASE_URL = 'https://mbxrepo.azureedge.net/prod';
const IMAGE_SIZE = '_298x160.webp';
// Função para construir URL completa da imagem
function buildImageUrl(imagePath) {
if (!imagePath) return null;
try {
let cleanPath = imagePath.replace(/^repository\//, '');
cleanPath = cleanPath.toLowerCase();
if (!cleanPath.includes('.webp') && !cleanPath.includes('.jpg')) {
cleanPath = `${cleanPath}${IMAGE_SIZE}`;
}
const pathParts = cleanPath.split('/');
const encodedPath = pathParts.map(part => encodeURIComponent(part)).join('/');
const fullUrl = `${MOBLIX_CDN_BASE_URL}/${encodedPath}`;
new URL(fullUrl);
return fullUrl;
} catch {
return null;
}
}
// Função para obter primeira imagem válida
function getFirstValidImage(imagePaths) {
if (!imagePaths || !Array.isArray(imagePaths) || imagePaths.length === 0) {
return null;
}
for (const path of imagePaths) {
const url = buildImageUrl(path);
if (url) return url;
}
return null;
}
// Função para obter imagem aleatória (recomendado)
function getRandomValidImage(imagePaths, seed) {
if (!imagePaths || !Array.isArray(imagePaths) || imagePaths.length === 0) {
return null;
}
const validImages = [];
for (const path of imagePaths) {
const url = buildImageUrl(path);
if (url) validImages.push(url);
}
if (validImages.length === 0) return null;
if (validImages.length === 1) return validImages[0];
let randomIndex;
if (seed !== undefined) {
const seedStr = String(seed);
let hash = 0;
for (let i = 0; i < seedStr.length; i++) {
const char = seedStr.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
randomIndex = Math.abs(hash) % validImages.length;
} else {
randomIndex = Math.floor(Math.random() * validImages.length);
}
return validImages[randomIndex];
}
function OfertasVoos() {
const [ofertas, setOfertas] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function carregarOfertas() {
try {
const response = await fetch('/api/ofertas/buscar', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.NEXT_PUBLIC_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
internacional: true,
quantidade: 12,
shuffle: true
})
});
const data = await response.json();
if (data.success) {
setOfertas(data.data);
}
} catch (error) {
console.error('Erro ao carregar ofertas:', error);
} finally {
setLoading(false);
}
}
carregarOfertas();
}, []);
if (loading) {
return <div>Carregando ofertas...</div>;
}
return (
<div className="ofertas-grid">
{ofertas.map((oferta) => {
// Seleção aleatória com seed para variedade visual
const seed = oferta.idGeral || oferta.id || `${oferta.origem.iata}-${oferta.destino.iata}`;
const imagemDestino = getRandomValidImage(oferta.destino.urlImagens, seed);
const imagemOrigem = getRandomValidImage(oferta.origem.urlImagens, seed);
const imagemPrincipal = imagemDestino || imagemOrigem;
return (
<div key={oferta.id} className="oferta-card">
{imagemPrincipal && (
<img src={imagemPrincipal} alt={`${oferta.destino.cidade}, ${oferta.destino.pais}`} />
)}
<div className="oferta-content">
<div className="rota">
{oferta.origem.cidade} → {oferta.destino.cidade}
</div>
<div className="preco">
{oferta.moeda} {oferta.precoMinimo.toFixed(2)}
</div>
<div className="companhia">
{oferta.companhiaAerea.nome}
</div>
<button>Ver Detalhes</button>
</div>
</div>
);
})}
</div>
);
}Casos de Uso
1. Home Page - Destaques
Exiba ofertas em destaque na página inicial:
// Buscar 6 ofertas internacionais aleatórias
const response = await fetch('/api/ofertas/buscar', {
method: 'POST',
body: JSON.stringify({
internacional: true,
quantidade: 6,
shuffle: true
})
});2. Landing Page - Destino Específico
Filtre ofertas por origem e destino diretamente na API:
// Buscar ofertas de São Paulo para Miami
const response = await fetch('/api/ofertas/buscar', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
internacional: true,
origem: 'GRU',
destino: 'MIA',
quantidade: 10,
shuffle: true
})
});
const ofertas = await response.json();Vantagem: Filtros aplicados no servidor resultam em menos dados transferidos e melhor performance.
3. Newsletter - Ofertas Semanais
Envie ofertas diferentes a cada semana:
// Sem shuffle para manter consistência
const response = await fetch('/api/ofertas/buscar', {
method: 'POST',
body: JSON.stringify({
internacional: true,
quantidade: 10,
shuffle: false
})
});4. Comparador de Preços
Mostre as melhores ofertas por região:
const [nacionais, internacionais] = await Promise.all([
fetch('/api/ofertas/buscar', {
method: 'POST',
body: JSON.stringify({ internacional: false, quantidade: 20 })
}).then(r => r.json()),
fetch('/api/ofertas/buscar', {
method: 'POST',
body: JSON.stringify({ internacional: true, quantidade: 20 })
}).then(r => r.json())
]);5. Buscar Apenas Ofertas com Milhas
Filtre ofertas que podem ser pagas com milhas:
// Buscar apenas ofertas com milhas
const response = await fetch('/api/ofertas/buscar', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
internacional: true,
apenasMilhas: true,
quantidade: 15,
shuffle: true
})
});
const data = await response.json();
// As ofertas retornadas terão pontosAdulto > 0
// indicando que aceitam pagamento com milhasIntegração com Busca de Voos
Quando um usuário clicar em uma oferta, redirecione para a busca de voos:
function handleOfertaClick(oferta) {
// Calcular data de partida (ex: 30 dias à frente)
const departureDate = new Date();
departureDate.setDate(departureDate.getDate() + 30);
// Calcular data de retorno (ex: 7 dias depois)
const returnDate = new Date(departureDate);
returnDate.setDate(returnDate.getDate() + 7);
// Redirecionar para busca
const searchParams = new URLSearchParams({
origin: oferta.origem.iata,
destination: oferta.destino.iata,
departureDate: departureDate.toISOString().split('T')[0],
returnDate: returnDate.toISOString().split('T')[0],
passengers: '1',
cabinClass: 'economy'
});
window.location.href = `/voos/buscar?${searchParams.toString()}`;
}Dicas de UX
Melhores Práticas
- Embaralhe ofertas para evitar repetição visual
- Exiba imagens atraentes dos destinos usando seleção aleatória para variedade
- Destaque o preço como elemento principal
- Adicione CTAs claros como "Ver Voos" ou "Buscar"
- Mostre restrições de forma clara mas não intrusiva
- Use cache de 1-4 horas (ofertas mudam pouco)
- Selecione imagens aleatoriamente do array
urlImagenspara que cards com mesmo origem/destino mostrem imagens diferentes
Layout Sugerido
┌───────────────────────────────────┐
│ [Imagem do Destino] │
│ │
├───────────────────────────────────┤
│ São Paulo → Miami │
│ │
│ R$ 1.299,00 │
│ LATAM Airlines │
│ │
│ [ Ver Voos ] │
└───────────────────────────────────┘Cache e Performance
Recomendações de Cache
// Próximo.js - Cache de 2 horas
export const revalidate = 7200;
// Ou usar SWR
import useSWR from 'swr';
function OfertasVoos() {
const { data, error } = useSWR(
'ofertas',
() => fetch('/api/ofertas/buscar', {
method: 'POST',
body: JSON.stringify({ internacional: true, quantidade: 12 })
}).then(r => r.json()),
{
refreshInterval: 7200000, // 2 horas
revalidateOnFocus: false
}
);
}Performance
- Latência típica: 200-500ms
- Cache recomendado: 1-4 horas
- Refresh: A cada 2-4 horas para manter ofertas atualizadas
Erros Comuns
| Código | Erro | Solução |
|---|---|---|
401 | Unauthorized | Verifique sua API key |
400 | Invalid parameters | Verifique tipos dos parâmetros |
429 | Quota exceeded | Reduza frequência ou adicione créditos |
500 | Internal server error | Tente novamente ou contate suporte |
Próximos Passos
Após exibir ofertas, integre com:
- Busca de Voos - Para buscar voos específicos
- Calendário de Preços - Para mostrar variação de preços
- Autocomplete - Para formulários de busca