Agriculture and Energy APIs - Wells, Soil and Tenure by Legal Land Description
Query AER wells, pipelines, Crown tenure, soil and crop data by Canadian legal land description. Reports, filters, paging and vector tiles.
What You'll Build
The Agriculture API and the Energy API return parcel intelligence keyed by a Dominion Land Survey legal land description. Send NW-11-5-6-W2 and get soil classification, land capability, crop rotation and drought for that quarter section. Send 7-10-13-9-W4 and get every well, pipeline segment, facility and Crown disposition on that LSD, with 12 months of production.
This guide covers getting a key, the /report endpoint, include=, the single-section endpoints, paging through Energy collections, the cross-parcel endpoints for operators and tenure, the response contract, rate limits, and the matching vector tiles. It's written for ag-tech and lending teams scoring farmland, and for energy software and GIS teams that need oil and gas well data by legal land description across Alberta, Saskatchewan and Manitoba.
Prerequisites
- An Agriculture, Energy, or Agriculture + Energy API key. Subscribe on the API page
- Node.js 18+ or Python 3.8+
- Basic familiarity with REST APIs and JSON
There are three products. The Agriculture API serves /ag/*, the Energy API serves /energy/*, and the Agriculture + Energy API is one key that works on both, drawing on one monthly allowance. Each comes in Developer, Professional and Business plans, plus Enterprise. See pricing for current prices. To keep separate keys for development and production, follow the API key management guide.
Base URL and Authentication
Every REST endpoint lives under https://developer.townshipcanada.com, with the key in the X-API-Key header:
curl "https://developer.townshipcanada.com/energy/report?legal_location=7-10-13-9-W4" \
-H "X-API-Key: your_api_key"
Store keys in server-side environment variables. The vector tiles use a different host and a query parameter, covered below.
Coverage
Both APIs cover Alberta, Saskatchewan and Manitoba. A BC (NTS) location returns 400 with code bc_not_supported.
- Agriculture is keyed by quarter section. An LSD input resolves to its containing quarter section: the response echoes your input in
legal_location, names the quarter it describes inresolved_legal_location, and setsgrainto"lsd"or"quarter_section". Wetlands are published for Alberta only. - Energy is keyed by LSD (
7-10-13-9-W4). Alberta reads AER data, Saskatchewan reads GeoAtlas, and Manitoba reads the Petroleum Branch. Manitoba publishes no open pipeline, facility or production layer, so those sections come back empty and are listed inmeta.unavailable.
The Report Endpoints
GET /ag/report?legal_location=NW-11-5-6-W2
GET /energy/report?legal_location=7-10-13-9-W4

The Agriculture report returns productivity (LSRS and CLI scores, classes and limiters), cropping, soil, land_use, drought, wetlands, hydrology, parcel_context and provincial_detail. The Energy report returns summary, production, tenure, wells, pipelines, facilities and alternative_energy.
Request only what you need with include=
include takes a comma-separated list of section names. It is a projection, not a filter: sections you leave out are never queried, so a narrow request is faster.
curl "https://developer.townshipcanada.com/ag/report?legal_location=NW-11-5-6-W2&include=soil,drought,geometry" \
-H "X-API-Key: your_api_key"
geometry is a section name too. The parcel polygon is never in the default report; include=geometry attaches it as GeoJSON under parcel.geometry. Otherwise parcel.geometry is null. An unknown section name returns 400 invalid_parameter listing the valid values.
| API | Valid include values |
|---|---|
| Agriculture | productivity, cropping, soil, land_use, drought, wetlands, hydrology, parcel_context, provincial_detail, geometry |
| Energy | summary, production, tenure, wells, pipelines, facilities, alternative_energy, geometry |
Single-section endpoints
Each of these returns one section, with the same legal_location parameter as /report:
- Agriculture:
/ag/productivity,/ag/cropping,/ag/soil,/ag/land-use,/ag/drought,/ag/wetlands,/ag/hydrology,/ag/parcel-context - Energy:
/energy/summary,/energy/production,/energy/alternative-energy
provincial_detail and geometry have no route of their own. Request them through /ag/report?include=.... A section with no data for that parcel is a 200 with a null payload, not a 404.
Each section call counts as one request, the same as a full report. If you need three sections for one parcel, /report?include=soil,drought,hydrology is one request instead of three.
The Response Contract
Both APIs follow the same rules, so one client handles both.
Array sections are envelopes. Every array in an Energy report is shaped {total, returned, truncated, more, rows}. total is the true count, truncated is true when returned < total, and more is the path of the collection endpoint that pages through all of it:
{
"legal_location": "7-10-13-9-W4",
"province": "AB",
"parcel": { "area_ha": 16.19, "centroid": { "lat": 50.06, "lng": -111.12 }, "geometry": null },
"wells": {
"total": 3,
"returned": 3,
"truncated": false,
"more": "/energy/wells?legal_location=7-10-13-9-W4",
"rows": [{ "...": "..." }]
},
"units": {
"length": "m",
"area": "ha",
"depth": "m",
"pressure": "kPa",
"oil": "m3",
"gas": "e3m3"
},
"meta": { "unavailable": [], "sources": { "...": "..." } }
}
Inside /energy/report, wells, pipelines and facilities are capped at 50 rows and tenure at 20. Check truncated before you assume you have every row.
meta.unavailable explains gaps. Each entry is {section, reason}, with reason source_error, timeout or not_published_in_province. A Saskatchewan quarter section comes back with:
"meta": {
"unavailable": [{ "section": "wetlands", "reason": "not_published_in_province" }],
"sources": { "...": "..." }
}
meta.sources names the upstream dataset per section. Treat a null section listed in meta.unavailable as "not known", not "none".
Errors have one shape. Every error from these APIs is {error: {code, message}}:
{
"error": {
"code": "invalid_legal_location",
"message": "'legal_location' must be a Legal Subdivision (e.g. 10-36-42-3-W5)."
}
}
The codes are invalid_parameter, missing_parameter, invalid_legal_location, bc_not_supported (all 400), not_found (404, a valid location with no data), rate_limit_exceeded (429) and internal_error (503). Branch on code, not on message. The full list is in errors, limits and OpenAPI.
Energy Collections: Filters and Pagination
Four collection endpoints page past the report's caps, one LSD at a time. Rows are identical to the report's envelope rows.
| Endpoint | Filters | Sort keys |
|---|---|---|
/energy/wells | status (active, abandoned, suspended, orphan), operator | id, status, operator, depth |
/energy/pipelines | substance, status, operator | id, length, h2s (default) |
/energy/facilities | category, status, operator | id, name, status |
/energy/tenure | tenure_kind (png, mineral, all), status, operator, expires_within_days | expiry, urgency (default), area, id |
All four take dir (asc or desc), page (1-based, default 1) and per_page (1 to 200, default 50). The response is {legal_location, province, rows, meta}, and meta carries total, page, per_page, sort, dir and every filter echoed back, null where you didn't supply it. Page count is Math.ceil(meta.total / meta.per_page).
curl "https://developer.townshipcanada.com/energy/wells?legal_location=7-10-13-9-W4&status=abandoned&sort=depth&dir=desc&per_page=100" \
-H "X-API-Key: your_api_key"
Cross-Parcel Endpoints
Five Energy endpoints are keyed on an operator, a disposition or a licence, and take no legal_location:
/energy/operators: a directory of licensees with active, abandoned and orphan well counts and disposition totals. Search withq(name or BA code), sort byorphan(default),active,abandoned,nameordispositions./energy/operators/{name}: one licensee, by BA code, display name or slug.view=summary(default) returns liability counts, transfers, tenure totals and a footprint of LSDs.view=dispositions,view=wellsandview=lsdsare paged lists./energy/tenure/expiring: Crown PNG and mineral dispositions expiring withinexpires_within_days(1 to 3650, default 365), filterable byoperator,type,tenure_kindandq. Perpetual dispositions never enter the window./energy/dispositions/{number}: one disposition. Use the PNG number, orcategory:numberfor mineral tenure with the colon percent-encoded (metallic%3A0512345678). This is theidevery tenure row carries./energy/pipelines/{licence}: every segment on one AER pipeline licence, withmetashaped{total, returned, truncated}.
curl "https://developer.townshipcanada.com/energy/tenure/expiring?expires_within_days=180&tenure_kind=png&sort=expiry" \
-H "X-API-Key: your_api_key"
JavaScript Example
This script fetches the Energy summary and wells for one LSD, then follows more when the report's wells array is truncated:
const API_KEY = process.env.TOWNSHIP_CANADA_API_KEY;
const BASE_URL = "https://developer.townshipcanada.com";
async function get(path) {
const response = await fetch(`${BASE_URL}${path}`, {
headers: { "X-API-Key": API_KEY }
});
const body = await response.json();
if (!response.ok) {
throw new Error(`HTTP ${response.status} ${body.error?.code}: ${body.error?.message}`);
}
return body;
}
async function allWells(lsd) {
const report = await get(
`/energy/report?legal_location=${encodeURIComponent(lsd)}&include=summary,wells`
);
if (!report.wells.truncated) return report.wells.rows;
// Page the collection endpoint the envelope points at
const rows = [];
for (let page = 1; ; page++) {
const result = await get(`${report.wells.more}&page=${page}&per_page=200`);
rows.push(...result.rows);
if (page >= Math.ceil(result.meta.total / result.meta.per_page)) break;
}
return rows;
}
allWells("7-10-13-9-W4")
.then((wells) => {
const orphans = wells.filter((w) => w.is_orphan);
console.log(`${wells.length} wells, ${orphans.length} orphan`);
})
.catch(console.error);
Python Example
The Agriculture equivalent, asking for three sections and the polygon, and reporting any section the source couldn't serve:
import os
import requests
API_KEY = os.environ["TOWNSHIP_CANADA_API_KEY"]
BASE_URL = "https://developer.townshipcanada.com"
def ag_report(legal_location, include):
response = requests.get(
f"{BASE_URL}/ag/report",
headers={"X-API-Key": API_KEY},
params={"legal_location": legal_location, "include": ",".join(include)},
timeout=30,
)
if not response.ok:
error = response.json().get("error", {})
raise RuntimeError(f"HTTP {response.status_code} {error.get('code')}: {error.get('message')}")
return response.json()
report = ag_report("NW-11-5-6-W2", ["productivity", "soil", "drought", "geometry"])
print(report["resolved_legal_location"], report["province"])
print("LSRS:", report["productivity"]["lsrs"])
print("Soil order:", report["soil"]["classification"]["order"])
for gap in report["meta"]["unavailable"]:
print(f"Unavailable: {gap['section']} ({gap['reason']})")
Vector Tiles
The same data renders as Mapbox Vector Tiles from maps.townshipcanada.com, authenticated with an api_key query parameter:
https://maps.townshipcanada.com/ag/{archive}/{z}/{x}/{y}.mvt?api_key=YOUR_API_KEY
https://maps.townshipcanada.com/energy/{archive}/{z}/{x}/{y}.mvt?api_key=YOUR_API_KEY
https://maps.townshipcanada.com/energy/{archive}.json?api_key=YOUR_API_KEY (TileJSON)
The tiles are their own products, the Ag Maps API (18 archives: soils, capability, crop rotation, Crown land, pastures, the ALR) and the Energy Maps API (12 archives: wells, pipelines, facilities, production, tenure), with their own keys. An archive outside your product returns 403.
Three things trip people up:
source-layeris not always the archive name.ag_summary_qscarries layerag_summary;og_activity_lsdcarriesog_activity.- Rollups are zoom-banded.
ag_summary,aafc_crop_summary,lsrs_productivity,og_activityandlsd_production_12moeach ship as three archives:_twp(z0-6),_sec(z6-9) and the base archive (z9+). Add all three as sources, or the map is empty below z9. - Labels are separate archives (
_labels), exceptpipelines, which carries both.
In a MapLibre or Mapbox style, the Energy well-activity family looks like this:
"sources": {
"og-activity-twp": { "type": "vector", "minzoom": 0, "maxzoom": 6,
"tiles": ["https://maps.townshipcanada.com/energy/og_activity_twp/{z}/{x}/{y}.mvt?api_key=YOUR_API_KEY"] },
"og-activity-sec": { "type": "vector", "minzoom": 6, "maxzoom": 9,
"tiles": ["https://maps.townshipcanada.com/energy/og_activity_sec/{z}/{x}/{y}.mvt?api_key=YOUR_API_KEY"] },
"og-activity-lsd": { "type": "vector", "minzoom": 9, "maxzoom": 14,
"tiles": ["https://maps.townshipcanada.com/energy/og_activity_lsd/{z}/{x}/{y}.mvt?api_key=YOUR_API_KEY"] }
}
The full archive tables are in the Agriculture and Energy references. For the survey grid underneath, see the Maps API vector tile reference.
Rate Limiting
| Plan | Requests/month | Rate limit |
|---|---|---|
| Developer | 25 | 1 req/sec |
| Professional | 250 | 2 req/sec |
| Business | 1,000 | 5 req/sec |
| Enterprise | Custom | Custom |
The volumes are the same for the single and combined products. Every call counts: a full report, a section endpoint, a collection page and a cross-parcel lookup each use one request. Allowances are hard stops with no overage.
On 429, back off and retry with exponential delay and a retry cap. A monthly allowance is separate from throttling, so check usage before retrying repeatedly.
No Batch or Autocomplete Here
These APIs answer one parcel per request. They don't accept arrays of locations and don't offer type-ahead. For those, use the core APIs:
- To resolve a spreadsheet of descriptions or coordinates first, use the Batch API (guide), then call
/reportper parcel within your rate limit. - For type-ahead input, use Search and Autocomplete.
Batch Agriculture and Energy enrichment of large parcel lists is available on Enterprise.
FAQ
Is there an AER wells API by legal land description?
Yes. GET /energy/wells?legal_location=7-10-13-9-W4 returns every well on that LSD, with UWI, licence number, derived status, operator, fluid and total depth. In Alberta the rows come from AER data.
Can I get farmland soil data from an API?
Yes. GET /ag/soil?legal_location=NW-11-5-6-W2 returns soil classification, drainage and slope for the quarter section. /ag/productivity adds LSRS and CLI capability.
Do I need separate keys for Agriculture and Energy?
Not if you subscribe to Agriculture + Energy: one key works on both /ag/* and /energy/*. The vector tiles are separate products with their own keys.
Why is a section null?
Either the parcel has no data for it, or the source couldn't serve it. Check meta.unavailable: an entry there with not_published_in_province means the province doesn't publish that layer.
Does the API cover British Columbia?
Not yet. BC (NTS) locations return 400 bc_not_supported.
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.
API Troubleshooting & Best Practices
Fix common Township Canada API errors including Forbidden responses, CORS issues, rate limiting, and empty results. Includes debugging tips and integration patterns.