GeoJSON and vector tiles

Spatibase serves published map data in two forms: entries.geojson, a paged GeoJSON FeatureCollection with every field, and vector tiles (Mapbox Vector Tile, one layer named entries) described by a TileJSON document. This page is for developers putting Spatibase content into MapLibre GL, Mapbox GL, Leaflet, QGIS or ArcGIS.

Examples use $API for the project’s Content Delivery API base URL; $PROJECT_ID is the UUID in the project’s address in the web app:

bash
export API=https://api.spatibase.com/delivery/v1/projects/$PROJECT_ID

Should I use GeoJSON or vector tiles?

Use GeoJSON for small and medium projects when you want every field in one request; use vector tiles for large projects and GIS tools, because they load only what is on screen. Both contain only the main locations of published entries of enabled types.

entries.geojson Vector tiles
Content every field, nested values intact id, type, title, color and scalar fields only
Size up to 5000 features per request (page for more) any size, loaded tile by tile
Filters every /entries parameter type, subtypes and group only
Geometry exact coordinates (7 decimals) clipped and simplified per zoom level
Clients Leaflet, MapLibre or Mapbox geojson source, QGIS, scripts MapLibre or Mapbox vector source, QGIS

To list every published entry as just id, type, title and bounding box, without fields, use entries.index (compact index); it pages like entries.geojson.

GeoJSON

entries.geojson returns published entries that have a main location as a GeoJSON FeatureCollection:

text
https://api.spatibase.com/delivery/v1/projects/$PROJECT_ID/entries.geojson
  • It takes every /entries parameter: type, subtypes, group, q, filter[...], bbox, near, radius, within, withinEntry, withinField, sort, page and include. limit defaults to 1000, at most 5000.
  • The response has Content-Type: application/geo+json; charset=utf-8, with the same caching and errors as the rest of the Content Delivery API.
  • Left out: entries without a main location, entries of types with locationMode: none (even if a location is stored), drafts, unpublished entries and switched-off types. meta.total counts only the features that qualify.

To save a file:

bash
curl -g -o entries.geojson "$API/entries.geojson?limit=5000"

What does a GeoJSON feature contain?

Each feature has the entry id as its id, the main location as its geometry, and properties with id, type, title and then every field.

bash
curl -sg "$API/entries.geojson?type=store-location&limit=1"
json
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "id": "192a713e-995a-4cb6-a84a-a5da45e9646a",
      "geometry": {"type": "Point", "coordinates": [-124.0828, 40.8665]},
      "properties": {
        "id": "192a713e-995a-4cb6-a84a-a5da45e9646a",
        "type": "store-location",
        "title": "Pacific Coast Outdoors – Arcata",
        "rental-phone": "+1 707-555-0135",
        "region": {"id": "66f36784-bb65-4de3-a745-bfd294cdf017", "type": "region", "title": "Northern California Coast"},
        "opens-at": "09:00",
        "delivery-area": {"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Polygon", "coordinates": [...]}, "properties": {}}]}
      }
    }
  ],
  "meta": {"page": 1, "limit": 1, "total": 4, "totalPages": 4},
  "links": {
    "self": "https://api.spatibase.com/delivery/v1/projects/6806f635-636f-4814-ae20-988d4c84369e/entries.geojson?type=store-location&limit=1",
    "next": "https://api.spatibase.com/delivery/v1/projects/6806f635-636f-4814-ae20-988d4c84369e/entries.geojson?type=store-location&limit=1&page=2",
    "prev": null
  }
}
  • Field values have the same shapes as in the JSON API (Field values). Element apiKeys cannot be id, type or title, so fields never overwrite them.
  • Not included: typePath, groups, the timestamps and distance. near and sort=distance still filter and order the features. Use /entries when you need those keys.
  • geometry is a Point, LineString, Polygon or a Multi* form, in WGS84 [lng, lat].
  • meta and links are GeoJSON “foreign members” (RFC 7946, section 6.1), which map libraries ignore. To get more than 5000 features, follow links.next, or use vector tiles.
  • Nested values (objects and arrays) stay nested. MapLibre GL turns nested properties into JSON strings when it loads a geojson source, so JSON.parse them in event handlers, or load the entry from /entries/<id>.

Vector tiles

Vector tiles serve published main locations as Mapbox Vector Tiles at this URL template:

text
https://api.spatibase.com/delivery/v1/projects/$PROJECT_ID/tiles/{z}/{x}/{y}.pbf
  • XYZ scheme (y counted from the top, as in OpenStreetMap), Web Mercator, z from 0 to 22, x and y from 0 to 2^z - 1. Anything else is a 400 JSON error, such as Tile z must be an integer between 0 and 22; other extensions such as .mvt are a 404.
  • A tile with features is a 200 with Content-Type: application/vnd.mapbox-vector-tile (MVT version 2, not gzip-compressed). A tile with no features is 204 No Content, with the same Cache-Control and ETag as tiles with data.
  • Tiles are built on request from published main locations of enabled types whose locationMode is not none. For heavy traffic, put a CDN in front.

