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 import progress tracking. All endpoints return JSON responses and support filtering by catalog, dataset, date range, and geographic bounds.

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/temporalTemporal histogram
/api/v1/events/statsEvent statistics
/api/v1/events/geo/statsCluster size statistics
/api/v1/sources/statsData source statistics

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/wizard/*Import wizard workflows
/api/admin/*Admin operations
/api/import/*Import progress tracking
/api/import-jobs/*Import job management
/api/quotasUser quota status
/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 require authentication with collection-level access control.

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 /admin/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

There is currently no standalone API key system. All API access requires an authenticated session through the Payload CMS authentication system.

Common Query Parameters

Many endpoints share common filtering parameters:

ParameterTypeDescriptionExample
catalogstringFilter by catalog slugcatalog=my-catalog
datasetsstring[]Filter by dataset slugs (multiple)datasets=dataset-1&datasets=dataset-2
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

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
catalogstringNoFilter by catalog slug
datasetsstring[]NoFilter by dataset slugs
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, title: 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=tech-events&\ 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)
catalogstringNoFilter by catalog slug
datasetsstring[]NoFilter by dataset slugs
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", geometry: { type: "Point", coordinates: [longitude, latitude] }, properties: { id: number | string, type: "event-cluster" | "event-point", count?: number, // For clusters only title?: string, // For individual events eventIds?: number[] // For small clusters (≤10 events) } } ] }

Clustering Behavior:

  • High zoom (≥16): Individual events returned (no clustering)
  • Medium zoom (10-15): Events clustered within ~100-1000m radius
  • Low zoom (<10): Aggressive clustering for performance
  • Cluster threshold: 2+ events at same location

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=sf-events"

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
catalogstringNoFilter by catalog slug
datasetsstring[]NoFilter by dataset slugs
boundsJSONNoGeographic bounding box {north, south, east, west}
startDateISO 8601NoFilter events after this date
endDateISO 8601NoFilter events before this date
granularitystringNoTime bucket size: day, week, month, auto (default: auto)

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 }, counts: { datasets: number, catalogs: number }, topDatasets: string[], topCatalogs: string[] } }

Granularity Selection:

  • auto - Automatically selects appropriate granularity based on date range
  • day - Daily buckets
  • week - Weekly buckets
  • month - Monthly buckets

Example:

curl "https://your-instance.com/api/v1/events/temporal?\ catalog=tech-events&\ 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
catalogstringNoFilter by catalog slug
datasetsstring[]NoFilter by dataset slugs
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=tech-events"

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
catalogstringNoFilter by catalog slug
datasetsstring[]NoFilter by dataset slugs
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=tech-events"

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


Import Progress

Get Import Progress

Retrieve real-time progress information for a file import operation.

GET /api/import/:importId/progress

Path Parameters:

ParameterTypeRequiredDescription
importIdstringYesImport file ID

Response:

{ type: "import-file", id: string | number, status: string, originalName: string, datasetsCount: number, datasetsProcessed: number, overallProgress: number, // 0-100 jobs: [ { id: string | number, datasetId: string | number, datasetName?: string, stage: string, progress: number, // 0-100 rowsTotal: number, rowsProcessed: number, batchNumber: number, errors: number, duplicates: { internal: number, external: number }, schemaValidation?: {...}, geocodingProgress?: {...}, results?: {...} } ], errorLog?: string[], completedAt?: string, createdAt: string }

Import Stages:

  1. UPLOAD - File uploaded successfully
  2. SCHEMA_DETECTION - Analyzing file structure
  3. AWAITING_APPROVAL - User must approve detected schema
  4. VALIDATION - Validating data against schema
  5. GEOCODING - Geocoding location fields
  6. PROCESSING - Creating events in database
  7. COMPLETED - Import finished successfully
  8. FAILED - Import failed with errors

Example:

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

Implementation: apps/web/app/api/import/[importId]/progress/route.ts:72-139


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:

{ user: { id: string | number, email: string, role: "admin" | "user", trustLevel: "0" | "1" | "2" | "3" | "4" | "5" }, quotas: { fileUploadsPerDay: { used: number, limit: number, remaining: number, allowed: boolean, resetTime: string, description: "Maximum file uploads allowed per day" }, urlFetchesPerDay: {...}, importJobsPerDay: {...}, activeSchedules: {...}, totalEvents: {...}, eventsPerImport: {...}, maxFileSizeMB: { limit: number, description: "Maximum file size in megabytes" } }, summary: { hasUnlimitedAccess: boolean, nextResetTime: string } }

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

Response Headers:

X-RateLimit-Limit: <limit> X-RateLimit-Remaining: <remaining> X-RateLimit-Reset: <ISO 8601 timestamp>

Example:

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

Implementation: apps/web/app/api/quotas/route.ts:29-108


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. See the API Reference for auto-generated documentation from 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 Import Progress

async function pollImportProgress(importId: string) { const response = await fetch(`https://your-instance.com/api/import/${importId}/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: "my-catalog", 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