Overview
The geo service serves a public MapLibre viewer at
maps.replicity.ai and a
versioned API at /v1/*. Create keys at
replicity.ai/account.
The repo copy of this contract lives in docs/maps-api/.
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET |
/ |
Public | Map viewer |
GET |
/docs/ |
Public | This hub |
GET |
/healthz |
Public | Process + database ping |
GET |
/v1/cities |
Public | City catalog |
GET |
/v1/cities/{city_id}/layers |
Public | Published versions |
GET |
/v1/layers |
Public | Layer taxonomy |
GET |
/v1/me |
Optional key | Identity, caps, usage |
GET |
/v1/features |
maps:read |
Feature download |
GET |
/v1/features/{layer}/{id} |
maps:read |
One feature plus per-vertex Z / z_source |
GET |
/v1/features/estimate |
maps:read |
Feature count / byte estimate |
GET |
/v1/preview/{layer}/{z}/{x}/{y}.json |
maps:read |
Simplified GeoJSON tile |
GET |
/v1/basemap/config |
keyed maps:read |
Imagery tile template |
GET |
/v1/basemap/imagery/{z}/{y}/{x} |
keyed maps:read |
Satellite tile bytes |
Coordinates are EPSG:4326 (lon, lat). Z is NAVD88 orthometric metres on every vertex. GeoJSON includes Z even though RFC 7949 is 2D. OpenAPI / Swagger UI is not served.
Quick start
curl https://maps.replicity.ai/v1/cities
curl https://maps.replicity.ai/v1/layers
curl "https://maps.replicity.ai/v1/features?city=us-ny-nyc&layers=road_polygons&bbox=-74.01,40.75,-74.00,40.76&format=geojson"
curl -H "Authorization: Bearer rpl_live_…" \
"https://maps.replicity.ai/v1/features?city=us-ny-nyc&layers=road_polygons&bbox=-74.01,40.75,-74.00,40.76&format=fgb" \
-o roads.fgb
curl -H "Authorization: Bearer rpl_live_…" \
https://maps.replicity.ai/v1/basemap/config
curl -H "Authorization: Bearer rpl_live_…" \
"https://maps.replicity.ai/v1/basemap/imagery/17/{y}/{x}" \
-o tile.jpg
Authentication
/v1/* accepts either no Authorization
header (anonymous) or Authorization: Bearer <api_key>.
Anonymous tier
Missing key → synthetic principal
tier=anonymous, scopes=["maps:read"].
GeoJSON only. format=fgb returns 401. Basemap imagery
is keyed only (401 without a key). Only
redistributable dataset versions. Rate limit is a salted
IP hash. Kill switch: ANON_TIER_ENABLED=false.
API keys
Format rpl_live_<8-char-prefix>_<secret>.
Looked up by prefix, verified with argon2id, rejected if revoked.
In-process cache is 60 seconds (the revocation window).
Authorization: Bearer rpl_live_a1b2c3d4_XyZ…
Tiers and caps
| Anonymous | Authenticated | |
|---|---|---|
| Max bbox | 4 km² | 50 km² |
| Max features / request | 6,000 | 20,000 |
| Rate limit | 30 rpm / IP hash |
/v1/features: key
rate_limit_rpm (default 60).
/v1/basemap/*: 300 rpm
|
| Formats | geojson |
geojson, fgb |
| Non-redistributable | Never | Only internal tier or internal account |
Usage is logged and summarized on /v1/me.
CORS
Applies to /v1 only. Origins are never reflected.
Anonymous callers and keys with empty allowed_origins
use the public allowlist:
https://replicity.ai,
https://www.replicity.ai,
https://maps.replicity.ai,
http://localhost:3000.
Keys with allowed_origins set are limited to that list.
Preflight: 204 allowlisted, 403 otherwise.
Methods: GET, OPTIONS.
Headers: Authorization, Content-Type.
Endpoints
JSON errors are {"detail":"…"} unless noted.
GET /healthz
Always HTTP 200. Read ok.
{ "ok": true, "geo_db": "ok", "app_db": "ok" }
GET /v1/cities
Public catalog. 503 if the geo database is down.
[
{
"id": "us-ny-nyc",
"name": "New York City",
"display_name": "New York, USA",
"bbox": { "type": "Polygon", "coordinates": [[…]] },
"center": { "type": "Point", "coordinates": [-73.98, 40.705] },
"layers": ["road_centerlines", "road_edges", "road_polygons"],
"freshness": "2026-08-12T00:00:00+00:00"
}
]
GET /v1/cities/{city_id}/layers
Published versions for one city. Unknown city → 404.
[
{
"id": 1,
"layer": "road_polygons",
"version": "2026.08.0",
"license": "NYC Open Data Terms of Use",
"attribution": "City of New York",
"redistributable": true,
"vertical_crs": "NAVD88",
"horizontal_crs": "EPSG:4326",
"published_at": "2026-08-12T00:00:00+00:00",
"source_name": "NYC Planimetrics 2022 + LION 26A",
"feature_count": 18420
}
]
GET /v1/layers
Static taxonomy: id, display_name,
category, available,
geometry, description,
fields.
Live layers have available: true. Future layers
(lane_marking_observations, road_decals,
landmark_buildings,
water_bodies, vegetation,
fields) are advertised with
available: false and cannot be queried.
fields is a documentation subset — full GeoJSON
properties are under Data layers.
GET /v1/me
{
"tier": "anonymous",
"account_id": null,
"key_prefix": null,
"scopes": ["maps:read"],
"is_internal": false,
"caps": {
"max_bbox_km2": 4.0,
"max_features_per_request": 6000,
"rate_limit_rpm": 30,
"formats": ["geojson"]
},
"usage": {
"area_km2": 0.0,
"features_returned": 0,
"period": "calendar_month_utc",
"available": true
}
}
GET /v1/features
Metered download. Requires maps:read (anonymous has it).
| Param | Required | Description |
|---|---|---|
city |
yes | City id (us-ny-nyc) |
layers |
yes | Comma-separated layer ids |
bbox |
yes | minx,miny,maxx,maxy EPSG:4326 |
format |
no | geojson (default) or fgb |
detail |
no |
footprint (default) or recipe.
recipe adds the building recipe blocks and requires
format=geojson
|
limit |
no | 1–50000, capped by tier |
cursor |
no | Keyset: rows with id > cursor |
ST_Intersectsthe bbox envelope.-
Same
cursorapplied to every requested layer.limitis a shared remaining budget in request order. -
format=fgbneeds a key and exactly one layer. Content-Typeapplication/flatgeobuf. Encoder currently writes geometry +idonly. -
Headers:
X-Replicity-Attribution,X-Replicity-Next-Cursor.
format=geojson — application/geo+json:
{
"type": "FeatureCollection",
"attribution": "City of New York",
"features": [
{
"type": "Feature",
"id": 42,
"geometry": {
"type": "MultiPolygon",
"coordinates": [[[[-74.01, 40.75, 12.3]]]]
},
"properties": {
"id": 42,
"city_id": "us-ny-nyc",
"layer": "road_polygons",
"class": "carriageway",
"z_source": "pavement_edge_z"
}
}
]
}
properties is every table column except geom,
plus injected layer. Coordinates are
[lon, lat, z_metres].
attributes.vertex_sources is omitted from collection
responses; use feature detail for per-vertex provenance.
GET /v1/features/{layer}/{id}
One feature by id, with a vertices array for editor
debug. Requires maps:read. Query: city
(required). Unknown city or missing feature → 404. Closing ring
duplicates are omitted. Each vertex has lon,
lat, z_metres, and z_source
(from attributes.vertex_sources when present, else the
feature z_source).
{
"type": "Feature",
"id": 42,
"geometry": {
"type": "MultiPolygon",
"coordinates": [[[[-74.01, 40.75, 12.3]]]]
},
"properties": {
"id": 42,
"city_id": "us-ny-nyc",
"layer": "road_polygons",
"class": "carriageway",
"z_source": "pavement_edge_z"
},
"vertices": [
{
"index": 0,
"part": 0,
"ring": 0,
"lon": -74.01,
"lat": 40.75,
"z_metres": 12.3,
"z_source": "pavement_edge_z"
}
]
}
GET /v1/features/estimate
Same city, layers, and bbox
as features, but returns counts only. Shares the features rate
bucket and bbox-area cap.
{
"city": "us-ny-nyc",
"layers": ["road_polygons"],
"bbox": [-74.01, 40.70, -74.00, 40.71],
"area_km2": 0.936,
"feature_count": 128,
"bytes_estimate": 450000
}
bytes_estimate is
3 × SUM(ST_MemSize(geom)), a GeoJSON-sized
overestimate.
GET /v1/preview/{layer}/{z}/{x}/{y}.json
Simplified GeoJSON for a Web Mercator XYZ tile
({z}/{x}/{y}, OSM order — not the ArcGIS
{z}/{y}/{x} used by basemap). Anonymous
maps:read is enough. Optional city
query; otherwise features must sit in a catalog city that
intersects the tile.
-
Queryable layers only
(
road_polygons,road_centerlines,road_edges,lane_markings,lane_marking_groups). Unavailable catalog ids → 400layer is not available. -
ST_Simplifyat about two pixels of the tile. Properties areidandclassonly. -
Separate
preview:{key}bucket atPREVIEW_RATE_LIMIT_RPM(default 300). - Tile-keyed cache shared across callers. Tile area is checked against the bbox-area cap.
{
"type": "FeatureCollection",
"attribution": "City of New York",
"features": [
{
"type": "Feature",
"id": 42,
"geometry": { "type": "MultiPolygon", "coordinates": [[[[-74.01, 40.75, 12.3]]]] },
"properties": { "id": 42, "class": "carriageway" }
}
]
}
GET /v1/basemap/config
Keyed client bootstrap. Requires a minted key with
maps:read. Anonymous → 401.
{
"style": "imagery",
"tileSize": 256,
"minZ": 0,
"maxZ": 19,
"urlTemplate": "/v1/basemap/imagery/{z}/{y}/{x}",
"attribution": "Source: Esri, Vantor, Earthstar Geographics, and the GIS User Community"
}
urlTemplate is relative to
https://maps.replicity.ai. Tile indices are
{z}/{y}/{x} (ArcGIS order), not
{z}/{x}/{y}. Display attribution with the
imagery.
GET /v1/basemap/imagery/{z}/{y}/{x}
Keyed raster proxy for Esri World Imagery. The ArcGIS token stays
on the geo service. Basemap uses a separate per-key bucket
(BASEMAP_RATE_LIMIT_RPM, default 300) so a viewport of
tiles is not capped by the /v1/features 60 rpm limit.
- Missing/invalid key → 401. Missing
maps:read→ 403. -
zin[minZ, maxZ];xandyin0 .. 2^z - 1. -
Cache key is
(z, y, x), shared across keys. Origin miss fetches Esri; non-image / non-200 → 502Basemap origin failed. -
Every successful tile (origin and cache hit) is metered:
layers=["basemap_imagery"],area_km2= Web Mercator ground area of the tile. -
Headers:
X-Replicity-Attribution,Cache-Control: public, max-age=86400.
Data layers
| Id | Table | Geometry | Description |
|---|---|---|---|
road_polygons |
geo.road_polygon |
MultiPolygonZ | Carriageway, intersection, shoulder, driveway |
road_centerlines |
geo.road_centerline |
LineStringZ | Street centerlines |
road_edges |
geo.road_edge |
LineStringZ | Pavement, seam, structure, median edges |
lane_markings |
geo.lane_marking |
LineStringZ | Painted marking centerlines (continuous; dashes are attributes) |
lane_marking_groups |
geo.lane_marking_group |
LineStringZ | Groups binding paired markings (e.g. double yellow) |
sidewalks |
geo.sidewalk |
MultiPolygonZ | Sidewalks; curb snapped to PE; first-class material |
road_medians |
geo.road_median |
MultiPolygonZ | Median polygons; Z from nearest pavement edge |
plazas |
geo.plaza |
MultiPolygonZ | Public plazas (Overture pedestrian); Z from 3DEP |
parks |
geo.park |
MultiPolygonZ | Park / recreation; Z from 3DEP |
parking_lots |
geo.parking_lot |
MultiPolygonZ | Parking lots; per-vertex Z from nearest pavement edge |
buildings |
geo.building |
MultiPolygonZ | Footprints with procedural recipes |
Partitioned by city_id (only us-ny-nyc
today). No foreign keys between feature tables.
Catalog also advertises unavailable layers
(lane_marking_observations, road_decals,
landmark_buildings,
water_bodies, vegetation,
fields) with available: false.
Road z_source:
lidar_bridge_deck,
lidar_ground,
pavement_edge_z,
planimetric_point,
interpolated,
usgs_3dep_dem (road_polygon only,
at_grade only),
synthesized.
Marking z_source:
draped_road_polygon,
draped_road_centerline,
pavement_edge_z,
interpolated,
synthesized.
Surface z_source:
pavement_edge_z,
usgs_3dep_dem,
interpolated,
synthesized.
road_polygons
| Property | Type | Notes |
|---|---|---|
id | integer | Also Feature id |
city_id | string | Partition key |
dataset_version_id | integer | |
source | string | |
source_id | string? | |
class | string | carriageway | intersection | shoulder | driveway |
function | string? | CityGML TrafficArea function |
level | integer | 0 = at grade |
structure | string | at_grade | bridge | viaduct | tunnel |
structure_ref | string? | NBI number when matched |
primary_centerline_id | integer? | Not an FK |
centerline_ids | integer[] | Default [] |
z_source | string | See list above |
z_accuracy_m | number? | |
elevation_min | number? | |
elevation_max | number? | |
surface | string? | |
attributes | object | Default {}. May include vertex_sources, z_conflict_m (pavement-edge vs. centerline disagreement, metres), z_synthesized (true only when nothing on the polygon resolved to a measured/DEM Z) |
layer | string | Injected in GeoJSON |
road_centerlines
| Property | Type | Notes |
|---|---|---|
id | integer | |
city_id | string | |
dataset_version_id | integer | |
source | string | |
source_id | string? | |
from_node_id | integer? | |
to_node_id | integer? | |
level_from | integer | Default 0 |
level_to | integer | Default 0 |
structure | string | Default at_grade |
rw_type | string? | |
street_name | string? | |
z_source | string | |
z_accuracy_m | number? | |
attributes | object | |
layer | string | Injected in GeoJSON |
road_edges
| Property | Type | Notes |
|---|---|---|
id | integer | |
city_id | string | |
dataset_version_id | integer | |
polygon_id | integer | Parent polygon, not an FK |
adjacent_polygon_id | integer? | |
edge_type | string | pavement_edge | seam | structure_edge | median_edge |
level | integer | Default 0 |
z_source | string | |
attributes | object | |
layer | string | Injected in GeoJSON |
lane_markings
One continuous LineStringZ per marking (contract change
from advertised MultiLineStringZ). Dash gaps are
attributes. Gold-set rows are redistributable: false.
| Property | Type | Notes |
|---|---|---|
id | integer | |
city_id | string | |
stable_id | string | ULID, carried across versions |
marking_type | string | centerline | lane_divider | edge_line | … |
color | string | white | yellow | … |
pattern | string | solid | broken | … |
group_id | integer? | Not an FK |
dash_length_m | number? | With gap_length_m, dash_phase_m |
confidence | number | |
intersection_clipped | boolean | |
z_source | string | Lift vocabulary (see above) |
layer | string | Injected in GeoJSON |
lane_marking_groups
| Property | Type | Notes |
|---|---|---|
id | integer | |
city_id | string | |
stable_id | string | ULID |
group_type | string | single | double | … |
color | string | |
member_count | integer | |
member_stable_ids | string[] | |
z_source | string | Lift vocabulary |
layer | string | Injected in GeoJSON |
sidewalks
Dedicated sidewalk schema (not the shared surface-polygon column
set). Curb vertices snap to Pavement Edge. material
is a closed pavement vocab:
concrete | asphalt |
paving_stones | sett |
brick | rubber | gravel |
wood | metal | dirt |
grass | unknown.
NYC Planimetrics has no pavement-type field, so every NYC row is
concrete with
attributes.material_source = inferred_default.
| Property | Type | Notes |
|---|---|---|
id | integer | Also Feature id |
city_id | string | Partition key |
class | string | sidewalk |
material | string | Closed pavement vocab (see above) |
level | integer | Default 0 |
structure | string | at_grade | bridge | viaduct | tunnel |
z_source | string | Surface vocabulary |
attributes | object | NYC leftovers; material_source is inferred_default | tag_mapping | manual |
layer | string | Injected in GeoJSON |
Surface polygons
road_medians,
plazas, parks, and
parking_lots share one column set
(class, subtype, name,
level, structure, z_source,
elevation range, attributes). Each is its own
MultiPolygonZ table. City leftovers stay in
attributes.
| Layer | class | Z |
|---|---|---|
road_medians |
median |
Nearest Pavement Edge (XY unchanged) |
plazas |
pedestrian |
USGS 3DEP 1 m |
parks |
park | recreation |
USGS 3DEP 1 m |
parking_lots |
parking |
Per-vertex nearest Pavement Edge |
buildings
Ground-level footprints (MultiPolygonZ) carrying procedural
recipes. Height is the height_m column, not the geometry.
Geometry and lidar heights come from NYC DOITT Building Footprints, tags
are joined from OpenStreetMap on nycdoitt:bin, and
ground_elev_m is sampled from USGS 3DEP 1 m bare-earth DEM.
The nine recipe blocks — identity,
classification, massing, facade_rules,
materials, roof_system,
pedestrian_plane, appurtenances, and
provenance — are returned only with
detail=recipe. They are several kilobytes each, so the
default detail=footprint omits them.
| Property | Type | Notes |
|---|---|---|
id | integer | |
city_id | string | |
stable_id | string | ULID |
bin | string? | NYC Building Identification Number |
bbl | string? | Borough-Block-Lot |
name | string? | |
address | string? | |
primary_use | string | residential | office | retail | … | unknown |
building_form | string? | tower | slab | rowhouse | … |
year_built | integer? | |
height_m | number? | Lidar roof height above ground |
min_height_m | number? | Base of the massing |
storeys_above | integer? | |
footprint_area_m2 | number? | |
roof_form | string? | flat | gable | mansard | … |
lod_supported | string? | lod0 … lod4 |
completeness_overall | number? | 0..1; how much of the recipe is present |
confidence_overall | number? | 0..1; how much of it can be trusted |
ground_elev_m | number? | NAVD88 metres at the anchor |
z_source | string | usgs_3dep_dem |
layer | string | Injected in GeoJSON |
Errors
{ "detail": "bbox exceeds 4.0 km2 cap for this tier" }
| Code | When |
|---|---|
| 400 | Bad bbox/layers/format; bbox over cap; fgb with multiple layers; tile zoom/index out of range; unavailable preview layer |
| 401 | Bad/missing key; anonymous fgb; anonymous basemap |
| 403 | Missing maps:read; CORS preflight denied |
| 404 | Unknown city; feature not found |
| 429 | Rate limit; Retry-After seconds |
| 502 | Basemap origin failed |
| 503 | Geo DB down, or key lookup down with empty cache |