Skip to content

Coverage API

The Coverage API provides programmatic access to MeshMapper coverage grid-square data. Use it to draw coverage grids on your own maps or integrate MeshMapper data into external tools and dashboards.

Authentication

Access requires a Coverage API key. Each key is scoped to a specific region, a multiregion group, or a set of adjacent regions (see Multi-Region Keys). Self-service keys have a daily limit of 100 requests; keys issued by the MeshMapper team may carry a different limit (the 429 body's limit field shows yours).

Generating a Key

Regional administrators can generate their own API key directly from the admin panel:

  1. Log in to your region's admin panel
  2. Go to User Settings
  3. Scroll to the API Access section
  4. Enter a description/reason for the key (mandatory)
  5. Click Generate API Key

Each administrator is limited to one API key per region. The key is automatically scoped to your region with a fixed rate limit of 100 requests per day. If you need to replace your key, use the Regenerate button — this invalidates the old key immediately.

Unauthorized Access

MeshMapper utilizes API keys and rate limits to protect server resources and prevent access to data that regions do not wish to have shared externally. As such, accessing unauthorized API's, scraping for data, etc., is strictly prohibited and will result in action taken to protect the server and data (which may include IP or origin bans, removal of a region, etc.). The MeshMapper team is happy to review requests for data not provided in the API's below.

Endpoint

GET https://meshmapper.net/coverage.php?key=YOUR_API_KEY

Query Parameters

Parameter Required Description
key Yes Your Coverage API key.
include No Comma-separated list of optional sections to add to the response. Currently supports repeaters (e.g. ?include=repeaters) — see Repeater Fields.
fresh No fresh=1 skips the 15-minute server cache and builds the grid now. Single-region and group keys only; multi-region and global keys return HTTP 400 fresh_not_supported.
f_* No Filter the pings that go into the grid before it is built: by radio configuration, date, power or antenna. See Filtering.

Response Format

{
  "success": true,
  "region": "[IATA]",
  "region_name": "Ottawa, CA",
  "grid_size": { "lat": 0.0027, "lon": 0.00384 },
  "schema_version": 2,
  "generated_at": 1710547200,
  "data_age_seconds": 312,
  "total_squares": 1234,
  "point_count": 48210,
  "coverage_type_counts": { "BIDIR": 540, "TX": 60, "RX": 410, "DISC": 90, "DEAD": 12, "DROP": 122 },
  "type_bits": { "BIDIR": 1, "TX": 2, "RX": 4, "DISC": 8, "DEAD": 16, "DROP": 32 },
  "bbox": { "minLat": 45.108, "minLon": -76.351, "maxLat": 45.621, "maxLon": -75.299 },
  "radio_configs": { "906.875,250,10,5": 41200, "910.525,62.5,7,5": 6810, "906.875,62.5,7,5": 200 },
  "grid_squares": [
    {
      "grid_id": "16816_-19718",
      "bounds": {
        "south": 45.4032,
        "west": -75.7177,
        "north": 45.4059,
        "east": -75.7138
      },
      "coverage_type": "BIDIR",
      "fill_color": "#1e7e34",
      "border_color": "#14522d",
      "snr": 8.5,
      "timestamp": 1710547200,
      "count": 14,
      "snr_min": -2.1,
      "snr_max": 11.3,
      "status_mask": 37,
      "first_seen": 1710201600,
      "noise": 17.4,
      "effective": 2.43
    }
  ]
}

Backward compatibility

The fields documented here are additive — every field present in earlier versions of the API (grid_id, bounds, coverage_type, fill_color, border_color, snr, timestamp, and the original top-level fields) is unchanged. Existing integrations continue to work without modification; simply ignore any fields you do not need.

Field Reference

Top-Level Fields

Field Type Description
success boolean true if the request succeeded.
region string The region or group code this key is scoped to.
region_name string Human-readable name of the region (falls back to the code for groups).
grid_size object Grid square dimensions in degrees (lat and lon).
schema_version integer Response schema version. New fields are added without breaking the existing contract.
generated_at integer Unix timestamp of when this response was built (see Caching).
data_age_seconds integer or null Seconds between the most recent ping in the dataset and when the response was built — a freshness indicator. null if the region has no data.
total_squares integer Number of grid squares returned.
point_count integer Total number of pings aggregated across all grid squares.
coverage_type_counts object Number of grid squares per dominant coverage_type.
type_bits object Legend mapping each coverage type to the bit value used in a square's status_mask.
bbox object or null Bounding box covering all returned squares (minLat, minLon, maxLat, maxLon). null if empty.
radio_configs object Ping count per radio configuration seen in this response, most used first. Keys are the app's freqMHz,bwKHz,SF,CR string. Pings with no reported configuration are not counted. Use it to discover which values to filter on (see Filtering). Empty object when nothing reported one.
grid_squares array Array of grid square objects (see below).
repeaters array Only present when ?include=repeaters is set — see Repeater Fields.
filters object Only present on a filtered response: the filters that were applied, typed (see Filtering).

Grid Square Fields

Field Type Description
grid_id string Unique identifier for the grid cell in "latIndex_lonIndex" format.
bounds object Bounding box with south, west, north, east in decimal degrees.
coverage_type string Dominant type for the cell. One of: BIDIR, TX, RX, DISC, DEAD, DROP.
fill_color string Hex fill colour matching MeshMapper's map rendering.
border_color string Hex border colour matching MeshMapper's map rendering.
snr float or null Average signal-to-noise ratio (dB) across the cell's pings, if available.
timestamp integer or null Unix timestamp of the dominant (highest-priority, then newest) ping colouring this square, as stored (seconds; some older regions store milliseconds — values above 2e10 are milliseconds).
count integer Number of pings aggregated into this cell — a confidence/density indicator.
snr_min float or null Lowest SNR (dB) among the cell's pings.
snr_max float or null Highest SNR (dB) among the cell's pings.
status_mask integer Bitmask of all coverage types present in the cell (OR of type_bits). See Cell Quality and Status Mask.
first_seen integer or null Unix timestamp of the oldest ping in the cell, as stored (seconds; some older regions store milliseconds — values above 2e10 are milliseconds).
noise float or null Average noise level (dB above the receiver's noise floor) across the cell's pings.
effective float Average coverage-quality score for the cell, 0–3 (see below).

Repeater Fields

Returned in the top-level repeaters array only when the request includes ?include=repeaters. Each entry describes a repeater known to the region:

Field Type Description
hex string Repeater hex ID (prefix).
name string or null Repeater name.
lat float or null Latitude.
lon float or null Longitude.
last_heard integer or null Unix timestamp the repeater was last heard.
enabled integer 1 = active, 2 = flagged for an ID collision (still listed).
advert_bytes integer or null Advertised path-ID width in bytes.

Coverage Types

Each grid square's coverage_type is the dominant ping in that square — when multiple pings exist, the highest-priority ping wins (listed highest first below). Between pings of equal priority, the newest one wins. Pending pings (status 4) are left out entirely.

Priority Type Colour Description
1 (highest) BIDIR with a heard repeat Green (#1e7e34) Two-way confirmed link.
2 DISC Cyan (#17a2b8) Discovery or trace packet.
3 TX Orange (#fd7e14) Transmitted but not heard back.
4 RX Purple (#6f42c1) Heard traffic, without transmitting.
5 BIDIR with no heard repeat Green (#1e7e34) Two-way link recorded, but no repeat heard.
6 DEAD Grey (#6c757d) Repeater heard but no route.
7 (lowest) DROP Red (#bd2130) No connection.

Cell Quality and Status Mask

coverage_type reflects only the single dominant ping in a square. The effective and status_mask fields summarise the whole mix of pings in the cell.

effective — average quality (0–3)

effective is the mean, over every ping in the cell, of a per-ping quality score:

Ping type Score
BIDIR 3
TX, RX, DISC 2
DEAD 1
DROP 0

A cell that is entirely BIDIR scores 3.0; a cell that is mostly BIDIR with some failed pings scores lower. This makes effective ideal for a smooth red→green quality gradient, where coverage_type alone would only show the single best ping.

status_mask — which types are present

status_mask is a bitwise-OR of the type_bits for every type that appears anywhere in the cell. Use the top-level type_bits legend to decode it:

BIDIR=1  TX=2  RX=4  DISC=8  DEAD=16  DROP=32

status_mask = 37  →  1 (BIDIR) + 4 (RX) + 32 (DROP)
              i.e. the cell contains BIDIR, RX, and DROP pings.

Drawing Grid Squares

Each grid square can be reconstructed as a rectangle using the bounds object:

// Leaflet.js example
data.grid_squares.forEach(sq => {
    L.rectangle(
        [[sq.bounds.south, sq.bounds.west], [sq.bounds.north, sq.bounds.east]],
        {
            fillColor: sq.fill_color,
            color: sq.border_color,
            fillOpacity: 0.6,
            weight: 1
        }
    ).addTo(map);
});

The grid uses fixed cell sizes of 0.0027 degrees latitude by 0.00384 degrees longitude (approximately 300m squares). These match MeshMapper's Simplified Mode rendering.

Filtering

Single-region and group keys accept f_* query parameters that narrow which pings are aggregated into the grid. The grid is then built from the matching pings only, so total_squares, point_count, coverage_type_counts, bbox and radio_configs all describe the filtered set. Filters combine with AND.

Parameter Value Description
f_radio_freq 906.875,250,10,5 Exact radio configuration, the full freqMHz,bwKHz,SF,CR string as the app reports it. Digits, dots and commas, up to 40 characters.
f_freq 906.875 Frequency in MHz. Matches the whole frequency slot, so 906 does not match 906.875 and a kHz value such as 906875 matches nothing.
f_bw 62.5 Bandwidth in kHz.
f_sf 7 Spreading factor, 5 to 12.
f_cr 5 Coding rate, 5 to 8 (4/5 to 4/8).
f_days 30 Only pings from the last N days. 1 to 5 digits, at least 1.
f_dstart 1756684800000 Only pings at or after this time, as a Unix timestamp in milliseconds.
f_dend 1757289600000 Only pings at or before this time, milliseconds.
f_power 20 Transmit power, substring match against the reported value. Letters, digits, dots, spaces and hyphens, up to 40 characters.
f_extant 1 Only pings reported with an external antenna. f_extant=0 is the same as not sending it.

f_freq, f_bw, f_sf and f_cr can be given in any combination. Each one matches its own slot of the stored configuration exactly, so f_freq=910.525&f_bw=62.5&f_sf=7 matches every coding rate on that channel:

GET https://meshmapper.net/coverage.php?key=YOUR_API_KEY&f_freq=910.525&f_bw=62.5&f_sf=7

The response echoes what was applied:

{
  "success": true,
  "region": "YOW",
  "point_count": 6810,
  "radio_configs": { "910.525,62.5,7,5": 6810 },
  "filters": { "freq": "910.525", "bw": "62.5", "sf": "7" },
  "grid_squares": [ "…" ]
}

Things to know:

  • Discover before you filter. Read radio_configs from an unfiltered response to see which configurations a region actually has, with their ping counts. Values must match what the app reported, so 906.875 works and 906.8750 or 906875 does not.
  • Pings with no configuration are excluded by any radio filter. Older app builds did not report one, so a filtered total can be well below the unfiltered point_count even for the region's main channel. radio_configs shows how many pings carry a configuration at all.
  • A region that has never recorded a field returns an empty grid for a filter on that field (zero squares, point_count 0) rather than the full set.
  • Filtered responses are not cached server-side. Every filtered request builds the grid from the region's pings, so it is slower than an unfiltered call and it counts against the daily quota like any other request. Keep filtered polling to the same 15 minute or longer cadence.
  • Empty values are ignored. f_freq= is the same as not sending it.
  • Anything else is an error. An unknown f_ parameter (a typo included) returns HTTP 400 unsupported_filter; a malformed value returns HTTP 400 invalid_filter. Both carry param naming the offending parameter and a message saying what was expected. The API never silently serves the full region in place of a filter it did not understand. These 400 errors still use up a request from your daily limit.
  • Filtering can be briefly unavailable. If the server can't apply filters right now it returns HTTP 503 filters_unavailable; try again later.

Filters are not available on multi-region or global keys. Any f_ parameter on those keys, even an empty one, returns HTTP 400 filters_not_supported.

HTTP Caching and Compression

The API is built for efficient, low-frequency polling. Coverage data does not change second-to-second, so please poll sparingly.

  • Compression. Responses are gzip-compressed. Send Accept-Encoding: gzip (most HTTP clients do this automatically) to receive compressed data — payloads are much smaller.
  • Server-side cache. Responses carry Cache-Control: public, max-age=900 and are cached for up to 15 minutes. Polling more often than that returns identical data (and still counts toward your daily limit), so a poll interval of 15 minutes or longer is recommended. generated_at tells you when the cached data was built. Filtered responses (see Filtering) are built on every request and are not cached on the server. On single-region and group keys, fresh=1 skips the cache and rebuilds now.
  • Conditional requests. Each response includes an ETag (and Last-Modified). Send the ETag value back in an If-None-Match header; if nothing has changed since, you'll get a 304 Not Modified with an empty body, saving you the download.
# First request — note the ETag header
curl -s --compressed -D - "https://meshmapper.net/coverage.php?key=YOUR_API_KEY" -o coverage.json

# Later — only download if the data changed
curl -s --compressed -H 'If-None-Match: "THE_ETAG_VALUE"' \
     "https://meshmapper.net/coverage.php?key=YOUR_API_KEY"

Rate Limits

Each API key has a daily request limit. Self-service keys get 100 requests a day; keys issued by the MeshMapper team may carry a different limit (the 429 body's limit field shows yours). Counters are reset by a daily job at midnight UTC. Every request that reaches your data — including cache hits and 304 Not Modified responses — counts toward this limit.

When you exceed your limit, the API returns HTTP 429:

{
  "success": false,
  "error": "rate_limit_exceeded",
  "message": "Daily request limit reached",
  "limit": 100,
  "used": 100,
  "resets_in_hours": 12.5
}

resets_in_hours is counted from the last reset, so treat it as a rough guide; the actual reset happens at midnight UTC.

A separate per-IP throttle protects against bursts: about 30 requests in a rolling window, then a 120-second lockout. Exceeding it returns HTTP 429 with error: rate_limited and no Retry-After header. Spacing requests out (see Caching) avoids both.

Error Responses

HTTP Code Error Description
400 missing_key No API key provided.
400 invalid_region Region code on key not found.
400 unsupported_filter An f_ parameter that is not in the Filtering table. param names it.
400 invalid_filter A filter value of the wrong shape (for example f_sf=abc or a seconds timestamp in f_dstart). param and message say what was expected.
400 filters_not_supported An f_ parameter (even an empty one) on a multi-region or global key.
400 fresh_not_supported fresh=1 on a multi-region or global key.
400 too_many_regions A multi-region key with more than 6 member regions.
401 invalid_key API key not found, or not a Coverage key (message: Invalid or non-Coverage API key).
403 no_region No region assigned to this key, or the key's region has been deleted.
429 rate_limit_exceeded Daily request limit reached.
429 rate_limited Too many requests in a short period (per-IP throttle).
500 server_error Internal error (database not found, etc.).
503 filters_unavailable Filtering is temporarily unavailable. Try again later.
503 rebuilding Global feed only: a rebuild is in progress and no cached copy exists yet. Sent with Retry-After: 300.
507 over_memory_budget The data is too large to build in memory. Can happen on single regions as well as multi-region keys.

Managing Your Key

If you have a Coverage API key assigned to your admin account, you can view your current usage and regenerate your key from the User Settings tab in your region's Admin Portal. Regenerating a key invalidates the old one immediately.

Multi-Region Keys

A Coverage key can be scoped to a set of up to 6 regions (for example PDX,SEA,YVR) instead of a single region. The response merges every member region's coverage into one grid — the same payload shape as a single-region response — so it suits integrations that render adjacent regions as one continuous map.

Multi-region keys are not self-service: like global keys and keys with a custom limit, they are issued by the MeshMapper team (Master administrators) on request (the admin-panel self-service flow only creates single-region keys). Adjacent regions are the intended use — the merged grid serves them as one map.

Multi-region keys vs. Multiregion Groups

A Multiregion Group merges regions inside MeshMapper — shared map, leaderboards, collision detection, and admin panel. A multi-region key changes nothing about the regions themselves; it only merges their coverage data in this API's response. If a group already exists, a key can simply be scoped to the group's code instead. A multi-region key is for sets of regions that aren't (and shouldn't become) a group.

Response Format

Identical to a single-region response — one merged grid_squares array, same grid-square fields, ?include=repeaters supported (the repeaters array spans all members):

{
  "success": true,
  "region": "PDX,SEA,YVR",
  "region_name": "Portland, US + Seattle, US + Vancouver, CA",
  "grid_size": { "lat": 0.0027, "lon": 0.00384 },
  "schema_version": 2,
  "generated_at": 1710547200,
  "data_age_seconds": 312,
  "total_squares": 4102,
  "point_count": 131877,
  "coverage_type_counts": { "BIDIR": 1620, "TX": 214, "RX": 1467, "DISC": 305, "DEAD": 41, "DROP": 455 },
  "type_bits": { "BIDIR": 1, "TX": 2, "RX": 4, "DISC": 8, "DEAD": 16, "DROP": 32 },
  "bbox": { "minLat": 45.301, "minLon": -123.212, "maxLat": 49.394, "maxLon": -121.751 },
  "radio_configs": { "906.875,250,10,5": 118400, "910.525,62.5,7,5": 13477 },
  "grid_squares": [ "…same grid square objects as a single-region response, all members merged…" ],
  "regions": ["PDX", "SEA", "YVR"],
  "regions_skipped": 0
}

Note that regions and regions_skipped arrive after the grid_squares array — as with the global feed, use a standard JSON parser rather than assuming key order. The fields that differ from a single-region response:

Field Type Description
region string The normalized member set as a CSV, uppercased and sorted (e.g. "PDX,SEA,YVR").
region_name string Member region names joined with +.
regions array The member region codes, sorted — present only on multi-region responses.
regions_skipped integer Members whose region database does not exist yet (skipped, never fatal) — present only on multi-region responses.

Differences from Single-Region Keys

Behaviour Result
fresh=1 HTTP 400, fresh_not_supported — the merged build is cache-driven only.
f_* filter parameters HTTP 400, filters_not_supported.
Any member region unknown or deleted HTTP 400, invalid_region — the API fails closed and never serves a partial set.
More than 6 member regions HTTP 400, too_many_regions.
Region set too large to build in memory HTTP 507, over_memory_budget — ask for the key to be recreated with fewer regions.

If a member region is later renamed, merged, or removed from MeshMapper, the key is updated automatically to follow — a removed region simply drops out of the set.

Caching and Limits

Caching, compression, conditional requests, and the daily quota work exactly as for single-region keys: 15-minute server cache, ETag / If-None-Match for 304 Not Modified, gzip, and the key's daily limit (cache hits and 304s count). Response size scales with the number of member regions — poll at 15-minute intervals or longer.

Global Coverage Feed

A special global Coverage key returns data for every MeshMapper region in one request — no region list to maintain on your side. Global keys are not self-service: they are issued by the MeshMapper team on request, for integrations that genuinely need fleet-wide data (reach out via the usual channels if that's you).

The endpoint and authentication are identical — only the key differs:

GET https://meshmapper.net/coverage.php?key=YOUR_GLOBAL_KEY

Response Format

Instead of a single region's payload, a global key returns an envelope containing one section per region. Each section carries the same fields as a regional response (region, region_name, data_age_seconds, total_squares, point_count, coverage_type_counts, bbox, grid_squares, and repeaters when requested); grid_size and type_bits appear once at the top level since they are identical for every region.

{
  "success": true,
  "global": true,
  "schema_version": 2,
  "generated_at": 1710547200,
  "grid_size": { "lat": 0.0027, "lon": 0.00384 },
  "type_bits": { "BIDIR": 1, "TX": 2, "RX": 4, "DISC": 8, "DEAD": 16, "DROP": 32 },
  "regions": [
    {
      "region": "YOW",
      "region_name": "Ottawa, CA",
      "data_age_seconds": 312,
      "total_squares": 1234,
      "point_count": 48210,
      "coverage_type_counts": { "BIDIR": 540, "TX": 60, "RX": 410, "DISC": 90, "DEAD": 12, "DROP": 122 },
      "bbox": { "minLat": 45.108, "minLon": -76.351, "maxLat": 45.621, "maxLon": -75.299 },
      "grid_squares": [ "…same grid square objects as the regional API…" ]
    }
  ],
  "region_count": 214,
  "regions_skipped": 249
}
Field Type Description
global boolean Always true for a global-key response.
regions array One section per region with coverage data, in region-code order.
region_count integer Number of sections in regions. Note this field arrives after the regions array — use a standard JSON parser rather than assuming key order.
regions_skipped integer Regions omitted because they have no coverage data yet.

?include=repeaters works exactly as for regional keys, adding a repeaters array to each section. Sections do not carry radio_configs or filters. radio_configs belongs to single-region, group and multi-region responses; filters only to filtered single-region and group responses.

Response Size and Pagination

Expect a download in the single-digit megabytes (gzipped); decoded, the JSON can run to tens or low hundreds of megabytes. Size scales with the number of ~300 m grid cells that have coverage — not with raw ping counts — so growth over time is gradual.

There is no pagination or cursor, by design: the response is served as a single pre-built cached file, which is what keeps it fast and cheap for everyone. The per-region sections are the natural chunks instead:

  • Stream-parse rather than loading the whole document. Sections arrive one complete region at a time, in order, so an incremental JSON parser (e.g. ijson in Python) lets you process and discard each region as it arrives — peak memory stays at one region, not the whole world.
  • Poll with If-None-Match. On days where nothing changed you get a 304 Not Modified and transfer nothing (see below).
  • Need finer-grained fetching? Regional keys are the paginated form of this API — one independently cached, independently ETagged response per region. If your integration outgrows the single global document, ask about switching to per-region keys instead of requesting pagination here.

Caching and Timeouts

The global response aggregates the entire fleet, so it is cached more aggressively than regional responses:

  • The server cache lasts 6 hours (Cache-Control: public, max-age=21600). Polling more often returns identical data; once or twice a day is the intended usage.
  • A request that arrives after the cache has expired triggers a rebuild. The response streams region-by-region while it builds — data starts flowing immediately, but the complete download can take a minute. Configure a generous total timeout in your HTTP client (the connection is never idle, so per-read timeouts are fine at their defaults). All other requests are served instantly from cache.
  • ETag / If-None-Match conditional requests work exactly as for regional keys, and a 304 Not Modified is by far the cheapest way to poll. The response that triggers a rebuild carries no ETag; the next (cached) response does.

Differences from Regional Keys

Behaviour Result
fresh=1 HTTP 400, fresh_not_supported — global rebuilds are cache-driven only.
f_* filter parameters HTTP 400, filters_not_supported.
Rebuild already in progress elsewhere Served the previous cached copy; if no cached copy exists yet, HTTP 503 with error: rebuilding and Retry-After: 300 — retry after the indicated delay.