Using Township Canada API with OpenLayers
Display Canadian survey grids as vector tiles and search legal land descriptions in your OpenLayers application using the Township Canada API.
Build a web map that displays Canadian survey grids (DLS and NTS), searches legal land descriptions, and identifies grid cells on click — all using OpenLayers and the Township Canada API.
OpenLayers has native support for the Mapbox Vector Tile (MVT) format through ol/source/VectorTile and ol/format/MVT. That means Township Canada's survey grid tiles load directly without any plugins or format conversion.
What you'll build
By the end of this guide, you'll have a web page that:
- Displays DLS township, section, and LSD grid boundaries on an OpenLayers map
- Changes grid line weight based on zoom level (townships thick, LSDs thin)
- Searches a legal land description and pans to the result with a highlighted parcel boundary
- Shows a popup with the grid cell's descriptor when you click on the map
- Includes autocomplete suggestions as you type
Prerequisites
- A Township Canada API key — subscribe to the Maps API and Search API from the Developer Portal, then create an API key from your account settings
- Basic knowledge of HTML and JavaScript
Step 1: Set up the project
Create an index.html file with OpenLayers loaded from CDN. This guide uses OpenLayers v9.x, which is the current stable release.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Township Canada + OpenLayers</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/ol@9/ol.css"
/>
<script src="https://cdn.jsdelivr.net/npm/ol@9/dist/ol.js"></script>
<style>
html,
body {
margin: 0;
padding: 0;
height: 100%;
}
#map {
width: 100%;
height: 100vh;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
const TC_API_KEY = "YOUR_TOWNSHIP_CANADA_API_KEY";
// Base OpenStreetMap layer
const osm = new ol.layer.Tile({
source: new ol.source.OSM()
});
const map = new ol.Map({
target: "map",
layers: [osm],
view: new ol.View({
center: ol.proj.fromLonLat([-114, 51]), // Calgary, AB
zoom: 9
})
});
</script>
</body>
</html>
Open this file in a browser. You should see an OpenStreetMap base map centred on Calgary.
Step 2: Add Township survey grid layers
Township Canada serves survey grid boundaries as vector tiles. Each province and grid level is a separate URL path, and the tile format is MVT (Mapbox Vector Tile). OpenLayers reads MVT natively via ol/format/MVT.
Tile URL pattern
https://maps.townshipcanada.com/{province}/{layer}/{z}/{x}/{y}.mvt?api_key=YOUR_API_KEY
Available grid layers
DLS grid (provinces: ab, sk, mb, bc):
| URL layer | source-layer | text-field | Source zoom | Layer zoom |
|---|---|---|---|---|
twp | {prov}_twp | — | 0–14 | 6–12 |
twp-label | {prov}_twp_label | {descriptor} | 0–14 | 10–12 |
sec | {prov}_sec | — | 9–14 | 12–14 |
sec-label | {prov}_sec_label | {section} | 9–14 | 12–14 |
qtr | {prov}_qtr | — | 9–14 | 12–14 |
qtr-label | {prov}_qtr_label | {descriptor} | 9–14 | 12–14 |
lsd | {prov}_lsd | — | 9–14 | 14–20 |
lsd-label | {prov}_lsd_label | {lsd} | 9–14 | 14–20 |
MB River Lots (province: mb):
| URL layer | source-layer | text-field | Source zoom | Layer zoom |
|---|---|---|---|---|
river-lots | mb_river_lots | — | 0–14 | 12–20 |
river-lots-label | mb_river_lots_label | {descriptor} | 0–14 | 12–20 |
NTS grid (province: bc):
| URL layer | source-layer | text-field | Source zoom | Layer zoom |
|---|---|---|---|---|
series | bc_series | — | 0–14 | 0–10 |
series-label | bc_series_label | {descriptor} | 0–14 | 7–10 |
block | bc_block | — | 9–14 | 10–13 |
block-label | bc_block_label | {descriptor} | 9–14 | 10–13 |
unit | bc_unit | — | 9–14 | 13–14 |
unit-label | bc_unit_label | {descriptor} | 9–14 | 13–14 |
qtr-unit | bc_qtr_unit | — | 9–14 | 14–20 |
qtr-unit-label | bc_qtr_unit_label | {descriptor} | 9–14 | 14–20 |
Ontario (province: on):
| URL layer | source-layer | text-field | Source zoom | Layer zoom |
|---|---|---|---|---|
twp | on_twp | — | 0–14 | 6–12 |
twp-label | on_twp_label | {descriptor} | 0–14 | 6–12 |
con | on_con | — | 0–14 | 12–14 |
con-label | on_con_label | {descriptor} | 0–14 | 12–14 |
lot | on_lot | — | 0–14 | 14–20 |
lot-label | on_lot_label | {descriptor} | 0–14 | 14–20 |
Replace {prov} with the province code (ab, sk, mb, bc) in the source-layer column. The URL uses hyphens (e.g. twp-label) while the source-layer inside the tile data uses underscores (e.g. ab_twp_label).
Available provinces: ab, sk, mb, bc, on
Adding DLS grid layers
Add the following inside your <script> block, after the map is created. This adds separate VectorTileLayer instances for township, section, and LSD grids with zoom-dependent styling so each grid level appears at an appropriate zoom.
const TC_TILES = "https://maps.townshipcanada.com";
function makeTileLayer(province, layer, minZoom, maxZoom, lineWidth, lineColor) {
return new ol.layer.VectorTile({
minZoom: minZoom,
maxZoom: maxZoom,
source: new ol.source.VectorTile({
format: new ol.format.MVT(),
url: `${TC_TILES}/${province}/${layer}/{z}/{x}/{y}.mvt?api_key=${TC_API_KEY}`
}),
style: new ol.style.Style({
stroke: new ol.style.Stroke({
color: lineColor,
width: lineWidth
})
})
});
}
// Township grid — visible from zoom 6 to 11
const twpLayer = makeTileLayer("ab", "twp", 6, 11, 1.5, "#2d5a47");
// Section grid — visible from zoom 11 to 13
const secLayer = makeTileLayer("ab", "sec", 11, 13, 1.0, "#4a7c59");
// LSD grid — visible from zoom 13 and above
const lsdLayer = makeTileLayer("ab", "lsd", 13, 20, 0.5, "#6b9e7a");
map.addLayer(twpLayer);
map.addLayer(secLayer);
map.addLayer(lsdLayer);
Zoom in from Alberta. Township borders load first, then sections appear at mid-zoom, and individual LSDs appear when you zoom close. This mirrors the actual DLS survey hierarchy — each level subdivides the one above.
To show grids for other provinces, create additional layers using 'sk', 'mb', 'bc', or 'on' as the province argument. The tile URL structure is identical.
Step 3: Zoom-dependent styling with style functions
The approach above uses a fixed style per layer. A more flexible alternative is a single layer with a style function that reads the current map resolution and adjusts stroke width accordingly. This gives you one layer that smoothly transitions across zoom levels.
// Resolution thresholds (EPSG:3857 metres per pixel)
const RES_TWP = 4000; // ~zoom 7
const RES_SEC = 600; // ~zoom 11
const RES_LSD = 150; // ~zoom 13
function gridStyleFn(feature, resolution) {
let width, color;
if (resolution > RES_TWP) {
width = 2;
color = "#2d5a47";
} else if (resolution > RES_SEC) {
width = 1.5;
color = "#2d5a47";
} else if (resolution > RES_LSD) {
width = 1;
color = "#4a7c59";
} else {
width = 0.5;
color = "#6b9e7a";
}
return new ol.style.Style({
stroke: new ol.style.Stroke({ color, width })
});
}
// Single combined layer using a style function
const gridLayer = new ol.layer.VectorTile({
source: new ol.source.VectorTile({
format: new ol.format.MVT(),
url: `${TC_TILES}/ab/lsd/{z}/{x}/{y}.mvt?api_key=${TC_API_KEY}`
}),
style: gridStyleFn
});
map.addLayer(gridLayer);
The LSD tileset contains all smaller grid divisions, so one tile source covers township, section, and LSD boundaries. The style function then adjusts line thickness based on how zoomed in the user is.
Step 4: Search and pan to a location
Use the Search API to convert a legal land description like NW-25-24-1-W5 to coordinates and move the map to that location.
Search API endpoint
GET https://developer.townshipcanada.com/search/legal-location?location={query}
X-API-Key: YOUR_API_KEY
Response format
The Search API returns a GeoJSON FeatureCollection with two features: a polygon (the parcel boundary) and a point (the centroid):
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon",
"coordinates": [[[-114.0625, 51.5625], ...]]
},
"properties": {
"descriptor": "NW-25-24-1-W5",
"quarter_section": "NW",
"section": 25,
"township": 24,
"range": 1,
"meridian": "W5",
"survey_system": "DLS",
"province": "AB"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-114.03125, 51.53125]
},
"properties": {
"descriptor": "NW-25-24-1-W5",
"shape": "centroid"
}
}
]
}
Search implementation
Add a vector layer to hold the highlighted search result, then fetch from the Search API and add the polygon as a feature:
// Layer to display the highlighted parcel boundary
const searchResultLayer = new ol.layer.Vector({
source: new ol.source.Vector(),
style: new ol.style.Style({
stroke: new ol.style.Stroke({ color: "#2d5a47", width: 2 }),
fill: new ol.style.Fill({ color: "rgba(45, 90, 71, 0.15)" })
})
});
map.addLayer(searchResultLayer);
const TC_API = "https://developer.townshipcanada.com";
const geojsonFormat = new ol.format.GeoJSON();
async function searchAndPanTo(query) {
const response = await fetch(
`${TC_API}/search/legal-location?location=${encodeURIComponent(query)}`,
{ headers: { "X-API-Key": TC_API_KEY } }
);
const data = await response.json();
if (!data.features || data.features.length === 0) {
console.error("No results found for:", query);
return;
}
const centroid = data.features.find((f) => f.properties.shape === "centroid");
const polygon = data.features.find((f) => f.geometry.type === "MultiPolygon");
if (!centroid) return;
// Pan and zoom to the centroid
const [lng, lat] = centroid.geometry.coordinates;
map.getView().animate({
center: ol.proj.fromLonLat([lng, lat]),
zoom: 14,
duration: 1500
});
// Draw the parcel boundary
if (polygon) {
searchResultLayer.getSource().clear();
const feature = geojsonFormat.readFeature(polygon, {
dataProjection: "EPSG:4326",
featureProjection: "EPSG:3857"
});
searchResultLayer.getSource().addFeature(feature);
}
}
// Example: search for a quarter section near Edmonton
searchAndPanTo("NW-25-24-1-W5");
OpenLayers stores coordinates in EPSG:3857 (Web Mercator), but the Search API returns GeoJSON in EPSG:4326 (longitude/latitude). The readFeature call handles the projection conversion.
Step 5: Click-to-identify grid cells
Add a click handler that reads vector tile features at the clicked pixel and shows a popup with the descriptor.
First, add the popup HTML above the map <div>:
<div
id="popup"
style="
display: none;
position: absolute;
background: #fff;
padding: 10px 14px;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 14px;
pointer-events: none;
white-space: nowrap;
"
></div>
Then add the click handler in JavaScript:
const popup = document.getElementById("popup");
// Create an overlay to anchor the popup to the map
const overlay = new ol.Overlay({
element: popup,
positioning: "bottom-center",
offset: [0, -8]
});
map.addOverlay(overlay);
map.on("click", (e) => {
let found = false;
// Check vector tile layers (twpLayer, secLayer, lsdLayer)
map.forEachFeatureAtPixel(
e.pixel,
(feature, layer) => {
if (found) return;
const props = feature.getProperties();
const descriptor = props.legal_location || props.descriptor || props.name;
if (descriptor) {
popup.innerHTML = `<strong>${descriptor}</strong>`;
overlay.setPosition(e.coordinate);
popup.style.display = "block";
found = true;
}
},
{
layerFilter: (layer) => layer === twpLayer || layer === secLayer || layer === lsdLayer
}
);
if (!found) {
popup.style.display = "none";
}
});
// Change cursor on hover
map.on("pointermove", (e) => {
const hit = map.hasFeatureAtPixel(e.pixel, {
layerFilter: (layer) => layer === twpLayer || layer === secLayer || layer === lsdLayer
});
map.getTargetElement().style.cursor = hit ? "pointer" : "";
});
Click any township, section, or LSD cell and the popup shows the legal land description, for example TWP-24-1-W5 or SW-14-24-1-W5.
Step 6: Add autocomplete search
Build a search box with autocomplete using the Autocomplete API.
Autocomplete endpoint
GET https://developer.townshipcanada.com/autocomplete/legal-location?location={query}&limit=3
X-API-Key: YOUR_API_KEY
The response is a GeoJSON FeatureCollection with up to 10 matching locations:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-114.03125, 51.53125] },
"properties": {
"descriptor": "NW-25-24-1-W5",
"survey_system": "DLS",
"province": "AB"
}
}
]
}
Add the search box HTML above the map <div>:
<div
id="search-container"
style="
position: absolute; top: 10px; left: 10px; z-index: 1; width: 320px;
"
>
<input
id="search-input"
type="text"
placeholder="Search legal land description..."
style="width: 100%; padding: 10px 14px; font-size: 14px;
border: 1px solid #ccc; border-radius: 6px;
box-shadow: 0 2px 6px rgba(0,0,0,0.1); box-sizing: border-box;"
/>
<ul
id="suggestions"
style="
list-style: none; margin: 4px 0 0; padding: 0; background: #fff;
border-radius: 6px; box-shadow: 0 2px 6px rgba(0,0,0,0.15);
display: none;
"
></ul>
</div>
Then add the JavaScript:
const searchInput = document.getElementById("search-input");
const suggestionsEl = document.getElementById("suggestions");
let debounceTimer;
searchInput.addEventListener("input", (e) => {
clearTimeout(debounceTimer);
const query = e.target.value.trim();
if (query.length < 2) {
suggestionsEl.style.display = "none";
return;
}
debounceTimer = setTimeout(() => fetchSuggestions(query), 300);
});
searchInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
suggestionsEl.style.display = "none";
searchAndPanTo(searchInput.value.trim());
}
});
async function fetchSuggestions(query) {
const center = ol.proj.toLonLat(map.getView().getCenter());
const response = await fetch(
`${TC_API}/autocomplete/legal-location` +
`?location=${encodeURIComponent(query)}&limit=3` +
`&proximity=${center[0].toFixed(4)},${center[1].toFixed(4)}`,
{ headers: { "X-API-Key": TC_API_KEY } }
);
const data = await response.json();
suggestionsEl.innerHTML = "";
if (!data.features || data.features.length === 0) {
suggestionsEl.style.display = "none";
return;
}
data.features.forEach((feature) => {
const li = document.createElement("li");
li.textContent = `${feature.properties.legal_location} (${feature.properties.province})`;
li.style.cssText =
"padding: 10px 14px; cursor: pointer; border-bottom: 1px solid #eee; font-size: 14px;";
li.addEventListener("mouseenter", () => (li.style.backgroundColor = "#f5f5f5"));
li.addEventListener("mouseleave", () => (li.style.backgroundColor = "#fff"));
li.addEventListener("click", () => {
searchInput.value = feature.properties.legal_location;
suggestionsEl.style.display = "none";
searchAndPanTo(feature.properties.legal_location);
});
suggestionsEl.appendChild(li);
});
suggestionsEl.style.display = "block";
}
document.addEventListener("click", (e) => {
if (!document.getElementById("search-container").contains(e.target)) {
suggestionsEl.style.display = "none";
}
});
The proximity parameter passes the current map centre to bias autocomplete results toward the visible area. A user zoomed into Saskatchewan sees Saskatchewan results first.
Full working example
Here's the complete HTML file combining all steps. Replace YOUR_TOWNSHIP_CANADA_API_KEY with your actual key.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Township Canada + OpenLayers</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/ol@9/ol.css"
/>
<script src="https://cdn.jsdelivr.net/npm/ol@9/dist/ol.js"></script>
<style>
html,
body {
margin: 0;
padding: 0;
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
#map {
width: 100%;
height: 100vh;
}
#search-container {
position: absolute;
top: 10px;
left: 10px;
z-index: 1;
width: 320px;
}
#search-input {
width: 100%;
padding: 10px 14px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 6px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
}
#suggestions {
list-style: none;
margin: 4px 0 0;
padding: 0;
background: #fff;
border-radius: 6px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
display: none;
}
#suggestions li {
padding: 10px 14px;
cursor: pointer;
border-bottom: 1px solid #eee;
font-size: 14px;
}
#suggestions li:hover {
background: #f5f5f5;
}
#suggestions li:last-child {
border-bottom: none;
}
#popup {
display: none;
position: absolute;
background: #fff;
padding: 10px 14px;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
font-size: 14px;
pointer-events: none;
white-space: nowrap;
}
</style>
</head>
<body>
<div id="search-container">
<input
id="search-input"
type="text"
placeholder="Search legal land description..."
/>
<ul id="suggestions"></ul>
</div>
<div id="popup"></div>
<div id="map"></div>
<script>
// --- Configuration ---
const TC_API_KEY = "YOUR_TOWNSHIP_CANADA_API_KEY";
const TC_TILES = "https://maps.townshipcanada.com";
const TC_API = "https://developer.townshipcanada.com";
// --- Base map ---
const map = new ol.Map({
target: "map",
layers: [new ol.layer.Tile({ source: new ol.source.OSM() })],
view: new ol.View({
center: ol.proj.fromLonLat([-114, 51]),
zoom: 9
})
});
// --- Grid layers ---
function makeTileLayer(province, layer, minZoom, maxZoom, lineWidth, lineColor) {
return new ol.layer.VectorTile({
minZoom,
maxZoom,
source: new ol.source.VectorTile({
format: new ol.format.MVT(),
url: `${TC_TILES}/${province}/${layer}/{z}/{x}/{y}.mvt?api_key=${TC_API_KEY}`
}),
style: new ol.style.Style({
stroke: new ol.style.Stroke({ color: lineColor, width: lineWidth })
})
});
}
const twpLayer = makeTileLayer("ab", "twp", 6, 11, 1.5, "#2d5a47");
const secLayer = makeTileLayer("ab", "sec", 11, 13, 1.0, "#4a7c59");
const lsdLayer = makeTileLayer("ab", "lsd", 13, 20, 0.5, "#6b9e7a");
map.addLayer(twpLayer);
map.addLayer(secLayer);
map.addLayer(lsdLayer);
// --- Search result layer ---
const geojsonFormat = new ol.format.GeoJSON();
const searchResultLayer = new ol.layer.Vector({
source: new ol.source.Vector(),
style: new ol.style.Style({
stroke: new ol.style.Stroke({ color: "#2d5a47", width: 2 }),
fill: new ol.style.Fill({ color: "rgba(45, 90, 71, 0.15)" })
})
});
map.addLayer(searchResultLayer);
// --- Popup overlay ---
const popup = document.getElementById("popup");
const overlay = new ol.Overlay({
element: popup,
positioning: "bottom-center",
offset: [0, -8]
});
map.addOverlay(overlay);
// --- Search API ---
async function searchAndPanTo(query) {
const response = await fetch(
`${TC_API}/search/legal-location?location=${encodeURIComponent(query)}`,
{ headers: { "X-API-Key": TC_API_KEY } }
);
const data = await response.json();
if (!data.features || data.features.length === 0) return;
const centroid = data.features.find((f) => f.properties.shape === "centroid");
const polygon = data.features.find((f) => f.geometry.type === "MultiPolygon");
if (!centroid) return;
const [lng, lat] = centroid.geometry.coordinates;
map.getView().animate({
center: ol.proj.fromLonLat([lng, lat]),
zoom: 14,
duration: 1500
});
if (polygon) {
searchResultLayer.getSource().clear();
const feature = geojsonFormat.readFeature(polygon, {
dataProjection: "EPSG:4326",
featureProjection: "EPSG:3857"
});
searchResultLayer.getSource().addFeature(feature);
}
}
// --- Click to identify ---
map.on("click", (e) => {
let found = false;
popup.style.display = "none";
map.forEachFeatureAtPixel(
e.pixel,
(feature) => {
if (found) return;
const props = feature.getProperties();
const descriptor = props.legal_location || props.descriptor || props.name;
if (descriptor) {
popup.innerHTML = `<strong>${descriptor}</strong>`;
overlay.setPosition(e.coordinate);
popup.style.display = "block";
found = true;
}
},
{
layerFilter: (layer) => layer === twpLayer || layer === secLayer || layer === lsdLayer
}
);
});
map.on("pointermove", (e) => {
const hit = map.hasFeatureAtPixel(e.pixel, {
layerFilter: (layer) => layer === twpLayer || layer === secLayer || layer === lsdLayer
});
map.getTargetElement().style.cursor = hit ? "pointer" : "";
});
// --- Autocomplete ---
const searchInput = document.getElementById("search-input");
const suggestionsEl = document.getElementById("suggestions");
let debounceTimer;
searchInput.addEventListener("input", (e) => {
clearTimeout(debounceTimer);
const query = e.target.value.trim();
if (query.length < 2) {
suggestionsEl.style.display = "none";
return;
}
debounceTimer = setTimeout(() => fetchSuggestions(query), 300);
});
searchInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
suggestionsEl.style.display = "none";
searchAndPanTo(searchInput.value.trim());
}
});
async function fetchSuggestions(query) {
const center = ol.proj.toLonLat(map.getView().getCenter());
const response = await fetch(
`${TC_API}/autocomplete/legal-location` +
`?location=${encodeURIComponent(query)}&limit=3` +
`&proximity=${center[0].toFixed(4)},${center[1].toFixed(4)}`,
{ headers: { "X-API-Key": TC_API_KEY } }
);
const data = await response.json();
suggestionsEl.innerHTML = "";
if (!data.features || data.features.length === 0) {
suggestionsEl.style.display = "none";
return;
}
data.features.forEach((feature) => {
const li = document.createElement("li");
li.textContent = `${feature.properties.legal_location} (${feature.properties.province})`;
li.addEventListener("click", () => {
searchInput.value = feature.properties.legal_location;
suggestionsEl.style.display = "none";
searchAndPanTo(feature.properties.legal_location);
});
suggestionsEl.appendChild(li);
});
suggestionsEl.style.display = "block";
}
document.addEventListener("click", (e) => {
if (!document.getElementById("search-container").contains(e.target)) {
suggestionsEl.style.display = "none";
}
});
</script>
</body>
</html>
Save the file, replace the API key, and open it in a browser. You'll see DLS township, section, and LSD grids across Alberta, a search box with autocomplete, and click-to-identify on any grid cell.
Next steps
- Add grids for Saskatchewan or Manitoba by creating additional
makeTileLayercalls with'sk'or'mb'as the province argument - Add Manitoba river lots using the
river-lotslayer (mb/river-lots) - Add NTS grid layers for British Columbia using
series,block,unit, andqtr-unitlayer names (bc/series,bc/block, etc.) - Add Ontario geographic township grids using
twp,con, andlotlayer names (on/twp,on/con,on/lot) - When using
forEachFeatureAtPixelto inspect grid cells, note that source-layer names use underscores (e.g.ab_twp,bc_series) even though the URL path uses hyphens - Use the Batch API to plot hundreds of LLD locations as point markers at once
- Replace the OSM base map with a satellite imagery source using
ol/source/XYZfor ahttps://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}URL
Related guides
- API Integration Guide — API endpoints, authentication, and key management
- Maps API — Vector Tiles — Full vector tile layer reference and available tilesets
- Mapbox GL JS Integration — The same workflow using Mapbox GL JS
- What is a Legal Land Description? — DLS, NTS, and Canadian survey systems explained
Related Guides
Legal Land Description API Integration Guide
Integrate legal land description APIs into your applications. Convert LLDs to coordinates, add autocomplete search, process batch records, and display DLS/NTS grid maps. REST API with JSON responses.
Managing API Keys for Development, Staging, and Production
Create and manage multiple Township Canada API keys for different environments. Naming conventions, key rotation, environment variables, and CI/CD setup.
Building Autocomplete Search with the Township Canada API
Build a search-as-you-type component for legal land descriptions using the Township Canada Autocomplete API. Includes debouncing, proximity biasing, and examples in vanilla JS and React.