Get Trending Searches
Get popular search destinations across all members
/public/properties/web/search/trendingOverview
Returns trending searches across all members, providing unique results per location ordered by popularity within a specified time window. No authentication required, making this useful for showcasing popular destinations to all users.
Authentication
No authentication required. This is a public endpoint with rate limiting.
Rate Limiting
- Limit: 20 requests per minute per IP address
- Window: 60 seconds
- Headers: Rate limit information included in response headers
Query Parameters
limitquerynumberdefault: 4Number of unique locations to return (1-50)
offsetquerynumberdefault: 0Number of items to skip for pagination
daysBackquerynumberdefault: 30How many days back to count searches (1-365)
Request
curl -X GET "https://api.journey.com/v1/public/properties/web/search/trending"Response
itemsarrayrequiredArray of trending search items (one per location)
trending search items
locationobjectrequiredLocation object from the search
location object
placeIdstringGoogle Places ID for location-based searches
querystringSearch query text used
boundsobjectGeographic bounds for map-based searches
bounds object
northeastobjectrequiredNortheast corner coordinates
southwestobjectrequiredSouthwest corner coordinates
searchTypestringrequiredType of search performed
Options: location, property, bounds
checkInstringCheck-in date (YYYY-MM-DD) or null if not specified
checkOutstringCheck-out date (YYYY-MM-DD) or null if not specified
flexibilitystring|objectFlexibility setting for flexible searches (weekend/week/month or custom object)
guestsnumberrequiredNumber of guests searched for
filtersobjectrequiredSearch filters applied (price, property types, etc.)
createdAtstringrequiredWhen this trending search pattern was last recorded (ISO timestamp)
searchCountnumberNumber of times this pattern was searched (may be omitted for privacy)
popularityScorenumberRelative popularity score (0-100)
limitnumberrequiredPage size (limit) used
offsetnumberrequiredOffset for pagination
totalnumberrequiredTotal number of items returned (may be less than limit)
Response
{
"items": [
{
"location": {
"placeId": "ChIJ-wSojYBEQogR5PMnOeL97CE",
"query": "Cancun, Mexico"
},
"searchType": "location",
"checkIn": null,
"checkOut": null,
"flexibility": "weekend",
"guests": 2,
"filters": {
"price": {
"min": 200,
"max": 600
},
"propertyTypes": ["resort"]
},
"createdAt": "2025-01-15T14:30:00.000Z",
"popularityScore": 95
},
{
"location": {
"placeId": "ChIJ88g-4Y8FGGERd8UiUqT7AUl",
"query": "Tulum, Mexico"
},
"searchType": "location",
"checkIn": "2025-02-14",
"checkOut": "2025-02-17",
"flexibility": null,
"guests": 2,
"filters": {
"price": {
"min": 150,
"max": 400
},
"predefinedFilters": [1, 3]
},
"createdAt": "2025-01-15T12:45:00.000Z",
"popularityScore": 87
},
{
"location": {
"placeId": "ChIJdd4hrwm2hEcRmSrV3Vo6llI",
"query": "Rome, Italy"
},
"searchType": "location",
"checkIn": null,
"checkOut": null,
"flexibility": {
"months": ["2025-04", "2025-05", "2025-06"]
},
"guests": 4,
"filters": {
"propertyTypes": ["hotel", "boutique"],
"rooms": {
"bedrooms": 2
}
},
"createdAt": "2025-01-15T10:20:00.000Z",
"popularityScore": 82
},
{
"location": {
"placeId": "ChIJOwg_06VPwokRYv534QaPC8g",
"query": "New York, NY"
},
"searchType": "location",
"checkIn": "2025-03-15",
"checkOut": "2025-03-18",
"flexibility": null,
"guests": 1,
"filters": {
"price": {
"max": 300
},
"predefinedFilters": [2, 5]
},
"createdAt": "2025-01-15T09:15:00.000Z",
"popularityScore": 78
}
],
"limit": 4,
"offset": 0,
"total": 4
}Use Cases
Trending Destinations Showcase
const TrendingDestinations = ({ timeframe = 'week' }) => {
const [trending, setTrending] = useState([]);
const [loading, setLoading] = useState(true);
const daysBack = {
week: 7,
month: 30,
quarter: 90
}[timeframe] || 30;
useEffect(() => {
const fetchTrending = async () => {
setLoading(true);
try {
const data = await getTrendingSearches({
limit: 8,
daysBack
});
setTrending(data.items);
} catch (error) {
console.error('Failed to fetch trending searches:', error);
} finally {
setLoading(false);
}
};
fetchTrending();
}, [daysBack]);
const formatLocation = (item) => {
if (item.location.query) {
return item.location.query;
}
if (item.location.bounds) {
// Try to derive location from bounds (could use reverse geocoding)
return 'Popular Area';
}
return 'Unknown Location';
};
const formatSearchDetails = (item) => {
const details = [];
if (item.guests > 1) {
details.push(`${item.guests} guests`);
}
if (item.flexibility) {
if (typeof item.flexibility === 'string') {
details.push(`${item.flexibility} stays`);
} else if (item.flexibility.months) {
details.push(`flexible dates`);
}
} else if (item.checkIn && item.checkOut) {
const checkIn = new Date(item.checkIn);
const checkOut = new Date(item.checkOut);
const nights = Math.ceil((checkOut - checkIn) / (1000 * 60 * 60 * 24));
details.push(`${nights} nights`);
}
if (item.filters.price?.min || item.filters.price?.max) {
const min = item.filters.price.min || 0;
const max = item.filters.price.max || '∞';
details.push(`$${min}-${max}/night`);
}
return details.join(' • ');
};
const handleTrendingClick = (item) => {
// Navigate to search with similar parameters
const searchParams = {
location: item.location,
guests: item.guests,
filters: item.filters
};
if (item.flexibility) {
searchParams.flexibility = item.flexibility;
} else if (item.checkIn && item.checkOut) {
searchParams.checkIn = item.checkIn;
searchParams.checkOut = item.checkOut;
}
// Navigate to search page
window.location.href = `/search?${encodeURIComponent(JSON.stringify(searchParams))}`;
};
if (loading) {
return (
<div className="trending-destinations loading">
<h2>Trending Destinations</h2>
<div className="loading-placeholder">Loading trending searches...</div>
</div>
);
}
return (
<div className="trending-destinations">
<div className="trending-header">
<h2>Trending This {timeframe.charAt(0).toUpperCase() + timeframe.slice(1)}</h2>
<p>Discover where Journey members are planning their next adventures</p>
</div>
<div className="trending-grid">
{trending.map((item, index) => (
<div
key={index}
className="trending-card"
onClick={() => handleTrendingClick(item)}
>
<div className="trend-rank">#{index + 1}</div>
<div className="trend-content">
<h3 className="destination">{formatLocation(item)}</h3>
<div className="search-details">
{formatSearchDetails(item)}
</div>
<div className="popularity">
<div className="popularity-bar">
<div
className="popularity-fill"
style={{ width: `${item.popularityScore}%` }}
/>
</div>
<span className="popularity-score">{item.popularityScore}% popularity</span>
</div>
<div className="search-type">
{item.searchType === 'location' && '📍 Location search'}
{item.searchType === 'bounds' && '🗺️ Area search'}
{item.searchType === 'property' && '🏨 Property search'}
</div>
</div>
<div className="trend-arrow">→</div>
</div>
))}
</div>
{trending.length === 0 && (
<div className="no-trends">
<h3>No trending searches</h3>
<p>Check back soon for popular destinations!</p>
</div>
)}
</div>
);
};Homepage Inspiration Section
const HomepageInspiration = () => {
const [weeklyTrends, setWeeklyTrends] = useState([]);
const [monthlyTrends, setMonthlyTrends] = useState([]);
useEffect(() => {
const fetchTrends = async () => {
try {
const [weekly, monthly] = await Promise.all([
getTrendingSearches({ limit: 4, daysBack: 7 }),
getTrendingSearches({ limit: 4, daysBack: 30 })
]);
setWeeklyTrends(weekly.items);
setMonthlyTrends(monthly.items);
} catch (error) {
console.error('Failed to fetch trends:', error);
}
};
fetchTrends();
}, []);
const renderTrendingLocation = (item, index) => {
const location = item.location.query || 'Popular Area';
const isFlexible = !!item.flexibility;
const hasSpecificDates = !!(item.checkIn && item.checkOut);
return (
<div
key={index}
className="inspiration-card"
onClick={() => navigateToSearch(item)}
>
<div className="card-content">
<h4>{location}</h4>
<div className="search-pattern">
{item.guests > 1 && <span>{item.guests} guests</span>}
{isFlexible && <span>Flexible dates</span>}
{hasSpecificDates && (
<span>
{new Date(item.checkIn).toLocaleDateString('en-US', {
month: 'short', day: 'numeric'
})} -
{new Date(item.checkOut).toLocaleDateString('en-US', {
month: 'short', day: 'numeric'
})}
</span>
)}
</div>
<div className="popularity-indicator">
<div className="trend-badge">
🔥 {item.popularityScore}% trending
</div>
</div>
</div>
</div>
);
};
return (
<section className="homepage-inspiration">
<div className="inspiration-header">
<h2>Travel Inspiration</h2>
<p>See where our community is exploring</p>
</div>
<div className="trends-sections">
<div className="trend-section">
<h3>🔥 Hot This Week</h3>
<div className="trend-cards">
{weeklyTrends.slice(0, 4).map(renderTrendingLocation)}
</div>
</div>
<div className="trend-section">
<h3>📈 Popular This Month</h3>
<div className="trend-cards">
{monthlyTrends.slice(0, 4).map(renderTrendingLocation)}
</div>
</div>
</div>
</section>
);
};Analytics Dashboard (Internal)
const TrendingAnalytics = () => {
const [trends, setTrends] = useState([]);
const [timeframe, setTimeframe] = useState(7);
const [analytics, setAnalytics] = useState(null);
useEffect(() => {
const fetchTrendingAnalytics = async () => {
try {
const data = await getTrendingSearches({
limit: 50,
daysBack: timeframe
});
setTrends(data.items);
// Calculate analytics
const analytics = calculateTrendingAnalytics(data.items);
setAnalytics(analytics);
} catch (error) {
console.error('Failed to fetch trending analytics:', error);
}
};
fetchTrendingAnalytics();
}, [timeframe]);
const calculateTrendingAnalytics = (items) => {
const searchTypes = items.reduce((acc, item) => {
acc[item.searchType] = (acc[item.searchType] || 0) + 1;
return acc;
}, {});
const destinations = items
.filter(item => item.location.query)
.map(item => item.location.query);
const avgGuests = items.reduce((sum, item) => sum + item.guests, 0) / items.length;
const flexibleSearches = items.filter(item => item.flexibility).length;
const dateSpecificSearches = items.filter(item => item.checkIn && item.checkOut).length;
return {
searchTypes,
topDestinations: destinations.slice(0, 10),
avgGuests: Math.round(avgGuests * 10) / 10,
flexibilityRate: (flexibleSearches / items.length) * 100,
specificDateRate: (dateSpecificSearches / items.length) * 100
};
};
return (
<div className="trending-analytics">
<div className="analytics-header">
<h2>Trending Search Analytics</h2>
<select
value={timeframe}
onChange={(e) => setTimeframe(parseInt(e.target.value))}
>
<option value={7}>Last 7 days</option>
<option value={30}>Last 30 days</option>
<option value={90}>Last 90 days</option>
</select>
</div>
{analytics && (
<div className="analytics-grid">
<div className="analytics-card">
<h3>Search Types</h3>
<div className="chart">
{Object.entries(analytics.searchTypes).map(([type, count]) => (
<div key={type} className="chart-bar">
<span>{type}: {count}</span>
</div>
))}
</div>
</div>
<div className="analytics-card">
<h3>Search Patterns</h3>
<div className="metrics">
<div>Avg guests: {analytics.avgGuests}</div>
<div>Flexible dates: {analytics.flexibilityRate.toFixed(1)}%</div>
<div>Specific dates: {analytics.specificDateRate.toFixed(1)}%</div>
</div>
</div>
<div className="analytics-card">
<h3>Top Destinations</h3>
<div className="destination-list">
{analytics.topDestinations.map((dest, index) => (
<div key={index}>{index + 1}. {dest}</div>
))}
</div>
</div>
</div>
)}
</div>
);
};Privacy and Data
Privacy Considerations
- No individual search tracking exposed
- Aggregated and anonymized data only
- Search counts may be omitted for additional privacy
- Popularity scores normalize individual behavior
Data Aggregation
- Unique per location: Only one result per geographic area
- Time-based: Results limited to specified time window
- Popularity ranking: Ordered by search frequency and recency
- Filter normalization: Common filter patterns highlighted
Rate Limiting
Implementation
- IP-based rate limiting
- Generous limits for public access
- Headers provide remaining quota information
- Designed for website integration
Best Practices
const withRateLimit = (apiCall) => {
return async (...args) => {
try {
return await apiCall(...args);
} catch (error) {
if (error.status === 429) {
// Handle rate limit with exponential backoff
const retryAfter = error.headers?.['Retry-After'] || 60;
console.warn(`Rate limited. Retrying after ${retryAfter}s`);
await new Promise(resolve =>
setTimeout(resolve, retryAfter * 1000)
);
return apiCall(...args);
}
throw error;
}
};
};
const rateLimitedGetTrending = withRateLimit(getTrendingSearches);Related Endpoints
- Property Search - Execute searches based on trending patterns