Journey Docs
Public APIAvailabilities

Get Batch Availability

Get unified availability for multiple properties with source indicators

POST/public/properties/availabilities

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

No authentication required. This is a public endpoint.

Request Body

propertyIdsbodystring[]required

Property IDs (internal IDs or document IDs)
Maximum: 100 properties

dateFrombodystring

Start date filter (YYYY-MM-DD format)

dateTobodystring

End date filter (YYYY-MM-DD format)

checkBookingConflictsbodybooleandefault: true

Whether 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

resultsarrayrequired

Availability results for each property

result items
propertyIdstringrequired

Property ID that was queried

sourcestringrequired

Source of the availability data
Options: liteapi, strapi, none

availabilitiesarrayrequired

Availability data for each day (same structure as single property endpoint)

availability items
datestringrequired

Date in YYYY-MM-DD format

pricenumberrequired

Nightly rate in USD

isAvailablebooleanrequired

Whether the date is available for booking

restrictionsobject

Stay restrictions for this date

restrictions object
minNightsnumberrequired

Minimum nights required

maxNightsnumber

Maximum nights allowed (null if unlimited)

closedForArrivalboolean

Whether arrivals are closed for this date

closedForDepartureboolean

Whether departures are closed for this date

lastSyncAtstring

When the data was last synced (ISO timestamp, null if never synced)

bookingConflictsarrayrequired

Booking conflicts affecting availability

conflict items
checkInDatestringrequired

Check-in date of the conflict

checkOutDatestringrequired

Check-out date of the conflict

typestringrequired

Type of booking conflict
Options: pending_payment_intent, confirmed_booking

expiresAtstring

When the hold expires (only for pending payment intents)

metaobjectrequired

Response metadata (same structure as single property endpoint)

meta object
resolvedIdsobjectrequired

Resolved property IDs across all sources

sourcesCheckedstring[]required

Sources that were checked for availability data

summaryobjectrequired

Summary of batch results

summary object
totalnumberrequired

Total number of properties queried

bySourceobjectrequired

Count by data source

by source
liteapinumberrequired

Count of properties with LiteAPI data

strapinumberrequired

Count of properties with Strapi data (PMS/Airtable synced)

nonenumberrequired

Count 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));
    }
  }
};

On this page