Skip to main content

Overview

The getOccupancyHistory endpoint retrieves historical hourly snapshots of parking occupancy data. It supports time range filtering and optional zone-specific queries for detailed analytics.

Endpoint

GET /get-occupancy-history

Authentication

CORS-enabled endpoint with wildcard origin (*). No authentication required for read access.

Query Parameters

days
integer
default:"1"
Number of days of history to retrieve. Valid range: 1-30. Values outside this range are clamped.
zoneId
string
Optional zone ID to filter results (e.g., “zone_1764307623391”). If provided, returns detailed zone-specific data. If omitted, returns summary for all zones.

Response

success
boolean
Always true for successful requests
days
integer
Actual number of days returned (after clamping 1-30)
zoneId
string
The zone ID filter applied, or null if showing all zones
count
integer
Number of hourly snapshots returned
from
string
ISO 8601 timestamp of the start of the time range
to
string
ISO 8601 timestamp of the end of the time range
samples
array
Array of hourly snapshot objects
samples[].hour_key
string
Unique hour identifier in format YYYY-MM-DD-HH (UTC)
samples[].ts
integer
Unix timestamp in milliseconds
samples[].global
object
Global parking statistics across all zones:
  • free: Number of available spots
  • occupied: Number of occupied spots
  • reserved: Number of reserved spots
  • total: Total number of spots
  • occupancyPct: Occupancy percentage (0-100)
samples[].zone
object
Zone-specific statistics (only present when zoneId parameter is provided)
samples[].zones_summary
array
Summary array of all zones (only present when zoneId is NOT provided). Each element contains:
  • id: Zone identifier
  • occupancyPct: Occupancy percentage
  • free: Available spots
  • occupied: Occupied spots
  • reserved: Reserved spots
  • total: Total spots

Success Response

  • 200 OK - Returns occupancy history data
  • 204 No Content - OPTIONS preflight response

Error Responses

  • 405 Method Not Allowed - Request method is not GET
  • 500 Internal Server Error - Database or server error

Code Examples

Get Last 7 Days (All Zones)

curl -X GET "https://YOUR_REGION-YOUR_PROJECT.cloudfunctions.net/get-occupancy-history?days=7"

Get Zone-Specific Data

curl -X GET "https://YOUR_REGION-YOUR_PROJECT.cloudfunctions.net/get-occupancy-history?days=3&zoneId=zone_1764307623391"

Response Examples

All Zones Summary

{
  "success": true,
  "days": 1,
  "zoneId": null,
  "count": 24,
  "from": "2026-03-04T10:00:00.000Z",
  "to": "2026-03-05T10:00:00.000Z",
  "samples": [
    {
      "hour_key": "2026-03-04-10",
      "ts": 1709550000000,
      "global": {
        "free": 12,
        "occupied": 20,
        "reserved": 4,
        "total": 36,
        "occupancyPct": 67
      },
      "zones_summary": [
        {
          "id": "zone_1764307623391",
          "occupancyPct": 70,
          "free": 6,
          "occupied": 12,
          "reserved": 2,
          "total": 20
        },
        {
          "id": "zone_1764306251630",
          "occupancyPct": 63,
          "free": 6,
          "occupied": 8,
          "reserved": 2,
          "total": 16
        }
      ]
    }
  ]
}

Single Zone Detail

{
  "success": true,
  "days": 1,
  "zoneId": "zone_1764307623391",
  "count": 24,
  "from": "2026-03-04T10:00:00.000Z",
  "to": "2026-03-05T10:00:00.000Z",
  "samples": [
    {
      "hour_key": "2026-03-04-10",
      "ts": 1709550000000,
      "global": {
        "free": 12,
        "occupied": 20,
        "reserved": 4,
        "total": 36,
        "occupancyPct": 67
      },
      "zone": {
        "free": 6,
        "occupied": 12,
        "reserved": 2,
        "total": 20,
        "occupancyPct": 70
      }
    }
  ]
}

Data Visualization Examples

Chart.js Line Graph

const response = await fetch('/get-occupancy-history?days=7');
const data = await response.json();

const chartData = {
  labels: data.samples.map(s => s.hour_key),
  datasets: [{
    label: 'Occupancy %',
    data: data.samples.map(s => s.global.occupancyPct),
    borderColor: 'rgb(75, 192, 192)',
    tension: 0.1
  }]
};

new Chart(ctx, {
  type: 'line',
  data: chartData,
  options: {
    scales: {
      y: {
        min: 0,
        max: 100,
        title: { display: true, text: 'Occupancy %' }
      }
    }
  }
});

Peak Hours Analysis

const response = await fetch('/get-occupancy-history?days=7');
const data = await response.json();

// Group by hour of day (0-23)
const hourlyStats = {};
data.samples.forEach(sample => {
  const hour = new Date(sample.ts).getUTCHours();
  if (!hourlyStats[hour]) {
    hourlyStats[hour] = { total: 0, count: 0 };
  }
  hourlyStats[hour].total += sample.global.occupancyPct;
  hourlyStats[hour].count += 1;
});

// Find peak hour
let peakHour = 0;
let peakOccupancy = 0;

for (let hour = 0; hour < 24; hour++) {
  if (hourlyStats[hour]) {
    const avg = hourlyStats[hour].total / hourlyStats[hour].count;
    if (avg > peakOccupancy) {
      peakOccupancy = avg;
      peakHour = hour;
    }
  }
}

console.log(`Peak hour: ${peakHour}:00 UTC with ${peakOccupancy.toFixed(1)}% average occupancy`);

Time Range Clamping

The days parameter is validated and clamped:
const daysParam = parseInt(req.query.days || '1', 10);
const days = isNaN(daysParam) ? 1 : Math.min(Math.max(daysParam, 1), 30);
Examples:
  • days=0 → Returns 1 day
  • days=45 → Returns 30 days (max)
  • days=invalid → Returns 1 day (default)

Performance Considerations

  • Indexed Query: Uses Firestore index on ts field for efficient time-based queries
  • Payload Optimization: Zone summary excludes spot-level details to reduce response size
  • Retention Policy: Historical data is automatically cleaned by Save Hourly Snapshot after 30 days

Build docs developers (and LLMs) love