Which layer and properties do tiles have?

Every tile has one layer named entries (extent 4096, buffer 64) with points, lines and polygons mixed. Geometries are clipped to the tile and simplified for the zoom level, so very small polygons and lines can disappear at low zoom. Multi* geometries become multi-part features.

Large projects stay light at low zoom. From zoom 0 to 8 a tile keeps one point per small grid cell (the entry with the lowest id stands for the others), so a world tile of a project with thousands of points stays well under 150 KB. Every point in those tiles has a point_count property, the number of entries it stands for (1 for a lone point). From zoom 9 on, tiles hold every point and have no point_count. Lines and polygons are not thinned, filters apply before thinning, and the same tile always gives the same bytes. Use point_count to size circles a little or to label a cluster; a click handler should still read the entry through /entries/<id>.

Property Type Value
id string the entry id. The MVT feature id is not set; use promoteId for feature state
type string the type apiKey
title string the published title, at most 255 characters
color string the type’s color (Type color in the app), #rrggbb
point_count number only at zoom 0 to 8, on points: how many entries this point stands for
<apiKey> string, number or boolean scalar fields of the kinds text (not the JSON editor), number, boolean, date (single values, not ranges), choice, color, identifier and phone
  • Strings are cut at 255 characters. Multiple choice values are joined with ", ", for example "amenities": "restrooms, parking, camping". Empty fields are absent, not null.
  • Values follow the field’s current settings, like the JSON output: a choice that is no longer an option is left out, and a field switched between single and multiple choice, or a date whose mode changed, shows the current shape. A field switched to a date range leaves the tiles.
  • Not in tiles: markdown, reference, link, images, files, contact, geo, embed, location, JSON text and date ranges. Load the entry from /entries/<id> when someone clicks a feature.
  • The fixed properties win over an element with the same apiKey; only color can clash.

Filter tiles

Tiles accept type (including subtypes unless subtypes=false) and group, with the same rules and 400 errors as /entries. Every other parameter (filter[...], q, bbox, near, sort, include) is ignored without an error.

text
https://api.spatibase.com/delivery/v1/projects/$PROJECT_ID/tiles/{z}/{x}/{y}.pbf?type=park

To filter by field values, use style expressions on the client, for example ['>=', ['get', 'rating'], 4.5].

How are tiles cached?

Tiles use the Content Delivery API’s caching headers (Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=86400) and an ETag that changes with the project’s content version. A publish reaches map viewers within that cache window, and the tile URLs themselves never change.

TileJSON

tiles.json is a TileJSON 3.0 document that tells a map library the tile URL, zoom range, bounds and field names. Point a vector source at it instead of hard-coding the tile template.

bash
curl -s "$API/tiles.json"
json
{
  "tilejson": "3.0.0",
  "name": "Pacific Coast Outdoors",
  "description": "Parks, regions, stores and trip ideas along the Pacific coast of California, Oregon and Washington.",
  "version": "12",
  "scheme": "xyz",
  "tiles": ["https://api.spatibase.com/delivery/v1/projects/6806f635-636f-4814-ae20-988d4c84369e/tiles/{z}/{x}/{y}.pbf"],
  "minzoom": 0,
  "maxzoom": 22,
  "bounds": [-124.85, 37.8, -122.45, 48.42],
  "center": [-123.65, 43.11, 6],
  "vector_layers": [{
    "id": "entries",
    "description": "Published entries with a main location",
    "fields": {"id": "String", "type": "String", "title": "String", "color": "String", "point_count": "Number", "slug": "String", "amenities": "String", "rating": "Number", "opens-at": "String"},
    "minzoom": 0,
    "maxzoom": 22
  }]
}
  • version is the project’s content version as a string. description is left out when the project has none.
  • bounds is the extent of all published main locations; a project without any gets [-180, -85.0511, 180, 85.0511]. center is the middle of bounds at zoom 6.
  • fields lists every tile property: Number for number fields, Boolean for boolean fields, String for the rest. It covers the scalar elements of all enabled types, so a field can be listed even if no tile contains it.
  • The tiles template has no query string, except ?api_key= when the TileJSON of a private project was requested with one. A source built from the TileJSON URL is therefore unfiltered; for a filtered source, pass the tiles array yourself (see the MapLibre example below).

How do tiles and GeoJSON work in a private project?

Tiles, TileJSON and GeoJSON of a private project need an API key like every other Content Delivery API endpoint. Map libraries and QGIS load tile URLs without custom headers, so request tiles.json?api_key=$API_KEY: the tiles template in that response carries ?api_key= as well.

  • If you write the tile template yourself, append ?api_key=$API_KEY, or &api_key=$API_KEY after ?type=....
  • In MapLibre GL or Mapbox GL you can send a header instead, only to Spatibase URLs:
