Custom zones
List zones by point, bounding box, key or metadata, search them by area, and read the cells of a cell zone.
A zone is a polygon in a zone layer: a sales territory, a service area, a delivery boundary. Each zone has a name, an optional Lookup Key, free-form metadata and a polygon. A cell zone is a zone in a cell layer: a set of H3 hexagons with up to 8 integer values per cell. This page covers both.
Not Geoapify-compatible. /v1/custom/ is our own API. See Custom layers for how to list the layers and Authentication for keys.
Quick start
List the zones of a layer that cover a point:
curl -s "https://api.data.graphitelab.ai/v1/custom/layers/dead_zones/zones?lat=39.77&lon=-86.16&apiKey=$GRAPHITE_API_KEY"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"place_id": "gp:layer:dead_zones:6d40db31-795a-4282-9bb8-5c418ea78ed9",
"boundary_type": "custom:dead_zones",
"name": "Downtown demo zone",
"lookup_key": "downtown",
"metadata": { "tier": "gold", "population": 12000 },
"lon": -86.16,
"lat": 39.77,
"datasource": { "sourcename": "The Graphite Lab", "attribution": "© The Graphite Lab", "raw": { "layer": "dead_zones", "revision": 2 } }
},
"geometry": { "type": "Point", "coordinates": [-86.16, 39.77] },
"bbox": [-86.17, 39.76, -86.15, 39.78]
}
],
"next_cursor": null
}A zone's place_id is gp:layer:<slug>:<uuid>, and that whole Place ID is the {id} of the single-zone routes, for example GET /v1/custom/zones/gp:layer:dead_zones:6d40db31-795a-4282-9bb8-5c418ea78ed9. A bare UUID is 404. Lists carry a representative point unless you ask for more with geometry.
Endpoints
| Operation | Path |
|---|---|
| List or look up | GET /v1/custom/layers/{layer}/zones |
| Search by area | POST /v1/custom/layers/{layer}/zones/search |
| Read one | GET /v1/custom/zones/{id} |
| Read the cells | GET /v1/custom/zones/{id}/cells |
| Draw the cells | GET /v1/custom/zones/{id}/geometry |
{layer} is a slug (^[a-z0-9_]{2,40}$) and {id} is a zone UUID. A location layer answers 404 on the zone routes: use locations.
Finding zones
GET /v1/custom/layers/{layer}/zones filters, all combined with AND:
| Parameter | Meaning |
|---|---|
lookup_key | Exact match. |
lat + lon | Zones that cover the point. Both or neither. On a cell layer, the zones whose cells include the point's cell (exact cell only). |
bbox | west,south,east,north. Zones whose box meets it. |
meta[key]=value | Metadata contains the member. Repeatable, at most 20. Values parse as JSON (2100, true), else stay strings. |
limit | 1 to 1,000. Default 100. |
cursor | next_cursor of the previous page. |
geometry | point (default), geometry_1000, geometry_5000, geometry_10000 or full. The middle three simplify the polygon to about that many meters of tolerance. |
The response is a FeatureCollection with next_cursor (null on the last page).
Searching by area
POST /v1/custom/layers/{layer}/zones/search takes a JSON body. It is a read: it costs 1 rivet like any other request.
curl -s -X POST "https://api.data.graphitelab.ai/v1/custom/layers/dead_zones/zones/search" \
-H "Authorization: Bearer $GRAPHITE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"intersects":{"type":"Polygon","coordinates":[[[-86.165,39.765],[-86.155,39.765],[-86.155,39.775],[-86.165,39.775],[-86.165,39.765]]]}}'On a zone layer the body is {intersects, meta?, limit?, cursor?}: intersects (Polygon or MultiPolygon) is required, and bbox, area_id and cells are 400. On a cell layer give exactly one of:
intersects: a GeoJSON Polygon or MultiPolygon.bbox:[west, south, east, north].area_id: a built-in boundary Place ID (gp:osm:orgp:pc:). An unknown area is 404.
On a cell layer you can also send cells: true to list each zone's overlapping cell IDs. geometry is a query parameter here, as on the list route. The body limit is 10 MB.
Reading one zone
GET /v1/custom/zones/{id} returns the full polygon by default (geometry=point for a representative point). A cell zone comes back as a Point at the center of its bbox plus n_cells; its cells come from /cells. An unknown or malformed id is 404 Unknown zone.
Cell zones
A cell layer stores each zone as H3 cells at one resolution (h3_resolution on the layer), plus up to 8 integer values per cell (for example households or signal strength). Use it for coverage maps and any data too large or irregular for polygons.
Searching a cell layer turns the area into H3 cells and reports how many of them each zone holds:
curl -s -X POST "https://api.data.graphitelab.ai/v1/custom/layers/coverage/zones/search" \
-H "Authorization: Bearer $GRAPHITE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"bbox":[-86.2,39.73,-86.11,39.8],"cells":true}'{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"place_id": "gp:layer:coverage:65993a22-a146-49af-993e-1e9664d6b1e2",
"boundary_type": "custom:coverage",
"name": "Demo cell zone",
"lookup_key": "demo-cells",
"n_cells": 4,
"lon": -86.176143,
"lat": 39.761894,
"cell_count": 4,
"coverage_pct": 33.33,
"cells": ["872661a20ffffff", "872661a21ffffff", "872661a23ffffff", "872661a2effffff"],
"datasource": { "sourcename": "The Graphite Lab", "attribution": "© The Graphite Lab", "raw": { "layer": "coverage", "revision": 3 } }
},
"geometry": { "type": "Point", "coordinates": [-86.176143, 39.761894] },
"bbox": [-86.20638292050609, 39.73213198322349, -86.1459032217768, 39.79165694025217]
}
],
"area_cells": 12,
"next_cursor": null
}area_cells is the number of cells covering the searched area; coverage_pct is cell_count as a share of it.
Cells of a zone
GET /v1/custom/zones/{id}/cells:
| Parameter | Meaning |
|---|---|
format | list (default) for cell IDs, count for {h3_resolution, count}. |
bbox | west,south,east,north: only cells inside. A box over 500,000 cells is 413. |
limit | List page size, 1 to 100,000. Default 10,000. |
values | true adds the per-cell values, as one array per value key, aligned with cells. |
cursor | next_cursor of the previous page. |
A zone that is not in a cell layer is 404 Not a cell zone.
Full reference: read the cells
Drawing a zone
GET /v1/custom/zones/{id}/geometry answers application/geo+json.
mode=outline(default): one Feature with a MultiPolygon outline of all cells. Refused with 413 above 50,000 cells.mode=hexes: a FeatureCollection with one hexagon per cell, each carryingh3,zoneand its values.max_cellscaps the cells drawn (default 50,000, at most 200,000). A bigger zone is 413; narrow it withbbox.
Full reference: draw the cells
Public layer: fcc_fixed
fcc_fixed is a read-only cell layer that we load and maintain: fixed broadband availability from the FCC National Broadband Map (Broadband Data Collection), vintage J26. It covers the United States, including Alaska, Hawaii and the territories present in the FCC data; Canada has no data.
| Property | Value |
|---|---|
| Kind and resolution | cell, H3 resolution 8 (about 0.74 km² per hex) |
fallback_rings | 2 |
| Zones | One per provider and technology |
lookup_key | <provider_id>-<technology_code>, for example 130077-50 (AT&T, fiber) |
metadata | provider_id, frn, brand_name, technology, technology_code, vintage |
Each cell value describes one provider and technology in one hex:
| Value | Meaning |
|---|---|
n_loc | Locations in the hex that the provider claims to serve |
hex_loc | All locations in the hex |
down_max, up_max | Highest advertised download and upload speed among those locations, in Mbps |
down_med, up_med | Median advertised download and upload speed, in Mbps |
ll_pct | Share of the claimed locations (0 to 100) where the provider reports low-latency service |
Read the numbers with these caveats:
- Speeds are advertised by providers, not measured.
- A row says a provider claims service to
n_locof the hex'shex_lochomes, not to every home. - No row is not a verified lack of service: it means no provider claims service to homes in this hex.
- Some rows carry
down_max0 (anddown_med,up_max,up_med0): the source did not report a speed. Treat 0 as not reported, not as no service. - A zone's point and
bboxspan the whole country. Uselookup_key=130077-50to fetch one provider and technology, orlatandlonfor the zones whose cells include one hex. The ring fallback does not apply on the zone list. - The data has a vintage (J26). It appears in
datasource.attributionand in each zone'smetadata.vintage.
Attribution (required). Show datasource.attribution to end users: "Source: FCC National Broadband Map (Broadband Data Collection), vintage J26". Licence: US federal work, no copyright restriction.
Public layer: mlab_ndt_zip
mlab_ndt_zip is a read-only zone layer that we load and maintain: measured internet speed from the M-Lab NDT data set, aggregated by ISP for each US ZIP (ZCTA5) and Canadian FSA, for tests from 2025-11-01 to 2026-10-31.
| Property | Value |
|---|---|
| Kind | zone |
| Zones | One per US ZIP (ZCTA5) or Canadian FSA |
lookup_key | US-<ZIP> or CA-<FSA>, for example US-46204, CA-M5G |
name | US ZIP 46204, CA FSA M5G |
Each zone's metadata holds the whole aggregate:
| Field | Meaning |
|---|---|
country, postal_code | US or CA, and the ZIP or FSA |
period | Test window, 2025-11_2026-10 |
tests, unique_ips | All download tests in the zone, and the distinct IP addresses behind them |
isps | One entry per ISP, largest first (see below) |
Each isps entry:
| Field | Meaning |
|---|---|
asn, name | The ISP's autonomous system number and name |
tests, unique_ips | Download tests and distinct IPs for this ISP in the zone |
median_download_mbps | Median measured download speed, Mbps |
upload_tests, upload_unique_ips, median_upload_mbps | Same for upload; null when uploads fail the thresholds below |
median_min_rtt_ms | Median of the minimum round-trip time, ms |
asn_class | fixed_isp, mobile, hosting_cdn_transit, other_unknown or unknown |
Fetch one ZIP by key:
curl -s "https://api.data.graphitelab.ai/v1/custom/layers/mlab_ndt_zip/zones?lookup_key=US-46204&apiKey=$GRAPHITE_API_KEY"Read the numbers with these caveats:
- An ISP is listed only with at least 100 download tests from at least 25 unique IPs.
- Upload medians are null when the upload tests fail the same thresholds.
- ZIPs with no ZCTA are dropped: 11.7% of US tests, mostly PO-box ZIPs, and 0.5% of Canadian tests.
- Zone polygons are simplified (about 100 m).
- Location is IP-based, with a median accuracy radius of 20 km. A ZIP's numbers describe nearby networks, not the street.
- Mobile classes geolocate worst, and mobile carriers also sell home internet, so filter on
asn_class. It is oftenunknown(every Canadian ISP). - No ZIP at a point means too few tests, not zero speed. Show the layer's
absence_noteto your users. - The data covers 2025-11-01 to 2026-10-31 (
period).
Attribution (required). Show datasource.attribution to end users: "The M-Lab NDT Data Set, 2025-11-01 to 2026-10-31. https://measurementlab.net/tests/ndt". Licence: CC0 1.0.
Limits
| Limit | Value |
|---|---|
| Search body | 10 MB |
| Page size (zones) | 1,000 |
meta[] filters | 20 |
| Search area (cell layer) | 500,000 cells (413 above) |
Cell IDs per search page (cells: true) | 100,000 (413 above) |
Cells per page (/cells) | 100,000 |
Cells drawn (/geometry) | 200,000 (413 above) |
Errors
| Status | When |
|---|---|
| 400 | Bad parameter, bad cursor, bad search body, lat without lon. |
| 401 | Missing or invalid API key. |
| 402 | Your organization is out of rivets (insufficient_rivets) or has no plan (no_plan). |
| 403 | Your organization does not have data API access. |
| 404 | Unknown layer, a location layer, unknown or malformed zone id (Unknown zone), a non-cell zone on /cells (Not a cell zone), unknown area_id. |
| 413 | Search area, cell IDs or drawn cells over their limit, or body over 10 MB. |
| 429 | Over 60 requests per minute. |
| 500, 503 | Unexpected gateway error, or the custom layer store is unavailable or busy. Retry. |
Errors use the error shape.
Try in Playground (signed in to your Graphite Lab organization). Signed out, use the paste-your-key widget on the reference page.