# Get Batch Availability
> Source: /api-reference/public/availabilities/get-batch-availability
> Get unified availability for multiple properties with source indicators
> Endpoint: POST /public/properties/availabilities

## Overview [#overview]

Returns availability for up to 100 properties with source indicators. Each property is resolved from the highest priority source (LiteAPI > Strapi) and includes booking conflicts from pending payment intents and confirmed bookings.

## Authentication [#authentication]

No authentication required. This is a public endpoint.

## Request Body [#request-body]

<ParamField body="propertyIds" type="string[]" required>
  Property IDs (internal IDs or document IDs)
  <br />Maximum: 100 properties
</ParamField>

<ParamField body="dateFrom" type="string">
  Start date filter (YYYY-MM-DD format)
</ParamField>

<ParamField body="dateTo" type="string">
  End date filter (YYYY-MM-DD format)
</ParamField>

<ParamField body="checkBookingConflicts" type="boolean" default="true">
  Whether to check for booking conflicts (pending payment intents and confirmed bookings)
</ParamField>

<RequestExample>

```bash cURL
curl -X POST "https://api.journey.com/v1/public/properties/availabilities" \
  -H "Content-Type: application/json" \
  -d '{
    "propertyIds": ["abc123", "def456", "ghi789"],
    "dateFrom": "2025-01-15",
    "dateTo": "2025-03-15",
    "checkBookingConflicts": true
  }'
```

```typescript TypeScript
const getBatchAvailability = async (requestData) => {
  const response = await fetch('https://api.journey.com/v1/public/properties/availabilities', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      propertyIds: ['abc123', 'def456', 'ghi789'],
      dateFrom: '2025-01-15',
      dateTo: '2025-03-15',
      checkBookingConflicts: true
    })
  });
  
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }
  
  return response.json();
};
```

```javascript JavaScript
const getBatchAvailability = async (requestData) => {
  const response = await fetch('https://api.journey.com/v1/public/properties/availabilities', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      propertyIds: ['abc123', 'def456', 'ghi789'],
      dateFrom: '2025-01-15',
      dateTo: '2025-03-15',
      checkBookingConflicts: true
    })
  });
  
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }
  
  return response.json();
};
```

</RequestExample>

## Response [#response]

<ResponseField name="results" type="array" required>
  Availability results for each property
  
  <Expandable title="result items">
    <ResponseField name="propertyId" type="string" required>
      Property ID that was queried
    </ResponseField>
    
    <ResponseField name="source" type="string" required>
      Source of the availability data
      <br />Options: `liteapi`, `strapi`, `none`
    </ResponseField>
    
    <ResponseField name="availabilities" type="array" required>
      Availability data for each day (same structure as single property endpoint)
      
      <Expandable title="availability items">
        <ResponseField name="date" type="string" required>
          Date in YYYY-MM-DD format
        </ResponseField>
        
        <ResponseField name="price" type="number" required>
          Nightly rate in USD
        </ResponseField>
        
        <ResponseField name="isAvailable" type="boolean" required>
          Whether the date is available for booking
        </ResponseField>
        
        <ResponseField name="restrictions" type="object">
          Stay restrictions for this date
          
          <Expandable title="restrictions object">
            <ResponseField name="minNights" type="number" required>
              Minimum nights required
            </ResponseField>
            
            <ResponseField name="maxNights" type="number">
              Maximum nights allowed (null if unlimited)
            </ResponseField>
            
            <ResponseField name="closedForArrival" type="boolean">
              Whether arrivals are closed for this date
            </ResponseField>
            
            <ResponseField name="closedForDeparture" type="boolean">
              Whether departures are closed for this date
            </ResponseField>
          </Expandable>
        </ResponseField>
      </Expandable>
    </ResponseField>
    
    <ResponseField name="lastSyncAt" type="string">
      When the data was last synced (ISO timestamp, null if never synced)
    </ResponseField>
    
    <ResponseField name="bookingConflicts" type="array" required>
      Booking conflicts affecting availability
      
      <Expandable title="conflict items">
        <ResponseField name="checkInDate" type="string" required>
          Check-in date of the conflict
        </ResponseField>
        
        <ResponseField name="checkOutDate" type="string" required>
          Check-out date of the conflict
        </ResponseField>
        
        <ResponseField name="type" type="string" required>
          Type of booking conflict
          <br />Options: `pending_payment_intent`, `confirmed_booking`
        </ResponseField>
        
        <ResponseField name="expiresAt" type="string">
          When the hold expires (only for pending payment intents)
        </ResponseField>
      </Expandable>
    </ResponseField>
    
    <ResponseField name="meta" type="object" required>
      Response metadata (same structure as single property endpoint)
      
      <Expandable title="meta object">
        <ResponseField name="resolvedIds" type="object" required>
          Resolved property IDs across all sources
        </ResponseField>
        
        <ResponseField name="sourcesChecked" type="string[]" required>
          Sources that were checked for availability data
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="summary" type="object" required>
  Summary of batch results
  
  <Expandable title="summary object">
    <ResponseField name="total" type="number" required>
      Total number of properties queried
    </ResponseField>
    
    <ResponseField name="bySource" type="object" required>
      Count by data source
      
      <Expandable title="by source">
        <ResponseField name="liteapi" type="number" required>
          Count of properties with LiteAPI data
        </ResponseField>
        
        <ResponseField name="strapi" type="number" required>
          Count of properties with Strapi data (PMS/Airtable synced)
        </ResponseField>
        
        <ResponseField name="none" type="number" required>
          Count of properties with no data
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseExample>

