Journey Docs
Public APIProperties

Get Property Details

Get comprehensive property information without authentication

GET/public/properties/{propertyId}

Overview

Returns detailed property information including basic details, amenities, location, and optionally hydrated related data such as suggested places, key features, and restaurants. Available to all users without authentication.

Authentication

No authentication required. This is a public endpoint.

Path Parameters

propertyIdpathstringrequired

Property ID (internal numeric ID or document ID)

Query Parameters

hydratequerystring[]

Additional data to include in response
Options: suggestedPlaces, keyFeatures, restaurants

Request

curl -X GET "https://api.journey.com/v1/public/properties/abc123def456ghi789jkl012"

Response

idnumberrequired

Internal property ID

documentIdstringrequired

Strapi document ID

namestringrequired

Property name

slugstringrequired

URL-friendly property identifier

descriptionstring

Property description

shortDescriptionstring

Brief property description

propertyTypestringrequired

Type of property (hotel, resort, villa, etc.)

starRatingnumber

Star rating (1-5)

guestRatingnumber

Guest review rating

reviewCountnumber

Number of reviews

addressobjectrequired

Property location information

address object
streetstring

Street address

citystringrequired

City name

statestringrequired

State or province

countrystringrequired

Country name

postalCodestring

Postal/ZIP code

latitudenumberrequired

Latitude coordinate

longitudenumberrequired

Longitude coordinate

brandobject

Brand information

brand object
idnumberrequired

Brand ID

namestringrequired

Brand name

logostring

Brand logo URL

descriptionstring

Brand description

heroImageobject

Main property image

hero image
urlstringrequired

Full resolution image URL

thumbnailUrlstring

Thumbnail image URL

altstring

Alt text for accessibility

galleryarray

Property image gallery

gallery items
urlstringrequired

Image URL

thumbnailUrlstring

Thumbnail URL

altstring

Alt text

typestring

Image type (exterior, interior, amenity, etc.)

amenitiesstring[]required

List of property amenities

policiesobject

Property policies

policies object
checkInstring

Check-in time

checkOutstring

Check-out time

petPolicystring

Pet policy details

smokingPolicystring

Smoking policy

childPolicystring

Children policy

contactInfoobject

Property contact information

contact info
phonestring

Primary phone number

emailstring

Contact email

websitestring

Property website URL

suggestedPlacesarray

Nearby places of interest (when hydrated)

suggested places
namestringrequired

Place name

categorystring

Place category (restaurant, attraction, etc.)

distancenumber

Distance in kilometers

ratingnumber

Place rating

descriptionstring

Place description

keyFeaturesarray

Property key features (when hydrated)

key features
titlestringrequired

Feature title

descriptionstring

Feature description

iconstring

Feature icon identifier

restaurantsarray

On-property restaurants (when hydrated)

restaurants
namestringrequired

Restaurant name

cuisinestring

Cuisine type

descriptionstring

Restaurant description

hoursobject

Operating hours

reservationsRequiredboolean

Whether reservations are required

Response

{
  "id": 12345,
  "documentId": "abc123def456ghi789jkl012",
  "name": "Journey Beach Resort & Spa",
  "slug": "journey-beach-resort-spa",
  "description": "An oceanfront paradise offering luxury accommodations, world-class dining, and rejuvenating spa experiences. Perfect for romantic getaways and family vacations.",
  "shortDescription": "Oceanfront luxury resort with spa and dining",
  "propertyType": "Resort",
  "starRating": 5,
  "guestRating": 4.7,
  "reviewCount": 1247,
  "address": {
    "street": "123 Ocean Drive",
    "city": "Cancun",
    "state": "Quintana Roo",
    "country": "Mexico",
    "postalCode": "77500",
    "latitude": 21.1619,
    "longitude": -86.8515
  },
  "brand": {
    "id": 1,
    "name": "Journey Resorts",
    "logo": "https://cdn.journey.com/brands/journey-resorts-logo.svg",
    "description": "Luxury resorts in breathtaking destinations"
  },
  "heroImage": {
    "url": "https://cdn.journey.com/properties/beach-resort-hero.jpg",
    "thumbnailUrl": "https://cdn.journey.com/properties/beach-resort-hero-thumb.jpg",
    "alt": "Oceanfront resort with infinity pool at sunset"
  },
  "gallery": [
    {
      "url": "https://cdn.journey.com/properties/beach-resort-pool.jpg",
      "thumbnailUrl": "https://cdn.journey.com/properties/beach-resort-pool-thumb.jpg",
      "alt": "Infinity pool overlooking the ocean",
      "type": "amenity"
    },
    {
      "url": "https://cdn.journey.com/properties/beach-resort-suite.jpg",
      "thumbnailUrl": "https://cdn.journey.com/properties/beach-resort-suite-thumb.jpg",
      "alt": "Ocean view suite interior",
      "type": "interior"
    }
  ],
  "amenities": [
    "Private Beach",
    "Infinity Pool",
    "Full-Service Spa",
    "Fine Dining Restaurant",
    "Fitness Center",
    "Business Center",
    "Concierge Service",
    "Room Service",
    "WiFi",
    "Parking",
    "Airport Shuttle"
  ],
  "policies": {
    "checkIn": "3:00 PM",
    "checkOut": "11:00 AM",
    "petPolicy": "Pets not allowed",
    "smokingPolicy": "Non-smoking property",
    "childPolicy": "Children of all ages welcome"
  },
  "contactInfo": {
    "phone": "+52 998 123 4567",
    "email": "reservations@journeybeachresort.com",
    "website": "https://www.journeybeachresort.com"
  }
}

