# Property Search
> Source: /api-reference/public/properties/search
> Search properties by location with availability and pricing (no authentication required)
> Endpoint: POST /public/properties/web/search

## Overview [#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 [#authentication]

No authentication required. This is a public endpoint.

## Request Body [#request-body]

<ParamField body="location" type="object" required>
  Location search criteria
  
  <Expandable title="location object">
    <ParamField body="placeId" type="string">
      Google Places ID for location-based search
    </ParamField>
    
    <ParamField body="bounds" type="object">
      Geographic bounds for map-based search
      
      <Expandable title="bounds object">
        <ParamField body="northeast" type="object" required>
          <Expandable title="northeast coordinates">
            <ParamField body="latitude" type="number" required>Northeast corner latitude</ParamField>
            <ParamField body="longitude" type="number" required>Northeast corner longitude</ParamField>
          </Expandable>
        </ParamField>
        
        <ParamField body="southwest" type="object" required>
          <Expandable title="southwest coordinates">
            <ParamField body="latitude" type="number" required>Southwest corner latitude</ParamField>
            <ParamField body="longitude" type="number" required>Southwest corner longitude</ParamField>
          </Expandable>
        </ParamField>
      </Expandable>
    </ParamField>
    
    <ParamField body="propertyId" type="number">
      Specific property ID for single property search
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="checkIn" type="string">
  Check-in date (YYYY-MM-DD). If provided, availability and pricing will be included
</ParamField>

<ParamField body="checkOut" type="string">
  Check-out date (YYYY-MM-DD). Required if checkIn is provided
</ParamField>

<ParamField body="guests" type="number">
  Number of guests (minimum: 1)
</ParamField>

<ParamField body="filters" type="object">
  Search filters
  
  <Expandable title="filters object">
    <ParamField body="price" type="object">
      Price range filter
      
      <Expandable title="price filter">
        <ParamField body="min" type="number">Minimum price per night</ParamField>
        <ParamField body="max" type="number">Maximum price per night</ParamField>
      </Expandable>
    </ParamField>
    
    <ParamField body="predefinedFilters" type="number[]">
      Predefined filter IDs (e.g., pet-friendly, pool, spa)
    </ParamField>
    
    <ParamField body="rooms" type="object">
      Room requirements
      
      <Expandable title="rooms filter">
        <ParamField body="bedrooms" type="number">Number of bedrooms</ParamField>
        <ParamField body="beds" type="number">Number of beds</ParamField>
        <ParamField body="bathrooms" type="number">Number of bathrooms</ParamField>
      </Expandable>
    </ParamField>
    
    <ParamField body="propertyTypes" type="array">
      Property type IDs or names to include
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="pagination" type="object">
  Pagination options
  
  <Expandable title="pagination object">
    <ParamField body="limit" type="number" default="20">Maximum results per page (1-100)</ParamField>
    <ParamField body="offset" type="number" default="0">Number of results to skip</ParamField>
  </Expandable>
</ParamField>

<RequestExample>

```bash cURL - Location Search with Dates
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
    }
  }'
```

```bash cURL - Map Bounds Search
curl -X POST "https://api.journey.com/v1/public/properties/web/search" \
  -H "Content-Type: application/json" \
  -d '{
    "location": {
      "bounds": {
        "northeast": {
          "latitude": 21.2,
          "longitude": -86.8
        },
        "southwest": {
          "latitude": 21.1,
          "longitude": -86.9
        }
      }
    },
    "guests": 2
  }'
```

```typescript TypeScript
const searchProperties = async (searchParams) => {
  const response = await fetch('https://api.journey.com/v1/public/properties/web/search', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      location: {
        placeId: 'ChIJ-wSojYBEQogR5PMnOeL97CE'
      },
      checkIn: '2026-06-01',
      checkOut: '2026-06-03',
      guests: 2,
      filters: {
        price: {
          min: 100,
          max: 500
        },
        predefinedFilters: [1, 3], // Pool, Spa
        propertyTypes: ['resort', 'hotel']
      },
      pagination: {
        limit: 20,
        offset: 0
      }
    })
  });
  
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }
  
  return response.json();
};
```

```javascript JavaScript
const searchProperties = async (searchParams) => {
  const response = await fetch('https://api.journey.com/v1/public/properties/web/search', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(searchParams)
  });
  
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }
  
  return response.json();
};
```

</RequestExample>

## Response [#response]

