Skip to main content

Slippy Map Tile & Quadkey Calculator API

REAL DATA

Convert lat/lng to slippy map tile z/x/y and Bing quadkeys, read tile bounds in degrees or EPSG:3857 metres, and list every tile covering a bounding box.

Authoritative reference data or standards computation

Base URL
/api/map-tiles
Capabilities
5 routes
Last updated
July 30, 2026
Data sources · 3

GET /api/map-tiles/from-point

Exact floor() point query: bounds are half-open, so a coordinate on an internal edge belongs to the tile east and south of it.

Live requestRuns against the public API
No key required
GET/api/map-tiles/from-point?lat=55.6761&lng=12.5683&zoom=14

Request parameters

Latitude in decimal degrees, ±85.05112878 — Bing Maps' published clip constant, the exact projection edge arctan(sinh(π)) = 85.05112877980659° rounded up in the tenth decimal. Latitudes beyond it are a 400: Web Mercator has no tile rows there, and clamping would answer a different question than the one you asked.

Longitude in decimal degrees, -180 to 180 inclusive. lng=180 lands in the easternmost column (2^zoom − 1) and is never normalized to -180 — the same closed-edge rule /api/geohash uses.

Zoom level (OGC tile matrix id), 0–24. Each level doubles the matrix: 2^zoom tiles per side. 24 is this API's documented ceiling because that is what OGC Annex D.1 tabulates for WebMercatorQuad — the standard itself allows an arbitrary number of levels, so it is not a universal limit.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/map-tiles/from-point?lat=55.6761&lng=12.5683&zoom=14"
const res = await fetch("https://randomapi.dev/api/map-tiles/from-point?lat=55.6761&lng=12.5683&zoom=14");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/map-tiles/from-point?lat=55.6761&lng=12.5683&zoom=14").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/map-tiles/from-point?lat=55.6761&lng=12.5683&zoom=14"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 3
lat float required

Latitude in decimal degrees, ±85.05112878 — Bing Maps' published clip constant, the exact projection edge arctan(sinh(π)) = 85.05112877980659° rounded up in the tenth decimal. Latitudes beyond it are a 400: Web Mercator has no tile rows there, and clamping would answer a different question than the one you asked.

allowed: -85.05112878 – 85.05112878
example: lat=55.6761
lng float required

Longitude in decimal degrees, -180 to 180 inclusive. lng=180 lands in the easternmost column (2^zoom − 1) and is never normalized to -180 — the same closed-edge rule /api/geohash uses.

allowed: -180 – 180
example: lng=12.5683
zoom int required

Zoom level (OGC tile matrix id), 0–24. Each level doubles the matrix: 2^zoom tiles per side. 24 is this API's documented ceiling because that is what OGC Annex D.1 tabulates for WebMercatorQuad — the standard itself allows an arbitrary number of levels, so it is not a universal limit.

allowed: 0 – 24
example: zoom=14
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=zoom,x
exclude list

Return all fields except these (comma-separated).

example: exclude=pixelInTile
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 16
zoom integer (0-24)

Tile matrix id / zoom level. The matrix is 2^zoom tiles per side.

example: 14

x integer

Tile column (OGC `tileCol`): 0 at 180° W, increasing east.

example: 8763

y integer

Tile row (OGC `tileRow`) with `cornerOfOrigin: topLeft` — 0 at the north edge, increasing SOUTH. This is the XYZ convention used by Google Maps, OpenStreetMap, Bing, MapLibre and Leaflet.

example: 5128

tmsY integer

The same row in the OSGeo Tile Map Service convention (`bottomLeft` origin): 2^zoom − 1 − y. Always returned, so the flip is never something you have to guess.

example: 11255

quadkey string nullable

Bing Maps quadkey: one base-4 digit per zoom level, most significant first, X bit = +1 and Y bit = +2. A parent's quadkey is always its child's minus the last digit. Null at zoom 0, where the length-equals-zoom rule would give an empty string — null rather than "" so it cannot be mistaken for a missing value. Always built from the topLeft row; TMS has no quadkey.

example: 12021000113011

zxy string

The conventional `{z}/{x}/{y}` path segment, handy as a cache key. This API serves no map imagery — it is a string, not a URL.

example: 14/8763/5128

bounds object

`{ west, south, east, north }` in WGS 84 decimal degrees, as unrounded float64 results of the published inverse formulas. The rectangle is half-open — `[west, east)` by `[north, south)` — closed only at the global edges.

example: {"west":12.54638671875,"south":55.665193184436056,"east":12.568359375,"north":55.67758441108952}

boundsMercator object

`{ minX, minY, maxX, maxY }` in EPSG:3857 metres, from exact grid arithmetic off OGC Annex D.1's `pointOfOrigin` (±20037508.342789244) rather than by projecting `bounds`. Evaluated top-down from the north edge, as OGC §6.1.1 derives it.

example: {"minX":1396657.3808267415,"minY":7492051.764399836,"maxX":1399103.3657318652,"maxY":7494497.749304961}

center object

`{ lat, lng }` — the tile's centre. `lng` is the arithmetic midpoint; `lat` is the inverse projection of the pixel-space midpoint (row + 0.5), which is the true centre in the projection and differs from the mean of `north` and `south` around the tenth decimal.

example: {"lat":55.67138928829547,"lng":12.557373046875}

groundResolution object

`{ atEquator, atCenterLatitude }` — metres per pixel for 256-px tiles, cos(φ)·2πR/(256·2^zoom). The spherical EPSG:3857 approximation: OGC Annex D.1 NOTE 1 warns the tabulated cell sizes are only valid next to the equator in the E-W direction.

example: {"atEquator":9.554628535647032,"atCenterLatitude":5.388222805829274}

scaleDenominator object

`{ atEquator, atCenterLatitude, atCenterLatitude96dpi }`. The first two use OGC §6.1.1's standardized rendering pixel size of 0.28 mm — `atEquator` is the value tabulated in Annex D.1. The third uses Bing's 96-dpi convention (25.4/96 ≈ 0.2646 mm). The two conventions differ by about 5.8 %, which is the most common source of "your scale is wrong" confusion.

example: {"atEquator":34123.67334159654,"atCenterLatitude":19243.652877961697,"atCenterLatitude96dpi":20364.936588961038}

parent object nullable

`{ z, x, y, quadkey }` of the containing tile one level coarser — floor(x/2), floor(y/2). Null at zoom 0; its inner `quadkey` is null when the parent is zoom 0.

example: {"z":13,"x":4381,"y":2564,"quadkey":"1202100011301"}

children object[]

The four tiles one level finer, in quadkey-digit order (0 = NW, 1 = NE, 2 = SW, 3 = SE), each `{ z, x, y, quadkey }`. An empty array at zoom 24, this API's documented ceiling — never null.

example: [{"z":15,"x":17526,"y":10256,"quadkey":"120210001130110"},{"z":15,"x":17527,"y":10256,"quadkey":"120210001130111"},{"z":15,"x":17526,"y":10257,"quadkey":"120210001130112"},{"z":15,"x":17527,"y":10257,"quadkey":"120210001130113"}]

pixelX float

Global pixel X at this zoom for 256-px tiles, fractional and unrounded. Bing's reference sample rounds to the nearest integer pixel and clips to mapSize − 1; this API does neither, because flooring the fraction is what yields the OGC/OSM tile index and rounding first can move a point within half a pixel of an edge into the next tile.

example: 2243583.308231111

pixelY float

Global pixel Y at this zoom, same rules: fractional, unrounded, unclipped.

example: 1312798.6718793944

pixelInTile object

`{ x, y }` — the fractional offset inside the tile, normally 0 ≤ v < 256. At the closed edges of the domain, where the tile index is clamped, it can land exactly on 256 (lng = 180) or a hair outside the range (a latitude at the accepted limit, by about 2.6e-5 px at zoom 14) — the sub-millimetre artefact described on this page, reported rather than hidden.

example: {"x":255.30823111115023,"y":30.67187939444557}

Documented examples

