API reference

Every endpoint with its parameters, what it costs against your plan, a request to copy and an answer the API really gave.

Basics

Base URL https://geo.runningcat.dev. Send your key as Authorization: Bearer <key> or x-api-key: <key>; keys come from https://geo.runningcat.dev/account. Coordinates are [lon, lat] in degrees (WGS 84), times ISO 8601 in UTC, distances in km or m as the field name says. Errors answer {"error": "...", "reason": "..."} with the status codes listed per operation.

Every answer says what it cost and what is left: X-Request-Cost, X-RateLimit-Limit and X-RateLimit-Remaining (per minute), X-RateLimit-Day-Limit and X-RateLimit-Day-Remaining.

Credits to show: map data © OpenStreetMap contributors (ODbL); weather and air quality from Open-Meteo.com (CC BY 4.0); terrain from Mapterhorn; air quality stations and river gauges as each answer's credits or the data sources page lists.

The same reference as an OpenAPI 3.1 file: /openapi.json. Load it into Postman, Insomnia or a code generator, or give it to an AI assistant.

Reports

Several answers about one point, area or route in one request.

GET /v1/point

Everything about a point. Place, time zone, elevation, weather, air quality, air quality stations and river gauges at a point in one request. include picks the parts (comma-separated: place, timezone, elevation, weather, air, airStations, rivers; all by default). A part that fails comes back in errors with the reason; the others still answer.

Cost: One request whatever the parts.

Parameters

lonLongitude, −180 to 180
latLatitude, −90 to 90
include optionalComma-separated parts: place, timezone, elevation, weather, air, airStations, rivers
lang optionalLanguage of place names, e.g. en, de, pl; local names when absent
days optionalForecast days, 1 to 14 (default 2)
hours optionalAir quality hours, 1 to 120 (default 24)
pastHours optionalPast hours of the air quality model before the current hour, 0 to 48 (default 0)
radiusKm optionalSearch radius for stations and gauges, 1 to 100 km (default 10)

Request

curl 'https://geo.runningcat.dev/v1/point?lon=6.1432&lat=46.2044&include=place,timezone,elevation&lang=en' \
  -H "Authorization: Bearer YOUR_KEY"

Example answer

{
 "lon": 6.1432,
 "lat": 46.2044,
 "place": {
  "lon": 6.1432,
  "lat": 46.2044,
  "country": {
   "code": "CH",
   "name": "Switzerland"
  },
  "region": {
   "name": "Geneva",
   "code": "CH-GE"
  },
  "locality": {
   "name": "Geneva",
   "type": "city",
   "distanceKm": 0.39
  },
  "nearestPlace": {
   "name": "Saint-Gervais",
   "type": "neighbourhood",
   "distanceKm": 0.26
  },
  "areas": [
   {
    "level": 2,
    "name": "Switzerland",
    "code": "CH"
   },
   {
    "level": 4,
    "name": "Geneva",
    "code": "CH-GE"
   },
   {
    "level": 8,
    "name": "Geneva"
   }
  ],
  "timezone": {
   "tz": "Europe/Zurich",
   "offsetMin": 60
  }
 },
 "timezone": {
  "tz": "Europe/Zurich",
  "offsetMin": 60
 },
 "elevation": {
  "ele": 374
 }
}

GET /v1/area

Everything over an area. Weather and air quality grids, elevation, air quality stations, river gauges and map layers over a box. include picks the parts (weather, air, airStations, elevation, rivers, layers; all but layers by default). Grids have about cells cells (at most 400) listed from the north-west corner row by row, hourly from the current hour; cells × hours at most 9600. Stations, gauges and layers need a box of at most 16 square degrees.

Cost: One request per part; layers one per layer.

Parameters

bboxwest,south,east,north in degrees, at most 3600 square degrees
include optionalComma-separated parts: weather, air, airStations, elevation, rivers, layers
cells optionalGrid cells, 1 to 400 (default 100)
hours optionalHours, 1 to 120 (default 24)
layers optionalComma-separated layers: protected_area, route, piste, aerialway, boundary (default protected_area, route, piste, aerialway)

Request

curl 'https://geo.runningcat.dev/v1/area?bbox=6.0,46.1,6.3,46.3&include=weather,elevation&cells=4&hours=2' \
  -H "Authorization: Bearer YOUR_KEY"

Example answer

{
 "bbox": [
  6,
  46.1,
  6.3,
  46.3
 ],
 "rows": 2,
 "cols": 2,
 "lon": [
  6.075,
  6.225
 ],
 "lat": [
  46.25,
  46.15
 ],
 "weather": {
  "times": [
   "2026-11-12T12:00:00Z",
   "2026-11-12T13:00:00Z"
  ],
  "values": {
   "temperatureC": [
    [
     11.6,
     12.3,
     12.2,
     12
    ],
    [
     11.8,
     12.3,
     12.3,
     12.2
    ]
   ],
   "feelsLikeC": [
    [
     10.4,
     11.4,
     10.9,
     11.5
    ],
    [
     10.1,
     11,
     11.1,
     11.6
    ]
   ],
   "precipitationMm": [
    [
     0.8,
     0.9,
     1,
     1.1
    ],
    [
     0.2,
     0.4,
     0.4,
     0.7
    ]
   ],
   "weatherCode": [
    [
     61,
     61,
     61,
     61
    ],
    [
     61,
     61,
     61,
     61
    ]
   ],
   "windKmh": [
    [
     9.4,
     9.2,
     11.7,
     6.6
    ],
    [
     12.8,
     12.1,
     12,
     8.4
    ]
   ],
   "windDirDeg": [
    [
     40,
     21,
     34,
     351
    ],
    [
     38,
     27,
     33,
     10
    ]
   ],
   "gustKmh": [
    [
     16.2,
     18.4,
     22.3,
     16.6
    ],
    [
     24.8,
     20.9,
     24.1,
     19.8
    ]
   ],
   "cloudPct": [
    [
     100,
     100,
     100,
     100
    ],
    [
     100,
     100,
     100,
     100
    ]
   ],
   "humidityPct": [
    [
     94,
     94,
     95,
     98
    ],
    [
     91,
     94,
     95,
     99
    ]
   ],
   "pressureHpa": [
    [
     992.9,
     992.9,
     992.9,
     993.1
    ],
    [
     992,
     992.1,
     992.1,
     992.2
    ]
   ],
   "boundaryLayerM": [
    [
     205,
     185,
     210,
     145
    ],
    [
     230,
     180,
     185,
     120
    ]
   ]
  }
 },
 "elevation": {
  "values": [
   453.2,
   435,
   424.3,
   526.6
  ]
 }
}

POST /v1/route/report

Everything along a route. Elevation profile, weather at the time you reach each point, time zones, air quality stations, river gauges, map layers, surfaces and land cover along a route in one request. include picks the parts (profile, weather, timezones, airStations, rivers, layers, surfaces, landcover; by default profile, weather, timezones, airStations, rivers, layers); the other fields are those of the single endpoints below. A route is GeoJSON {"type": "LineString", "coordinates": [[lon, lat], ...]} or a MultiLineString of sections in order, up to 20,000 km.

Cost: One request per part.

Body

routeGeoJSON LineString or MultiLineString or GeoJSON LineString or MultiLineString
include optionalarray of profile, weather, timezones, airStations, rivers, layers, surfaces, landcover, at least 1 items
departure optionalstring (ISO time with offset)
speedKmh optionalnumber, 2 to 80
stepKm optionalnumber, 0.5 to 100
stepM optionalnumber, 10 to 1000
layers optionalarray of protected_area, route, piste, aerialway, boundary, at least 1 items
bufferM optionalnumber, 0 to 1000
bufferKm optionalnumber, 0.1 to 20
lang optionalstring

Request

curl -X POST 'https://geo.runningcat.dev/v1/route/report' \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"route":{"type":"LineString","coordinates":[[6.1432,46.2044],[6.15,46.19],[6.175,46.17],[6.2,46.15]]},"include":["profile","timezones","surfaces","landcover"]}'

Example answer

