Machine access
For bots
Heritage Map is a directory of long-running American businesses, and the directory is readable by machine. This page is the whole contract: where the API lives, what it returns, and the rules it enforces.
Base URL
Production: https://heritagemap.net — every endpoint sits under /api/v1/. This prototype runs locally at http://localhost:4000; swap the host and nothing else changes.
The OpenAPI specification is served at /api/v1/openapi.json and interactive documentation at /api/v1/docs. The spec is generated from the schemas the server validates against, so the two cannot drift.
Endpoints
| Method | Path | Returns |
|---|---|---|
| GET | /api/v1/businesses | Approved listings, keyset-paginated. Filters combine with AND: q (full-text), category, subcategory (group/sub), state, precision (exact|city), verified, bbox (minLng,minLat,maxLng,maxLat), near (lat,lng) with radius_km, sort (name|distance|newest). |
| GET | /api/v1/businesses/:id | Full detail for one approved listing: heritage statement, phone, website, hours, timestamps. |
| GET | /api/v1/categories | The 16 taxonomy groups with nested subcategories and per-group listing counts. |
| GET | /api/v1/stats | Directory counts: businesses by precision and verification, category and subcategory totals. |
| GET | /api/v1/zips/:zip | Centroid for a five-digit ZIP, for near-me resolution. |
| GET | /api/v1/showcase | The oldest approved listing per category group plus live register counts. |
| GET | /api/v1/config | Public map defaults: center, zoom, cluster threshold, tile template. |
Reads are anonymous and public. Listings whose precision is city-level never expose a stored point: they return location: null, approximate: true, and a town centroid rounded to three decimals.
Examples
Responses below are quoted verbatim from the live register.
curl https://heritagemap.net/api/v1/stats{"businesses":{"total":9,"exact":1,"city":8,"verified":9,"pending":0},"categories":16,"subcategories":212}curl "https://heritagemap.net/api/v1/businesses?state=MT&verified=true&limit=2"{
"data": [
{
"id": 435,
"name": "Mineral City LLC (Aviation Division)",
"city": "Kalispell", "state": "MT",
"precision": "exact", "approximate": false,
"location": { "lat": 48.1810693, "lng": -114.3040493 },
"address": "1880 Highway 93 South, Kalispell, MT 59901",
"verified": true, "founded": 2021,
"tags": ["aerial-photography", "aviation", "veteran-owned"],
"category": { "slug": "arts-media-publishing", "label": "Arts, Media & Publishing" },
"subcategory": { "slug": "photography", "label": "Photography", "key": "arts-media-publishing/photography" }
}
],
"page": {
"next": null,
"limit": 2
}
}curl -i -H 'If-None-Match: W/"j--WoVawpy3l40VbG1sus9dfrtw1rWVn"' \
https://heritagemap.net/api/v1/categoriesHTTP/1.1 304 Not Modifiedcurl "https://heritagemap.net/api/v1/businesses?limit=101"{"error":{"code":"BAD_REQUEST","message":"querystring/limit must be <= 100","requestId":"ce90970f-aa1c-4bad-83f8-2c1baa10dd74"}}Pagination
Every list endpoint uses keyset (cursor) pagination — there is no offset anywhere. The envelope is always:
{ "data": [ … ], "page": { "next": "<cursor>" | null, "limit": 20 } }limit defaults to 20 and caps at 100; anything higher is a 400, never silently clamped. Feed the previous page’s page.next back as ?cursor=; treat it as opaque. next is null on the last page. Sort keys carry an id tiebreaker, so a full walk visits every row exactly once.
Rate limits
Anonymous clients get 100 requests per minute per source IP. API keys carry their own per-key limit, set at issuance (default 60 per minute). The limiter counts every request, including the ones it rejects; GET /api/v1/health is exempt so probes stay free.
Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds). Past the ceiling the answer is 429 with {"error":{"code":"RATE_LIMITED"}} and a Retry-After header — honour it and you will not see a second one.
Caching
Every read endpoint sends a weak content-hash ETag and honours If-None-Match with a 304, as in the third example above. Cache-Control is public, max-age=60 for lists, detail, stats and config, and public, max-age=300 for categories and zips — the near-static reference data.
API keys
Send a key as X-Api-Key: hmk_…. Two scopes exist: read:directory for the listings and read:stats for the counts. A key the server does not know is 403 INVALID_API_KEY, a revoked one 403 KEY_REVOKED, and a valid key outside its scopes 403 INSUFFICIENT_SCOPE — a bad key fails loudly rather than degrading to anonymous.
Keys are issued and revoked by the directory’s administrators; the plaintext is shown once at issuance. Request one at hello@heritagemap.net with a line about what you are building.