Journey Docs
Members APIDiscover

Get Featured Content

Get personalized featured content for the discover screen

GET/members/me/features/discover/featured

Overview

Returns personalized featured content for the discover screen, including perks, upcoming/current stays, featured properties, and offers. Content is customized based on membership tier, preferences, and booking history.

Authentication

Requires a valid Clerk JWT token in the Authorization header.

Query Parameters

propertiesLimitquerynumber

Maximum number of featured properties to include

propertiesOffsetquerynumber

Offset for featured properties pagination

Request

curl -X GET "http://localhost:3000/v1/members/me/features/discover/featured" \
  -H "Authorization: Bearer YOUR_CLERK_JWT"

Response

Returns an array of featured content items, each with a specific type and data structure.

itemsarrayrequired

Array of featured content items

Content Types
itemTypestringrequired

Type of featured content: "perk", "offer", "stay-past", "stay-current", "stay-upcoming", or "property"

itemDataobjectrequired

Content data (structure varies by itemType)

Response

200 Response - Mixed Featured Content
[
  {
    "itemType": "perk",
    "itemData": {
      "id": 123,
      "documentId": "perk_luxury_airport_lounge",
      "name": "Priority Airport Lounge Access",
      "description": "Access to premium airport lounges worldwide",
      "headline": "Relax in style before your flight",
      "membershipTierName": "Premium",
      "image": {
        "url": "https://cdn.example.com/perks/lounge-access.jpg",
        "alt": "Airport lounge interior"
      },
      "icon": {
        "url": "https://cdn.example.com/icons/lounge.svg",
        "alt": "Lounge icon"
      }
    }
  },
  {
    "itemType": "stay-upcoming",
    "itemData": {
      "property": {
        "id": 456,
        "documentId": "prop_luxury_resort_bali",
        "type": "Resort",
        "name": "Luxury Resort Bali",
        "brandName": "Journey Select",
        "maxGuests": 4,
        "nightlyCost": 350,
        "address": {
          "city": "Ubud",
          "state": "Bali",
          "country": "Indonesia",
          "latitude": -8.5069,
          "longitude": 115.2625
        },
        "tagIds": [1, 5, 12],
        "heroImage": {
          "url": "https://cdn.example.com/properties/bali-resort-hero.jpg",
          "alt": "Luxury resort pool view"
        }
      },
      "booking": {
        "id": 789,
        "documentId": "booking_456_789",
        "bookingStatus": "CONFIRMED",
        "checkInDate": "2025-02-15",
        "checkOutDate": "2025-02-20",
        "guestCount": 2,
        "totalPrice": 1750
      }
    }
  },
  {
    "itemType": "offer",
    "itemData": {
      "offerId": "offer_spa_package_2025",
      "name": "Rejuvenating Spa Package",
      "shortDescription": "3-hour luxury spa treatment",
      "description": "Complete wellness experience including massage, facial, and access to spa facilities",
      "pointsRequired": 15000,
      "retailValue": 450,
      "redemptionType": "spa_treatment",
      "heroImageUrl": "https://cdn.example.com/offers/spa-package.jpg",
      "howToRedeem": "Book directly through Journey app or website",
      "rules": "Valid at participating spa locations. Subject to availability.",
      "isGlobal": true,
      "partnerLabel": "Luxury Spa Partners"
    }
  },
  {
    "itemType": "property",
    "itemData": {
      "id": 321,
      "documentId": "prop_mountain_lodge_aspen",
      "name": "Mountain Lodge Aspen",
      "brandName": "Journey Collection",
      "type": "Lodge",
      "baseRoomCategory": "Mountain View Suite",
      "nightlyCost": 480,
      "guestRating": 4.8,
      "maxGuests": 6,
      "amenities": ["Ski-in/Ski-out", "Spa", "Fine Dining"],
      "address": {
        "city": "Aspen",
        "state": "Colorado",
        "country": "United States",
        "latitude": 39.1911,
        "longitude": -106.8175
      },
      "heroImage": {
        "url": "https://cdn.example.com/properties/aspen-lodge-hero.jpg",
        "alt": "Mountain lodge exterior with snow"
      },
      "gallery": [
        {
          "url": "https://cdn.example.com/properties/aspen-lodge-1.jpg",
          "alt": "Lodge interior"
        },
        {
          "url": "https://cdn.example.com/properties/aspen-lodge-2.jpg",
          "alt": "Mountain view from room"
        }
      ]
    }
  }
]

Content Types

Perk Items (itemType: "perk")

  • Purpose: Display membership tier benefits and perks
  • Personalization: Based on current membership tier
  • Examples: Lounge access, room upgrades, late checkout

Offer Items (itemType: "offer")

  • Purpose: Show available point redemption offers
  • Personalization: Based on point balance and preferences
  • Examples: Spa treatments, dining credits, experience packages

Stay Items (itemType: "stay-past" | "stay-current" | "stay-upcoming")

  • Purpose: Highlight past, current, or upcoming reservations
  • stay-past: Completed stays with rebooking options
  • stay-current: Active reservations with check-in details
  • stay-upcoming: Future reservations with trip planning

Property Items (itemType: "property")

  • Purpose: Featured properties for discovery
  • Personalization: Based on search history and preferences
  • Examples: New properties, trending destinations, seasonal recommendations