```json 200 Response - Mixed Source Data
{
  "results": [
    {
      "propertyId": "abc123",
      "source": "liteapi",
      "availabilities": [
        {
          "date": "2025-01-15",
          "price": 250.00,
          "isAvailable": true,
          "restrictions": {
            "minNights": 2,
            "maxNights": 14,
            "closedForArrival": false,
            "closedForDeparture": false
          }
        },
        {
          "date": "2025-01-16",
          "price": 275.00,
          "isAvailable": false,
          "restrictions": {
            "minNights": 2,
            "maxNights": 14,
            "closedForArrival": false,
            "closedForDeparture": false
          }
        }
      ],
      "lastSyncAt": "2025-01-14T08:30:00Z",
      "bookingConflicts": [
        {
          "checkInDate": "2025-01-20",
          "checkOutDate": "2025-01-25",
          "type": "pending_payment_intent",
          "expiresAt": "2025-01-15T10:30:00Z"
        }
      ],
      "meta": {
        "resolvedIds": {
          "documentId": "abc123def456ghi789jkl012",
          "liteapiId": "HTL_12345",
          "pmsId": "prop_001",
          "hostName": "boutique-resort"
        },
        "sourcesChecked": ["liteapi", "strapi"]
      }
    },
    {
      "propertyId": "def456",
      "source": "strapi",
      "availabilities": [
        {
          "date": "2025-01-15",
          "price": 180.00,
          "isAvailable": true,
          "restrictions": {
            "minNights": 1,
            "maxNights": null,
            "closedForArrival": false,
            "closedForDeparture": false
          }
        }
      ],
      "lastSyncAt": "2025-01-13T14:22:00Z",
      "bookingConflicts": [],
      "meta": {
        "resolvedIds": {
          "documentId": "def456ghi789jkl012mno345",
          "liteapiId": null,
          "pmsId": "prop_002",
          "hostName": "mountain-lodge"
        },
        "sourcesChecked": ["liteapi", "strapi"]
      }
    },
    {
      "propertyId": "ghi789",
      "source": "none",
      "availabilities": [],
      "lastSyncAt": null,
      "bookingConflicts": [],
      "meta": {
        "resolvedIds": {
          "documentId": null,
          "liteapiId": null,
          "pmsId": null,
          "hostName": null
        },
        "sourcesChecked": ["liteapi", "strapi"]
      }
    }
  ],
  "summary": {
    "total": 3,
    "bySource": {
      "liteapi": 1,
      "strapi": 1,
      "none": 1
    }
  }
}
```

```json 400 Error - Too Many Properties
{
  "statusCode": 400,
  "message": [
    "propertyIds must contain no more than 100 elements"
  ],
  "error": "Bad Request"
}
```