js
const map = new maplibregl.Map({
  container: 'map',
  style: 'https://tiles.openfreemap.org/styles/liberty',
  transformRequest: url => url.startsWith('https://api.spatibase.com/delivery/')
    ? { url, headers: { 'X-API-Key': API_KEY } }
    : { url }
})
  • Only read keys (for a website or app) work in ?api_key=; edit keys for AI assistants are refused there.
  • A key used in a web page is visible to anyone who can open the page. Give each site its own key. Details are in Authentication.

MapLibre GL JS and Mapbox GL JS

MapLibre GL JS reads the TileJSON URL directly as a vector source. This snippet draws areas and points in their type colors, and adds a second source that only contains parks:

js
const API = `https://api.spatibase.com/delivery/v1/projects/${PROJECT_ID}`

map.on('load', () => {
  map.addSource('spatibase', {
    type: 'vector',
    url: `${API}/tiles.json`,
    promoteId: { entries: 'id' }   // feature.id = entry id, for feature-state and click handling
  })
  map.addLayer({
    id: 'entry-areas', type: 'fill', source: 'spatibase', 'source-layer': 'entries',
    filter: ['==', ['geometry-type'], 'Polygon'],
    paint: { 'fill-color': ['get', 'color'], 'fill-opacity': 0.35 }
  })
  map.addLayer({
    id: 'entry-lines', type: 'line', source: 'spatibase', 'source-layer': 'entries',
    filter: ['==', ['geometry-type'], 'LineString'],
    paint: { 'line-color': ['get', 'color'], 'line-width': 3 }
  })
  map.addLayer({
    id: 'entry-points', type: 'circle', source: 'spatibase', 'source-layer': 'entries',
    filter: ['==', ['geometry-type'], 'Point'],
    paint: { 'circle-radius': 6, 'circle-color': ['get', 'color'] }
  })
  // Filtered tiles: pass the template directly, with the query string
  map.addSource('parks', {
    type: 'vector',
    tiles: [`${API}/tiles/{z}/{x}/{y}.pbf?type=park`],
    minzoom: 0, maxzoom: 22
  })
})
  • The snippet assumes a map with a basemap style already loaded, for example style: 'https://tiles.openfreemap.org/styles/liberty' (OpenFreeMap: free, no key). Spatibase serves your entries, not a basemap. Do not use tile.openstreetmap.org as a basemap in production; OpenStreetMap’s public tile servers are not meant for apps.
  • On click, e.features[0].properties holds the tile properties only. Fetch /entries/<id> for images, descriptions and other fields.
  • Mapbox GL JS uses the same source and layer definitions (style specification v8); Mapbox GL JS itself needs a Mapbox access token.
  • For a geojson source, set data to the entries.geojson?limit=5000 URL. A complete page is in the Quickstart.
  • Leaflet draws GeoJSON with L.geoJSON; the Quickstart has a complete page.

QGIS

QGIS can load Spatibase as a vector tile layer (fast, few attributes) or as a GeoJSON layer (every attribute, a snapshot). The steps follow the QGIS 3 menus.

Vector tiles:

  1. Choose Layer, Add Layer, Add Vector Tile Layer….
  2. Choose New, then New Generic Connection….
  3. Enter any name, and as URL the tile template https://api.spatibase.com/delivery/v1/projects/$PROJECT_ID/tiles/{z}/{x}/{y}.pbf (add ?type=... to filter, ?api_key=... for a private project). Set the minimum zoom to 0 and the maximum zoom to 22. Leave the style URL empty.
  4. Click OK, then Add. QGIS draws the layer entries with a default style; style it by the type or color attributes.

GeoJSON with every attribute:

  1. Choose Layer, Add Layer, Add Vector Layer….
  2. Set the source type to Protocol: HTTP(S), cloud, etc. with type HTTP/HTTPS/FTP.
  3. As URI, enter https://api.spatibase.com/delivery/v1/projects/$PROJECT_ID/entries.geojson?limit=5000 (the default limit is 1000).
  4. Click Add. Nested values such as images, references and geo fields are not flat attributes; depending on the QGIS version they appear as JSON text or are left out. The layer is a snapshot: reload it to see newly published content.

ArcGIS

ArcGIS Online and ArcGIS Pro can read the GeoJSON URL, which is the simplest way in. In ArcGIS Online’s Map Viewer, add a layer from a URL and give it https://api.spatibase.com/delivery/v1/projects/$PROJECT_ID/entries.geojson?limit=5000. The Content Delivery API allows any origin, so the request works from the browser.

ArcGIS draws vector tiles through a style document (a Mapbox GL style JSON or a VectorTileServer), and Spatibase serves no style document. To use the tiles, write a style JSON with a vector source pointing at the tile template and layers on the source layer entries, host it yourself, and add that style’s URL.

Updated

Docs support

How can we help?

A person on our team reads every message. We include the page you are on, so you don't have to explain where.

Or write to hello@spatibase.com. For outages, check System status.

Talk to us

Not sure it’s a fit? Let’s find out

Tell us about your use case and what you need, from data model to security and deployment. In 30 minutes we’ll look at it together and tell you honestly if we can help.

Or write to hello@spatibase.com.