Personalization Factors

Membership Tier

  • Benefits: Tier-specific perks highlighted
  • Offers: Tier-exclusive redemption options
  • Properties: Access to tier-restricted properties

Booking History

  • Past Stays: Similar properties recommended
  • Preferences: Property types and locations
  • Seasonal Patterns: Timing-based recommendations

Current Context

  • Active Bookings: Trip-related content prioritized
  • Point Balance: Affordable offers highlighted
  • Location: Regional properties and offers

Use Cases

const FeaturedHeroCarousel = () => {
  const [featuredContent, setFeaturedContent] = useState([]);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    const loadFeaturedContent = async () => {
      try {
        const response = await fetch('/api/members/me/features/discover/featured?propertiesLimit=5');
        const content = await response.json();
        setFeaturedContent(content);
      } catch (error) {
        console.error('Failed to load featured content:', error);
      } finally {
        setLoading(false);
      }
    };
    
    loadFeaturedContent();
  }, []);
  
  if (loading) return <FeaturedContentSkeleton />;
  
  return (
    <Carousel>
      {featuredContent.map((item, index) => (
        <FeaturedCard key={index} item={item} />
      ))}
    </Carousel>
  );
};

Content Type Handling

const FeaturedCard = ({ item }) => {
  switch (item.itemType) {
    case 'perk':
      return <PerkCard perk={item.itemData} />;
      
    case 'offer':
      return <OfferCard offer={item.itemData} />;
      
    case 'stay-upcoming':
      return <UpcomingStayCard stay={item.itemData} />;
      
    case 'stay-current':
      return <CurrentStayCard stay={item.itemData} />;
      
    case 'stay-past':
      return <PastStayCard stay={item.itemData} />;
      
    case 'property':
      return <PropertyCard property={item.itemData} />;
      
    default:
      return null;
  }
};

Personalized Loading

const loadPersonalizedFeatured = async (preferences = {}) => {
  const params = new URLSearchParams();
  
  // Limit properties based on screen size
  const isMobile = window.innerWidth < 768;
  params.set('propertiesLimit', isMobile ? '3' : '8');
  
  // Offset for pagination
  if (preferences.offset) {
    params.set('propertiesOffset', preferences.offset.toString());
  }
  
  const response = await fetch(`/api/members/me/features/discover/featured?${params}`);
  return response.json();
};

Content Filtering

Client-side Filtering

const filterFeaturedContent = (content, filters) => {
  return content.filter(item => {
    // Filter by content type
    if (filters.contentTypes && !filters.contentTypes.includes(item.itemType)) {
      return false;
    }
    
    // Filter offers by point requirements
    if (item.itemType === 'offer' && filters.maxPoints) {
      if (item.itemData.pointsRequired > filters.maxPoints) {
        return false;
      }
    }
    
    // Filter properties by price range
    if (item.itemType === 'property' && filters.priceRange) {
      const price = item.itemData.nightlyCost;
      if (price < filters.priceRange.min || price > filters.priceRange.max) {
        return false;
      }
    }
    
    return true;
  });
};

Content Prioritization

const prioritizeFeaturedContent = (content, userContext) => {
  return content.sort((a, b) => {
    // Prioritize upcoming stays
    if (a.itemType.startsWith('stay-') && !b.itemType.startsWith('stay-')) {
      return -1;
    }
    
    // Prioritize affordable offers
    if (a.itemType === 'offer' && b.itemType === 'offer') {
      const aAffordable = a.itemData.pointsRequired <= userContext.pointBalance;
      const bAffordable = b.itemData.pointsRequired <= userContext.pointBalance;
      
      if (aAffordable && !bAffordable) return -1;
      if (!aAffordable && bAffordable) return 1;
    }
    
    return 0;
  });
};

Performance Optimization

Caching Strategy

const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
const featuredContentCache = new Map();

const getCachedFeaturedContent = async (userId, params = {}) => {
  const cacheKey = `featured_${userId}_${JSON.stringify(params)}`;
  const cached = featuredContentCache.get(cacheKey);
  
  if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
    return cached.data;
  }
  
  const fresh = await loadPersonalizedFeatured(params);
  featuredContentCache.set(cacheKey, {
    data: fresh,
    timestamp: Date.now()
  });
  
  return fresh;
};

Progressive Loading

const useProgressiveFeaturedContent = () => {
  const [content, setContent] = useState([]);
  const [loading, setLoading] = useState(true);
  const [loadingMore, setLoadingMore] = useState(false);
  
  const loadInitial = useCallback(async () => {
    setLoading(true);
    try {
      const initial = await fetch('/api/members/me/features/discover/featured?propertiesLimit=3');
      const data = await initial.json();
      setContent(data);
    } finally {
      setLoading(false);
    }
  }, []);
  
  const loadMore = useCallback(async () => {
    setLoadingMore(true);
    try {
      const more = await fetch(`/api/members/me/features/discover/featured?propertiesLimit=5&propertiesOffset=${content.length}`);
      const data = await more.json();
      setContent(prev => [...prev, ...data]);
    } finally {
      setLoadingMore(false);
    }
  }, [content.length]);
  
  return { content, loading, loadingMore, loadInitial, loadMore };
};

On this page