{
 "profile": {
  "routeLengthKm": 7.567,
  "stepM": 10,
  "ascentM": 814,
  "descentM": 109,
  "minM": 374,
  "maxM": 1093.7,
  "structures": [
   {
    "fromKm": 0.139,
    "toKm": 0.178,
    "kind": "tunnel",
    "name": "Passage de la Petite-Corraterie",
    "maxheight": null,
    "maxheightM": null
   },
   {
    "fromKm": 1.05,
    "toKm": 1.07,
    "kind": "tunnel",
    "name": null,
    "maxheight": "default",
    "maxheightM": null
   },
   {
    "fromKm": 3.89,
    "toKm": 3.91,
    "kind": "bridge",
    "name": null,
    "maxheight": null,
    "maxheightM": null
   }
  ],
  "profile": [
   {
    "km": 0,
    "lon": 6.1432,
    "lat": 46.2044,
    "ele": 374
   },
   {
    "km": 0.01,
    "lon": 6.14324,
    "lat": 46.204315,
    "ele": 374.4
   },
   {
    "km": 0.02,
    "lon": 6.143281,
    "lat": 46.204229,
    "ele": 374.5
   }
  ]
 },
 "timezones": {
  "at": "2026-11-12T12:35:36.495Z",
  "routeLengthKm": 7.6,
  "timezones": [
   {
    "tz": "Europe/Zurich",
    "fromKm": 0,
    "toKm": 5.6,
    "offsetMin": 60
   },
   {
    "tz": "Europe/Paris",
    "fromKm": 5.6,
    "toKm": 7.6,
    "offsetMin": 60
   }
  ]
 },
 "surfaces": {
  "km": {
   "paved": 2.76,
   "unpaved": 0.32,
   "unknown": 4.49
  },
  "highwayKm": {
   "none": 1.95,
   "footway": 1.53,
   "service": 1.25,
   "path": 0.7,
   "residential": 0.54,
   "tertiary": 0.44,
   "track": 0.32,
   "cycleway": 0.2,
   "living_street": 0.18,
   "unclassified": 0.16,
   "pedestrian": 0.14,
   "secondary": 0.06,
   "primary": 0.04,
   "motorway": 0.04,
   "secondary_link": 0.02,
   "platform": 0.02
  },
  "runs": [
   {
    "fromKm": 0,
    "toKm": 0.02,
    "surface": "asphalt",
    "highway": "residential",
    "tracktype": null,
    "smoothness": null
   },
   {
    "fromKm": 0.02,
    "toKm": 0.04,

…

Places and time zones

What lies at a point and what time it is there.

GET /v1/where

Where a point is. Country, region, locality, the nearest named place, protected areas and the time zone at a point.

Cost: One request.

Parameters

lonLongitude, −180 to 180
latLatitude, −90 to 90
lang optionalLanguage of place names, e.g. en, de, pl; local names when absent

Request

curl 'https://geo.runningcat.dev/v1/where?lon=6.1432&lat=46.2044&lang=en' \
  -H "Authorization: Bearer YOUR_KEY"

Example answer

{
 "lon": 6.1432,
 "lat": 46.2044,
 "country": {
  "code": "CH",
  "name": "Switzerland"
 },
 "region": {
  "name": "Geneva",
  "code": "CH-GE"
 },
 "locality": {
  "name": "Geneva",
  "type": "city",
  "distanceKm": 0.39
 },
 "nearestPlace": {
  "name": "Saint-Gervais",
  "type": "neighbourhood",
  "distanceKm": 0.26
 },
 "areas": [
  {
   "level": 2,
   "name": "Switzerland",
   "code": "CH"
  },
  {
   "level": 4,
   "name": "Geneva",
   "code": "CH-GE"
  },
  {
   "level": 8,
   "name": "Geneva"
  }
 ],
 "timezone": {
  "tz": "Europe/Zurich",
  "offsetMin": 60
 }
}

POST /v1/where

Where many points are. The same for up to 1000 points, in order. at (ISO time) sets the moment for the time zone offsets; now by default.

Cost: One request.

Body

pointsarray of [lon, lat], 1 to 1000 items
lang optionalstring
at optionalstring (ISO time with offset)

Request

curl -X POST 'https://geo.runningcat.dev/v1/where' \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"points":[[6.1432,46.2044],[19.94,50.06]],"lang":"en"}'

Example answer

{
 "places": [
  {
   "lon": 6.1432,
   "lat": 46.2044,
   "country": {
    "code": "CH",
    "name": "Switzerland"
   },
   "region": {
    "name": "Geneva",
    "code": "CH-GE"
   },
   "locality": {
    "name": "Geneva",
    "type": "city",
    "distanceKm": 0.39
   },
   "nearestPlace": {
    "name": "Saint-Gervais",
    "type": "neighbourhood",
    "distanceKm": 0.26
   },
   "areas": [
    {
     "level": 2,
     "name": "Switzerland",
     "code": "CH"
    },
    {
     "level": 4,
     "name": "Geneva",
     "code": "CH-GE"
    },
    {
     "level": 8,
     "name": "Geneva"
    }
   ],
   "timezone": {
    "tz": "Europe/Zurich",
    "offsetMin": 60
   }
  },
  {
   "lon": 19.94,
   "lat": 50.06,
   "country": {
    "code": "PL",
    "name": "Poland"
   },
   "region": {
    "name": "Lesser Poland Voivodeship",
    "code": "PL-12"
   },
   "locality": {
    "name": "Krakow",
    "type": "city",
    "distanceKm": 0.31
   },
   "nearestPlace": {
    "name": "Krakow",
    "type": "city",
    "distanceKm": 0.31
   },
   "areas": [
    {
     "level": 2,
     "name": "Poland",
     "code": "PL"
    },
    {
     "level": 4,
     "name": "Lesser Poland Voivodeship",
     "code": "PL-12"
    },
    {
     "level": 6,
     "name": "Krakow"
    }
   ],
   "timezone": {
    "tz": "Europe/Warsaw",
    "offsetMin": 60
   }
  }
 ]
}

POST /v1/timezones

Time zones of points. IANA time zone and its offset in minutes east of UTC for up to 10,000 points, at at (now by default).

Cost: One request.

Body

pointsarray of [lon, lat], 1 to 10000 items
at optionalstring (ISO time with offset)

Request

curl -X POST 'https://geo.runningcat.dev/v1/timezones' \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"points":[[6.1432,46.2044],[-73.98,40.75]]}'

Example answer

{
 "timezones": [
  {
   "tz": "Europe/Zurich",
   "offsetMin": 60
  },
  {
   "tz": "America/New_York",
   "offsetMin": -300
  }
 ]
}

POST /v1/route/timezones

Time zones along a route. The stretches of a route in each time zone, with their offsets at at. A route is GeoJSON {"type": "LineString", "coordinates": [[lon, lat], ...]} or a MultiLineString of sections in order, up to 20,000 km.

Cost: One request.

Body

routeGeoJSON LineString or MultiLineString or GeoJSON LineString or MultiLineString
at optionalstring (ISO time with offset)

Request

curl -X POST 'https://geo.runningcat.dev/v1/route/timezones' \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"route":{"type":"LineString","coordinates":[[6.1432,46.2044],[6.15,46.19],[6.175,46.17],[6.2,46.15]]}}'

Example answer

{
 "routeLengthKm": 7.6,
 "at": "2026-11-12T12:35:38.786Z",
 "timezones": [
  {
   "tz": "Europe/Zurich",
   "fromKm": 0,
   "toKm": 5.6,
   "offsetMin": 60
  },
  {
   "tz": "Europe/Paris",
   "fromKm": 5.6,
   "toKm": 7.6,
   "offsetMin": 60
  }
 ]
}

POST /v1/places/search

Find places by name. Villages, towns, cities and other named places matching a query: exact names first, then names starting with the query, larger and more populous first. Each name comes as "Place, Region, Country", in locale where the place has it. Place names only, no street addresses.

Cost: One request.

Body

querystring, 2 to 120 characters
limit optionalinteger, 1 to 10 (default 6)
locale optionalstring

Request

curl -X POST 'https://geo.runningcat.dev/v1/places/search' \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"Chamonix","limit":2,"locale":"en"}'

Example answer

{
 "places": [
  {
   "name": "Chamonix, Auvergne-Rhône-Alpes, France",
   "lat": 46.0295495,
   "lon": 6.6117818
  },
  {
   "name": "Chamonix-Mont-Blanc, Auvergne-Rhône-Alpes, France",
   "lat": 45.9230969,
   "lon": 6.8700009
  }
 ]
}

Elevation and terrain

Heights, profiles and terrain tiles.

GET /v1/elevation

Height of a point. Height above sea level in metres (0.1 m steps); open sea is 0.

Cost: One request.

Parameters

lonLongitude, −180 to 180
latLatitude, −90 to 90

Request

curl 'https://geo.runningcat.dev/v1/elevation?lon=7.6586&lat=45.9763' \
  -H "Authorization: Bearer YOUR_KEY"

Example answer

{
 "ele": 4449.1
}

POST /v1/elevation

Heights of points. Heights of the ground for up to 10,000 points, in order; null where there is no terrain. Each point is read on its own, so a point in a tunnel gets the mountain above it and one on a bridge the valley below; for a route or a recorded track use POST /v1/route/profile (with at: "points" to keep its own points), which follows the road there.

Cost: One request.

Body

pointsarray of [lon, lat], 1 to 10000 items

Request

curl -X POST 'https://geo.runningcat.dev/v1/elevation' \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"points":[[7.6586,45.9763],[6.1432,46.2044]]}'

Example answer

{
 "elevations": [
  4449.1,
  374
 ]
}

POST /v1/route/profile

Elevation profile of a route. Heights every stepM metres (10 to 1000; chosen by length when absent), with climb, descent, lowest and highest point. A profile has at most 5,000 samples, so on a long route the step grows past the one asked for (10 m holds up to about 50 km, 20 m up to 100 km); the answer's stepM is the step used. at: "points" gives a height at each point of the route as sent instead, for recorded tracks: no resampling, distances stay the track's, up to 50,000 points (more answers 413), stepM null in the answer. On bridges and in tunnels (OSM bridge and tunnel ways the route runs along) the heights follow the road: a straight line between the heights just beyond each end, not the valley under a bridge or the mountain over a tunnel. A recorded track whose points stray from a tunnel or bridge where GPS was lost still counts as on it within 80 m, as long as no other road is nearer than 25 m. structures lists those stretches (fromKm, toKm, kind bridge or tunnel, name, and the clearance: maxheight as tagged in OSM, maxheightM in metres when it is a number, null when untagged; default means no sign and room for the country's general limit, below_default no sign and less room than that); it is null when they could not be looked up and the heights are the terrain's. heights: "terrain" gives the ground under every point instead. For a recorded track send it here with at: "points" rather than to POST /v1/elevation, which reads each point on its own and so cannot follow a road through a tunnel. A route is GeoJSON {"type": "LineString", "coordinates": [[lon, lat], ...]} or a MultiLineString of sections in order, up to 20,000 km.

Cost: One request.

Body

routeGeoJSON LineString or MultiLineString or GeoJSON LineString or MultiLineString
stepM optionalnumber, 10 to 1000
at optionalstep, points
heights optionalroad, terrain

Request

curl -X POST 'https://geo.runningcat.dev/v1/route/profile' \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"route":{"type":"LineString","coordinates":[[6.1432,46.2044],[6.15,46.19],[6.175,46.17],[6.2,46.15]]},"stepM":500}'

Example answer

{
 "routeLengthKm": 7.567,
 "stepM": 500,
 "ascentM": 733,
 "descentM": 31,
 "minM": 375.1,
 "maxM": 1090.8,
 "structures": [
  {
   "fromKm": 0.139,
   "toKm": 0.178,
   "kind": "tunnel",
   "name": "Passage de la Petite-Corraterie",
   "maxheight": null,
   "maxheightM": null
  },
  {
   "fromKm": 1.05,
   "toKm": 1.07,
   "kind": "tunnel",
   "name": null,
   "maxheight": "default",
   "maxheightM": null
  },
  {
   "fromKm": 3.89,
   "toKm": 3.91,
   "kind": "bridge",
   "name": null,
   "maxheight": null,
   "maxheightM": null
  }
 ],
 "profile": [
  {
   "km": 0,
   "lon": 6.1432,
   "lat": 46.2044,
   "ele": 375.1
  },
  {
   "km": 0.5,
   "lon": 6.145218,
   "lat": 46.200126,
   "ele": 383.6
  },
  {
   "km": 1,
   "lon": 6.147237,
   "lat": 46.195852,
   "ele": 385.8
  }
 ]
}

GET /v1/dem/{z}/{x}/{y}.png

Terrain tiles (Terrarium). Terrain tiles for hillshading, 3D terrain and contours, in Terrarium encoding: height in metres = R × 256 + G + B / 256 − 32768. {y}.png is 256 px, zoom 0 to 13; {y}.webp is 512 px lossless, zoom 0 to 12. All land is covered; there is no zoom 14, so a contour layer drawn at 14 sets maxzoom 13 on its source. 404 where there is no tile. Credit: terrain from Mapterhorn.

Cost: One request per 50 tiles.

Parameters