<ResponseField name="properties" type="array" required>
  Array of matching properties
  
  <Expandable title="property items">
    <ResponseField name="id" type="number" required>
      Internal property ID
    </ResponseField>
    
    <ResponseField name="documentId" type="string" required>
      Strapi document ID
    </ResponseField>
    
    <ResponseField name="name" type="string" required>
      Property name
    </ResponseField>
    
    <ResponseField name="slug" type="string" required>
      URL-friendly identifier
    </ResponseField>
    
    <ResponseField name="propertyType" type="string" required>
      Property type (hotel, resort, villa, etc.)
    </ResponseField>
    
    <ResponseField name="starRating" type="number">
      Star rating (1-5)
    </ResponseField>
    
    <ResponseField name="guestRating" type="number">
      Guest review rating
    </ResponseField>
    
    <ResponseField name="reviewCount" type="number">
      Number of reviews
    </ResponseField>
    
    <ResponseField name="shortDescription" type="string">
      Brief property description
    </ResponseField>
    
    <ResponseField name="address" type="object" required>
      Property location
      
      <Expandable title="address object">
        <ResponseField name="city" type="string" required>City name</ResponseField>
        <ResponseField name="state" type="string">State or province</ResponseField>
        <ResponseField name="country" type="string" required>Country name</ResponseField>
        <ResponseField name="latitude" type="number" required>Latitude coordinate</ResponseField>
        <ResponseField name="longitude" type="number" required>Longitude coordinate</ResponseField>
      </Expandable>
    </ResponseField>
    
    <ResponseField name="heroImage" type="object">
      Main property image
      
      <Expandable title="hero image">
        <ResponseField name="url" type="string" required>Full resolution image URL</ResponseField>
        <ResponseField name="thumbnailUrl" type="string">Thumbnail image URL</ResponseField>
        <ResponseField name="alt" type="string">Alt text for accessibility</ResponseField>
      </Expandable>
    </ResponseField>
    
    <ResponseField name="amenities" type="string[]" required>
      Key property amenities
    </ResponseField>
    
    <ResponseField name="brand" type="object">
      Brand information
      
      <Expandable title="brand object">
        <ResponseField name="name" type="string" required>Brand name</ResponseField>
        <ResponseField name="logo" type="string">Brand logo URL</ResponseField>
      </Expandable>
    </ResponseField>
    
    <ResponseField name="pricing" type="object">
      Pricing information (only when dates provided)
      
      <Expandable title="pricing object">
        <ResponseField name="baseRoomCategory" type="string">
          Base room type for pricing
        </ResponseField>
        
        <ResponseField name="nightlyCost" type="number">
          Nightly cost in cents (USD)
        </ResponseField>
        
        <ResponseField name="totalCost" type="number">
          Total cost for stay in cents (USD)
        </ResponseField>
        
        <ResponseField name="currency" type="string">
          Currency code (USD)
        </ResponseField>
        
        <ResponseField name="taxesIncluded" type="boolean">
          Whether taxes are included in pricing
        </ResponseField>
      </Expandable>
    </ResponseField>
    
    <ResponseField name="availability" type="object">
      Availability status (only when dates provided)
      
      <Expandable title="availability object">
        <ResponseField name="isAvailable" type="boolean" required>
          Whether property has availability for requested dates
        </ResponseField>
        
        <ResponseField name="availableRooms" type="number">
          Number of available room types
        </ResponseField>
        
        <ResponseField name="restrictions" type="object">
          Any booking restrictions
          
          <Expandable title="restrictions">
            <ResponseField name="minNights" type="number">Minimum nights required</ResponseField>
            <ResponseField name="maxNights" type="number">Maximum nights allowed</ResponseField>
          </Expandable>
        </ResponseField>
      </Expandable>
    </ResponseField>
    
    <ResponseField name="distance" type="number">
      Distance from search location in kilometers
    </ResponseField>
    
    <ResponseField name="searchScore" type="number">
      Relevance score for search ranking (0-100)
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="total" type="number" required>
  Total number of properties matching search criteria
</ResponseField>

<ResponseField name="pagination" type="object" required>
  Pagination information
  
  <Expandable title="pagination object">
    <ResponseField name="limit" type="number" required>Current page limit</ResponseField>
    <ResponseField name="offset" type="number" required>Current offset</ResponseField>
    <ResponseField name="hasMore" type="boolean" required>Whether more results are available</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="filters" type="object">
  Applied filter summary and available filter options
  
  <Expandable title="filters object">
    <ResponseField name="applied" type="object">
      Summary of applied filters
    </ResponseField>
    
    <ResponseField name="available" type="object">
      Available filter options based on results
      
      <Expandable title="available filters">
        <ResponseField name="priceRange" type="object">
          <Expandable title="price range">
            <ResponseField name="min" type="number">Minimum price found</ResponseField>
            <ResponseField name="max" type="number">Maximum price found</ResponseField>
          </Expandable>
        </ResponseField>
        
        <ResponseField name="propertyTypes" type="array">
          Available property types in results
        </ResponseField>
        
        <ResponseField name="amenities" type="array">
          Common amenities that can be filtered
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseExample>

```json 200 Response - Search Results with Dates
{
  "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"
      ]
    }
  }
}
```

```json 200 Response - Search without Dates
{
  "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"
      },
      "distance": 2.3,
      "searchScore": 95
    }
  ],
  "total": 15,
  "pagination": {
    "limit": 10,
    "offset": 0,
    "hasMore": true
  },
  "filters": {
    "applied": {
      "location": "Cancun, Mexico area",
      "guests": 2
    },
    "available": {
      "propertyTypes": [
        "Resort",
        "Hotel",
        "Villa"
      ],
      "amenities": [
        "Beach Access",
        "Pool",
        "Spa",
        "Restaurant",
        "WiFi"
      ]
    }
  }
}
```

```json 400 Error - Invalid Search Parameters
{
  "statusCode": 400,
  "message": [
    "location is required",
    "checkOut is required when checkIn is provided"
  ],
  "error": "Bad Request"
}
```

</ResponseExample>

## Use Cases [#use-cases]

### Property Search Interface [#property-search-interface]
```typescript
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 [#search-with-map-integration]
```typescript
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 [#search-strategies]

### Location Search Types [#location-search-types]
- **Place ID**: Most accurate, uses Google Places
- **Bounds**: Geographic area search for map integration  
- **Property ID**: Direct property lookup

### Ranking Algorithm [#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 [#performance-optimization]
```typescript
// 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 [#related-endpoints]

- [Trending Searches](/api-reference/public/properties/get-trending-searches) - Popular search destinations
- [Property Details](/api-reference/public/properties/get-property-details) - Detailed property information