```json 400 Error - Invalid Date Format
{
  "statusCode": 400,
  "message": [
    "dateFrom must be a valid ISO 8601 date string"
  ],
  "error": "Bad Request"
}
```

</ResponseExample>

## Use Cases [#use-cases]

### Multi-Property Comparison [#multi-property-comparison]
```typescript
const PropertyAvailabilityComparison = ({ propertyIds, dates }) => {
  const [batchData, setBatchData] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    const fetchBatchAvailability = async () => {
      try {
        const data = await getBatchAvailability({
          propertyIds,
          dateFrom: dates.from,
          dateTo: dates.to,
          checkBookingConflicts: true
        });
        setBatchData(data);
      } catch (error) {
        console.error('Failed to fetch batch availability:', error);
      } finally {
        setLoading(false);
      }
    };
    
    fetchBatchAvailability();
  }, [propertyIds, dates]);
  
  if (loading) return <div>Loading availability comparison...</div>;
  if (!batchData) return <div>No availability data</div>;
  
  const propertiesWithData = batchData.results.filter(result => result.source !== 'none');
  
  const comparison = propertiesWithData.map(property => {
    const availableDays = property.availabilities.filter(day => day.isAvailable);
    const prices = availableDays.map(day => day.price);
    
    return {
      propertyId: property.propertyId,
      source: property.source,
      availableDays: availableDays.length,
      totalDays: property.availabilities.length,
      availabilityRate: (availableDays.length / property.availabilities.length) * 100,
      minPrice: Math.min(...prices),
      maxPrice: Math.max(...prices),
      avgPrice: prices.reduce((sum, price) => sum + price, 0) / prices.length,
      hasConflicts: property.bookingConflicts.length > 0,
      lastUpdated: property.lastSyncAt
    };
  });
  
  return (
    <div className="availability-comparison">
      <div className="summary-stats">
        <h3>Availability Summary</h3>
        <p>Properties checked: {batchData.summary.total}</p>
        <p>With LiteAPI data: {batchData.summary.bySource.liteapi}</p>
        <p>With Strapi data: {batchData.summary.bySource.strapi}</p>
        <p>No data found: {batchData.summary.bySource.none}</p>
      </div>
      
      <div className="comparison-table">
        {comparison.map(property => (
          <div key={property.propertyId} className="property-row">
            <div className="property-id">{property.propertyId}</div>
            <div className="availability-rate">
              {property.availabilityRate.toFixed(1)}% available
            </div>
            <div className="price-range">
              ${property.minPrice} - ${property.maxPrice}
              <small>(avg: ${property.avgPrice.toFixed(2)})</small>
            </div>
            <div className="data-source">{property.source}</div>
            {property.hasConflicts && (
              <div className="conflicts-badge">Has conflicts</div>
            )}
          </div>
        ))}
      </div>
    </div>
  );
};
```

### Search Results Enrichment [#search-results-enrichment]
```typescript
const enrichSearchResultsWithAvailability = async (searchResults, dateRange) => {
  const propertyIds = searchResults.map(result => result.propertyId);
  
  // Split into chunks of 100 (API limit)
  const chunks = [];
  for (let i = 0; i < propertyIds.length; i += 100) {
    chunks.push(propertyIds.slice(i, i + 100));
  }
  
  // Fetch availability for all chunks
  const availabilityPromises = chunks.map(chunk => 
    getBatchAvailability({
      propertyIds: chunk,
      dateFrom: dateRange.from,
      dateTo: dateRange.to,
      checkBookingConflicts: true
    })
  );
  
  const availabilityResults = await Promise.all(availabilityPromises);
  
  // Flatten and index by property ID
  const availabilityMap = new Map();
  availabilityResults.forEach(batch => {
    batch.results.forEach(result => {
      availabilityMap.set(result.propertyId, result);
    });
  });
  
  // Enrich search results with availability data
  return searchResults.map(result => {
    const availability = availabilityMap.get(result.propertyId);
    
    if (!availability || availability.source === 'none') {
      return {
        ...result,
        availability: null,
        hasAvailability: false
      };
    }
    
    const availableDays = availability.availabilities.filter(day => day.isAvailable);
    const prices = availableDays.map(day => day.price);
    
    return {
      ...result,
      availability: {
        source: availability.source,
        availableDays: availableDays.length,
        totalDays: availability.availabilities.length,
        isFullyAvailable: availableDays.length === availability.availabilities.length,
        minPrice: Math.min(...prices),
        maxPrice: Math.max(...prices),
        hasConflicts: availability.bookingConflicts.length > 0,
        lastUpdated: availability.lastSyncAt
      },
      hasAvailability: true
    };
  });
};
```

