Get Batch Availability
Get unified availability for multiple properties with source indicators
/public/properties/availabilitiesOverview
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
No authentication required. This is a public endpoint.
Request Body
propertyIdsbodystring[]requiredProperty IDs (internal IDs or document IDs)
Maximum: 100 properties
dateFrombodystringStart date filter (YYYY-MM-DD format)
dateTobodystringEnd date filter (YYYY-MM-DD format)
checkBookingConflictsbodybooleandefault: trueWhether to check for booking conflicts (pending payment intents and confirmed bookings)
Request
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
}'Response
resultsarrayrequiredAvailability results for each property
result items
propertyIdstringrequiredProperty ID that was queried
sourcestringrequiredSource of the availability data
Options: liteapi, strapi, none
availabilitiesarrayrequiredAvailability data for each day (same structure as single property endpoint)
availability items
datestringrequiredDate in YYYY-MM-DD format
pricenumberrequiredNightly rate in USD
isAvailablebooleanrequiredWhether the date is available for booking
restrictionsobjectStay restrictions for this date
restrictions object
minNightsnumberrequiredMinimum nights required
maxNightsnumberMaximum nights allowed (null if unlimited)
closedForArrivalbooleanWhether arrivals are closed for this date
closedForDeparturebooleanWhether departures are closed for this date
lastSyncAtstringWhen the data was last synced (ISO timestamp, null if never synced)
bookingConflictsarrayrequiredBooking conflicts affecting availability
conflict items
checkInDatestringrequiredCheck-in date of the conflict
checkOutDatestringrequiredCheck-out date of the conflict
typestringrequiredType of booking conflict
Options: pending_payment_intent, confirmed_booking
expiresAtstringWhen the hold expires (only for pending payment intents)
metaobjectrequiredResponse metadata (same structure as single property endpoint)
meta object
resolvedIdsobjectrequiredResolved property IDs across all sources
sourcesCheckedstring[]requiredSources that were checked for availability data
summaryobjectrequiredSummary of batch results
summary object
totalnumberrequiredTotal number of properties queried
bySourceobjectrequiredCount by data source
by source
liteapinumberrequiredCount of properties with LiteAPI data
strapinumberrequiredCount of properties with Strapi data (PMS/Airtable synced)
nonenumberrequiredCount of properties with no data
Response
{
"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
}
}
}Use Cases
Multi-Property Comparison
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
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
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
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
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
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
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
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
- Get Property Availability - Single property detailed availability
- Property Search - Search with availability filtering
- Get Property Rates - Detailed pricing information