Alternative Endpoint: By Slug

You can also retrieve property details using a URL-friendly slug:

slugpathstringrequired

Property slug (URL-friendly identifier)

Request

curl -X GET "https://api.journey.com/v1/public/properties/by-slug/journey-beach-resort-spa?hydrate=keyFeatures"

Use Cases

Property Detail Page

const PropertyDetailPage = ({ propertyId }) => {
  const [property, setProperty] = useState(null);
  const [loading, setLoading] = useState(true);
  const [selectedImage, setSelectedImage] = useState(0);
  
  useEffect(() => {
    const fetchProperty = async () => {
      try {
        const data = await getPropertyDetails(propertyId, [
          'suggestedPlaces',
          'keyFeatures', 
          'restaurants'
        ]);
        setProperty(data);
      } catch (error) {
        console.error('Failed to fetch property:', error);
      } finally {
        setLoading(false);
      }
    };
    
    fetchProperty();
  }, [propertyId]);
  
  if (loading) return <div className="loading">Loading property details...</div>;
  if (!property) return <div className="error">Property not found</div>;
  
  return (
    <div className="property-detail">
      {/* Hero Section */}
      <div className="hero-section">
        <div className="image-gallery">
          <img 
            src={property.heroImage.url} 
            alt={property.heroImage.alt}
            className="hero-image"
          />
          <div className="gallery-thumbnails">
            {property.gallery?.map((image, index) => (
              <img
                key={index}
                src={image.thumbnailUrl}
                alt={image.alt}
                onClick={() => setSelectedImage(index)}
                className={selectedImage === index ? 'selected' : ''}
              />
            ))}
          </div>
        </div>
        
        <div className="property-header">
          <div className="brand-info">
            {property.brand?.logo && (
              <img src={property.brand.logo} alt={property.brand.name} />
            )}
            <span>{property.brand?.name}</span>
          </div>
          
          <h1>{property.name}</h1>
          <p className="short-description">{property.shortDescription}</p>
          
          <div className="rating-info">
            <div className="star-rating">
              {'★'.repeat(property.starRating || 0)}
              {'☆'.repeat(5 - (property.starRating || 0))}
            </div>
            
            {property.guestRating && (
              <div className="guest-rating">
                {property.guestRating}/5 ({property.reviewCount} reviews)
              </div>
            )}
          </div>
          
          <div className="location">
            📍 {property.address.city}, {property.address.country}
          </div>
        </div>
      </div>
      
      {/* Key Features */}
      {property.keyFeatures && (
        <section className="key-features">
          <h2>Why Stay Here</h2>
          <div className="features-grid">
            {property.keyFeatures.map((feature, index) => (
              <div key={index} className="feature-card">
                <div className={`feature-icon icon-${feature.icon}`}></div>
                <h3>{feature.title}</h3>
                <p>{feature.description}</p>
              </div>
            ))}
          </div>
        </section>
      )}
      
      {/* Description & Amenities */}
      <section className="details">
        <div className="description">
          <h2>About</h2>
          <p>{property.description}</p>
        </div>
        
        <div className="amenities">
          <h2>Amenities</h2>
          <div className="amenities-grid">
            {property.amenities.map((amenity, index) => (
              <div key={index} className="amenity-item">
                {amenity}
              </div>
            ))}
          </div>
        </div>
      </section>
      
      {/* Restaurants */}
      {property.restaurants && property.restaurants.length > 0 && (
        <section className="restaurants">
          <h2>Dining</h2>
          <div className="restaurants-list">
            {property.restaurants.map((restaurant, index) => (
              <div key={index} className="restaurant-card">
                <h3>{restaurant.name}</h3>
                <div className="cuisine-type">{restaurant.cuisine}</div>
                <p>{restaurant.description}</p>
                
                <div className="restaurant-details">
                  {restaurant.reservationsRequired && (
                    <span className="reservations-required">
                      Reservations Required
                    </span>
                  )}
                  
                  {restaurant.hours && (
                    <div className="hours">
                      {Object.entries(restaurant.hours).map(([period, time]) => (
                        <div key={period}>
                          <strong>{period}:</strong> {time}
                        </div>
                      ))}
                    </div>
                  )}
                </div>
              </div>
            ))}
          </div>
        </section>
      )}
      
      {/* Suggested Places */}
      {property.suggestedPlaces && property.suggestedPlaces.length > 0 && (
        <section className="nearby">
          <h2>Explore Nearby</h2>
          <div className="places-list">
            {property.suggestedPlaces.map((place, index) => (
              <div key={index} className="place-card">
                <h3>{place.name}</h3>
                <div className="place-meta">
                  <span className="category">{place.category}</span>
                  <span className="distance">{place.distance}km away</span>
                  {place.rating && (
                    <span className="rating">⭐ {place.rating}</span>
                  )}
                </div>
                <p>{place.description}</p>
              </div>
            ))}
          </div>
        </section>
      )}
      
      {/* Policies & Contact */}
      <section className="policies-contact">
        <div className="policies">
          <h3>Policies</h3>
          <div className="policy-item">
            <strong>Check-in:</strong> {property.policies?.checkIn}
          </div>
          <div className="policy-item">
            <strong>Check-out:</strong> {property.policies?.checkOut}
          </div>
          {property.policies?.petPolicy && (
            <div className="policy-item">
              <strong>Pets:</strong> {property.policies.petPolicy}
            </div>
          )}
        </div>
        
        <div className="contact">
          <h3>Contact</h3>
          {property.contactInfo?.phone && (
            <div>📞 {property.contactInfo.phone}</div>
          )}
          {property.contactInfo?.email && (
            <div>✉️ {property.contactInfo.email}</div>
          )}
          {property.contactInfo?.website && (
            <div>
              <a href={property.contactInfo.website} target="_blank" rel="noopener">
                🌐 Visit Website
              </a>
            </div>
          )}
        </div>
      </section>
    </div>
  );
};