zZoom
xTile column
yTile row

Request

curl 'https://geo.runningcat.dev/v1/dem/12/2117/1446.png' \
  -H "Authorization: Bearer YOUR_KEY"

Example answer: image/png or image/webp

Map layers

Protected areas, trails, pistes, lifts, boundaries and land cover.

GET /v1/layers/{layer}

A map layer in a box. Protected areas, hiking and cycling routes, ski pistes, lifts or administrative boundaries in a box, as a GeoJSON FeatureCollection with OSM tags; truncated is true when limit cut it. Layers: protected_area, route, piste, aerialway, boundary. kinds narrows a layer (route: hiking, bicycle, mtb, ...; boundary: admin levels such as 2, 4).

Cost: One request.

Parameters

layerprotected_area, route, piste, aerialway, boundary
bboxwest,south,east,north in degrees, at most 16 square degrees
kinds optionalComma-separated kinds within the layer
limit optionalFeatures, 1 to 5000 (default 2000)
lang optionalLanguage of place names, e.g. en, de, pl; local names when absent

Request

curl 'https://geo.runningcat.dev/v1/layers/protected_area?bbox=6.0,46.1,6.3,46.3&limit=2&lang=en' \
  -H "Authorization: Bearer YOUR_KEY"

Example answer

{
 "type": "FeatureCollection",
 "truncated": true,
 "features": [
  {
   "type": "Feature",
   "id": "R5555535",
   "properties": {
    "layer": "protected_area",
    "kind": "protected_area",
    "name": "Parc naturel régional du Haut-Jura",
    "tags": {
     "name": "Parc naturel régional du Haut-Jura",
     "name:br": "Park natur rannvroel ar Jura-Uhel",
     "name:de": "Regionaler Naturpark Hochjura",
     "name:en": "Jura Mountains Regional Natural Park",
     "name:fr": "Parc naturel régional du Haut-Jura",
     "website": "https://www.parc-haut-jura.fr/",
     "boundary": "protected_area",
     "wikidata": "Q3364633",
     "wikipedia": "fr:Parc naturel régional du Haut-Jura",
     "iucn_level": "5",
     "protect_class": "5",
     "protection_title": "parc naturel régional"
    }
   },
   "geometry": {
    "type": "Polygon",
    "coordinates": [
     [
      [
       6.065334,
       46.3
      ],
      [
       6.064507,
       46.298717
      ],
      [
       6.063409,
       46.297955
      ]
     ]
    ]
   }
  },
  {
   "type": "Feature",
   "id": "R14980670",
   "properties": {
    "layer": "protected_area",
    "kind": "protected_area",
    "name": "Réserve naturelle de la haute chaîne du Jura",
    "tags": {
     "name": "Réserve naturelle de la haute chaîne du Jura",
     "access": "restricted",
     "leisure": "nature_reserve",
     "name:it": "Riserva naturale dell'Alta Catena del Giura",
     "website": "https://www.rnn-hautechainedujura.fr/",
     "boundary": "protected_area",
     "wikidata": "Q3457394",
     "wikipedia": "fr:Réserve naturelle nationale de la haute chaîne du Jura",
     "protect_class": "4",
     "protection_title": "Réserve naturelle nationale"
    }
   },
   "geometry": {
    "type": "Polygon",
    "coordinates": [
     [
     
…

GET /v1/layers/landcover

Land cover in a box. What the ground is, from OpenStreetMap areas: forest, scrub, grassland, farmland, wetland, water, glacier, bare, park, built (kind). A GeoJSON FeatureCollection, largest areas first, clipped to the box; the box is at most 0.1 square degrees because forests and fields come by the thousand. kinds keeps some classes.

Cost: One request.

Parameters

bboxwest,south,east,north, at most 0.1 square degrees
kinds optionalComma-separated classes: forest, scrub, grassland, farmland, wetland, water, glacier, bare, park, built
limit optionalFeatures, 1 to 5000 (default 2000)

Request

curl 'https://geo.runningcat.dev/v1/layers/landcover?bbox=6.10,46.15,6.30,46.30&limit=2' \
  -H "Authorization: Bearer YOUR_KEY"

Example answer

{
 "type": "FeatureCollection",
 "truncated": true,
 "features": [
  {
   "type": "Feature",
   "id": "R332617",
   "properties": {
    "layer": "landcover",
    "kind": "water",
    "name": "Le Léman",
    "tags": {
     "ele": "372",
     "boat": "yes",
     "name": "Le Léman",
     "salt": "no",
     "tidal": "no",
     "water": "lake",
     "name:ca": "Llac Léman",
     "name:cs": "Ženevské jezero",
     "name:da": "Lemansøen",
     "name:de": "Genfersee",
     "name:en": "Lake Geneva",
     "name:es": "Lago Lemán",
     "name:et": "Genfi järv",
     "name:fr": "Le Léman",
     "name:he": "אגם ז'נבה",
     "name:hr": "Ženevsko jezero",
     "name:hu": "Genfi-tó",
     "name:is": "Genfarvatn",
     "name:it": "Lago Lemano",
     "name:ja": "レマン湖",
     "name:lb": "Genferséia",
     "name:nl": "Meer van Genève",
     "name:pl": "Jezioro Genewskie",
     "name:ru": "Женевское озеро",
     "name:sk": "Ženevské jazero",
     "name:sl": "Ženevsko jezero",
     "name:sr": "Женевско језеро",
     "name:tt": "Женева күле",
     "name:uk": "Женевське озеро",
     "natural": "water",
     "alt_name": "Lac Léman;Lac de Genève",
     "name:frp": "Lèman",
     "swimming": "yes",
     "wikidata": "Q6403",
     "ref:gwlnr": "CH0097570000",
     "wikipedia": "fr:Léman",
     "ref:gewiss": "9757",
     "short_name": "Léman",
     "alt_name:da": "Genevesøen",
     "alt_name:de": "Genfer See",
     "alt_name:en": "Lake Leman",
     "alt_name:es": "Lago de Ginebra",
     "alt_name:et": "Leman järv",
     "alt_name:fr": "Lac Léman;Lac de Genève",
     "alt_name:hr": "Lemansko jezero",
     "alt_name:hu": "Leman-tó",
     "alt_name:it": "Lago di Ginevra",
     "alt_name:lb": "Lemanerséi",
     "alt_name:sr": "Леманско језеро",
     "intermittent": "no",
     "name:sr-Latn": "Ženevsko jeze
…

POST /v1/route/landcover

Land cover along a route. What the ground is along a route, every 50 m (coarser over 250 km): kilometres per class and the stretches of each, in route order. A point takes the class of the smallest area it lies in (a wood inside a park is forest); unmapped where OpenStreetMap maps no area (often roads, villages and fields nobody drew). A route is GeoJSON {"type": "LineString", "coordinates": [[lon, lat], ...]} or a MultiLineString of sections in order, up to 20,000 km.

Cost: One request.

Body

routeGeoJSON LineString or MultiLineString or GeoJSON LineString or MultiLineString

Request

curl -X POST 'https://geo.runningcat.dev/v1/route/landcover' \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"route":{"type":"LineString","coordinates":[[6.1432,46.2044],[6.15,46.19],[6.175,46.17],[6.2,46.15]]}}'

Example answer

{
 "routeLengthKm": 7.57,
 "stepM": 50,
 "km": {
  "built": 3.1,
  "park": 1.7,
  "forest": 1.07,
  "grassland": 0.85,
  "unmapped": 0.65,
  "farmland": 0.15,
  "water": 0.05
 },
 "runs": [
  {
   "fromKm": 0,
   "toKm": 0.25,
   "class": "built"
  },
  {
   "fromKm": 0.25,
   "toKm": 0.3,
   "class": "park"
  },
  {
   "fromKm": 0.3,
   "toKm": 0.35,
   "class": "built"
  }
 ]
}

POST /v1/route/layers

Map layers along a route. The layer features a route runs through or within bufferM (0 to 1000, default 30) of, with the stretches of the route inside each. Layers protected_area, route, piste, aerialway by default. A route is GeoJSON {"type": "LineString", "coordinates": [[lon, lat], ...]} or a MultiLineString of sections in order, up to 20,000 km.

Cost: One request.

Body

routeGeoJSON LineString or MultiLineString or GeoJSON LineString or MultiLineString
layers optionalarray of protected_area, route, piste, aerialway, boundary, at least 1 items
bufferM optionalnumber, 0 to 1000
lang optionalstring

Request

curl -X POST 'https://geo.runningcat.dev/v1/route/layers' \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"route":{"type":"LineString","coordinates":[[6.1432,46.2044],[6.15,46.19],[6.175,46.17],[6.2,46.15]]}}'

Example answer

{
 "routeLengthKm": 7.57,
 "stepM": 50,
 "bufferM": 30,
 "features": [
  {
   "layer": "route",
   "id": "R11063902",
   "kind": "hiking",
   "name": "ViaJacobi",
   "tags": {
    "to": "Genève (frontière)",
    "ref": "4",
    "from": "Genève",
    "name": "ViaJacobi",
    "route": "hiking",
    "network": "nwn",
    "distance": "9",
    "operator": "Wanderland Schweiz",
    "osmc:symbol": "green:green::4:white"
   },
   "stretches": [
    {
     "fromKm": 0,
     "toKm": 0.2
    },
    {
     "fromKm": 0.75,
     "toKm": 0.75
    }
   ],
   "km": 0.2
  },
  {
   "layer": "route",
   "id": "R1307899",
   "kind": "bicycle",
   "name": "Cyclévasion Genève-Salève",
   "tags": {
    "to": "Pas-de-l'Échelle",
    "ref": "104",
    "from": "Genève",
    "name": "Cyclévasion Genève-Salève",
    "route": "bicycle",
    "name:fr": "Cyclévasion Genève-Salève",
    "network": "lcn",
    "distance": "7",
    "operator": "Etat de Genève et Ville de Genève"
   },
   "stretches": [
    {
     "fromKm": 0,
     "toKm": 0
    },
    {
     "fromKm": 0.5,
     "toKm": 0.6
    }
   ],
   "km": 0.1
  },
  {
   "layer": "route",
   "id": "R1230605",
   "kind": "hiking",
   "name": "Eaux d'Arve - Pinchat",
   "tags": {
    "name": "Eaux d'Arve - Pinchat",
    "route": "hiking",
    "network": "lwn",
    "osmc:symbol": "yellow::yellow_diamond"
   },
   "stretches": [
    {
     "fromKm": 2.95,
     "toKm": 3
    }
   ],
   "km": 0.05
  }
 ]
}

Weather

Forecasts for a point, an area and a route on the way.

GET /v1/weather

Forecast for a point. Current conditions, hourly and daily forecast for up to 14 days (default 7), corrected to the height of the point; the local time zone comes along. Hours carry temperature, feels-like, precipitation, weather code, wind, gusts, cloud cover, humidity, sea-level pressure (pressureHpa) and the height of the boundary layer (boundaryLayerM, from the global GFS model everywhere, about 13 km cells). Units: °C, mm, km/h, degrees, percent, hPa, m; times in UTC.

Cost: One request.

Parameters

lonLongitude, −180 to 180
latLatitude, −90 to 90
days optional1 to 14 (default 7)

Request

curl 'https://geo.runningcat.dev/v1/weather?lon=6.1432&lat=46.2044&days=2' \
  -H "Authorization: Bearer YOUR_KEY"

Example answer

{
 "lon": 6.1432,
 "lat": 46.2044,
 "elevationM": 374,
 "current": {
  "time": "2026-11-12T12:30:00Z",
  "temperatureC": 12.7,
  "feelsLikeC": 11.7,
  "precipitationMm": 0.1,
  "weatherCode": 61,
  "windKmh": 9.8,
  "windDirDeg": 36,
  "gustKmh": 23,
  "cloudPct": 100,
  "humidityPct": 93,
  "pressureHpa": 992.5,
  "boundaryLayerM": 205,
  "isDay": true
 },
 "hourly": [
  {
   "time": "2026-11-11T23:00:00Z",
   "temperatureC": 10.1,
   "feelsLikeC": 9.7,
   "precipitationMm": 0,
   "weatherCode": 3,
   "windKmh": 2.6,
   "windDirDeg": 164,
   "gustKmh": 5.4,
   "cloudPct": 100,
   "humidityPct": 97,
   "pressureHpa": 1002.4,
   "boundaryLayerM": 10,
   "isDay": false
  },
  {
   "time": "2026-11-12T00:00:00Z",
   "temperatureC": 9.9,
   "feelsLikeC": 9.3,
   "precipitationMm": 0,
   "weatherCode": 3,
   "windKmh": 2.9,
   "windDirDeg": 150,
   "gustKmh": 4.3,
   "cloudPct": 100,
   "humidityPct": 97,
   "pressureHpa": 1001.7,
   "boundaryLayerM": 10,
   "isDay": false
  },
  {
   "time": "2026-11-12T01:00:00Z",
   "temperatureC": 10.2,
   "feelsLikeC": 9.8,
   "precipitationMm": 0,
   "weatherCode": 3,
   "windKmh": 1.6,
   "windDirDeg": 117,
   "gustKmh": 5.4,
   "cloudPct": 100,
   "humidityPct": 95,
   "pressureHpa": 1000.8,
   "boundaryLayerM": 10,
   "isDay": false
  }
 ],
 "daily": [
  {
   "date": "2026-11-12",
   "minC": 9.9,
   "maxC": 12.7,
   "precipitationMm": 15.7,
   "windMaxKmh": 10.8,
   "gustMaxKmh": 23,
   "weatherCode": 61,
   "sunrise": "2026-11-12T06:32:07Z",
   "sunset": "2026-11-12T16:06:42Z"
  },
  {
   "date": "2026-11-13",
   "minC": 3.9,
   "maxC": 11.5,
   "precipitationMm": 1.3,
   "windMaxKmh": 25.2,
   "gustMaxKmh": 57.2,
   "weatherCode": 61,
   "sunrise": "2026-11-13T06:33:33Z",
   "sunset": "2026-11-13T16:05:32Z"
  }
 ],
 "timezone": {

…

GET /v1/weather/area

Forecast grid over an area. Forecast on a grid of about cells cells (at most 400), listed from the north-west corner row by row: values.<name>[step][cell], with times for the steps. hours steps come back, stepHours apart, from pastHours before the current hour. cells × steps at most 9600. Keys given the bulk_grid permission (on request) may ask for up to 20,000 cells and 250,000 values; such grids are built one at a time behind other requests, so they take longer.

Cost: One request.

Parameters

bboxwest,south,east,north in degrees, at most 3600 square degrees
cells optional1 to 400 (default 100)
stepDeg optionalInstead of cells: a lattice with cell centres at west + k·stepDeg and north − k·stepDeg, 0.01 to 10 degrees, so the cells can sit on a model's own nodes (CAMS Europe: every 0.1° at x.x5°, CAMS Global: every 0.4°)
hours optionalHow many steps come back, 1 to 120 (default 24)
stepHours optionalHours between two steps, 1 to 6 (default 1)
pastHours optionalStart this many hours before the current hour, 0 to 48 (default 0); the window may span up to 168 hours

Request

curl 'https://geo.runningcat.dev/v1/weather/area?bbox=6.0,46.1,6.3,46.3&cells=4&hours=2' \
  -H "Authorization: Bearer YOUR_KEY"

Example answer

{
 "bbox": [
  6,
  46.1,
  6.3,
  46.3
 ],
 "rows": 2,
 "cols": 2,
 "lon": [
  6.075,
  6.225
 ],
 "lat": [
  46.25,
  46.15
 ],
 "times": [
  "2026-11-12T12:00:00Z",
  "2026-11-12T15:00:00Z"
 ],
 "values": {
  "temperatureC": [
   [
    11.6,
    12.3,
    12.2,
    12
   ],
   [
    11.4,
    12.1,
    12.1,
    11.9
   ]
  ],
  "feelsLikeC": [
   [
    10.4,
    11.4,
    10.9,
    11.5
   ],
   [
    10.5,
    10.9,
    11.1,
    10.9
   ]
  ],
  "precipitationMm": [
   [
    0.8,
    0.9,
    1,
    1.1
   ],
   [
    1.2,
    1.1,
    1.1,
    1.4
   ]
  ],
  "weatherCode": [
   [
    61,
    61,
    61,
    61
   ],
   [
    61,
    61,
    61,
    61
   ]
  ],
  "windKmh": [
   [
    9.4,
    9.2,
    11.7,
    6.6
   ],
   [
    6.9,
    10.8,
    9.9,
    9.1
   ]
  ],
  "windDirDeg": [
   [
    40,
    21,
    34,
    351
   ],
   [
    43,
    26,
    19,
    342
   ]
  ],
  "gustKmh": [
   [
    16.2,
    18.4,
    22.3,
    16.6
   ],
   [
    19.4,
    21.2,
    19.1,
    19.8
   ]
  ],
  "cloudPct": [
   [
    100,
    100,
    100,
    100
   ],
   [
    100,
    100,
    100,
    100
   ]
  ],
  "humidityPct": [
   [
    94,
    94,
    95,
    98
   ],
   [
    93,
    94,
    96,
    96
   ]
  ],
  "pressureHpa": [
   [
    992.9,
    992.9,
    992.9,
    993.1
   ],
   [
    991.7,
    991.5,
    991.7,
    991.7
   ]
  ],
  "boundaryLayerM": [
   [
    205,
    185,
    210,
    145
   ],
   [
    200,
    120,
    130,
    100
   ]
  ]
 }
}

POST /v1/weather/current

Current weather at many points. Current conditions at up to 2,000 points in one request, in order (for example a wind field for a map): the same fields as current in /v1/weather, the boundary layer height included. Over 400 points the request waits its turn behind other large requests.

Cost: One request.

Body

pointsarray of [lon, lat], 1 to 2000 items

Request

curl -X POST 'https://geo.runningcat.dev/v1/weather/current' \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"points":[[6.1432,46.2044],[19.94,50.06]]}'

Example answer

{
 "points": [
  {
   "lon": 6.1432,
   "lat": 46.2044,
   "current": {
    "time": "2026-11-12T12:30:00Z",
    "temperatureC": 12.7,
    "feelsLikeC": 11.7,
    "precipitationMm": 0.1,
    "weatherCode": 61,
    "windKmh": 9.8,
    "windDirDeg": 36,
    "gustKmh": 23,
    "cloudPct": 100,
    "humidityPct": 93,
    "pressureHpa": 992.5,
    "boundaryLayerM": 205,
    "isDay": true
   }
  },
  {
   "lon": 19.94,
   "lat": 50.06,
   "current": {
    "time": "2026-11-12T12:30:00Z",
    "temperatureC": 8,
    "feelsLikeC": 7,
    "precipitationMm": 0,
    "weatherCode": 2,
    "windKmh": 1.1,
    "windDirDeg": 162,
    "gustKmh": 4.7,
    "cloudPct": 80,
    "humidityPct": 89,
    "pressureHpa": 997.8,
    "boundaryLayerM": 585,
    "isDay": true
   }
  }
 ]
}

POST /v1/route/weather

Weather along a route on the way. The forecast at points along the route for the time you reach each of them, leaving at departure (now by default) at speedKmh (default 20), every stepKm; a summary gives the coldest and warmest, the wettest hour, the strongest headwind and gust. A route is GeoJSON {"type": "LineString", "coordinates": [[lon, lat], ...]} or a MultiLineString of sections in order, up to 20,000 km.

Cost: One request.

Body

routeGeoJSON LineString or MultiLineString or GeoJSON LineString or MultiLineString
departure optionalstring (ISO time with offset)
speedKmh optionalnumber, 2 to 80
stepKm optionalnumber, 0.5 to 100

Request

curl -X POST 'https://geo.runningcat.dev/v1/route/weather' \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"route":{"type":"LineString","coordinates":[[6.1432,46.2044],[6.15,46.19],[6.175,46.17],[6.2,46.15]]},"speedKmh":18}'

Example answer

{
 "routeLengthKm": 7.6,
 "departure": "2026-11-12T12:35:43.550Z",
 "arrival": "2026-11-12T13:00:57.025Z",
 "speedKmh": 18,
 "summary": {
  "minTemperatureC": 8.4,
  "maxTemperatureC": 12.7,
  "maxPrecipitationMm": 0.7,
  "maxHeadwindKmh": -3.1,
  "maxGustKmh": 27
 },
 "points": [
  {
   "km": 0,
   "lon": 6.1432,
   "lat": 46.2044,
   "time": "2026-11-12T12:35:43.550Z",
   "temperatureC": 12.7,
   "feelsLikeC": 11.7,
   "precipitationMm": 0.4,
   "weatherCode": 61,
   "windKmh": 9.9,
   "windDirDeg": 37,
   "gustKmh": 21.1,
   "cloudPct": 100,
   "humidityPct": 92,
   "pressureHpa": 992.1,
   "boundaryLayerM": 230,
   "isDay": true,
   "headwindKmh": -3.3,
   "crosswindKmh": 9.3
  },
  {
   "km": 5,
   "lon": 6.17818,
   "lat": 46.16746,
   "time": "2026-11-12T12:52:23.550Z",
   "temperatureC": 12.2,
   "feelsLikeC": 11.4,
   "precipitationMm": 0.5,
   "weatherCode": 61,
   "windKmh": 8.8,
   "windDirDeg": 31,
   "gustKmh": 19.2,
   "cloudPct": 100,
   "humidityPct": 95,
   "pressureHpa": 992.2,
   "boundaryLayerM": 180,
   "isDay": true,
   "headwindKmh": -3.5,
   "crosswindKmh": 8.1
  },
  {
   "km": 7.6,
   "lon": 6.2,
   "lat": 46.15,
   "time": "2026-11-12T13:00:57.025Z",
   "temperatureC": 8.4,
   "feelsLikeC": 6.3,
   "precipitationMm": 0.7,
   "weatherCode": 61,
   "windKmh": 11.2,
   "windDirDeg": 33,
   "gustKmh": 27,
   "cloudPct": 100,
   "humidityPct": 100,
   "pressureHpa": 992.3,
   "boundaryLayerM": 120,
   "isDay": true,
   "headwindKmh": -3.1,
   "crosswindKmh": 10.8
  }
 ]
}

Air quality

Forecasts and what stations measure now.

GET /v1/air

Air quality forecast for a point. Current and hourly PM2.5, PM10, nitrogen dioxide, ozone, sulphur dioxide, carbon monoxide, dust, UV index, European and US AQI; in Europe also ammonia and pollen (alder, birch, grass, mugwort, olive, ragweed). µg/m³; pollen in grains/m³.

Cost: One request.

Parameters

lonLongitude, −180 to 180
latLatitude, −90 to 90
hours optionalHours from the current one, 1 to 120 (default 48)
pastHours optionalPast hours of the model before the current hour, 0 to 48 (default 0): what the forecast said for the last hours, to set against what stations measured

Request

curl 'https://geo.runningcat.dev/v1/air?lon=6.1432&lat=46.2044&hours=2' \
  -H "Authorization: Bearer YOUR_KEY"

Example answer

{
 "lon": 6.1432,
 "lat": 46.2044,
 "current": {
  "time": "2026-11-12T12:00:00Z",
  "pm25": 3,
  "pm10": 4.5,
  "nitrogenDioxide": 9.5,
  "ozone": 45,
  "sulphurDioxide": 0.3,
  "carbonMonoxide": 143,
  "ammonia": 1.2,
  "dust": 0,
  "uvIndex": 0.5,
  "europeanAqi": 19,
  "usAqi": 38,
  "alderPollen": 0,
  "birchPollen": 0,
  "grassPollen": 0,
  "mugwortPollen": 0,
  "olivePollen": 0,
  "ragweedPollen": 0
 },
 "hourly": [
  {
   "time": "2026-11-12T12:00:00Z",
   "pm25": 3,
   "pm10": 4.5,
   "nitrogenDioxide": 9.5,
   "ozone": 45,
   "sulphurDioxide": 0.3,
   "carbonMonoxide": 143,
   "ammonia": 1.2,
   "dust": 0,
   "uvIndex": 0.5,
   "europeanAqi": 19,
   "usAqi": 38,
   "alderPollen": 0,
   "birchPollen": 0,
   "grassPollen": 0,
   "mugwortPollen": 0,
   "olivePollen": 0,
   "ragweedPollen": 0
  },
  {
   "time": "2026-11-12T13:00:00Z",
   "pm25": 2.7,
   "pm10": 4.3,
   "nitrogenDioxide": 9.2,
   "ozone": 48,
   "sulphurDioxide": 0.3,
   "carbonMonoxide": 140,
   "ammonia": 1.2,
   "dust": 0,
   "uvIndex": 0.3,
   "europeanAqi": 18,
   "usAqi": 38,
   "alderPollen": 0,
   "birchPollen": 0,
   "grassPollen": 0,
   "mugwortPollen": 0,
   "olivePollen": 0,
   "ragweedPollen": 0
  }
 ]
}

GET /v1/air/area

Air quality grid over an area. The air quality values of /v1/air on a grid, shaped like the weather grid, with the same hours, stepHours and pastHours. Keys given the bulk_grid permission (on request) may ask for up to 20,000 cells and 250,000 values; such grids are built one at a time behind other requests, so they take longer.

Cost: One request.

Parameters

bboxwest,south,east,north in degrees, at most 3600 square degrees
cells optional1 to 400 (default 100)
stepDeg optionalInstead of cells: a lattice with cell centres at west + k·stepDeg and north − k·stepDeg, 0.01 to 10 degrees, so the cells can sit on a model's own nodes (CAMS Europe: every 0.1° at x.x5°, CAMS Global: every 0.4°)
hours optionalHow many steps come back, 1 to 120 (default 24)
stepHours optionalHours between two steps, 1 to 6 (default 1)
pastHours optionalStart this many hours before the current hour, 0 to 48 (default 0); the window may span up to 168 hours

Request

curl 'https://geo.runningcat.dev/v1/air/area?bbox=6.0,46.1,6.3,46.3&cells=4&hours=2' \
  -H "Authorization: Bearer YOUR_KEY"

Example answer

{
 "bbox": [
  6,
  46.1,
  6.3,
  46.3
 ],
 "rows": 2,
 "cols": 2,
 "lon": [
  6.075,
  6.225
 ],
 "lat": [
  46.25,
  46.15
 ],
 "times": [
  "2026-11-12T12:00:00Z",
  "2026-11-12T13:00:00Z"
 ],
 "values": {
  "pm25": [
   [
    3,
    3.5,
    3.6,
    4.3
   ],
   [
    2.5,
    3.2,
    3.1,
    4.4
   ]
  ],
  "pm10": [
   [
    3.7,
    4.9,
    4.7,
    5.6
   ],
   [
    3.5,
    4.9,
    4.5,
    5.2
   ]
  ],
  "nitrogenDioxide": [
   [
    6.5,
    10.5,
    10.9,
    14.1
   ],
   [
    6.2,
    9.1,
    9.7,
    13.8
   ]
  ],
  "ozone": [
   [
    54,
    43,
    45,
    35
   ],
   [
    55,
    44,
    48,
    38
   ]
  ],
  "sulphurDioxide": [
   [
    0.3,
    0.3,
    0.3,
    0.3
   ],
   [
    0.2,
    0.3,
    0.3,
    0.3
   ]
  ],
  "carbonMonoxide": [
   [
    136,
    154,
    153,
    165
   ],
   [
    130,
    147,
    149,
    165
   ]
  ],
  "ammonia": [
   [
    0.9,
    1.1,
    1.2,
    1
   ],
   [
    0.9,
    1.2,
    1.3,
    1.1
   ]
  ],
  "dust": [
   [
    0,
    0,
    0,
    0
   ],
   [
    0,
    0,
    0,
    0
   ]
  ],
  "uvIndex": [
   [
    0.5,
    0.5,
    0.4,
    0.4
   ],
   [
    0.3,
    0.4,
    0.3,
    0.3
   ]
  ],
  "europeanAqi": [
   [
    18,
    21,
    21,
    25
   ],
   [
    18,
    18,
    19,
    25
   ]
  ],
  "usAqi": [
   [
    30,
    42,
    38,
    39
   ],
   [
    29,
    42,
    38,
    39
   ]
  ],
  "alderPollen": [
   [
    0,
    0,
    0,
    0
   ],
   [
    0,
    0,
    0,
    0
   ]
  ],
  "birchPollen": [
   [
    0,
    0,
    0,
    0
   ],
   [
    0,
    0,
    0,
    0
   ]
  ],
  "grassPollen": [
   [
    0,
    0,
    0,
    0
   ],
   [
    0,
    0,
    0,
    0
   ]
  ],
  "mugwortPollen": [
   [
    0,
    0,
    0,
    0
   ],
   [
    0,
    0,
    0,
    0
   ]
  ]
…

GET /v1/air/stations

Measured air quality at stations. The latest value per pollutant (last 3 hours; 6 for EEA stations) at official stations (kind=government) and volunteer sensors (kind=citizen), nearest first: around a point (lon, lat, radiusKm 1 to 100, default 25) or in a box (bbox, nearest its centre first). µg/m³; credits names the sources to show.

Cost: One request.

Parameters

lon optionalLongitude, −180 to 180
lat optionalLatitude, −90 to 90
radiusKm optional1 to 100 (default 25)
bbox optionalwest,south,east,north in degrees, at most 16 square degrees
limit optionalAround a point 1 to 100 (default 10); in a box 1 to 1000 (default 200)
kind optionalgovernment or citizen; both when absent

Request

curl 'https://geo.runningcat.dev/v1/air/stations?lon=19.94&lat=50.06&bbox=6.0,46.1,6.3,46.3&limit=2' \
  -H "Authorization: Bearer YOUR_KEY"

Example answer

{
 "stations": [
  {
   "id": "syngeos:10043",
   "name": "Kraków, Warszawska 24",
   "kind": "citizen",
   "lon": 19.94143,
   "lat": 50.07168,
   "distanceKm": 1.3,
   "time": "2026-11-12T10:37:38.000Z",
   "values": {
    "pm10": 19,
    "pm25": 23
   }
  },
  {
   "id": "syngeos:10082",
   "name": "Kraków, Warszawska 24",
   "kind": "citizen",
   "lon": 19.94367,
   "lat": 50.07152,
   "distanceKm": 1.31,
   "time": "2026-11-12T11:05:27.000Z",
   "values": {
    "o3": 38,
    "pm10": 11,
    "pm25": 11
   }
  }
 ],
 "credits": [
  "Syngeos sensor network (https://syngeos.pl)"
 ]
}

POST /v1/route/air/stations

Air quality stations along a route. Stations within bufferKm (0.1 to 20) of the route, in route order with alongKm; up to limit (at most 500). A route is GeoJSON {"type": "LineString", "coordinates": [[lon, lat], ...]} or a MultiLineString of sections in order, up to 20,000 km.

Cost: One request.

Body

routeGeoJSON LineString or MultiLineString or GeoJSON LineString or MultiLineString
bufferKm optionalnumber, 0.1 to 20
limit optionalinteger, 1 to 500
kind optionalgovernment, citizen

Request

curl -X POST 'https://geo.runningcat.dev/v1/route/air/stations' \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"route":{"type":"LineString","coordinates":[[19.9,50.07],[19.98,50.05]]},"limit":2}'

Example answer

{
 "stations": [
  {
   "id": "gios:400",
   "name": "Kraków, Aleja Krasińskiego",
   "kind": "government",
   "lon": 19.92619,
   "lat": 50.05768,
   "distanceKm": 0.6,
   "alongKm": 2.25,
   "time": "2026-11-12T12:00:00.000Z",
   "values": {
    "co": 544,
    "no2": 38.2,
    "pm10": 37.7,
    "pm25": 19.8
   }
  },
  {
   "id": "syngeos:10043",
   "name": "Kraków, Warszawska 24",
   "kind": "citizen",
   "lon": 19.94143,
   "lat": 50.07168,
   "distanceKm": 1.25,
   "alongKm": 2.75,
   "time": "2026-11-12T10:37:38.000Z",
   "values": {
    "pm10": 19,
    "pm25": 23
   }
  }
 ],
 "credits": [
  "Źródło danych: GIOŚ - EKOINFONET; current data are unverified and may change, CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)",
  "Syngeos sensor network (https://syngeos.pl)"
 ]
}

GET /v1/air/stations/{id}/history

Readings of one station. Hourly readings per pollutant for the last days (1 to 7, default 7). The id comes from the station lists.

Cost: One request.

Parameters

idsource:station, e.g. gios:400
days optional1 to 7 (default 7)

Request

curl 'https://geo.runningcat.dev/v1/air/stations/gios:400/history?days=1' \
  -H "Authorization: Bearer YOUR_KEY"

Example answer

{
 "id": "syngeos:10043",
 "readings": {
  "pm10": [
   {
    "time": "2026-11-11T14:00:00.000Z",
    "value": 15
   },
   {
    "time": "2026-11-11T15:00:00.000Z",
    "value": 17
   },
   {
    "time": "2026-11-11T16:00:00.000Z",
    "value": 17
   }
  ],
  "pm25": [
   {
    "time": "2026-11-11T13:00:00.000Z",
    "value": 11
   },
   {
    "time": "2026-11-11T14:00:00.000Z",
    "value": 11
   },
   {
    "time": "2026-11-11T15:00:00.000Z",
    "value": 12
   }
  ]
 },
 "credits": [
  "Syngeos sensor network (https://syngeos.pl)"
 ]
}

Rivers

Water levels and discharge at river gauges.

GET /v1/rivers

River gauges. The latest water level (cm above the gauge zero), discharge (m³/s), change over a day and the warning state at gauges, nearest first: around a point (lon, lat, radiusKm 1 to 100, default 25) or in a box (bbox).

Cost: One request.

Parameters

lon optionalLongitude, −180 to 180
lat optionalLatitude, −90 to 90
radiusKm optional1 to 100 (default 25)
bbox optionalwest,south,east,north in degrees, at most 16 square degrees
limit optionalAround a point 1 to 100 (default 10); in a box 1 to 1000 (default 200)

Request

curl 'https://geo.runningcat.dev/v1/rivers?lon=6.1432&lat=46.2044&bbox=6.0,46.1,6.3,46.3&limit=2' \
  -H "Authorization: Bearer YOUR_KEY"

Example answer

{
 "gauges": [
  {
   "id": "hubeau:V035501202",
   "name": "[Le Rhône] en Suisse - Halle de l'Ile - Barrage du Seujet (OFEV)",
   "river": null,
   "lon": 6.1416,
   "lat": 46.20457,
   "distanceKm": 0.13,
   "time": "2026-11-12T12:00:00.000Z",
   "levelCm": null,
   "change24hCm": null,
   "dischargeM3s": 107,
   "warningCm": null,
   "alarmCm": null,
   "state": null
  },
  {
   "id": "bafu:2606",
   "name": "Genève, Halle de l'Île",
   "river": "Rhône",
   "lon": 6.14154,
   "lat": 46.20466,
   "distanceKm": 0.13,
   "time": "2026-11-12T12:20:00.000Z",
   "levelCm": null,
   "change24hCm": null,
   "dischargeM3s": 110.375,
   "warningCm": null,
   "alarmCm": null,
   "state": null
  }
 ]
}

POST /v1/route/rivers

River gauges along a route. Gauges within bufferKm (0.1 to 20, default 2) of the route, nearest first. A route is GeoJSON {"type": "LineString", "coordinates": [[lon, lat], ...]} or a MultiLineString of sections in order, up to 20,000 km.

Cost: One request.

Body

routeGeoJSON LineString or MultiLineString or GeoJSON LineString or MultiLineString
bufferKm optionalnumber, 0.1 to 20

Request

curl -X POST 'https://geo.runningcat.dev/v1/route/rivers' \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"route":{"type":"LineString","coordinates":[[6.1432,46.2044],[6.15,46.19],[6.175,46.17],[6.2,46.15]]}}'

Example answer

{
 "gauges": [
  {
   "id": "bafu:2606",
   "name": "Genève, Halle de l'Île",
   "river": "Rhône",
   "lon": 6.14154,
   "lat": 46.20466,
   "distanceKm": 0.13,
   "time": "2026-11-12T12:20:00.000Z",
   "levelCm": null,
   "change24hCm": null,
   "dischargeM3s": 110.375,
   "warningCm": null,
   "alarmCm": null,
   "state": null
  },
  {
   "id": "hubeau:V035501202",
   "name": "[Le Rhône] en Suisse - Halle de l'Ile - Barrage du Seujet (OFEV)",
   "river": null,
   "lon": 6.1416,
   "lat": 46.20457,
   "distanceKm": 0.13,
   "time": "2026-11-12T12:00:00.000Z",
   "levelCm": null,
   "change24hCm": null,
   "dischargeM3s": 107,
   "warningCm": null,
   "alarmCm": null,
   "state": null
  },
  {
   "id": "bafu:2170",
   "name": "Genève, Bout du Monde",
   "river": "Arve",
   "lon": 6.15935,
   "lat": 46.1803,
   "distanceKm": 0.16,
   "time": "2026-11-12T12:20:00.000Z",
   "levelCm": 37949.2,
   "change24hCm": 15.3,
   "dischargeM3s": 64.84,
   "warningCm": null,
   "alarmCm": null,
   "state": null
  }
 ]
}

GET /v1/rivers/{id}/history

Readings of one gauge. Readings for the last days (1 to 30, default 7). The id comes from the gauge lists.

Cost: One request.

Parameters

idsource:station, e.g. hubeau:V035501202
days optional1 to 30 (default 7)

Request

curl 'https://geo.runningcat.dev/v1/rivers/hubeau:V035501202/history?days=1' \
  -H "Authorization: Bearer YOUR_KEY"

Example answer

{
 "id": "hubeau:V035501202",
 "readings": [
  {
   "time": "2026-11-11T12:40:00.000Z",
   "levelCm": null,
   "dischargeM3s": 83.7
  },
  {
   "time": "2026-11-11T13:00:00.000Z",
   "levelCm": null,
   "dischargeM3s": 92.2
  },
  {
   "time": "2026-11-11T13:10:00.000Z",
   "levelCm": null,
   "dischargeM3s": 87.9
  }
 ]
}

Routing

Routes, loops and the surfaces of a route.

POST /v1/route

A route through points. A route through 2 to 50 points on OpenStreetMap data. Profiles: bike, city, road-bike, quiet-road-bike, gravel, mtb, hiking, car, shortest (default bike; city keeps to cycleways and off busy roads). alternative 1 to 3 gives other routes. The answer is a LineString with a height at every point, its length, climb and riding time, and surfaces: kilometres paved, unpaved and unknown, kilometres per highway value, and the stretches of each way kind with its OSM surface, highway, tracktype (grade1 solid to grade5 soft) and smoothness (null where the way has no such tag), so a stretch without a surface tag can still be judged by the kind of way. format: "gpx" answers a GPX file instead. 404 no_route when a point cannot be reached with the profile; 504 timeout after two minutes (add waypoints or split the trip).

Cost: One request and one route per started 100 km of the route found (50 km for mtb).

Body

pointsarray of [lon, lat], 2 to 50 items
profile optionalbike, city, road-bike, quiet-road-bike, gravel, mtb, hiking, car, shortest
alternative optionalinteger, 0 to 3
format optionalgeojson, gpx

Request

curl -X POST 'https://geo.runningcat.dev/v1/route' \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"points":[[6.1432,46.2044],[6.2,46.15]],"profile":"bike"}'

Example answer

{
 "profile": "bike",
 "lengthKm": 18.82,
 "ascentM": 735,
 "durationS": 8696,
 "geometry": {
  "type": "LineString",
  "coordinates": [
   [
    6.143198,
    46.204407,
    374
   ],
   [
    6.143206,
    46.204408,
    374
   ],
   [
    6.143399,
    46.204372,
    374
   ]
  ]
 },
 "surfaces": {
  "km": {
   "paved": 18,
   "unpaved": 0.04,
   "unknown": 0.79
  },
  "highwayKm": {
   "tertiary": 6.34,
   "cycleway": 5.23,
   "secondary": 3.46,
   "residential": 2.25,
   "unclassified": 0.68,
   "primary": 0.47,
   "track": 0.2,
   "pedestrian": 0.07,
   "footway": 0.07,
   "primary_link": 0.06
  },
  "runs": [
   {
    "fromKm": 0,
    "toKm": 0.235,
    "surface": "asphalt",
    "highway": "residential",
    "tracktype": null,
    "smoothness": null
   },
   {
    "fromKm": 0.235,
    "toKm": 0.308,
    "surface": "asphalt",
    "highway": "pedestrian",
    "tracktype": null,
    "smoothness": null
   },
   {
    "fromKm": 0.308,
    "toKm": 0.584,
    "surface": "asphalt",
    "highway": "unclassified",
    "tracktype": null,
    "smoothness": null
   }
  ]
 }
}

POST /v1/route/loop

A round trip from a point. A loop of about distanceKm (5 to 200) back to start, setting off towards direction (degrees, 0 = north, default 0); another direction gives another loop. The length usually lands within 10 % of the asked one. If no loop can be made that way (a glacier, the sea), the other directions are tried. The answer has the shape of /v1/route.

Cost: One request and one route per started 100 km of the route found (50 km for mtb).

Body

start[lon, lat]
distanceKmnumber, 5 to 200
profile optionalbike, city, road-bike, quiet-road-bike, gravel, mtb, hiking, car, shortest
direction optionalnumber, 0 to 360

Request

curl -X POST 'https://geo.runningcat.dev/v1/route/loop' \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"start":[6.1432,46.2044],"distanceKm":25,"profile":"bike","direction":90}'

Example answer

{
 "profile": "bike",
 "lengthKm": 25.807,
 "ascentM": 141,
 "durationS": 4932,
 "geometry": {
  "type": "LineString",
  "coordinates": [
   [
    6.143198,
    46.204407,
    374
   ],
   [
    6.143206,
    46.204408,
    374
   ],
   [
    6.143201,
    46.204436,
    374
   ]
  ]
 },
 "surfaces": {
  "km": {
   "paved": 16.01,
   "unpaved": 0.05,
   "unknown": 9.75
  },
  "highwayKm": {
   "none": 7.07,
   "secondary": 6.25,
   "residential": 4.64,
   "tertiary": 2.58,
   "cycleway": 2.51,
   "living_street": 0.96,
   "unclassified": 0.89,
   "footway": 0.26,
   "service": 0.21,
   "primary": 0.21,
   "path": 0.13,
   "pedestrian": 0.08
  },
  "runs": [
   {
    "fromKm": 0,
    "toKm": 0.01,
    "surface": "asphalt",
    "highway": "residential",
    "tracktype": null,
    "smoothness": null
   },
   {
    "fromKm": 0.01,
    "toKm": 0.044,
    "surface": "asphalt",
    "highway": "residential",
    "tracktype": null,
    "smoothness": "excellent"
   },
   {
    "fromKm": 0.044,
    "toKm": 0.104,
    "surface": "asphalt",
    "highway": "residential",
    "tracktype": null,
    "smoothness": null
   }
  ]
 }
}

POST /v1/route/surfaces

Surfaces of any track. The OSM surface along a track you already have (an imported GPX, a drawn line): every 20 m the track takes the nearest road or path within 25 m. The answer has the shape of surfaces in /v1/route: kilometres paved, unpaved and unknown, kilometres per highway, and stretches with surface, highway, tracktype and smoothness. The track is not re-routed. unknown surface: the way has no surface tag (its highway, tracktype and smoothness still come along), or no way lies within 25 m (then all three are null). heightLimits lists the stretches on ways with a height limit, tunnels and passages under bridges included, so you can tell whether a car with bikes on the roof fits: maxheight as tagged in OSM (the lower of maxheight and maxheight:physical), maxheightM in metres (feet converted), or below_default, meaning no sign but less room than the country's general limit (maxheightM null); name of the way. Only what is tagged on the ways: a height barrier mapped as a point on the road is not in the list, and a way with no tag may still be low. A route is GeoJSON {"type": "LineString", "coordinates": [[lon, lat], ...]} or a MultiLineString of sections in order, up to 20,000 km.

Cost: One request.

Body

routeGeoJSON LineString or MultiLineString or GeoJSON LineString or MultiLineString

Request

curl -X POST 'https://geo.runningcat.dev/v1/route/surfaces' \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"route":{"type":"LineString","coordinates":[[6.1432,46.2044],[6.15,46.19],[6.175,46.17],[6.2,46.15]]}}'

Example answer

{
 "km": {
  "paved": 2.76,
  "unpaved": 0.32,
  "unknown": 4.49
 },
 "highwayKm": {
  "none": 1.95,
  "footway": 1.53,
  "service": 1.25,
  "path": 0.7,
  "residential": 0.54,
  "tertiary": 0.44,
  "track": 0.32,
  "cycleway": 0.2,
  "living_street": 0.18,
  "unclassified": 0.16,
  "pedestrian": 0.14,
  "secondary": 0.06,
  "primary": 0.04,
  "motorway": 0.04,
  "secondary_link": 0.02,
  "platform": 0.02
 },
 "runs": [
  {
   "fromKm": 0,
   "toKm": 0.02,
   "surface": "asphalt",
   "highway": "residential",
   "tracktype": null,
   "smoothness": null
  },
  {
   "fromKm": 0.02,
   "toKm": 0.04,
   "surface": "unknown",
   "highway": "cycleway",
   "tracktype": null,
   "smoothness": null
  },
  {
   "fromKm": 0.04,
   "toKm": 0.059,
   "surface": "asphalt",
   "highway": "pedestrian",
   "tracktype": null,
   "smoothness": "good"
  }
 ],
 "heightLimits": []
}

Stops along a route

Water, food, shops and more along a route.

GET /v1/route-poi/categories

Stop categories. The categories and subtypes /v1/route-poi/corridor answers with: the OSM tags behind each, the default distance from the route, whether it is in the default search, and GPX and FIT waypoint symbols. version (also the ETag) changes when the catalog does.

Cost: One request.

Request

curl 'https://geo.runningcat.dev/v1/route-poi/categories' \
  -H "Authorization: Bearer YOUR_KEY"

Example answer

{
 "version": "a266ea079477",
 "defaultCategories": [
  "grocery",
  "fuel",
  "food"
 ],
 "maxDistanceM": 5000,
 "maxCorridorKm2": 2000,
 "fallbackExport": {
  "gpx": "Waypoint",
  "fit": "generic"
 },
 "categories": [
  {
   "id": "grocery",
   "inDefaultSearch": true,
   "defaultDistanceM": 200,
   "sources": [
    "db",
    "tiles"
   ],
   "export": {
    "gpx": "Shopping Center",
    "fit": "store"
   },
   "subtypes": [
    {
     "id": "supermarket",
     "osm": [
      {
       "shop": [
        "supermarket"
       ]
      }
     ],
     "export": {
      "gpx": "Shopping Center",
      "fit": "store"
     }
    },
    {
     "id": "convenience",
     "osm": [
      {
       "shop": [
        "convenience"
       ]
      }
     ],
     "export": {
      "gpx": "Convenience Store",
      "fit": "store"
     }
    },
    {
     "id": "bakery",
     "osm": [
      {
       "shop": [
        "bakery"
       ]
      }
     ],
     "export": {
      "gpx": "Convenience Store",
      "fit": "food"
     }
    }
   ]
  },
  {
   "id": "fuel",
   "inDefaultSearch": true,
   "defaultDistanceM": 200,
   "sources": [
    "db",
    "tiles"
   ],
   "export": {
    "gpx": "Gas Station",
    "fit": "store"
   },
   "subtypes": [
    {
     "id": "fuel",
     "osm": [
      {
       "amenity": [
        "fuel"
       ]
      }
     ],
     "export": {
      "gpx": "Gas Station",
      "fit": "store"
     }
    }
   ]
  },
  {
   "id": "food",
   "inDefaultSearch": true,
   "defaultDistanceM": 200,
   "sources": [
    "db",
    "tiles"
   ],
   "export": {
    "gpx": "Restaurant",
    "fit": "food"
   },
   "subtypes": [
    {
     "id": "cafe",
     "osm": [
      {
       "amenity": [
        "cafe"
       ]
      }
     ],
     "export": {
      "gpx": "Restaurant",
      "f
…

POST /v1/route-poi/corridor

Stops along a route. Water, food, groceries, toilets, bike shops and fuel (and on request places to sleep, sights, charging, benches, picnic spots) along a route, each with its kilometre along the route and its distance from it, plus the stretches of more than 40 km without water and food (gaps). The route comes as points of {lat, lon, d} (d: metres from the start), or as sections of them for separate pieces; up to 1,000 points. Distances: each category's default, maxDistanceM for all, or distancesM per category, up to 5,000 m; route length × the widest distance at most 2,000 km·km (1,000 km at 2 km, 400 km at 5 km), else 413. Chain stores carry their brand logo and icon URLs.

Cost: One request.

Body

points optionalarray of object, 2 to 1000 items
sections optionalarray of array of object, 2 to 1000 items, 1 to 64 items
categories optionalarray of grocery, fuel, food, water, toilets, bike, sleep, sights, charging, bench, picnic, at most 11 items
maxDistanceM optionalinteger, 1 to 5000
distancesM optionalobject
includeSprings optionalboolean

Request

curl -X POST 'https://geo.runningcat.dev/v1/route-poi/corridor' \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"points":[{"lat":46.2044,"lon":6.1432,"d":0},{"lat":46.19,"lon":6.15,"d":1666}],"categories":["water","food"]}'

Example answer

{
 "pois": [
  {
   "id": "node/12456128201",
   "category": "food",
   "subtype": "restaurant",
   "name": "Little Kitchen",
   "lat": 46.205307,
   "lon": 6.1434257,
   "flags": {},
   "tags": {
    "amenity": "restaurant"
   },
   "distanceAlongRouteKm": 0,
   "offsetFromRouteM": 102
  },
  {
   "id": "node/1379366890",
   "category": "water",
   "subtype": "drinking_water",
   "name": null,
   "lat": 46.2047788,
   "lon": 6.1417476,
   "flags": {
    "hasWater": true
   },
   "tags": {
    "amenity": "fountain",
    "drinking_water": "yes"
   },
   "distanceAlongRouteKm": 0,
   "offsetFromRouteM": 120
  },
  {
   "id": "node/3032374079",
   "category": "food",
   "subtype": "restaurant",
   "name": "Le Paradis",
   "lat": 46.2054609,
   "lon": 6.1419829,
   "flags": {},
   "tags": {
    "website": "https://www.terrasse-paradis.ch/",
    "addr:street": "Quai Turrettini",
    "amenity": "restaurant"
   },
   "distanceAlongRouteKm": 0,
   "offsetFromRouteM": 151
  }
 ],
 "totals": {
  "grocery": 0,
  "fuel": 0,
  "food": 79,
  "water": 19,
  "toilets": 0,
  "bike": 0
 },
 "totalCount": 98,
 "gaps": [],
 "gapCount": 0,
 "routeLengthKm": 7.568,
 "partial": false,
 "cached": false,
 "source": "db",
 "coverage": [
  {
   "sectionIndex": 0,
   "segmentIndexes": [
    0,
    1,
    2
   ]
  }
 ]
}

GET /v1/route-poi/tile/{row}/{col}

All stops in a tile. Every stop of the default categories in one 0.1° tile (row = floor(lat × 10), col = floor(lon × 10)), for apps that cache stops by area.

Cost: One request.

Parameters

rowfloor(lat × 10)
colfloor(lon × 10)

Request

curl 'https://geo.runningcat.dev/v1/route-poi/tile/462/61' \
  -H "Authorization: Bearer YOUR_KEY"

Example answer

{
 "pois": [
  {
   "id": "node/10009315581",
   "category": "water",
   "subtype": "drinking_water",
   "name": null,
   "lat": 46.2185561,
   "lon": 6.150866,
   "flags": {
    "hasWater": true
   },
   "tags": {
    "amenity": "drinking_water",
    "access": "yes",
    "fee": "no"
   }
  },
  {
   "id": "node/10079250380",
   "category": "food",
   "subtype": "restaurant",
   "name": "Hanae",
   "lat": 46.2043595,
   "lon": 6.1563109,
   "flags": {},
   "tags": {
    "opening_hours": "Tu-Sa 12:00-14:00,19:00-22:00",
    "website": "https://hanae-restaurant.ch/",
    "addr:street": "Rue des Pierres-du-Niton",
    "addr:housenumber": "5",
    "addr:city": "Genève",
    "phone": "+41 22 736 93 93",
    "amenity": "restaurant"
   }
  },
  {
   "id": "node/10096957554",
   "category": "food",
   "subtype": "fast_food",
   "name": "River Side",
   "lat": 46.2013962,
   "lon": 6.1553355,
   "flags": {},
   "tags": {
    "opening_hours": "Mo 19:00-01:00; Tu 19:00-02:00; We 19:00-02:30; Th 19:00-03:00; Fr, Sa 19:00-04:00; Su 19:00-00:00",
    "phone": "+41227362365",
    "amenity": "fast_food"
   }
  }
 ],
 "complete": true,
 "source": "db",
 "fetchedAt": "2026-11-12T12:36:13.181Z",
 "row": 462,
 "col": 61,
 "version": 1
}

OpenStreetMap data

Overpass QL and OSM features by tag.

POST /v1/overpass/interpreter

Overpass QL. Overpass QL queries answered from our copy of OpenStreetMap, in the Overpass JSON format. Send the query as the body (text/plain), as the form field data, or as ?data= on GET; /v1/overpass/api/interpreter works too, and the key may come as ?key=. open_at=YYYY-MM-DDTHH:MM (local time) keeps only the places open then. Errors come back as remark, as Overpass does. See the Overpass QL examples page for what is supported.

Cost: One request.

Parameters

open_at optionalLocal time such as 2026-11-10T06:30

Request

curl -X POST 'https://geo.runningcat.dev/v1/overpass/interpreter' \
  -H "Authorization: Bearer YOUR_KEY" \
  --data-binary 'node["amenity"="drinking_water"](around:300,46.2044,6.1432);out;'

Example answer

{
 "version": 0.6,
 "generator": "Running Cat Geo API",
 "osm3s": {
  "timestamp_osm_base": "2026-11-02T21:21:26+00:00",
  "copyright": "The data included in this document is from www.openstreetmap.org. The data is made available under ODbL."
 },
 "elements": [
  {
   "type": "node",
   "id": 897590805,
   "lat": 46.2054894,
   "lon": 6.1451637,
   "tags": {
    "amenity": "drinking_water",
    "fountain": "bottle_refill",
    "check_date": "2023-05-24"
   }
  },
  {
   "type": "node",
   "id": 1906525422,
   "lat": 46.2028922,
   "lon": 6.1448299,
   "tags": {
    "amenity": "drinking_water",
    "designation": "potable water"
   }
  }
 ]
}

POST /v1/poi/corridor

OSM features along a route by tag. OSM nodes and ways (with center) whose tags match filters ({"amenity": ["cafe", "restaurant"], ...}, 1 to 50 keys) within buffer_m of the route. 422 not_covered outside the map data. A route is GeoJSON {"type": "LineString", "coordinates": [[lon, lat], ...]} or a MultiLineString of sections in order, up to 20,000 km.

Cost: One request.

Body

routeGeoJSON LineString or MultiLineString or GeoJSON LineString or MultiLineString
buffer_mnumber, 1 to 20000
filtersobject

Request

curl -X POST 'https://geo.runningcat.dev/v1/poi/corridor' \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"route":{"type":"LineString","coordinates":[[6.1432,46.2044],[6.15,46.19],[6.175,46.17],[6.2,46.15]]},"buffer_m":300,"filters":{"amenity":["drinking_water"]}}'

Example answer

{
 "source": "db",
 "elements": [
  {
   "type": "node",
   "id": 897590805,
   "lat": 46.2054894,
   "lon": 6.1451637,
   "tags": {
    "amenity": "drinking_water",
    "fountain": "bottle_refill",
    "check_date": "2023-05-24"
   }
  },
  {
   "type": "node",
   "id": 1485121637,
   "lat": 46.1903548,
   "lon": 6.1505338,
   "tags": {
    "amenity": "drinking_water"
   }
  },
  {
   "type": "node",
   "id": 1769544502,
   "lat": 46.1919462,
   "lon": 6.1465644,
   "tags": {
    "amenity": "drinking_water",
    "fountain": "drinking",
    "check_date": "2024-01-30"
   }
  }
 ]
}

POST /v1/poi/boxes

OSM features in boxes by tag. The same in up to 16 boxes [west, south, east, north] of at most 1 square degree each.

Cost: One request.

Body

boxesarray of [lon, lat], 1 to 16 items
filtersobject

Request

curl -X POST 'https://geo.runningcat.dev/v1/poi/boxes' \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"boxes":[[6.14,46.2,6.16,46.21]],"filters":{"amenity":["cafe"]}}'

Example answer

{
 "source": "db",
 "elements": [
  {
   "type": "node",
   "id": 626085414,
   "lat": 46.206384,
   "lon": 6.1450353,
   "tags": {
    "name": "Starbucks",
    "brand": "Starbucks",
    "amenity": "cafe",
    "cuisine": "coffee_shop",
    "takeaway": "yes",
    "addr:city": "Genève",
    "official_name": "Starbucks Coffee",
    "brand:wikidata": "Q37158",
    "brand:wikipedia": "en:Starbucks",
    "opening_hours": "Mo-Fr 06:30-20:30; Sa 07:00-20:30; Su 07:30-20:30",
    "source:opening_hours": "atp",
    "check_date:opening_hours": "2026-11-05",
    "phone": "+41 43 421 42 26",
    "website": "https://www.starbucks.co.uk/store-locator/40024-96840/quai-des-bergues"
   }
  },
  {
   "type": "node",
   "id": 656492497,
   "lat": 46.2009495,
   "lon": 6.1415985,
   "tags": {
    "name": "Café du Grütli",
    "phone": "+41 22 321 51 58",
    "amenity": "cafe",
    "cuisine": "regional",
    "website": "https://www.cafedugrutli.ch/",
    "diet:vegan": "yes",
    "wheelchair": "yes",
    "addr:street": "Rue du Général-Dufour",
    "opening_hours": "Mo-Th 08:00-24:00, Fr 16:00-23:00, Sa 16:00-01:00, Su 16:00-23:00",
    "addr:housenumber": "16",
    "diet:gluten_free": "yes"
   }
  },
  {
   "type": "node",
   "id": 767144613,
   "lat": 46.207876,
   "lon": 6.1416771,
   "tags": {
    "name": "Chatime",
    "brand": "Chatime",
    "branch": "KINAYO",
    "amenity": "cafe",
    "cuisine": "bubble_tea",
    "takeaway": "yes",
    "brand:wikidata": "Q16829306"
   }
  }
 ]
}

POST /v1/roads/along

Roads along a route. Highway ways within buffer_m (1 to 200) of a route, with their OSM tags and geometry. For surface statistics /v1/route/surfaces does the matching for you. A route is GeoJSON {"type": "LineString", "coordinates": [[lon, lat], ...]} or a MultiLineString of sections in order, up to 20,000 km.

Cost: One request.

Body

routeGeoJSON LineString or MultiLineString or GeoJSON LineString or MultiLineString
buffer_mnumber, 1 to 200

Request

curl -X POST 'https://geo.runningcat.dev/v1/roads/along' \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"route":{"type":"LineString","coordinates":[[6.1432,46.2044],[6.15,46.19],[6.175,46.17],[6.2,46.15]]},"buffer_m":20}'

Example answer

{
 "source": "db",
 "ways": [
  {
   "id": 4272838,
   "tags": {
    "lit": "yes",
    "name": "Rue Lombard",
    "lanes": "2",
    "busway": "lane",
    "oneway": "yes",
    "highway": "secondary",
    "surface": "asphalt",
    "cycleway": "share_busway",
    "maxspeed": "50",
    "lanes:psv": "1",
    "psv:lanes": "yes|designated"
   },
   "geometry": [
    [
     6.147473,
     46.195835
    ],
    [
     6.147557,
     46.19577
    ],
    [
     6.147715,
     46.195644
    ]
   ]
  },
  {
   "id": 5070216,
   "tags": {
    "lit": "yes",
    "name": "Boulevard de la Tour",
    "lanes": "3",
    "oneway": "yes",
    "highway": "secondary",
    "surface": "asphalt",
    "maxspeed": "30",
    "cycleway:right": "lane"
   },
   "geometry": [
    [
     6.14679,
     46.195418
    ],
    [
     6.146854,
     46.195432
    ],
    [
     6.146914,
     46.195452
    ]
   ]
  },
  {
   "id": 5070228,
   "tags": {
    "lit": "yes",
    "name": "Rue de la Cité",
    "bicycle": "designated",
    "highway": "pedestrian",
    "surface": "sett",
    "segregated": "no"
   },
   "geometry": [
    [
     6.14362,
     46.203894
    ],
    [
     6.143629,
     46.203875
    ],
    [
     6.143649,
     46.203835
    ]
   ]
  }
 ]
}

Service

The state of the data.

GET /v1/status

Data status. The loaded OpenStreetMap regions, when they were imported and how fresh their data is, and how busy the server is.

Cost: One request.

Request

curl 'https://geo.runningcat.dev/v1/status' \
  -H "Authorization: Bearer YOUR_KEY"

Example answer

{
 "import": {
  "regions": [
   "africa",
   "antarctica",
   "australia-oceania"
  ],
  "version": 31,
  "import_id": 31,
  "imported_at": "2026-11-08T17:55:10.220669+00:00",
  "switched_at": "2026-11-08T17:55:10.220669+00:00",
  "data_timestamp": "2026-11-02T21:21:26+00:00",
  "region_details": [
   {
    "region": "africa",
    "import_id": 29,
    "switched_at": "2026-11-08T17:53:57.686042+00:00",
    "data_timestamp": "2026-11-07T21:21:36+00:00"
   },
   {
    "region": "antarctica",
    "import_id": 30,
    "switched_at": "2026-11-08T17:55:10.220669+00:00",
    "data_timestamp": "2026-11-07T21:21:36+00:00"
   },
   {
    "region": "australia-oceania",
    "import_id": 28,
    "switched_at": "2026-11-07T09:06:14.889892+00:00",
    "data_timestamp": "2026-11-05T21:23:02+00:00"
   }
  ]
 },
 "flyover": {
  "memory": 0,
  "store": 0,
  "db": 0,
  "uncovered": 0,
  "failed": 0,
  "memory_bytes": 0,
  "db_slots": {
   "active": 0,
   "waiting": 0
  }
 },
 "requests": {
  "running": 1,
  "waiting": 0
 }
}

The free plan gives 500 requests a day, for commercial apps too. Create an account with Google or your email and make your keys right away.