Journey Docs
Public APIProperties

Property Search

Search properties by location with availability and pricing (no authentication required)

POST/public/properties/web/search

Overview

Search for properties by location with optional dates, guest count, and filters. When dates are provided, returns availability status and pricing. No estimated points calculation since this is unauthenticated access showing public pricing.

Authentication

No authentication required. This is a public endpoint.

Request Body

locationbodyobjectrequired

Location search criteria

location object
placeIdbodystring

Google Places ID for location-based search

boundsbodyobject

Geographic bounds for map-based search

bounds object
northeastbodyobjectrequired
northeast coordinates
latitudebodynumberrequired
Northeast corner latitude
longitudebodynumberrequired
Northeast corner longitude
southwestbodyobjectrequired
southwest coordinates
latitudebodynumberrequired
Southwest corner latitude
longitudebodynumberrequired
Southwest corner longitude
propertyIdbodynumber

Specific property ID for single property search

checkInbodystring

Check-in date (YYYY-MM-DD). If provided, availability and pricing will be included

checkOutbodystring

Check-out date (YYYY-MM-DD). Required if checkIn is provided

guestsbodynumber

Number of guests (minimum: 1)

filtersbodyobject

Search filters

filters object
pricebodyobject

Price range filter

price filter
minbodynumber
Minimum price per night
maxbodynumber
Maximum price per night
predefinedFiltersbodynumber[]

Predefined filter IDs (e.g., pet-friendly, pool, spa)

roomsbodyobject

Room requirements

rooms filter
bedroomsbodynumber
Number of bedrooms
bedsbodynumber
Number of beds
bathroomsbodynumber
Number of bathrooms
propertyTypesbodyarray

Property type IDs or names to include

paginationbodyobject

Pagination options

pagination object
limitbodynumberdefault: 20
Maximum results per page (1-100)
offsetbodynumberdefault: 0
Number of results to skip

Request

curl -X POST "https://api.journey.com/v1/public/properties/web/search" \
  -H "Content-Type: application/json" \
  -d '{
    "location": {
      "placeId": "ChIJ-wSojYBEQogR5PMnOeL97CE"
    },
    "checkIn": "2026-06-01",
    "checkOut": "2026-06-03",
    "guests": 2,
    "filters": {
      "price": {
        "min": 100,
        "max": 500
      }
    },
    "pagination": {
      "limit": 10,
      "offset": 0
    }
  }'

Response

propertiesarrayrequired

Array of matching properties

property items
idnumberrequired

Internal property ID

documentIdstringrequired

Strapi document ID

namestringrequired

Property name

slugstringrequired

URL-friendly identifier

propertyTypestringrequired

Property type (hotel, resort, villa, etc.)

starRatingnumber

Star rating (1-5)

guestRatingnumber

Guest review rating

reviewCountnumber

Number of reviews

shortDescriptionstring

Brief property description

addressobjectrequired

Property location

address object
citystringrequired
City name
statestring
State or province
countrystringrequired
Country name
latitudenumberrequired
Latitude coordinate
longitudenumberrequired
Longitude coordinate
heroImageobject

Main property image

hero image
urlstringrequired
Full resolution image URL
thumbnailUrlstring
Thumbnail image URL
altstring
Alt text for accessibility
amenitiesstring[]required

Key property amenities

brandobject

Brand information

brand object
namestringrequired
Brand name
logostring
Brand logo URL
pricingobject

Pricing information (only when dates provided)

pricing object
baseRoomCategorystring

Base room type for pricing

nightlyCostnumber

Nightly cost in cents (USD)

totalCostnumber

Total cost for stay in cents (USD)

currencystring

Currency code (USD)

taxesIncludedboolean

Whether taxes are included in pricing

availabilityobject

Availability status (only when dates provided)

availability object
isAvailablebooleanrequired

Whether property has availability for requested dates

availableRoomsnumber

Number of available room types

restrictionsobject

Any booking restrictions

restrictions
minNightsnumber
Minimum nights required
maxNightsnumber
Maximum nights allowed
distancenumber

Distance from search location in kilometers

searchScorenumber

Relevance score for search ranking (0-100)

totalnumberrequired

Total number of properties matching search criteria

paginationobjectrequired

Pagination information

pagination object
limitnumberrequired
Current page limit
offsetnumberrequired
Current offset
hasMorebooleanrequired
Whether more results are available
filtersobject

Applied filter summary and available filter options

filters object
appliedobject

Summary of applied filters

availableobject

Available filter options based on results

available filters
priceRangeobject
price range
minnumber
Minimum price found
maxnumber
Maximum price found
propertyTypesarray

Available property types in results

amenitiesarray

Common amenities that can be filtered

Response

