Custom layers

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

OperationPath
List or look upGET /v1/custom/layers/{layer}/zones
Search by areaPOST /v1/custom/layers/{layer}/zones/search
Read oneGET /v1/custom/zones/{id}
Read the cellsGET /v1/custom/zones/{id}/cells
Draw the cellsGET /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.

Full reference: list zones

Finding zones

GET /v1/custom/layers/{layer}/zones filters, all combined with AND:

ParameterMeaning
lookup_keyExact match.
lat + lonZones that cover the point. Both or neither. On a cell layer, the zones whose cells include the point's cell (exact cell only).
bboxwest,south,east,north. Zones whose box meets it.
meta[key]=valueMetadata contains the member. Repeatable, at most 20. Values parse as JSON (2100, true), else stay strings.
limit1 to 1,000. Default 100.
cursornext_cursor of the previous page.
geometrypoint (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: or gp: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.

Full reference: search zones

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.

Full reference: get one 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:

ParameterMeaning
formatlist (default) for cell IDs, count for {h3_resolution, count}.
bboxwest,south,east,north: only cells inside. A box over 500,000 cells is 413.
limitList page size, 1 to 100,000. Default 10,000.
valuestrue adds the per-cell values, as one array per value key, aligned with cells.
cursornext_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 carrying h3, zone and its values.
  • max_cells caps the cells drawn (default 50,000, at most 200,000). A bigger zone is 413; narrow it with bbox.

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.

PropertyValue
Kind and resolutioncell, H3 resolution 8 (about 0.74 km² per hex)
fallback_rings2
ZonesOne per provider and technology
lookup_key<provider_id>-<technology_code>, for example 130077-50 (AT&T, fiber)
metadataprovider_id, frn, brand_name, technology, technology_code, vintage

Each cell value describes one provider and technology in one hex:

ValueMeaning
n_locLocations in the hex that the provider claims to serve
hex_locAll locations in the hex
down_max, up_maxHighest advertised download and upload speed among those locations, in Mbps
down_med, up_medMedian advertised download and upload speed, in Mbps
ll_pctShare 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_loc of the hex's hex_loc homes, 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_max 0 (and down_med, up_max, up_med 0): the source did not report a speed. Treat 0 as not reported, not as no service.
  • A zone's point and bbox span the whole country. Use lookup_key=130077-50 to fetch one provider and technology, or lat and lon for 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.attribution and in each zone's metadata.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.

PropertyValue
Kindzone
ZonesOne per US ZIP (ZCTA5) or Canadian FSA
lookup_keyUS-<ZIP> or CA-<FSA>, for example US-46204, CA-M5G
nameUS ZIP 46204, CA FSA M5G

Each zone's metadata holds the whole aggregate:

FieldMeaning
country, postal_codeUS or CA, and the ZIP or FSA
periodTest window, 2025-11_2026-10
tests, unique_ipsAll download tests in the zone, and the distinct IP addresses behind them
ispsOne entry per ISP, largest first (see below)

Each isps entry:

FieldMeaning
asn, nameThe ISP's autonomous system number and name
tests, unique_ipsDownload tests and distinct IPs for this ISP in the zone
median_download_mbpsMedian measured download speed, Mbps
upload_tests, upload_unique_ips, median_upload_mbpsSame for upload; null when uploads fail the thresholds below
median_min_rtt_msMedian of the minimum round-trip time, ms
asn_classfixed_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 often unknown (every Canadian ISP).
  • No ZIP at a point means too few tests, not zero speed. Show the layer's absence_note to 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

LimitValue
Search body10 MB
Page size (zones)1,000
meta[] filters20
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

StatusWhen
400Bad parameter, bad cursor, bad search body, lat without lon.
401Missing or invalid API key.
402Your organization is out of rivets (insufficient_rivets) or has no plan (no_plan).
403Your organization does not have data API access.
404Unknown layer, a location layer, unknown or malformed zone id (Unknown zone), a non-cell zone on /cells (Not a cell zone), unknown area_id.
413Search area, cell IDs or drawn cells over their limit, or body over 10 MB.
429Over 60 requests per minute.
500, 503Unexpected 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.

On this page