Property Search
Search properties by location with availability and pricing (no authentication required)
/public/properties/web/searchOverview
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
locationbodyobjectrequiredLocation search criteria
location object
placeIdbodystringGoogle Places ID for location-based search
boundsbodyobjectGeographic bounds for map-based search
bounds object
northeastbodyobjectrequirednortheast coordinates
latitudebodynumberrequiredlongitudebodynumberrequiredsouthwestbodyobjectrequiredsouthwest coordinates
latitudebodynumberrequiredlongitudebodynumberrequiredpropertyIdbodynumberSpecific property ID for single property search
checkInbodystringCheck-in date (YYYY-MM-DD). If provided, availability and pricing will be included
checkOutbodystringCheck-out date (YYYY-MM-DD). Required if checkIn is provided
guestsbodynumberNumber of guests (minimum: 1)
filtersbodyobjectSearch filters
filters object
pricebodyobjectPrice range filter
price filter
minbodynumbermaxbodynumberpredefinedFiltersbodynumber[]Predefined filter IDs (e.g., pet-friendly, pool, spa)
roomsbodyobjectRoom requirements
rooms filter
bedroomsbodynumberbedsbodynumberbathroomsbodynumberpropertyTypesbodyarrayProperty type IDs or names to include
paginationbodyobjectPagination options
pagination object
limitbodynumberdefault: 20offsetbodynumberdefault: 0Request
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
propertiesarrayrequiredArray of matching properties
property items
idnumberrequiredInternal property ID
documentIdstringrequiredStrapi document ID
namestringrequiredProperty name
slugstringrequiredURL-friendly identifier
propertyTypestringrequiredProperty type (hotel, resort, villa, etc.)
starRatingnumberStar rating (1-5)
guestRatingnumberGuest review rating
reviewCountnumberNumber of reviews
shortDescriptionstringBrief property description
addressobjectrequiredProperty location
address object
citystringrequiredstatestringcountrystringrequiredlatitudenumberrequiredlongitudenumberrequiredheroImageobjectMain property image
hero image
urlstringrequiredthumbnailUrlstringaltstringamenitiesstring[]requiredKey property amenities
brandobjectBrand information
brand object
namestringrequiredlogostringpricingobjectPricing information (only when dates provided)
pricing object
baseRoomCategorystringBase room type for pricing
nightlyCostnumberNightly cost in cents (USD)
totalCostnumberTotal cost for stay in cents (USD)
currencystringCurrency code (USD)
taxesIncludedbooleanWhether taxes are included in pricing
availabilityobjectAvailability status (only when dates provided)
availability object
isAvailablebooleanrequiredWhether property has availability for requested dates
availableRoomsnumberNumber of available room types
restrictionsobjectAny booking restrictions
restrictions
minNightsnumbermaxNightsnumberdistancenumberDistance from search location in kilometers
searchScorenumberRelevance score for search ranking (0-100)
totalnumberrequiredTotal number of properties matching search criteria
paginationobjectrequiredPagination information
pagination object
limitnumberrequiredoffsetnumberrequiredhasMorebooleanrequiredfiltersobjectApplied filter summary and available filter options
filters object
appliedobjectSummary of applied filters
availableobjectAvailable filter options based on results
available filters
priceRangeobjectprice range
minnumbermaxnumberpropertyTypesarrayAvailable property types in results
amenitiesarrayCommon 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..."
/>
);
};Related Endpoints
- Trending Searches - Popular search destinations
- Property Details - Detailed property information