{
  "properties": [
    {
      "id": 12345,
      "documentId": "abc123def456ghi789jkl012",
      "name": "Journey Beach Resort & Spa",
      "slug": "journey-beach-resort-spa",
      "propertyType": "Resort",
      "starRating": 5,
      "guestRating": 4.7,
      "reviewCount": 1247,
      "shortDescription": "Oceanfront luxury resort with spa and dining",
      "address": {
        "city": "Cancun",
        "state": "Quintana Roo",
        "country": "Mexico",
        "latitude": 21.1619,
        "longitude": -86.8515
      },
      "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"
      },
      "amenities": [
        "Private Beach",
        "Infinity Pool", 
        "Full-Service Spa",
        "Fine Dining Restaurant",
        "WiFi",
        "Parking"
      ],
      "brand": {
        "name": "Journey Resorts",
        "logo": "https://cdn.journey.com/brands/journey-resorts-logo.svg"
      },
      "pricing": {
        "baseRoomCategory": "Ocean View Room",
        "nightlyCost": 35000,
        "totalCost": 70000,
        "currency": "USD",
        "taxesIncluded": false
      },
      "availability": {
        "isAvailable": true,
        "availableRooms": 3,
        "restrictions": {
          "minNights": 2,
          "maxNights": 14
        }
      },
      "distance": 2.3,
      "searchScore": 95
    },
    {
      "id": 12346,
      "documentId": "def456ghi789jkl012mno345",
      "name": "Coastal Boutique Hotel",
      "slug": "coastal-boutique-hotel",
      "propertyType": "Hotel",
      "starRating": 4,
      "guestRating": 4.4,
      "reviewCount": 892,
      "shortDescription": "Intimate beachfront hotel with personalized service",
      "address": {
        "city": "Playa del Carmen",
        "state": "Quintana Roo",
        "country": "Mexico",
        "latitude": 20.6296,
        "longitude": -87.0739
      },
      "heroImage": {
        "url": "https://cdn.journey.com/properties/boutique-hotel-hero.jpg",
        "thumbnailUrl": "https://cdn.journey.com/properties/boutique-hotel-hero-thumb.jpg",
        "alt": "Boutique hotel courtyard with tropical gardens"
      },
      "amenities": [
        "Beach Access",
        "Pool",
        "Restaurant",
        "Concierge",
        "WiFi"
      ],
      "brand": {
        "name": "Independent",
        "logo": null
      },
      "pricing": {
        "baseRoomCategory": "Standard Room",
        "nightlyCost": 22000,
        "totalCost": 44000,
        "currency": "USD",
        "taxesIncluded": false
      },
      "availability": {
        "isAvailable": true,
        "availableRooms": 2,
        "restrictions": {
          "minNights": 1,
          "maxNights": null
        }
      },
      "distance": 8.7,
      "searchScore": 88
    }
  ],
  "total": 15,
  "pagination": {
    "limit": 10,
    "offset": 0,
    "hasMore": true
  },
  "filters": {
    "applied": {
      "location": "Cancun, Mexico area",
      "dates": "Jun 1-3, 2026",
      "guests": 2,
      "priceRange": "$100-$500"
    },
    "available": {
      "priceRange": {
        "min": 180,
        "max": 850
      },
      "propertyTypes": [
        "Resort",
        "Hotel",
        "Villa",
        "Boutique Hotel"
      ],
      "amenities": [
        "Beach Access",
        "Pool",
        "Spa",
        "Restaurant",
        "Fitness Center",
        "WiFi",
        "Parking"
      ]
    }
  }
}

Use Cases

Property Search Interface

