Journey Docs
Public APIProperties

Get Trending Searches

Get popular search destinations across all members

GET/public/properties/web/search/trending

Overview

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: 4

Number of unique locations to return (1-50)

offsetquerynumberdefault: 0

Number of items to skip for pagination

daysBackquerynumberdefault: 30

How many days back to count searches (1-365)

Request

curl -X GET "https://api.journey.com/v1/public/properties/web/search/trending"

Response

itemsarrayrequired

Array of trending search items (one per location)

trending search items
locationobjectrequired

Location object from the search

location object
placeIdstring

Google Places ID for location-based searches

querystring

Search query text used

boundsobject

Geographic bounds for map-based searches

bounds object
northeastobjectrequired

Northeast corner coordinates

southwestobjectrequired

Southwest corner coordinates

searchTypestringrequired

Type of search performed
Options: location, property, bounds

checkInstring

Check-in date (YYYY-MM-DD) or null if not specified

checkOutstring

Check-out date (YYYY-MM-DD) or null if not specified

flexibilitystring|object

Flexibility setting for flexible searches (weekend/week/month or custom object)

guestsnumberrequired

Number of guests searched for

filtersobjectrequired

Search filters applied (price, property types, etc.)

createdAtstringrequired

When this trending search pattern was last recorded (ISO timestamp)

searchCountnumber

Number of times this pattern was searched (may be omitted for privacy)

popularityScorenumber

Relative popularity score (0-100)

limitnumberrequired

Page size (limit) used

offsetnumberrequired

Offset for pagination

totalnumberrequired

Total 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

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);

On this page