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/v1API 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.
| Endpoint | Description |
|---|---|
/api/v1/events | Event listing with filters |
/api/v1/events/geo | Map clusters (GeoJSON) |
/api/v1/events/temporal | Temporal histogram |
/api/v1/events/stats | Event statistics |
/api/v1/events/geo/stats | Cluster size statistics |
/api/v1/sources/stats | Data 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.
| Endpoint | Description |
|---|---|
/api/health | System health check |
/api/auth/register | User registration (enumeration-safe) |
/api/wizard/* | Import wizard workflows |
/api/admin/* | Admin operations |
/api/import/* | Import progress tracking |
/api/import-jobs/* | Import job management |
/api/quotas | User quota status |
/api/newsletter/* | Newsletter subscription |
/api/webhooks/* | Webhook triggers |
/api/preview | Content 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:
- Login through the
/admin/logininterface or Payload’s authentication endpoints - Session cookie is automatically included in subsequent requests
- 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:
| Parameter | Type | Description | Example |
|---|---|---|---|
catalog | string | Filter by catalog slug | catalog=my-catalog |
datasets | string[] | Filter by dataset slugs (multiple) | datasets=dataset-1&datasets=dataset-2 |
bounds | JSON string | Geographic bounding box | bounds={"north":37.8,"south":37.7,"east":-122.3,"west":-122.5} |
startDate | ISO 8601 | Filter events after this date | startDate=2024-01-01 |
endDate | ISO 8601 | Filter events before this date | endDate=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/eventsQuery Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
catalog | string | No | Filter by catalog slug |
datasets | string[] | No | Filter by dataset slugs |
bounds | JSON | No | Geographic bounding box {north, south, east, west} |
startDate | ISO 8601 | No | Filter events after this date |
endDate | ISO 8601 | No | Filter events before this date |
page | integer | No | Page number (default: 1) |
limit | integer | No | Results per page (default: 100, max: 1000) |
sort | string | No | Sort 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/geoQuery Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
bounds | JSON | Yes | Geographic bounding box {north, south, east, west} |
zoom | integer | No | Map zoom level (default: 10, affects clustering) |
catalog | string | No | Filter by catalog slug |
datasets | string[] | No | Filter by dataset slugs |
startDate | ISO 8601 | No | Filter events after this date |
endDate | ISO 8601 | No | Filter 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/temporalQuery Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
catalog | string | No | Filter by catalog slug |
datasets | string[] | No | Filter by dataset slugs |
bounds | JSON | No | Geographic bounding box {north, south, east, west} |
startDate | ISO 8601 | No | Filter events after this date |
endDate | ISO 8601 | No | Filter events before this date |
granularity | string | No | Time 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 rangeday- Daily bucketsweek- Weekly bucketsmonth- 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/statsQuery Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
groupBy | string | Yes | Group results by catalog or dataset |
catalog | string | No | Filter by catalog slug |
datasets | string[] | No | Filter by dataset slugs |
bounds | JSON | No | Geographic bounding box {north, south, east, west} |
startDate | ISO 8601 | No | Filter events after this date |
endDate | ISO 8601 | No | Filter 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/statsQuery Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
catalog | string | No | Filter by catalog slug |
datasets | string[] | No | Filter by dataset slugs |
bounds | JSON | No | Geographic bounding box {north, south, east, west} |
startDate | ISO 8601 | No | Filter events after this date |
endDate | ISO 8601 | No | Filter 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/statsResponse:
{
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/progressPath Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
importId | string | Yes | Import 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:
UPLOAD- File uploaded successfullySCHEMA_DETECTION- Analyzing file structureAWAITING_APPROVAL- User must approve detected schemaVALIDATION- Validating data against schemaGEOCODING- Geocoding location fieldsPROCESSING- Creating events in databaseCOMPLETED- Import finished successfullyFAILED- 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/quotasAuthentication: 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 limiturlFetchesPerDay- Daily URL fetch limit for scheduled importsimportJobsPerDay- Daily import job creation limitactiveSchedules- Concurrent scheduled imports limittotalEvents- Lifetime event creation limiteventsPerImport- Single import size limitmaxFileSizeMB- 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/healthResponse:
{
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 Found429- 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:
| Parameter | Type | Default | Max | Description |
|---|---|---|---|---|
page | integer | 1 | - | Page number (1-indexed) |
limit | integer | 100 | 1000 | Results 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
eventTimestampfield 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 typesapps/web/app/api/v1/events/geo/route.ts- GeoJSON cluster typesapps/web/app/api/v1/events/temporal/route.ts- Histogram typesapps/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
- Usage Limits - Rate limiting, quotas, and trust levels
- HTTP Caching - Caching for scheduled URL imports
- API Reference - Auto-generated TypeScript documentation