Skip to Content
⚠️Active Development Notice: TimeTiles is under active development. Information may be placeholder content or not up-to-date.

REST API

The TimeTiles REST API provides programmatic access to event data, geospatial clustering, temporal histograms, and ingest progress tracking. Event read endpoints return JSON and share filters for catalog, dataset, date range, and geographic bounds; other routes have their own contracts.

Base URL

https://your-instance.com/api/v1

API Endpoint Structure

TimeTiles uses two categories of API endpoints with different purposes:

Versioned Public APIs (/api/v1/)

Purpose: Stable, public-facing data APIs designed for external consumers and integrations.

EndpointDescription
/api/v1/eventsEvent listing with filters
/api/v1/events/geoMap clusters (GeoJSON)
/api/v1/events/geo/statsCluster size statistics
/api/v1/events/boundsMap bounds
/api/v1/events/temporalTemporal histogram
/api/v1/events/statsEvent statistics
/api/v1/data-sourcesData source listing
/api/v1/sources/statsData source statistics
/api/v1/datasets/[id]/schema/inferSchema inference

Characteristics:

  • Versioned for backwards compatibility
  • Stable response formats
  • Designed for external API consumers
  • Breaking changes require new version (v2, v3, etc.)

Internal APIs (/api/)

Purpose: Frontend-specific APIs used by the TimeTiles application. Not intended for external consumption.