### Availability Monitoring Dashboard [#availability-monitoring-dashboard]
```typescript
const AvailabilityDashboard = ({ monitoredProperties, refreshInterval = 300000 }) => {
  const [dashboardData, setDashboardData] = useState(null);
  const [lastRefresh, setLastRefresh] = useState(new Date());
  
  const fetchAvailabilityData = useCallback(async () => {
    const tomorrow = new Date();
    tomorrow.setDate(tomorrow.getDate() + 1);
    const nextMonth = new Date();
    nextMonth.setMonth(nextMonth.getMonth() + 1);
    
    try {
      const data = await getBatchAvailability({
        propertyIds: monitoredProperties,
        dateFrom: tomorrow.toISOString().split('T')[0],
        dateTo: nextMonth.toISOString().split('T')[0],
        checkBookingConflicts: true
      });
      
      setDashboardData(data);
      setLastRefresh(new Date());
    } catch (error) {
      console.error('Dashboard refresh failed:', error);
    }
  }, [monitoredProperties]);
  
  useEffect(() => {
    fetchAvailabilityData();
    
    const interval = setInterval(fetchAvailabilityData, refreshInterval);
    return () => clearInterval(interval);
  }, [fetchAvailabilityData, refreshInterval]);
  
  if (!dashboardData) return <div>Loading dashboard...</div>;
  
  const metrics = dashboardData.results.map(property => {
    const availableDays = property.availabilities.filter(day => day.isAvailable);
    const totalRevenue = property.availabilities
      .filter(day => day.isAvailable)
      .reduce((sum, day) => sum + day.price, 0);
    
    return {
      propertyId: property.propertyId,
      source: property.source,
      availability: {
        rate: (availableDays.length / property.availabilities.length) * 100,
        days: availableDays.length,
        total: property.availabilities.length
      },
      revenue: {
        potential: totalRevenue,
        average: totalRevenue / availableDays.length || 0
      },
      conflicts: {
        total: property.bookingConflicts.length,
        pending: property.bookingConflicts.filter(c => c.type === 'pending_payment_intent').length,
        confirmed: property.bookingConflicts.filter(c => c.type === 'confirmed_booking').length
      },
      lastSync: property.lastSyncAt
    };
  });
  
  const summary = {
    totalProperties: dashboardData.summary.total,
    avgAvailability: metrics.reduce((sum, m) => sum + m.availability.rate, 0) / metrics.length,
    totalPotentialRevenue: metrics.reduce((sum, m) => sum + m.revenue.potential, 0),
    sourceMix: dashboardData.summary.bySource
  };
  
  return (
    <div className="availability-dashboard">
      <div className="dashboard-header">
        <h2>Availability Dashboard</h2>
        <div className="last-refresh">
          Last updated: {lastRefresh.toLocaleString()}
        </div>
        <button onClick={fetchAvailabilityData}>Refresh</button>
      </div>
      
      <div className="summary-cards">
        <div className="card">
          <h3>Overall Availability</h3>
          <div className="metric">{summary.avgAvailability.toFixed(1)}%</div>
        </div>
        
        <div className="card">
          <h3>Potential Revenue</h3>
          <div className="metric">${summary.totalPotentialRevenue.toLocaleString()}</div>
        </div>
        
        <div className="card">
          <h3>Data Sources</h3>
          <div className="source-breakdown">
            <div>LiteAPI: {summary.sourceMix.liteapi}</div>
            <div>Strapi: {summary.sourceMix.strapi}</div>
            <div>No Data: {summary.sourceMix.none}</div>
          </div>
        </div>
      </div>
      
      <div className="property-metrics">
        {metrics.map(metric => (
          <div key={metric.propertyId} className="property-metric">
            <h4>{metric.propertyId}</h4>
            <div className="metric-row">
              <span>Availability: {metric.availability.rate.toFixed(1)}%</span>
              <span>Revenue: ${metric.revenue.potential.toLocaleString()}</span>
              <span>Source: {metric.source}</span>
              {metric.conflicts.total > 0 && (
                <span className="conflicts">
                  {metric.conflicts.total} conflicts
                </span>
              )}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};
```