SEO-Friendly URLs

// Use slug-based routing for better SEO
const PropertyRouter = () => {
  return (
    <Routes>
      {/* SEO-friendly route */}
      <Route 
        path="/properties/:slug" 
        element={<PropertyPageBySlug />} 
      />
      
      {/* Fallback for direct ID access */}
      <Route 
        path="/properties/id/:propertyId" 
        element={<PropertyPageById />} 
      />
    </Routes>
  );
};

const PropertyPageBySlug = () => {
  const { slug } = useParams();
  const [property, setProperty] = useState(null);
  
  useEffect(() => {
    const fetchProperty = async () => {
      try {
        const data = await getPropertyBySlug(slug, ['keyFeatures']);
        setProperty(data);
        
        // Update page title and meta tags
        document.title = `${data.name} | Journey Resorts`;
        
        // Update meta description
        const metaDesc = document.querySelector('meta[name="description"]');
        if (metaDesc) {
          metaDesc.content = data.shortDescription || data.description;
        }
      } catch (error) {
        console.error('Property not found:', error);
      }
    };
    
    fetchProperty();
  }, [slug]);
  
  return property ? <PropertyDetailPage property={property} /> : <NotFound />;
};

Data Optimization

Selective Hydration

// Only request additional data when needed
const usePropertyDetails = (propertyId, options = {}) => {
  const [property, setProperty] = useState(null);
  const [loading, setLoading] = useState(true);
  
  const {
    includeRestaurants = false,
    includeSuggestedPlaces = false,
    includeKeyFeatures = false
  } = options;
  
  useEffect(() => {
    const fetchProperty = async () => {
      const hydrate = [];
      if (includeRestaurants) hydrate.push('restaurants');
      if (includeSuggestedPlaces) hydrate.push('suggestedPlaces');
      if (includeKeyFeatures) hydrate.push('keyFeatures');
      
      try {
        const data = await getPropertyDetails(propertyId, hydrate);
        setProperty(data);
      } finally {
        setLoading(false);
      }
    };
    
    fetchProperty();
  }, [propertyId, includeRestaurants, includeSuggestedPlaces, includeKeyFeatures]);
  
  return { property, loading };
};

Performance Considerations

  • Request only needed hydrated data
  • Use appropriate image sizes (thumbnail vs full resolution)
  • Implement lazy loading for gallery images
  • Cache property data for repeated access

On this page