EndpointDescription
/api/healthSystem health check
/api/auth/registerUser registration (enumeration-safe)
/api/ingest/*Ingestion schema preview, configure, validate
/api/ingest-jobs/*Ingestion job management (reset, retry)
/api/users/*Account management (password, email, deletion)
/api/account/*Account deletion summary
/api/admin/*Admin operations (schedule, jobs)
/api/quotasUser quota status
/api/feature-flagsFeature flag state
/api/data-exports/*Data export request and download
/api/scheduled-ingests/*Manual trigger for scheduled ingestion
/api/legal-noticesLegal notices
/api/catalogs/with-datasetsCatalogs with nested datasets
/api/geocoding/testGeocoding provider test
/api/newsletter/*Newsletter subscription
/api/webhooks/*Webhook triggers
/api/previewContent preview

Characteristics:

  • No version prefix (internal use only)
  • May change without notice
  • Coupled to frontend implementation
  • Not recommended for external integrations

Payload CMS REST API (/api/{collection})

Payload CMS auto-generates REST endpoints for all collections (e.g., /api/users, /api/events). These are used by the admin panel and enforce collection-level access control; whether authentication is required depends on the collection and operation.

For external integrations, prefer the versioned APIs (/api/v1/) which have stable response formats.

Authentication

TimeTiles uses Payload CMS session-based authentication. To access protected resources:

  1. Login through the /dashboard/login interface or Payload’s authentication endpoints
  2. Session cookie is automatically included in subsequent requests
  3. User permissions are enforced based on your account’s trust level and role

Public event reads allow anonymous access to public data. A session enables access to additional data permitted by the caller’s account; it does not bypass collection or route access controls.

Common Query Parameters

Many endpoints share common filtering parameters:

ParameterTypeDescriptionExample
catalogintegerFilter by numeric catalog IDcatalog=1
datasetsstringComma-separated numeric dataset IDsdatasets=2,3
boundsJSON stringGeographic bounding boxbounds={"north":37.8,"south":37.7,"east":-122.3,"west":-122.5}
startDateISO 8601Filter events after this datestartDate=2024-01-01
endDateISO 8601Filter events before this dateendDate=2024-12-31

Categorical (ff) and numeric range (rf) filters are JSON-encoded query parameters; see Dynamic Filters for their wire formats. The shared contract is EventFiltersSchema in apps/web/lib/schemas/events.ts.

Rate Limiting & Quotas

API requests are protected by both rate limiting (burst protection) and quotas (long-term limits):

  • Rate Limits: Based on trust level (0-5), with multiple time windows (burst, hourly, daily)
  • Quotas: Daily and lifetime limits on operations and resource consumption
  • Headers: Responses include X-RateLimit-* headers with limit information

See Usage Limits for details on rate limiting and quotas.

Endpoints

Events

List Events

Retrieve a paginated list of events with optional filters.

GET /api/v1/events

Query Parameters:

ParameterTypeRequiredDescription
catalogintegerNoFilter by numeric catalog ID
datasetsstringNoComma-separated numeric dataset IDs
boundsJSONNoGeographic bounding box {north, south, east, west}
startDateISO 8601NoFilter events after this date
endDateISO 8601NoFilter events before this date
pageintegerNoPage number (default: 1)
limitintegerNoResults per page (default: 100, max: 1000)
sortstringNoSort field (default: -eventTimestamp)

Response:

{ events: [ { id: number, dataset: { id: number, name?: string, catalog?: string }, data: Record<string, unknown>, // Event-specific data location: { longitude: number, latitude: number } | null, eventTimestamp: string, isValid: boolean } ], pagination: { page: number, limit: number, totalDocs: number, totalPages: number, hasNextPage: boolean, hasPrevPage: boolean, nextPage: number | null, prevPage: number | null } }

Example:

curl "https://your-instance.com/api/v1/events?\ catalog=1&\ startDate=2024-01-01&\ endDate=2024-12-31&\ page=1&\ limit=100"

Implementation: apps/web/app/api/v1/events/route.ts


Get Map Clusters

Retrieve clustered or individual event points for map visualization. Uses PostGIS server-side clustering for performance with large datasets.

GET /api/v1/events/geo

Query Parameters:

ParameterTypeRequiredDescription
boundsJSONYesGeographic bounding box {north, south, east, west}
zoomintegerNoMap zoom level (default: 10, affects clustering)
catalogintegerNoFilter by numeric catalog ID
datasetsstringNoComma-separated numeric dataset IDs
startDateISO 8601NoFilter events after this date
endDateISO 8601NoFilter events before this date

Response:

Returns a GeoJSON FeatureCollection with cluster and point features:

{ type: "FeatureCollection", features: [ { type: "Feature", id: number | string, geometry: { type: "Point", coordinates: [longitude, latitude] }, properties: { type: "event-cluster" | "event-location", count?: number, // Events represented by the feature title?: string, // For individual events eventId?: number, // When the location represents one event sourceCells?: string[], // H3 cells represented by a cluster h3Cell?: string } } ] }

Clustering Behavior:

The default algorithm is H3; clusterAlgorithm also accepts grid-k and dbscan. Resolution depends on zoom and the clustering parameters, not a fixed zoom cutoff. At the finest H3 resolution, co-located events remain grouped as event-location features.

See MapClustersQuerySchema and ClusterFeatureSchema in apps/web/lib/schemas/events.ts for the full parameter and response contracts.

Example:

curl "https://your-instance.com/api/v1/events/geo?\ bounds=%7B%22north%22%3A37.8%2C%22south%22%3A37.7%2C%22east%22%3A-122.3%2C%22west%22%3A-122.5%7D&\ zoom=12&\ catalog=1"

Implementation: apps/web/app/api/v1/events/geo/route.ts


Get Temporal Histogram

Retrieve temporal histogram data showing event distribution over time. Uses custom PostgreSQL function for efficient aggregation.

GET /api/v1/events/temporal

Query Parameters:

ParameterTypeRequiredDescription
catalogintegerNoFilter by numeric catalog ID
datasetsstringNoComma-separated numeric dataset IDs
boundsJSONNoGeographic bounding box {north, south, east, west}
startDateISO 8601NoFilter events after this date
endDateISO 8601NoFilter events before this date
targetBucketsintegerNoTarget number of histogram buckets (default: 30, max: 500)
minBucketsintegerNoMinimum number of buckets (default: 20, max: 500)
maxBucketsintegerNoMaximum number of buckets (default: 50, max: 500)

Response:

{ histogram: [ { date: string, // Bucket start (ISO 8601) dateEnd: string, // Bucket end (ISO 8601) count: number // Events in this bucket } ], metadata: { total: number, dateRange: { min: string | null, max: string | null }, bucketSizeSeconds: number | null, bucketCount: number, counts: { datasets: number, catalogs: number }, topDatasets: Array<{ id: number, name: string, count: number }>, topCatalogs: Array<{ id: number, name: string, count: number }> } }

Bucket Sizing:

The PostgreSQL function calculate_event_histogram automatically determines optimal bucket sizes based on the data range and the targetBuckets/minBuckets/maxBuckets parameters. The actual bucket size used is returned in metadata.bucketSizeSeconds.

If minBuckets exceeds maxBuckets, the route clamps it to maxBuckets. Source counts represent distinct datasets and catalogs with matching timestamped events. topDatasets and topCatalogs contain up to five sources ranked by event count (ties use ascending ID), under the same access controls and filters as the histogram.

Example:

curl "https://your-instance.com/api/v1/events/temporal?\ catalog=1&\ startDate=2024-01-01&\ endDate=2024-12-31"

Implementation: apps/web/app/api/v1/events/temporal/route.ts


Get Event Statistics

Aggregate event counts grouped by catalog or dataset.

GET /api/v1/events/stats

Query Parameters:

ParameterTypeRequiredDescription
groupBystringYesGroup results by catalog or dataset
catalogintegerNoFilter by numeric catalog ID
datasetsstringNoComma-separated numeric dataset IDs
boundsJSONNoGeographic bounding box {north, south, east, west}
startDateISO 8601NoFilter events after this date
endDateISO 8601NoFilter events before this date

Response:

{ items: [ { id: number | string, name: string, count: number } ], total: number, groupedBy: "catalog" | "dataset" }

Example:

curl "https://your-instance.com/api/v1/events/stats?\ groupBy=dataset&\ catalog=1"

Implementation: apps/web/app/api/v1/events/stats/route.ts


Get Cluster Statistics

Get percentile breakpoints for cluster sizes across the filtered dataset. Used for consistent cluster visualization across zoom levels.

GET /api/v1/events/geo/stats

Query Parameters:

ParameterTypeRequiredDescription
catalogintegerNoFilter by numeric catalog ID
datasetsstringNoComma-separated numeric dataset IDs
boundsJSONNoGeographic bounding box {north, south, east, west}
startDateISO 8601NoFilter events after this date
endDateISO 8601NoFilter events before this date

Response:

{ p20: number, // 20th percentile cluster size p40: number, // 40th percentile p60: number, // 60th percentile p80: number, // 80th percentile p100: number // Maximum cluster size }

Example:

curl "https://your-instance.com/api/v1/events/geo/stats?\ catalog=1"

Implementation: apps/web/app/api/v1/events/geo/stats/route.ts


Sources

Get Data Source Statistics

Get event counts grouped by catalog and dataset. Used for displaying totals in the filter UI.

GET /api/v1/sources/stats

Response:

{ catalogCounts: Record<string, number>, // catalog ID → event count datasetCounts: Record<string, number>, // dataset ID → event count totalEvents: number }

Example:

curl "https://your-instance.com/api/v1/sources/stats"

Implementation: apps/web/app/api/v1/sources/stats/route.ts


Ingestion Progress

Get Ingestion Progress

Retrieve real-time progress information for a data ingestion operation.

GET /api/ingest/:ingestId/progress

Path Parameters:

ParameterTypeRequiredDescription
ingestIdstringYesIngest file ID

Response:

{ type: "ingest-file", id: string | number, status: string, originalName: string, catalogId: number | null, datasetsCount: number, datasetsProcessed: number, overallProgress: number, // 0-100 estimatedCompletionTime: string | null, jobs: [ { id: string | number, datasetId: string | number, datasetName?: string, currentStage: string, overallProgress: number, // 0-100 estimatedCompletionTime: string | null, stages: [ { name: string, displayName: string, status: "pending" | "in_progress" | "completed" | "skipped", progress: number, // 0-100 weight: number, startedAt: string | null, completedAt: string | null, batches: { current: number, total: number }, currentBatch: { rowsProcessed: number, rowsTotal: number, percentage: number }, performance: { rowsPerSecond: number | null, estimatedSecondsRemaining: number | null } } ], errors: number, // Count of retained error details, not all row failures duplicates: { internal: number, external: number }, schemaValidation?: {...}, reviewReason: string | null, reviewDetails: Record<string, unknown> | null, results?: {...} } ], errorLog?: string | null, completedAt?: string, createdAt: string }

Ingestion Stages:

  1. analyze-duplicates - Finding duplicate rows
  2. detect-schema - Analyzing file structure and field types
  3. validate-schema - Comparing detected schema with existing dataset schema
  4. needs-review - Paused for user review in manual or automated imports; reviewReason and reviewDetails identify the data-quality or configuration issue
  5. create-schema-version - Persisting approved schema
  6. geocode-batch - Geocoding location fields
  7. create-events - Creating event records in database
  8. completed - Ingestion finished successfully
  9. failed - Ingestion failed with errors

Example:

curl "https://your-instance.com/api/ingest/123/progress"

Implementation: apps/web/app/api/ingest/[ingestId]/progress/route.ts


User Quotas

Get Quota Status

Retrieve current user’s quota status including usage and limits.

GET /api/quotas

Authentication: Required (user must be logged in)

Response:

{ quotas: { fileUploadsPerDay: { used: number, limit: number, remaining: number }, urlFetchesPerDay: {...}, // same shape importJobsPerDay: {...}, // same shape activeSchedules: {...}, // same shape totalEvents: {...}, // same shape eventsPerImport: {...}, // same shape maxFileSizeMB: { limit: number } } }

Quota Types:

  • fileUploadsPerDay - Daily file upload limit
  • urlFetchesPerDay - Daily URL fetch limit for scheduled imports
  • importJobsPerDay - Daily import job creation limit
  • activeSchedules - Concurrent scheduled imports limit
  • totalEvents - Lifetime event creation limit
  • eventsPerImport - Single import size limit
  • maxFileSizeMB - File size limit

Example:

curl -H "Cookie: payload-token=..." \ "https://your-instance.com/api/quotas"

Implementation: apps/web/app/api/quotas/route.ts


Health Check

System Health

Check system health and readiness.

GET /api/health

Response:

{ database: { status: "ok" | "error", message?: string }, migrations: { status: "ok" | "pending" | "error", pending?: string[] }, postgis: { status: "ok" | "not found" | "error", version?: string }, environment: { status: "ok" | "error", missing?: string[] } }

Status Codes:

  • 200 - System healthy (may have warnings)
  • 503 - System has critical errors

Example:

curl "https://your-instance.com/api/health"

Implementation: apps/web/app/api/health/route.ts


Error Responses

All endpoints return consistent error responses:

{ error: string, // Error type message?: string, // Human-readable message details?: string, // Additional error details code?: string // Error code (e.g., "MISSING_DB_FUNCTION") }

Common HTTP Status Codes:

  • 400 - Bad Request (invalid parameters)
  • 401 - Unauthorized (authentication required)
  • 403 - Forbidden (insufficient permissions)
  • 404 - Not Found
  • 429 - Too Many Requests (rate limit exceeded)
  • 500 - Internal Server Error

Rate Limit Errors:

{ success: false, error: "Rate limit exceeded", message: "Too many requests. Please wait 10 seconds.", limitType: "burst" | "hourly" | "daily", retryAfter: string // ISO 8601 timestamp }

Headers:

Retry-After: <seconds> X-RateLimit-Limit: <limit> X-RateLimit-Remaining: 0 X-RateLimit-Reset: <timestamp>

Data Model

TimeTiles uses a hierarchical data model:

Catalogs (top-level organization) └── Datasets (groupings within catalogs) └── Events (individual time-based data points)

Access Control

The system implements hierarchical access control:

  • Catalog-level: Controls access to all datasets and events within
  • Dataset-level: Inherits catalog permissions, can add additional restrictions
  • Event-level: Inherits dataset permissions

See the access control documentation for details.

Pagination

Paginated endpoints support standard pagination parameters:

ParameterTypeDefaultMaxDescription
pageinteger1-Page number (1-indexed)
limitinteger1001000Results per page

Response includes:

{ pagination: { page: number, limit: number, totalDocs: number, totalPages: number, hasNextPage: boolean, hasPrevPage: boolean, nextPage: number | null, prevPage: number | null } }

Performance Considerations

Caching

The API uses URL fetch caching for scheduled imports. See HTTP Caching for details.

Geospatial Queries

  • Map clustering uses PostGIS functions for server-side aggregation
  • Bounds filtering uses spatial indexes for fast queries
  • Zoom-based clustering automatically adjusts cluster resolution

Temporal Queries

  • Histogram uses custom PostgreSQL function for efficient time-based aggregation
  • Date filtering supports both eventTimestamp field and custom date fields in event data

TypeScript Types

The API routes have comprehensive TypeScript documentation in the source code.

Key type definitions:

  • apps/web/app/api/v1/events/route.ts - Event listing types
  • apps/web/app/api/v1/events/geo/route.ts - GeoJSON cluster types
  • apps/web/app/api/v1/events/temporal/route.ts - Histogram types
  • apps/web/payload-types.ts - Payload collection types

Examples

Fetch Events in San Francisco

const response = await fetch( "https://your-instance.com/api/v1/events?" + new URLSearchParams({ bounds: JSON.stringify({ north: 37.8, south: 37.7, east: -122.3, west: -122.5 }), startDate: "2024-01-01", endDate: "2024-12-31", limit: "100", }) ); const { events, pagination } = await response.json();

Get Map Clusters with Filter

const response = await fetch( "https://your-instance.com/api/v1/events/geo?" + new URLSearchParams({ bounds: JSON.stringify({ north: 37.8, south: 37.7, east: -122.3, west: -122.5 }), zoom: "12", catalog: "tech-events", }) ); const geojson = await response.json(); // Use with MapLibre, Leaflet, etc.

Check Ingest Progress

async function pollIngestProgress(ingestId: string) { const response = await fetch(`https://your-instance.com/api/ingest/${ingestId}/progress`); const progress = await response.json(); console.log(`Overall: ${progress.overallProgress}%`); progress.jobs.forEach((job) => { console.log(`${job.datasetName}: ${job.stage} (${job.progress}%)`); }); return progress; }

Get Temporal Event Histogram

const response = await fetch( "https://your-instance.com/api/v1/events/temporal?" + new URLSearchParams({ catalog: "1", startDate: "2024-01-01", endDate: "2024-12-31" }) ); const { histogram, metadata } = await response.json(); // histogram = [{ date: 1704067200000, dateEnd: 1706745600000, count: 150 }, ...] console.log(`Total events: ${metadata.total}`);

See Also

Last updated on