const PropertySearch = () => {
  const [searchParams, setSearchParams] = useState({
    location: null,
    checkIn: '',
    checkOut: '',
    guests: 2,
    filters: {
      price: { min: null, max: null },
      propertyTypes: [],
      amenities: []
    }
  });
  const [results, setResults] = useState(null);
  const [loading, setLoading] = useState(false);
  const [pagination, setPagination] = useState({ offset: 0, limit: 20 });
  
  const handleSearch = async (newParams = searchParams, newPagination = pagination) => {
    setLoading(true);
    
    try {
      const searchData = {
        location: newParams.location,
        ...(newParams.checkIn && newParams.checkOut && {
          checkIn: newParams.checkIn,
          checkOut: newParams.checkOut
        }),
        guests: newParams.guests,
        filters: newParams.filters,
        pagination: newPagination
      };
      
      const data = await searchProperties(searchData);
      
      if (newPagination.offset === 0) {
        setResults(data);
      } else {
        // Append to existing results for pagination
        setResults(prev => ({
          ...data,
          properties: [...(prev?.properties || []), ...data.properties]
        }));
      }
    } catch (error) {
      console.error('Search failed:', error);
    } finally {
      setLoading(false);
    }
  };
  
  const loadMore = () => {
    if (!results?.pagination.hasMore) return;
    
    const newPagination = {
      ...pagination,
      offset: pagination.offset + pagination.limit
    };
    setPagination(newPagination);
    handleSearch(searchParams, newPagination);
  };
  
  const renderProperty = (property) => (
    <div key={property.id} className="property-card">
      <div className="property-image">
        <img 
          src={property.heroImage?.thumbnailUrl || property.heroImage?.url} 
          alt={property.heroImage?.alt}
        />
        {property.starRating && (
          <div className="star-rating">
            {'★'.repeat(property.starRating)}
          </div>
        )}
      </div>
      
      <div className="property-info">
        <div className="property-header">
          <h3>{property.name}</h3>
          {property.brand?.logo && (
            <img src={property.brand.logo} alt={property.brand.name} className="brand-logo" />
          )}
        </div>
        
        <p className="location">{property.address.city}, {property.address.country}</p>
        <p className="description">{property.shortDescription}</p>
        
        <div className="property-meta">
          {property.guestRating && (
            <span className="rating">
              ⭐ {property.guestRating} ({property.reviewCount} reviews)
            </span>
          )}
          
          {property.distance && (
            <span className="distance">{property.distance.toFixed(1)}km away</span>
          )}
        </div>
        
        <div className="amenities">
          {property.amenities.slice(0, 4).map((amenity, index) => (
            <span key={index} className="amenity-tag">{amenity}</span>
          ))}
          {property.amenities.length > 4 && (
            <span className="more-amenities">+{property.amenities.length - 4} more</span>
          )}
        </div>
        
        {property.pricing && (
          <div className="pricing">
            <div className="price">
              ${(property.pricing.nightlyCost / 100).toFixed(0)}/night
            </div>
            <div className="total">
              Total: ${(property.pricing.totalCost / 100).toFixed(0)}
              {!property.pricing.taxesIncluded && <small>+ taxes</small>}
            </div>
            
            {property.availability && (
              <div className={`availability ${property.availability.isAvailable ? 'available' : 'unavailable'}`}>
                {property.availability.isAvailable 
                  ? `${property.availability.availableRooms} rooms available`
                  : 'No availability'
                }
              </div>
            )}
          </div>
        )}
        
        <button 
          className="view-details-btn"
          onClick={() => window.location.href = `/properties/${property.slug}`}
        >
          View Details
        </button>
      </div>
    </div>
  );
  
  return (
    <div className="property-search">
      {/* Search Form */}
      <SearchForm
        params={searchParams}
        onParamsChange={setSearchParams}
        onSearch={() => {
          setPagination({ offset: 0, limit: 20 });
          handleSearch();
        }}
        loading={loading}
      />
      
      {/* Results */}
      {results && (
        <div className="search-results">
          <div className="results-header">
            <h2>{results.total} properties found</h2>
            <div className="applied-filters">
              {Object.entries(results.filters.applied).map(([key, value]) => (
                <span key={key} className="filter-tag">
                  {key}: {value}
                </span>
              ))}
            </div>
          </div>
          
          <div className="properties-grid">
            {results.properties.map(renderProperty)}
          </div>
          
          {results.pagination.hasMore && (
            <button 
              onClick={loadMore}
              className="load-more-btn"
              disabled={loading}
            >
              {loading ? 'Loading...' : 'Load More'}
            </button>
          )}
        </div>
      )}
    </div>
  );
};

Search with Map Integration

const PropertySearchWithMap = () => {
  const [mapBounds, setMapBounds] = useState(null);
  const [properties, setProperties] = useState([]);
  const [selectedProperty, setSelectedProperty] = useState(null);
  
  const handleMapBoundsChange = async (bounds) => {
    setMapBounds(bounds);
    
    try {
      const results = await searchProperties({
        location: { bounds },
        guests: 2,
        pagination: { limit: 50, offset: 0 }
      });
      
      setProperties(results.properties);
    } catch (error) {
      console.error('Map search failed:', error);
    }
  };
  
  return (
    <div className="search-with-map">
      <div className="map-container">
        <PropertyMap
          properties={properties}
          selectedProperty={selectedProperty}
          onBoundsChange={handleMapBoundsChange}
          onPropertySelect={setSelectedProperty}
        />
      </div>
      
      <div className="results-panel">
        <div className="results-header">
          <h3>{properties.length} properties in this area</h3>
        </div>
        
        <div className="property-list">
          {properties.map(property => (
            <PropertyListItem
              key={property.id}
              property={property}
              selected={selectedProperty?.id === property.id}
              onClick={() => setSelectedProperty(property)}
            />
          ))}
        </div>
      </div>
    </div>
  );
};

Search Strategies

Location Search Types

  • Place ID: Most accurate, uses Google Places
  • Bounds: Geographic area search for map integration
  • Property ID: Direct property lookup

Ranking Algorithm

Properties are ranked by search score considering:

  • Distance from search location
  • Property rating and reviews
  • Availability (when dates provided)
  • Price competitiveness
  • Amenity match to filters

Performance Optimization

// Implement debounced search for better UX
const useDebounced = (value, delay) => {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    
    return () => clearTimeout(handler);
  }, [value, delay]);
  
  return debouncedValue;
};

const SearchWithDebounce = ({ onSearch }) => {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounced(query, 300);
  
  useEffect(() => {
    if (debouncedQuery) {
      onSearch(debouncedQuery);
    }
  }, [debouncedQuery, onSearch]);
  
  return (
    <input
      type="text"
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="Search destination..."
    />
  );
};

On this page