Build-generated requests and complete responses
3
Copenhagen at zoom 14
GET /api/map-tiles/from-point?lat=55.6761&lng=12.5683&zoom=14
{
  "data": {
    "zoom": 14,
    "x": 8763,
    "y": 5128,
    "tmsY": 11255,
    "quadkey": "12021000113011",
    "zxy": "14/8763/5128",
    "bounds": {
      "west": 12.54638671875,
      "south": 55.665193184436056,
      "east": 12.568359375,
      "north": 55.67758441108952
    },
    "boundsMercator": {
      "minX": 1396657.3808267415,
      "minY": 7492051.764399836,
      "maxX": 1399103.3657318652,
      "maxY": 7494497.749304961
    },
    "center": {
      "lat": 55.67138928829547,
      "lng": 12.557373046875
    },
    "groundResolution": {
      "atEquator": 9.554628535647032,
      "atCenterLatitude": 5.388222805829274
    },
    "scaleDenominator": {
      "atEquator": 34123.67334159654,
      "atCenterLatitude": 19243.652877961697,
      "atCenterLatitude96dpi": 20364.936588961038
    },
    "parent": {
      "z": 13,
      "x": 4381,
      "y": 2564,
      "quadkey": "1202100011301"
    },
    "children": [
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Zoom 0 is one tile for the whole world
GET /api/map-tiles/from-point?lat=0&lng=0&zoom=0
{
  "data": {
    "zoom": 0,
    "x": 0,
    "y": 0,
    "tmsY": 0,
    "quadkey": null,
    "zxy": "0/0/0",
    "bounds": {
      "west": -180,
      "south": -85.05112877980659,
      "east": 180,
      "north": 85.05112877980659
    },
    "boundsMercator": {
      "minX": -20037508.342789244,
      "minY": -20037508.342789244,
      "maxX": 20037508.342789244,
      "maxY": 20037508.342789244
    },
    "center": {
      "lat": 0,
      "lng": 0
    },
    "groundResolution": {
      "atEquator": 156543.03392804097,
      "atCenterLatitude": 156543.03392804097
    },
    "scaleDenominator": {
      "atEquator": 559082264.0287178,
      "atCenterLatitude": 559082264.0287178,
      "atCenterLatitude96dpi": 591658710.9091312
    },
    "parent": null,
    "children": [
      {
        "z": 1,
        "x": 0,
        "y": 0,
        "quadkey": "0""generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
The antimeridian edge stays in the last column
GET /api/map-tiles/from-point?lat=-33.8688&lng=180&zoom=8
{
  "data": {
    "zoom": 8,
    "x": 255,
    "y": 153,
    "tmsY": 102,
    "quadkey": "31133113",
    "zxy": "8/255/153",
    "bounds": {
      "west": 178.59375,
      "south": -34.30714385628804,
      "east": 180,
      "north": -33.13755119234615
    },
    "boundsMercator": {
      "minX": 19880965.3088612,
      "minY": -4070118.8821290657,
      "maxX": 20037508.342789244,
      "maxY": -3913575.8482010253
    },
    "center": {
      "lat": -33.72433966174759,
      "lng": 179.296875
    },
    "groundResolution": {
      "atEquator": 611.49622628141,
      "atCenterLatitude": 508.5926293833817
    },
    "scaleDenominator": {
      "atEquator": 2183915.0938621787,
      "atCenterLatitude": 1816402.2477977918,
      "atCenterLatitude96dpi": 1922239.8590867969
    },
    "parent": {
      "z": 7,
      "x": 127,
      "y": 76,
      "quadkey": "3113311"
    },
    "children": [
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}

GET /api/map-tiles/tile

Live requestRuns against the public API
No key required
GET/api/map-tiles/tile?z=14&x=8763&y=5128

Request parameters

Zoom level (OGC tile matrix id), 0–24. The matrix is 2^z tiles per side, which is what bounds 'x' and 'y' at this level.

Tile column, 0 at 180° W and increasing east. 16777215 is the absolute ceiling (2^24 − 1); the per-zoom bound 0..2^z − 1 is enforced against the 'z' you sent and a value past it is a 400 naming the matrix size.

Tile row, read according to 'scheme'. The response always reports both the topLeft row and tmsY, so the answer is unambiguous whichever convention you sent.

How to read the 'y' you supplied: xyz = OGC cornerOfOrigin topLeft (Google, OpenStreetMap, Bing, Leaflet, MapLibre), tms = OSGeo Tile Map Service, bottomLeft. 'x' is identical in both schemes.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/map-tiles/tile?z=14&x=8763&y=5128"
const res = await fetch("https://randomapi.dev/api/map-tiles/tile?z=14&x=8763&y=5128");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/map-tiles/tile?z=14&x=8763&y=5128").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/map-tiles/tile?z=14&x=8763&y=5128"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 4
z int required

Zoom level (OGC tile matrix id), 0–24. The matrix is 2^z tiles per side, which is what bounds 'x' and 'y' at this level.

allowed: 0 – 24
example: z=14
x int required

Tile column, 0 at 180° W and increasing east. 16777215 is the absolute ceiling (2^24 − 1); the per-zoom bound 0..2^z − 1 is enforced against the 'z' you sent and a value past it is a 400 naming the matrix size.

allowed: 0 – 16777215
example: x=8763
y int required

Tile row, read according to 'scheme'. The response always reports both the topLeft row and tmsY, so the answer is unambiguous whichever convention you sent.

allowed: 0 – 16777215
example: y=5128
scheme enum

How to read the 'y' you supplied: xyz = OGC cornerOfOrigin topLeft (Google, OpenStreetMap, Bing, Leaflet, MapLibre), tms = OSGeo Tile Map Service, bottomLeft. 'x' is identical in both schemes.

default: xyz
allowed: xyz | tms
example: scheme=xyz
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=zoom,x
exclude list

Return all fields except these (comma-separated).

example: exclude=children
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 13
zoom integer (0-24)

Tile matrix id / zoom level. The matrix is 2^zoom tiles per side.

example: 14

x integer

Tile column (OGC `tileCol`): 0 at 180° W, increasing east.

example: 8763

y integer

Tile row (OGC `tileRow`) with `cornerOfOrigin: topLeft` — 0 at the north edge, increasing SOUTH. This is the XYZ convention used by Google Maps, OpenStreetMap, Bing, MapLibre and Leaflet.

example: 5128

tmsY integer

The same row in the OSGeo Tile Map Service convention (`bottomLeft` origin): 2^zoom − 1 − y. Always returned, so the flip is never something you have to guess.

example: 11255

quadkey string nullable

Bing Maps quadkey: one base-4 digit per zoom level, most significant first, X bit = +1 and Y bit = +2. A parent's quadkey is always its child's minus the last digit. Null at zoom 0, where the length-equals-zoom rule would give an empty string — null rather than "" so it cannot be mistaken for a missing value. Always built from the topLeft row; TMS has no quadkey.

example: 12021000113011

zxy string

The conventional `{z}/{x}/{y}` path segment, handy as a cache key. This API serves no map imagery — it is a string, not a URL.

example: 14/8763/5128

bounds object

`{ west, south, east, north }` in WGS 84 decimal degrees, as unrounded float64 results of the published inverse formulas. The rectangle is half-open — `[west, east)` by `[north, south)` — closed only at the global edges.

example: {"west":12.54638671875,"south":55.665193184436056,"east":12.568359375,"north":55.67758441108952}

boundsMercator object

`{ minX, minY, maxX, maxY }` in EPSG:3857 metres, from exact grid arithmetic off OGC Annex D.1's `pointOfOrigin` (±20037508.342789244) rather than by projecting `bounds`. Evaluated top-down from the north edge, as OGC §6.1.1 derives it.

example: {"minX":1396657.3808267415,"minY":7492051.764399836,"maxX":1399103.3657318652,"maxY":7494497.749304961}

center object

`{ lat, lng }` — the tile's centre. `lng` is the arithmetic midpoint; `lat` is the inverse projection of the pixel-space midpoint (row + 0.5), which is the true centre in the projection and differs from the mean of `north` and `south` around the tenth decimal.

example: {"lat":55.67138928829547,"lng":12.557373046875}

groundResolution object

`{ atEquator, atCenterLatitude }` — metres per pixel for 256-px tiles, cos(φ)·2πR/(256·2^zoom). The spherical EPSG:3857 approximation: OGC Annex D.1 NOTE 1 warns the tabulated cell sizes are only valid next to the equator in the E-W direction.

example: {"atEquator":9.554628535647032,"atCenterLatitude":5.388222805829274}

scaleDenominator object

`{ atEquator, atCenterLatitude, atCenterLatitude96dpi }`. The first two use OGC §6.1.1's standardized rendering pixel size of 0.28 mm — `atEquator` is the value tabulated in Annex D.1. The third uses Bing's 96-dpi convention (25.4/96 ≈ 0.2646 mm). The two conventions differ by about 5.8 %, which is the most common source of "your scale is wrong" confusion.

example: {"atEquator":34123.67334159654,"atCenterLatitude":19243.652877961697,"atCenterLatitude96dpi":20364.936588961038}

parent object nullable

`{ z, x, y, quadkey }` of the containing tile one level coarser — floor(x/2), floor(y/2). Null at zoom 0; its inner `quadkey` is null when the parent is zoom 0.

example: {"z":13,"x":4381,"y":2564,"quadkey":"1202100011301"}

children object[]

The four tiles one level finer, in quadkey-digit order (0 = NW, 1 = NE, 2 = SW, 3 = SE), each `{ z, x, y, quadkey }`. An empty array at zoom 24, this API's documented ceiling — never null.

example: [{"z":15,"x":17526,"y":10256,"quadkey":"120210001130110"},{"z":15,"x":17527,"y":10256,"quadkey":"120210001130111"},{"z":15,"x":17526,"y":10257,"quadkey":"120210001130112"},{"z":15,"x":17527,"y":10257,"quadkey":"120210001130113"}]

Documented examples

Build-generated requests and complete responses
4
Central Copenhagen at zoom 14
GET /api/map-tiles/tile?z=14&x=8763&y=5128
{
  "data": {
    "zoom": 14,
    "x": 8763,
    "y": 5128,
    "tmsY": 11255,
    "quadkey": "12021000113011",
    "zxy": "14/8763/5128",
    "bounds": {
      "west": 12.54638671875,
      "south": 55.665193184436056,
      "east": 12.568359375,
      "north": 55.67758441108952
    },
    "boundsMercator": {
      "minX": 1396657.3808267415,
      "minY": 7492051.764399836,
      "maxX": 1399103.3657318652,
      "maxY": 7494497.749304961
    },
    "center": {
      "lat": 55.67138928829547,
      "lng": 12.557373046875
    },
    "groundResolution": {
      "atEquator": 9.554628535647032,
      "atCenterLatitude": 5.388222805829274
    },
    "scaleDenominator": {
      "atEquator": 34123.67334159654,
      "atCenterLatitude": 19243.652877961697,
      "atCenterLatitude96dpi": 20364.936588961038
    },
    "parent": {
      "z": 13,
      "x": 4381,
      "y": 2564,
      "quadkey": "1202100011301"
    },
    "children": [
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Microsoft's worked example: tile (3,5) at level 3 is quadkey 213
GET /api/map-tiles/tile?z=3&x=3&y=5
{
  "data": {
    "zoom": 3,
    "x": 3,
    "y": 5,
    "tmsY": 2,
    "quadkey": "213",
    "zxy": "3/3/5",
    "bounds": {
      "west": -45,
      "south": -66.51326044311186,
      "east": 0,
      "north": -40.979898069620134
    },
    "boundsMercator": {
      "minX": -5009377.085697312,
      "minY": -10018754.17139462,
      "maxX": 0,
      "maxY": -5009377.085697312
    },
    "center": {
      "lat": -55.77657301866768,
      "lng": -22.5
    },
    "groundResolution": {
      "atEquator": 19567.87924100512,
      "atCenterLatitude": 11005.396109490208
    },
    "scaleDenominator": {
      "atEquator": 69885283.00358972,
      "atCenterLatitude": 39304986.105322175,
      "atCenterLatitude96dpi": 41595197.89413623
    },
    "parent": {
      "z": 2,
      "x": 1,
      "y": 2,
      "quadkey": "21"
    },
    "children": [
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
The same tile addressed with a TMS row
GET /api/map-tiles/tile?z=14&x=8763&y=11255&scheme=tms
{
  "data": {
    "zoom": 14,
    "x": 8763,
    "y": 5128,
    "tmsY": 11255,
    "quadkey": "12021000113011",
    "zxy": "14/8763/5128",
    "bounds": {
      "west": 12.54638671875,
      "south": 55.665193184436056,
      "east": 12.568359375,
      "north": 55.67758441108952
    },
    "boundsMercator": {
      "minX": 1396657.3808267415,
      "minY": 7492051.764399836,
      "maxX": 1399103.3657318652,
      "maxY": 7494497.749304961
    },
    "center": {
      "lat": 55.67138928829547,
      "lng": 12.557373046875
    },
    "groundResolution": {
      "atEquator": 9.554628535647032,
      "atCenterLatitude": 5.388222805829274
    },
    "scaleDenominator": {
      "atEquator": 34123.67334159654,
      "atCenterLatitude": 19243.652877961697,
      "atCenterLatitude96dpi": 20364.936588961038
    },
    "parent": {
      "z": 13,
      "x": 4381,
      "y": 2564,
      "quadkey": "1202100011301"
    },
    "children": [
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Zoom 0 covers the whole projected world and has no quadkey
GET /api/map-tiles/tile?z=0&x=0&y=0
{
  "data": {
    "zoom": 0,
    "x": 0,
    "y": 0,
    "tmsY": 0,
    "quadkey": null,
    "zxy": "0/0/0",
    "bounds": {
      "west": -180,
      "south": -85.05112877980659,
      "east": 180,
      "north": 85.05112877980659
    },
    "boundsMercator": {
      "minX": -20037508.342789244,
      "minY": -20037508.342789244,
      "maxX": 20037508.342789244,
      "maxY": 20037508.342789244
    },
    "center": {
      "lat": 0,
      "lng": 0
    },
    "groundResolution": {
      "atEquator": 156543.03392804097,
      "atCenterLatitude": 156543.03392804097
    },
    "scaleDenominator": {
      "atEquator": 559082264.0287178,
      "atCenterLatitude": 559082264.0287178,
      "atCenterLatitude96dpi": 591658710.9091312
    },
    "parent": null,
    "children": [
      {
        "z": 1,
        "x": 0,
        "y": 0,
        "quadkey": "0""generatedAt": "2026-07-30T15:14:08.000Z"
  }
}

GET /api/map-tiles/quadkey

The quadkey's length is its zoom level, so one string carries the whole address. Nothing is coerced: a non-base-4 character or a stray space is a 400 naming the character and its position.

Live requestRuns against the public API
No key required
GET/api/map-tiles/quadkey?value=12021000113011

Request parameters

Bing Maps quadkey: 1–24 characters from the digits 0, 1, 2 and 3 (0 = NW, 1 = NE, 2 = SW, 3 = SE). Its length is the zoom level, so 24 characters is the ceiling. Whitespace is not stripped and invalid characters are a 400, never repaired.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/map-tiles/quadkey?value=12021000113011"
const res = await fetch("https://randomapi.dev/api/map-tiles/quadkey?value=12021000113011");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/map-tiles/quadkey?value=12021000113011").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/map-tiles/quadkey?value=12021000113011"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 1
value string required

Bing Maps quadkey: 1–24 characters from the digits 0, 1, 2 and 3 (0 = NW, 1 = NE, 2 = SW, 3 = SE). Its length is the zoom level, so 24 characters is the ceiling. Whitespace is not stripped and invalid characters are a 400, never repaired.

allowed: 1 – 24
example: value=12021000113011
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=zoom,x
exclude list

Return all fields except these (comma-separated).

example: exclude=children
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 13
zoom integer (0-24)

Tile matrix id / zoom level. The matrix is 2^zoom tiles per side.

example: 14

x integer

Tile column (OGC `tileCol`): 0 at 180° W, increasing east.

example: 8763

y integer

Tile row (OGC `tileRow`) with `cornerOfOrigin: topLeft` — 0 at the north edge, increasing SOUTH. This is the XYZ convention used by Google Maps, OpenStreetMap, Bing, MapLibre and Leaflet.

example: 5128

tmsY integer

The same row in the OSGeo Tile Map Service convention (`bottomLeft` origin): 2^zoom − 1 − y. Always returned, so the flip is never something you have to guess.

example: 11255

quadkey string

Bing Maps quadkey, echoed exactly as validated: one base-4 digit per zoom level, most significant first, X bit = +1 and Y bit = +2. A parent's quadkey is always its child's minus the last digit.

example: 12021000113011

zxy string

The conventional `{z}/{x}/{y}` path segment, handy as a cache key. This API serves no map imagery — it is a string, not a URL.

example: 14/8763/5128

bounds object

`{ west, south, east, north }` in WGS 84 decimal degrees, as unrounded float64 results of the published inverse formulas. The rectangle is half-open — `[west, east)` by `[north, south)` — closed only at the global edges.

example: {"west":12.54638671875,"south":55.665193184436056,"east":12.568359375,"north":55.67758441108952}

boundsMercator object

`{ minX, minY, maxX, maxY }` in EPSG:3857 metres, from exact grid arithmetic off OGC Annex D.1's `pointOfOrigin` (±20037508.342789244) rather than by projecting `bounds`. Evaluated top-down from the north edge, as OGC §6.1.1 derives it.

example: {"minX":1396657.3808267415,"minY":7492051.764399836,"maxX":1399103.3657318652,"maxY":7494497.749304961}

center object

`{ lat, lng }` — the tile's centre. `lng` is the arithmetic midpoint; `lat` is the inverse projection of the pixel-space midpoint (row + 0.5), which is the true centre in the projection and differs from the mean of `north` and `south` around the tenth decimal.

example: {"lat":55.67138928829547,"lng":12.557373046875}

groundResolution object

`{ atEquator, atCenterLatitude }` — metres per pixel for 256-px tiles, cos(φ)·2πR/(256·2^zoom). The spherical EPSG:3857 approximation: OGC Annex D.1 NOTE 1 warns the tabulated cell sizes are only valid next to the equator in the E-W direction.

example: {"atEquator":9.554628535647032,"atCenterLatitude":5.388222805829274}

scaleDenominator object

`{ atEquator, atCenterLatitude, atCenterLatitude96dpi }`. The first two use OGC §6.1.1's standardized rendering pixel size of 0.28 mm — `atEquator` is the value tabulated in Annex D.1. The third uses Bing's 96-dpi convention (25.4/96 ≈ 0.2646 mm). The two conventions differ by about 5.8 %, which is the most common source of "your scale is wrong" confusion.

example: {"atEquator":34123.67334159654,"atCenterLatitude":19243.652877961697,"atCenterLatitude96dpi":20364.936588961038}

parent object nullable

`{ z, x, y, quadkey }` of the containing tile one level coarser — floor(x/2), floor(y/2). Null at zoom 0; its inner `quadkey` is null when the parent is zoom 0.

example: {"z":13,"x":4381,"y":2564,"quadkey":"1202100011301"}

children object[]

The four tiles one level finer, in quadkey-digit order (0 = NW, 1 = NE, 2 = SW, 3 = SE), each `{ z, x, y, quadkey }`. An empty array at zoom 24, this API's documented ceiling — never null.

example: [{"z":15,"x":17526,"y":10256,"quadkey":"120210001130110"},{"z":15,"x":17527,"y":10256,"quadkey":"120210001130111"},{"z":15,"x":17526,"y":10257,"quadkey":"120210001130112"},{"z":15,"x":17527,"y":10257,"quadkey":"120210001130113"}]

Documented examples

Build-generated requests and complete responses
3
Decode a zoom-14 Copenhagen quadkey
GET /api/map-tiles/quadkey?value=12021000113011
{
  "data": {
    "zoom": 14,
    "x": 8763,
    "y": 5128,
    "tmsY": 11255,
    "quadkey": "12021000113011",
    "zxy": "14/8763/5128",
    "bounds": {
      "west": 12.54638671875,
      "south": 55.665193184436056,
      "east": 12.568359375,
      "north": 55.67758441108952
    },
    "boundsMercator": {
      "minX": 1396657.3808267415,
      "minY": 7492051.764399836,
      "maxX": 1399103.3657318652,
      "maxY": 7494497.749304961
    },
    "center": {
      "lat": 55.67138928829547,
      "lng": 12.557373046875
    },
    "groundResolution": {
      "atEquator": 9.554628535647032,
      "atCenterLatitude": 5.388222805829274
    },
    "scaleDenominator": {
      "atEquator": 34123.67334159654,
      "atCenterLatitude": 19243.652877961697,
      "atCenterLatitude96dpi": 20364.936588961038
    },
    "parent": {
      "z": 13,
      "x": 4381,
      "y": 2564,
      "quadkey": "1202100011301"
    },
    "children": [
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Microsoft's example quadkey 213
GET /api/map-tiles/quadkey?value=213
{
  "data": {
    "zoom": 3,
    "x": 3,
    "y": 5,
    "tmsY": 2,
    "quadkey": "213",
    "zxy": "3/3/5",
    "bounds": {
      "west": -45,
      "south": -66.51326044311186,
      "east": 0,
      "north": -40.979898069620134
    },
    "boundsMercator": {
      "minX": -5009377.085697312,
      "minY": -10018754.17139462,
      "maxX": 0,
      "maxY": -5009377.085697312
    },
    "center": {
      "lat": -55.77657301866768,
      "lng": -22.5
    },
    "groundResolution": {
      "atEquator": 19567.87924100512,
      "atCenterLatitude": 11005.396109490208
    },
    "scaleDenominator": {
      "atEquator": 69885283.00358972,
      "atCenterLatitude": 39304986.105322175,
      "atCenterLatitude96dpi": 41595197.89413623
    },
    "parent": {
      "z": 2,
      "x": 1,
      "y": 2,
      "quadkey": "21"
    },
    "children": [
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
A zoom-10 quadkey over County Galway, Ireland
GET /api/map-tiles/quadkey?value=0313102310
{
  "data": {
    "zoom": 10,
    "x": 486,
    "y": 332,
    "tmsY": 691,
    "quadkey": "0313102310",
    "zxy": "10/486/332",
    "bounds": {
      "west": -9.140625,
      "south": 53.120405283106564,
      "east": -8.7890625,
      "north": 53.33087298301705
    },
    "boundsMercator": {
      "minX": -1017529.7205322646,
      "minY": 7005300.768279834,
      "maxX": -978393.9620502554,
      "maxY": 7044436.526761843
    },
    "center": {
      "lat": 53.2257684357902,
      "lng": -8.96484375
    },
    "groundResolution": {
      "atEquator": 152.8740565703525,
      "atCenterLatitude": 91.52010458445703
    },
    "scaleDenominator": {
      "atEquator": 545978.7734655447,
      "atCenterLatitude": 326857.5163730609,
      "atCenterLatitude96dpi": 345902.757484562
    },
    "parent": {
      "z": 9,
      "x": 243,
      "y": 166,
      "quadkey": "031310231"
    },
    "children": [
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}

GET /api/map-tiles/cover

Implements OGC 17-083r4 Annex I.1. The count is always exact; the enumerated list is capped by maxTiles and the cap refuses rather than truncating.

Live requestRuns against the public API
No key required
GET/api/map-tiles/cover?minLat=55.6&minLng=12.4&maxLat=55.8&maxLng=12.7&zoom=12

Request parameters

South edge of the box in decimal degrees, ±85.05112878. Must be ≤ 'maxLat'.

West edge of the box in decimal degrees, -180 to 180. Must be ≤ 'maxLng': the tile matrix does not wrap, so an antimeridian-crossing box is a 400 asking you to split it.

North edge of the box in decimal degrees, ±85.05112878. Must be ≥ 'minLat'.

East edge of the box in decimal degrees, -180 to 180. Must be ≥ 'minLng'.

Zoom level (OGC tile matrix id), 0–24. Each level doubles the matrix: 2^zoom tiles per side. 24 is this API's documented ceiling because that is what OGC Annex D.1 tabulates for WebMercatorQuad — the standard itself allows an arbitrary number of levels, so it is not a universal limit.

Whether to enumerate the covering tiles. Set false to get 'tileRange' and the exact 'tileCount' for any box at any zoom with no cap at all — the escape hatch for "how many tiles would this be?".

Ceiling on the enumerated list, 256 by default and 1024 at most. A box needing more returns 400 with the real count — results are never truncated. It does not affect 'tileCount', and 'includeTiles=false' ignores it entirely.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/map-tiles/cover?minLat=55.6&minLng=12.4&maxLat=55.8&maxLng=12.7&zoom=12"
const res = await fetch("https://randomapi.dev/api/map-tiles/cover?minLat=55.6&minLng=12.4&maxLat=55.8&maxLng=12.7&zoom=12");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/map-tiles/cover?minLat=55.6&minLng=12.4&maxLat=55.8&maxLng=12.7&zoom=12").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/map-tiles/cover?minLat=55.6&minLng=12.4&maxLat=55.8&maxLng=12.7&zoom=12"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 7
minLat float required

South edge of the box in decimal degrees, ±85.05112878. Must be ≤ 'maxLat'.

allowed: -85.05112878 – 85.05112878
example: minLat=55.6
minLng float required

West edge of the box in decimal degrees, -180 to 180. Must be ≤ 'maxLng': the tile matrix does not wrap, so an antimeridian-crossing box is a 400 asking you to split it.

allowed: -180 – 180
example: minLng=12.4
maxLat float required

North edge of the box in decimal degrees, ±85.05112878. Must be ≥ 'minLat'.

allowed: -85.05112878 – 85.05112878
example: maxLat=55.8
maxLng float required

East edge of the box in decimal degrees, -180 to 180. Must be ≥ 'minLng'.

allowed: -180 – 180
example: maxLng=12.7
zoom int required

Zoom level (OGC tile matrix id), 0–24. Each level doubles the matrix: 2^zoom tiles per side. 24 is this API's documented ceiling because that is what OGC Annex D.1 tabulates for WebMercatorQuad — the standard itself allows an arbitrary number of levels, so it is not a universal limit.

allowed: 0 – 24
example: zoom=12
includeTiles boolean

Whether to enumerate the covering tiles. Set false to get 'tileRange' and the exact 'tileCount' for any box at any zoom with no cap at all — the escape hatch for "how many tiles would this be?".

default: true
example: includeTiles=true
maxTiles int

Ceiling on the enumerated list, 256 by default and 1024 at most. A box needing more returns 400 with the real count — results are never truncated. It does not affect 'tileCount', and 'includeTiles=false' ignores it entirely.

default: 256
allowed: 1 – 1024
example: maxTiles=1024
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=zoom,bbox
exclude list

Return all fields except these (comma-separated).

example: exclude=tiles
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 8
zoom integer (0-24)

Zoom level the range was computed at.

example: 12

bbox object

`{ west, south, east, north }` — the requested box echoed in OGC/GeoJSON axis order, so it reads the same way as in any GIS tool.

example: {"west":12.4,"south":55.6,"east":12.7,"north":55.8}

tileRange object

`{ minX, minY, maxX, maxY }`, all inclusive and clamped at both ends into 0..2^zoom − 1, from OGC Annex I.1's epsilon-guarded algorithm. An edge within 1e-6 of a tile span of a boundary is treated as lying on that boundary — about 40 m at zoom 0 and 2.4 mm at zoom 14 — so a point inside that band can be reported by /from-point in the adjacent tile.

example: {"minX":2189,"minY":1279,"maxX":2192,"maxY":1283}

columns integer

maxX − minX + 1.

example: 4

rows integer

maxY − minY + 1.

example: 5

tileCount integer

columns × rows — exact at every supported zoom, with or without the list. The worst case, the whole world at zoom 24, is 2^48 = 281474976710656, well inside the safe integer range.

example: 20

tilesIncluded boolean

True when 'tiles' lists every covering tile. Only ever false when includeTiles=false; the array is never a partial subset.

example: true

tiles object[]

`{ z, x, y, tmsY, quadkey }` per tile, row-major (y outer, x inner). Empty when tilesIncluded is false. Per-tile bounds are not repeated here — call /tile for those.

example: [{"z":12,"x":2189,"y":1279,"tmsY":2816,"quadkey":"120032223323"},{"z":12,"x":2190,"y":1279,"tmsY":2816,"quadkey":"120032223332"}]

Documented examples

Build-generated requests and complete responses
4
Twenty tiles cover Copenhagen at zoom 12
GET /api/map-tiles/cover?minLat=55.6&minLng=12.4&maxLat=55.8&maxLng=12.7&zoom=12
{
  "data": {
    "zoom": 12,
    "bbox": {
      "west": 12.4,
      "south": 55.6,
      "east": 12.7,
      "north": 55.8
    },
    "tileRange": {
      "minX": 2189,
      "minY": 1279,
      "maxX": 2192,
      "maxY": 1283
    },
    "columns": 4,
    "rows": 5,
    "tileCount": 20,
    "tilesIncluded": true,
    "tiles": [
      {
        "z": 12,
        "x": 2189,
        "y": 1279,
        "tmsY": 2816,
        "quadkey": "120032223323"
      },
      {
        "z": 12,
        "x": 2190,
        "y": 1279,
        "tmsY": 2816,
        "quadkey": "120032223332"
      },
      {
        "z": 12,
        "x": 2191,
        "y": 1279,
        "tmsY": 2816,
        "quadkey": "120032223333""generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
The whole projected world at zoom 2 is sixteen tiles
GET /api/map-tiles/cover?minLat=-85.05112878&minLng=-180&maxLat=85.05112878&maxLng=180&zoom=2
{
  "data": {
    "zoom": 2,
    "bbox": {
      "west": -180,
      "south": -85.05112878,
      "east": 180,
      "north": 85.05112878
    },
    "tileRange": {
      "minX": 0,
      "minY": 0,
      "maxX": 3,
      "maxY": 3
    },
    "columns": 4,
    "rows": 4,
    "tileCount": 16,
    "tilesIncluded": true,
    "tiles": [
      {
        "z": 2,
        "x": 0,
        "y": 0,
        "tmsY": 3,
        "quadkey": "00"
      },
      {
        "z": 2,
        "x": 1,
        "y": 0,
        "tmsY": 3,
        "quadkey": "01"
      },
      {
        "z": 2,
        "x": 2,
        "y": 0,
        "tmsY": 3,
        "quadkey": "10""generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Count only, no list — 3,630 tiles at zoom 16
GET /api/map-tiles/cover?minLat=55.6&minLng=12.4&maxLat=55.8&maxLng=12.7&zoom=16&includeTiles=false
{
  "data": {
    "zoom": 16,
    "bbox": {
      "west": 12.4,
      "south": 55.6,
      "east": 12.7,
      "north": 55.8
    },
    "tileRange": {
      "minX": 35025,
      "minY": 20472,
      "maxX": 35079,
      "maxY": 20537
    },
    "columns": 55,
    "rows": 66,
    "tileCount": 3630,
    "tilesIncluded": false,
    "tiles": []
  },
  "meta": {
    "endpoint": "map-tiles",
    "route": "/cover",
    "params": {
      "minLat": 55.6,
      "minLng": 12.4,
      "maxLat": 55.8,
      "maxLng": 12.7,
      "zoom": 16,
      "includeTiles": false,
      "maxTiles": 256
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Raise the cap to enumerate 924 tiles at zoom 15
GET /api/map-tiles/cover?minLat=55.6&minLng=12.4&maxLat=55.8&maxLng=12.7&zoom=15&maxTiles=1024
{
  "data": {
    "zoom": 15,
    "bbox": {
      "west": 12.4,
      "south": 55.6,
      "east": 12.7,
      "north": 55.8
    },
    "tileRange": {
      "minX": 17512,
      "minY": 10236,
      "maxX": 17539,
      "maxY": 10268
    },
    "columns": 28,
    "rows": 33,
    "tileCount": 924,
    "tilesIncluded": true,
    "tiles": [
      {
        "z": 15,
        "x": 17512,
        "y": 10236,
        "tmsY": 22531,
        "quadkey": "120032223323200"
      },
      {
        "z": 15,
        "x": 17513,
        "y": 10236,
        "tmsY": 22531,
        "quadkey": "120032223323201"
      },
      {
        "z": 15,
        "x": 17514,
        "y": 10236,
        "tmsY": 22531,
        "quadkey": "120032223323210""generatedAt": "2026-07-30T15:14:08.000Z"
  }
}

GET /api/map-tiles/mercator

Supply exactly one pair: lat+lng for the forward direction, or x+y for the inverse. Nothing is clamped in either direction.

Live requestRuns against the public API
No key required
GET/api/map-tiles/mercator?lat=55.6761&lng=12.5683

Request parameters

Latitude in decimal degrees for the forward direction, ±85.05112878 (Bing's published clip constant; the exact edge is 85.05112877980659°). Requires 'lng'.

Longitude in decimal degrees for the forward direction, -180 to 180. Requires 'lat'. lng=180 projects to exactly R·π = 20037508.342789244 m.

EPSG:3857 easting in metres for the inverse direction. Requires 'y'. ±20037508.344 is one symmetric limit shared with 'y', deliberately a hair wider than the projection's exact half-extent R·π = 20037508.342789244. The forward easting never exceeds R·π (lng=180 lands exactly on it) — it is the forward northing that overshoots, reaching ±20037508.343038857 at the extreme accepted latitude, and a tighter shared bound would reject this route's own output. The price on this axis, stated rather than hidden: x = ±20037508.344 inverts to longitude ±180.00000001087642°, about 1.1e-8° (1.2 mm) outside the 'lng' bound — the same closed-edge artefact as y and 'lat'.

EPSG:3857 northing in metres for the inverse direction. Requires 'x'. Inverted with atan(sinh(y/R)), the form that returns exactly the projection edge at y = R·π. This is the axis the wider bound exists for: the forward northing reaches ±20037508.343038857 at the extreme accepted latitude. The outermost y inverts to ±85.05112878074486°, about 7.4e-10° (0.08 mm) outside the 'lat' bound — the same closed-edge artefact as x and 'lng'.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/map-tiles/mercator?lat=55.6761&lng=12.5683"
const res = await fetch("https://randomapi.dev/api/map-tiles/mercator?lat=55.6761&lng=12.5683");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/map-tiles/mercator?lat=55.6761&lng=12.5683").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/map-tiles/mercator?lat=55.6761&lng=12.5683"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 4
lat float

Latitude in decimal degrees for the forward direction, ±85.05112878 (Bing's published clip constant; the exact edge is 85.05112877980659°). Requires 'lng'.

allowed: -85.05112878 – 85.05112878
example: lat=55.6761
lng float

Longitude in decimal degrees for the forward direction, -180 to 180. Requires 'lat'. lng=180 projects to exactly R·π = 20037508.342789244 m.

allowed: -180 – 180
example: lng=12.5683
x float

EPSG:3857 easting in metres for the inverse direction. Requires 'y'. ±20037508.344 is one symmetric limit shared with 'y', deliberately a hair wider than the projection's exact half-extent R·π = 20037508.342789244. The forward easting never exceeds R·π (lng=180 lands exactly on it) — it is the forward northing that overshoots, reaching ±20037508.343038857 at the extreme accepted latitude, and a tighter shared bound would reject this route's own output. The price on this axis, stated rather than hidden: x = ±20037508.344 inverts to longitude ±180.00000001087642°, about 1.1e-8° (1.2 mm) outside the 'lng' bound — the same closed-edge artefact as y and 'lat'.

allowed: -20037508.344 – 20037508.344
example: x=1399096.7561371
y float

EPSG:3857 northing in metres for the inverse direction. Requires 'x'. Inverted with atan(sinh(y/R)), the form that returns exactly the projection edge at y = R·π. This is the axis the wider bound exists for: the forward northing reaches ±20037508.343038857 at the extreme accepted latitude. The outermost y inverts to ±85.05112878074486°, about 7.4e-10° (0.08 mm) outside the 'lat' bound — the same closed-edge artefact as x and 'lng'.

allowed: -20037508.344 – 20037508.344
example: y=7494204.690890856
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=direction,latitude
exclude list

Return all fields except these (comma-separated).

example: exclude=northing
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 5
direction string

"forward" when lat/lng were supplied, "inverse" when x/y were.

example: forward

latitude float

WGS 84 latitude in degrees — echoed on forward, computed on inverse.

example: 55.6761

longitude float

WGS 84 longitude in degrees — echoed on forward, computed on inverse.

example: 12.5683

easting float

EPSG:3857 X in metres, R·λ — computed on forward, echoed on inverse.

example: 1399096.7561371003

northing float

EPSG:3857 Y in metres, R·atanh(sin φ) — computed on forward, echoed on inverse. Exactly 0 at the equator, which the tempting ln(tan(π/4 + φ/2)) form is not.

example: 7494204.690890856

Documented examples

Build-generated requests and complete responses
4
Project Copenhagen to EPSG:3857 metres
GET /api/map-tiles/mercator?lat=55.6761&lng=12.5683
{
  "data": {
    "direction": "forward",
    "latitude": 55.6761,
    "longitude": 12.5683,
    "easting": 1399096.7561371003,
    "northing": 7494204.690890856
  },
  "meta": {
    "endpoint": "map-tiles",
    "route": "/mercator",
    "params": {
      "lat": 55.6761,
      "lng": 12.5683
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Invert Web Mercator metres back to WGS 84
GET /api/map-tiles/mercator?x=1399096.7561371&y=7494204.690890856
{
  "data": {
    "direction": "inverse",
    "latitude": 55.6761,
    "longitude": 12.568299999999999,
    "easting": 1399096.7561371,
    "northing": 7494204.690890856
  },
  "meta": {
    "endpoint": "map-tiles",
    "route": "/mercator",
    "params": {
      "x": 1399096.7561371,
      "y": 7494204.690890856
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
The origin is exactly zero, zero
GET /api/map-tiles/mercator?lat=0&lng=0
{
  "data": {
    "direction": "forward",
    "latitude": 0,
    "longitude": 0,
    "easting": 0,
    "northing": 0
  },
  "meta": {
    "endpoint": "map-tiles",
    "route": "/mercator",
    "params": {
      "lat": 0,
      "lng": 0
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
The antimeridian is exactly R·π metres east
GET /api/map-tiles/mercator?lat=0&lng=180
{
  "data": {
    "direction": "forward",
    "latitude": 0,
    "longitude": 180,
    "easting": 20037508.342789244,
    "northing": 0
  },
  "meta": {
    "endpoint": "map-tiles",
    "route": "/mercator",
    "params": {
      "lat": 0,
      "lng": 180
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}

About this API

Coverage & behavior

Every web map you have ever panned is a pyramid of 256-pixel square tiles addressed {z}/{x}/{y}, and the grid is a published standard: WebMercatorQuad, tabulated in OGC 17-083r4 Annex D.1 and used by Google Maps, Bing Maps and OpenStreetMap tiles alike. This endpoint is the arithmetic of that grid — five pure functions, no dataset, no clock, no randomness. The same query returns the same bytes forever.

  • /from-point — a coordinate plus a zoom gives the tile that contains it, the quadkey, and the fractional global pixel position. x = floor(n·(lng + 180)/360) and y = floor(n·(0.5 − atanh(sin φ)/2π)) where n = 2^zoom.
  • /tile — a z/x/y address gives the tile's bounds in degrees and in EPSG:3857 metres, its quadkey, its TMS row, its ground resolution, its scale denominators, its parent and its four children. scheme=tms declares that the y you sent counts up from the south.
  • /quadkey — decode a Bing quadkey to exactly the same record. A quadkey's length is its zoom level.
  • /cover — every tile covering a bounding box at one zoom, with the exact count. It never truncates.
  • /mercator — project a coordinate to EPSG:3857 metres, or invert metres back to WGS 84.

Quadkeys

A quadkey is one base-4 digit per zoom level, most significant first: the X bit contributes 1 and the Y bit contributes 2, so 0 is the north-west child, 1 north-east, 2 south-west, 3 south-east. Microsoft's worked example is tileX = 3 (011₂), tileY = 5 (101₂) at level 3, interleaved to 100111₂ = "213". Two properties make quadkeys worth using: a parent's quadkey is a prefix of its children's (so "2" is the parent of "20""23"), and one string carries the whole address including the zoom. Zoom 0 is the exception — the rule "length equals zoom" would give the empty string, so quadkey is null there rather than "".

XYZ or TMS: which way is y counted?

OGC 17-083r4 §6.2 calls this cornerOfOrigin, and its default is topLeft — Table 8 notes that topLeft is "sometimes known as 'xyz'" and bottomLeft is the one "used by Tile Map Service". Row 0 is the north edge in XYZ (Google, OSM, Bing, MapLibre, Leaflet) and the south edge in TMS (OSGeo TMS, MBTiles). Every record here reports both y (topLeft) and tmsY = 2^zoom − 1 − y, so you never have to guess the flip; x is identical in both.

Zoom levels, resolution and scale

Zoom Matrix Tiles Ground resolution at the equator Scale denominator (0.28 mm)
0 1 × 1 1 156,543.0339 m/px 1 : 559,082,264
1 2 × 2 4 78,271.5170 m/px 1 : 279,541,132
2 4 × 4 16 39,135.7585 m/px 1 : 139,770,566
3 8 × 8 64 19,567.8792 m/px 1 : 69,885,283
4 16 × 16 256 9,783.9396 m/px 1 : 34,942,642
5 32 × 32 1,024 4,891.9698 m/px 1 : 17,471,321
6 64 × 64 4,096 2,445.9849 m/px 1 : 8,735,660
7 128 × 128 16,384 1,222.9925 m/px 1 : 4,367,830
8 256 × 256 65,536 611.4962 m/px 1 : 2,183,915
9 512 × 512 262,144 305.7481 m/px 1 : 1,091,958
10 1,024 × 1,024 1,048,576 152.8741 m/px 1 : 545,979
11 2,048 × 2,048 4,194,304 76.4370 m/px 1 : 272,989
12 4,096 × 4,096 16,777,216 38.2185 m/px 1 : 136,495
13 8,192 × 8,192 67,108,864 19.1093 m/px 1 : 68,247
14 16,384 × 16,384 268,435,456 9.5546 m/px 1 : 34,124
15 32,768 × 32,768 1,073,741,824 4.7773 m/px 1 : 17,062
16 65,536 × 65,536 4,294,967,296 2.3887 m/px 1 : 8,531
17 131,072 × 131,072 17,179,869,184 1.1943 m/px 1 : 4,265
18 262,144 × 262,144 68,719,476,736 59.7164 cm/px 1 : 2,133
19 524,288 × 524,288 274,877,906,944 29.8582 cm/px 1 : 1,066
20 1,048,576 × 1,048,576 1,099,511,627,776 14.9291 cm/px 1 : 533
21 2,097,152 × 2,097,152 4,398,046,511,104 7.4646 cm/px 1 : 267
22 4,194,304 × 4,194,304 17,592,186,044,416 3.7323 cm/px 1 : 133
23 8,388,608 × 8,388,608 70,368,744,177,664 1.8661 cm/px 1 : 67
24 16,777,216 × 16,777,216 281,474,976,710,656 0.9331 cm/px 1 : 33

Two honest caveats on those last two columns. Ground resolution is only valid near the equator in the E-W direction — OGC Annex D.1 NOTE 1 says so explicitly, because Mercator stretches the cell size towards the poles; that is why every record also returns groundResolution.atCenterLatitude, the same figure multiplied by cos φ. And there are two scale conventions in the wild: OGC §6.1.1 defines the scale denominator against a "standardized rendering pixel size" of 0.28 mm (the same value WMS 1.3.0 and WMTS 1.0 use — it was a common display's real pixel pitch in 2005), while Bing Maps computes map scale at 96 dpi, or about 0.2646 mm. The two differ by roughly 5.8 %. Both are returned, labelled, rather than picking one and leaving you to wonder.

Nothing is rounded, and that is deliberate

Every number is the unrounded float64 result of the published formula. Rounding degree bounds would break tile identity: at zoom 24 a column is 2.1e-5° wide, so rounding west to ten decimals can shift it across the edge into the previous column, and the value would no longer re-floor to the tile it came from. It also lets the endpoint check itself against the standard — at zoom 14 groundResolution.atEquator is 9.554628535647032 and scaleDenominator.atEquator is 34123.67334159654, digit for digit the values in Annex D.1. That exact agreement holds for 11 of the table's 25 rows; the other rows differ in the last digit or two because the published table is rounded to 15–16 significant digits, not because the arithmetic differs. The test suite pins all 25 rows to a relative tolerance of 1e-14 (worst observed: 3.6e-15).

The latitude limit is a 400, not a clamp

Web Mercator's square ends at arctan(sinh(π)) = 85.05112877980659°; there are no tile rows beyond it. Bing's reference implementation clips latitude to its published constant ±85.05112878 before projecting. This API does not clip. A latitude outside the accepted range returns 400 naming the value, because silently clamping answers a question you did not ask — the tile for 88° N is not the tile for 85.05° N.

There is one sub-millimetre subtlety, and it is worth stating precisely. Bing's constant 85.05112878 sits about 1.9e-10° outside the exact edge — 0.25 mm of projected northing — so a naive floor() at that input produces row −1. The tile index is therefore clamped into the matrix, and the same applies at lng = 180, whose column fraction is exactly n: it lands in the easternmost column 2^zoom − 1 and is never normalized to −180. That is index clamping inside a boundary sliver at the closed edge of the domain — not latitude clamping. Bounds are half-open, [west, east) by [north, south), so a point exactly on an internal edge belongs to the tile east and south of it: lat=0&lng=0 at zoom 1 is tile 1/1/1, quadkey "3".

/cover: exact counts, and a cap that refuses rather than truncates

/cover implements OGC Annex I.1's algorithm, including its epsilon guard of 1e-6 tile units — which is load-bearing, not a fudge. The exact north edge of row 1 at zoom 2 is 66.51326044311186°, and projecting it back lands on 0.9999999999999998 rows, one unit in the last place short of the boundary; without the epsilon that box picks up a spurious extra row. The cost is a tolerance band: a bbox edge within 1e-6 of a tile span of a boundary is treated as lying on it — about 40 m at zoom 0, 2.4 mm at zoom 14 — so a point inside that band can be reported by /from-point (an exact floor() point query) in the adjacent tile. Two deliberate deviations from the published pseudocode, both so that tileRange and tiles can only ever name tiles that exist. First: for a zero-area box on a boundary the pseudocode inverts the range, so the range is collapsed to the single tile containing the point, which is what /from-point answers for the same coordinate. Second: the pseudocode guards each end of the range on one side only, which assumes no bbox edge can land on the far edge of the matrix — in this closed domain one can, since lng = 180 gives a column fraction of exactly n and lat = -85.05112878 a row fraction a hair above n. Both ends are therefore clamped into 0..2^zoom − 1, the same closed-edge index clamping /from-point applies, so a box on the antimeridian or the south edge reports the easternmost column or southernmost row rather than a nonexistent index.

The enumerated list is capped by maxTiles (default 256, ceiling 1024) and the cap refuses instead of truncating, with the real count in the message. The Copenhagen box minLat=55.6&minLng=12.4&maxLat=55.8&maxLng=12.7 shows both refusals:

  • at zoom=15 it needs 924 tiles, so the request 400s while maxTiles is at its default 256 — and succeeds byte-for-byte identically with &maxTiles=1024;
  • at zoom=16 it needs 3,630, which no permitted maxTiles can satisfy, so that message does not offer the raise. Send includeTiles=false instead: it returns tileRange and the exact tileCount for any box at any zoom with no cap (the whole world at zoom 24 is 281,474,976,710,656 tiles, and the array stays empty).

There is deliberately no single bbox= param. Two orderings are both in live use — GeoJSON/OGC west,south,east,north and this platform's own lat-first minLat,minLng,maxLat,maxLng on /api/coordinates — and for many real boxes each reading is a valid box somewhere else on Earth. 12.4,55.6,12.7,55.8 is central Denmark under one reading and open water in the Arabian Sea off the Horn of Africa under the other; a misread box would return perfectly plausible tiles for the wrong continent, which is exactly the kind of silently-wrong answer this platform refuses to produce. Four named required params cannot be misread. The bbox response field uses OGC/GeoJSON order and says so.

/mercator and its honest closed-edge asymmetries

Forward is easting = R·λ, northing = R·atanh(sin φ) with R = 6378137; inverse is λ = x/(R·π)·180, φ = atan(sinh(y/R)). lat=0&lng=0 returns exactly 0, 0, and lng=180 returns exactly 20037508.342789244 — Annex D.1's pointOfOrigin magnitude.

The x/y bound is ±20037508.344, one symmetric limit a hair wider than R·π, and the reason is measured — but it belongs to the northing axis only. The forward easting never exceeds R·π: lng=180 lands exactly on 20037508.342789244. The forward northing does exceed it, reaching ±20037508.343038857 m at the extreme accepted latitude ±85.05112878, overshooting R·π by 0.25 mm, so a tighter shared bound would 400 this route's own result. Keeping one bound for both axes costs a sliver on each, and both slivers are stated here rather than left for you to find:

  • the inverse of the outermost y is 85.05112878074486°, roughly 7.4e-10° (0.08 mm) outside the lat bound, so a latitude read from the very edge of the y domain and fed straight back in can 400;
  • the inverse of the outermost x is 180.00000001087642°, roughly 1.1e-8° (1.2 mm) outside the lng bound — and outside WGS 84's longitude range — for the same reason, and it likewise 400s if fed back to lng.

Both are sub-millimetre-to-millimetre artefacts of a closed edge, not rounding bugs, and nothing is silently clamped to hide either one. Inside the domain the round trip is stable — every longitude inverts back to itself exactly, and so does every latitude short of the edge. The one exception is the edge itself: lat=85.05112878 projects to 20037508.343038857 and inverts to 85.05112878000003, 2.8e-14° past the lat bound. Same closed edge, smallest face of it.

What this endpoint honestly does not do

  1. It serves no map imagery. No tile images, no tile proxy, no upstream tile server, no URL templates for third-party providers — their terms are theirs to agree with. zxy is a path string for your own cache keys.
  2. Web Mercator is spherical and meant for pictures. The EPSG registry's own remark calls 3857 "Not a recognised geodetic system", notes that it develops ellipsoidal coordinates spherically, and records that against WGS 84 / World Mercator (EPSG:3395) it can be off by 0.7 % in scale and by up to 43 km of northing in the map — about 21 km on the ground. OGC Annex D.1 adds that WebMercatorQuad's use "should be limited to visualization", and its NOTE 4 recommends WorldMercatorWGS84Quad where positioning accuracy matters. Use /api/geodesy for real distances.
  3. WebMercatorQuad only — no WorldMercatorWGS84Quad, WorldCRS84Quad or GNOSISGlobalGrid, no custom tile matrix sets, no tile sizes other than 256 px, no @2x retina variants, no metatiles.
  4. No vector-tile internals — no MVT extents or geometry coordinates, no TileJSON, and no MBTiles translation beyond the documented tmsY flip.
  5. Zoom 0–24 only, because that is the range Annex D.1 tabulates. The standard allows more; this is an API boundary, not a universal one.
  6. No antimeridian-crossing bbox. The matrix does not wrap, so a crossing box is a 400 that tells you to split it in two.
  7. No geocoding and no coordinate parsing. Send decimal degrees; /api/coordinate-formats turns DMS, DDM and Maidenhead text into them.
  8. No neighbour or prefix-cell search/api/geohash owns that kind of reasoning.
  9. No dependency on, or recommendation of, Bing Maps as a service. Microsoft's Bing Maps Tile System document is cited here because it is the canonical published definition of the quadkey addressing scheme — the one Azure Maps and a long tail of tile libraries implement — and of the 96-dpi map-scale convention. The Bing Maps for Enterprise service is retired for free accounts and sunsets for enterprise customers on 2028-06-30; nothing here should be read as advice to use it.

Provenance

Implemented from the formulas in OGC 17-083r4 (a royalty-free OGC member-approved standard: §6.1.1 geometry, §6.2 corner of origin, Annex D.1 WebMercatorQuad, Annex I.1 bbox to tile indices), Microsoft's Bing Maps Tile System document (quadkeys, ground resolution, map scale at 96 dpi), and the EPSG:3857 record in the EPSG Geodetic Parameter Dataset. NOTE 2 of Annex D.1 is worth knowing if you meet old code: 3857 is the official EPSG code, and the unofficial 900913 — GOOGLE spelled with numbers — is still seen in the wild. The page most developers already know, OpenStreetMap's Slippy map tilenames, documents the same arithmetic as a community reference; it is not cited as a standard, and no prose or code was copied from it or from Microsoft's C# sample — the TypeScript here is written from the mathematics.

Use it for

  • Turn stored coordinates into tile keys so a rendering cache can be warmed or invalidated by z/x/y
  • Work out how many tiles an offline map region needs before downloading anything
  • Translate between XYZ rows and the TMS/MBTiles row order without an off-by-one bug
  • Debug a misplaced marker by checking whether a coordinate really falls in the tile you expected
  • Convert EPSG:3857 metres from a GIS export back into WGS 84 degrees

Frequently asked questions

How do I convert latitude and longitude to a map tile X and Y?

With n = 2^zoom, the column is floor(n · (lng + 180) / 360) and the row is floor(n · (0.5 − atanh(sin φ) / 2π)). /api/map-tiles/from-point?lat=55.6761&lng=12.5683&zoom=14 does it for you and also returns the quadkey, the fractional pixel position and the tile's bounds. The floor is what makes bounds half-open: a point exactly on an internal edge belongs to the tile east and south of it.

What is a quadkey and how do I convert a tile to one?

A quadkey is the tile's X and Y bits interleaved and read as base 4, one digit per zoom level — the X bit adds 1 and the Y bit adds 2. Microsoft's own example is tileX 3, tileY 5 at level 3, which interleaves to "213". Because each digit refines the previous one, a parent's quadkey is always a prefix of its children's, and /api/map-tiles/quadkey?value=213 decodes one back to z/x/y.

Why does this API return 400 for latitude 88 instead of clamping to 85.0511?

Because Web Mercator's square ends at arctan(sinh(π)) = 85.05112877980659° and there are no tile rows beyond it, so a clamped answer would be the tile for a different place than the one you asked about. Bing's reference implementation clips latitude to ±85.05112878; this API deliberately does not, and returns 400 naming the value instead.

What is the difference between XYZ and TMS tile Y coordinates?

They count rows from opposite edges. OGC 17-083r4 calls it cornerOfOrigin: topLeft (row 0 at the north edge) is the XYZ scheme used by Google, OpenStreetMap, Bing, Leaflet and MapLibre, while bottomLeft is the OSGeo Tile Map Service scheme. Every record here returns both y and tmsY = 2^zoom − 1 − y, and scheme=tms on /tile declares which convention the y you sent uses.

How many tiles do I need to cover a bounding box at zoom 16?

/api/map-tiles/cover returns the exact count and refuses to truncate. The Copenhagen box 55.6/12.4 to 55.8/12.7 is 20 tiles at zoom 12, 924 at zoom 15 and 3,630 at zoom 16. The enumerated list is capped by maxTiles (default 256, raisable to 1024) and a box over the cap returns 400 with the real count; includeTiles=false returns the range and the count for any box at any zoom with no cap at all.

Does this API serve map tile images?

No. It is arithmetic only — it renders no imagery, proxies no tile server and has no image route. The zxy field is a {z}/{x}/{y} string for your own cache keys, not a URL. Fetch pixels from a tile provider under that provider's own terms.

Standards & references