## Performance Considerations [#performance-considerations]

### Batch Size Limits [#batch-size-limits]
- **Maximum**: 100 properties per request
- **Recommendation**: 25-50 properties for optimal response time
- **Chunking**: Implement client-side batching for larger datasets

### Response Time Optimization [#response-time-optimization]
```typescript
const optimizedBatchRequest = async (propertyIds, options) => {
  // Split large requests into optimal chunks
  const chunkSize = 25;
  const chunks = [];
  
  for (let i = 0; i < propertyIds.length; i += chunkSize) {
    chunks.push(propertyIds.slice(i, i + chunkSize));
  }
  
  // Execute chunks in parallel with some limit
  const maxConcurrent = 3;
  const results = [];
  
  for (let i = 0; i < chunks.length; i += maxConcurrent) {
    const batchPromises = chunks
      .slice(i, i + maxConcurrent)
      .map(chunk => getBatchAvailability({
        propertyIds: chunk,
        ...options
      }));
    
    const batchResults = await Promise.all(batchPromises);
    results.push(...batchResults);
  }
  
  // Merge all results
  return {
    results: results.flatMap(batch => batch.results),
    summary: {
      total: results.reduce((sum, batch) => sum + batch.summary.total, 0),
      bySource: results.reduce((acc, batch) => ({
        liteapi: acc.liteapi + batch.summary.bySource.liteapi,
        strapi: acc.strapi + batch.summary.bySource.strapi,
        none: acc.none + batch.summary.bySource.none
      }), { liteapi: 0, strapi: 0, none: 0 })
    }
  };
};
```

### Caching Strategy [#caching-strategy]
```typescript
const AvailabilityCache = {
  cache: new Map(),
  TTL: 5 * 60 * 1000, // 5 minutes
  
  getCacheKey: (propertyIds, dateFrom, dateTo) => {
    return `${propertyIds.sort().join(',')}:${dateFrom}:${dateTo}`;
  },
  
  get: function(propertyIds, dateFrom, dateTo) {
    const key = this.getCacheKey(propertyIds, dateFrom, dateTo);
    const cached = this.cache.get(key);
    
    if (cached && Date.now() - cached.timestamp < this.TTL) {
      return cached.data;
    }
    
    this.cache.delete(key);
    return null;
  },
  
  set: function(propertyIds, dateFrom, dateTo, data) {
    const key = this.getCacheKey(propertyIds, dateFrom, dateTo);
    this.cache.set(key, {
      data,
      timestamp: Date.now()
    });
    
    // Clean up old entries
    if (this.cache.size > 1000) {
      const oldestKey = this.cache.keys().next().value;
      this.cache.delete(oldestKey);
    }
  }
};
```

## Error Handling [#error-handling]

### Partial Failures [#partial-failures]
- Individual property failures don't affect other results
- Properties with no data return empty availability arrays
- Source information indicates data quality for each property

### Retry Strategy [#retry-strategy]
```typescript
const robustBatchAvailability = async (propertyIds, options, retries = 3) => {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      return await getBatchAvailability({
        propertyIds,
        ...options
      });
    } catch (error) {
      if (attempt === retries || error.status === 400) {
        throw error;
      }
      
      // Exponential backoff
      const delay = Math.pow(2, attempt) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
};
```

## Related Endpoints [#related-endpoints]

- [Get Property Availability](/api-reference/public/availabilities/get-property-availability) - Single property detailed availability
- [Property Search](/api-reference/public/properties/search) - Search with availability filtering
- [Get Property Rates](/api-reference/public/rates/get-property-rates) - Detailed pricing information
