Partner coverage API
FreqMapper can give an approved third-party application a read-only combined coverage stream or the original type-specific feeds. Events always remain explicitly labelled so verified TX proof and passive RX evidence cannot be confused:
| Feed | Meaning |
|---|---|
/api/v1/integrations/coverage-events |
Preferred. One cursor-paginated stream containing both event types. Every event has event_type: verified_tx or event_type: passive_rx. |
/api/v1/integrations/verified-coverage |
An approved independent Watcher received a tagged FreqMapper mapping test. Every event is binary verified TX coverage. |
/api/v1/integrations/received-coverage |
The connected wardriving radio heard an eligible Meshtastic packet at the reported driving location. Signal quality and privacy-safe Watcher corroboration are included, but this is never verified TX coverage. |
The same bearer key and assigned region scope apply to all three feeds. A combined-feed cursor belongs only to that feed. Applications using the two older feeds must continue to save a separate cursor for each.
Use your API key
The production API is available at:
https://freqmapper.net
Use this production hostname for all partner API requests.
FreqMapper will provide the application with a bearer key. Store the key as a server-side secret and send it only in the HTTPS Authorization header:
Authorization: Bearer fm_live_REPLACE_WITH_THE_ISSUED_KEY
Keys are never accepted in a query string.
The receiving application should keep the key in its server-side secret or environment configuration. Do not embed it in browser JavaScript, a mobile application, screenshots, public configuration, source control, or support logs.
Preferred combined feed
Use this endpoint when one application needs both TX and RX evidence:
curl --fail-with-body \
--header "Authorization: Bearer $FREQMAPPER_PARTNER_TOKEN" \
"https://freqmapper.net/api/v1/integrations/coverage-events?limit=500"
The envelope has one opaque next_cursor. Events are ordered by published_at, then their type and source UUID. Save the cursor only after processing the complete page.
Every event contains these common fields:
| Field | Meaning |
|---|---|
event_id |
Globally discriminated deduplication key, prefixed with verified_tx: or passive_rx:. |
event_type |
verified_tx or passive_rx. Branch on this before reading type-specific fields. |
published_at |
Server ordering time used by the opaque cursor. Do not use it as the RF event time. |
occurred_at |
Mapping-test send time for TX or driving-radio reception time for RX. |
latitude, longitude |
The wardriving phone/radio location where the TX test was sent or RX packet was heard. |
region_iata, radio_node_id |
FreqMapper region and enrolled wardriving radio. |
watcher_count |
Distinct approved Watchers associated with this evidence. |
same_region_watcher_count, cross_region_watcher_count |
Privacy-safe regional split. Verified TX currently requires same-region Watchers, so its cross-region count is zero. |
A response can contain both shapes:
{
"schema_version": 1,
"generated_at": "2026-09-08T20:00:00Z",
"feed": "combined_coverage_events",
"event_types": ["verified_tx", "passive_rx"],
"rx_comparison_window_seconds": 30,
"rx_publication_delay_seconds": 60,
"has_more": false,
"next_cursor": "opaque-combined-value",
"events": [
{
"event_id": "verified_tx:a6f33a9a-d94b-46a1-9285-7b121aa6e528",
"event_type": "verified_tx",
"published_at": "2026-09-08T19:58:14Z",
"occurred_at": "2026-09-08T19:57:55Z",
"latitude": 40.76078,
"longitude": -111.89105,
"region_iata": "SLC",
"radio_node_id": "!a1b2c3d4",
"watcher_count": 2,
"same_region_watcher_count": 2,
"cross_region_watcher_count": 0,
"verification_id": "a6f33a9a-d94b-46a1-9285-7b121aa6e528",
"verified_at": "2026-09-08T19:58:14Z",
"mapping_test_sent_at": "2026-09-08T19:57:55Z",
"watcher_corroborated": true,
"evidence_type": "independent_watcher_verified",
"verified": true,
"verified_coverage": true
},
{
"event_id": "passive_rx:f1df7296-12a0-4c32-a85b-262b6dd44bcb",
"event_type": "passive_rx",
"published_at": "2026-09-08T19:59:14Z",
"occurred_at": "2026-09-08T19:58:14Z",
"latitude": 40.76079,
"longitude": -111.89106,
"region_iata": "SLC",
"radio_node_id": "!a1b2c3d4",
"watcher_count": 3,
"same_region_watcher_count": 2,
"cross_region_watcher_count": 1,
"reception_id": "f1df7296-12a0-4c32-a85b-262b6dd44bcb",
"received_at": "2026-09-08T19:58:14Z",
"location_at": "2026-09-08T19:58:13Z",
"location_accuracy_meters": 7.2,
"location_trust": "platform_unflagged",
"packet_type": "position",
"portnum": 3,
"rssi_dbm": -96.4,
"snr_db": 7.25,
"quality": "strong",
"hop_count": 0,
"path_classification": "direct",
"last_relay_node": null,
"watcher_corroborated": true,
"evidence_type": "device_receive",
"verified_coverage": false
}
]
}
RX publication waits 60 seconds from server acceptance. This covers the 30-second comparison window and allows normal Watcher delivery time. The cursor uses server acceptance rather than received_at, so a phone uploading an older offline reception does not place that event behind an already saved cursor.
Consumer processing
An integration can consume both evidence types with one polling loop and one combined cursor:
- Deduplicate using
event_id. - Apply the configured date gate to
occurred_at. - Associate
radio_node_idwith the consumer's existing radio record, if needed. - Validate and apply
latitudeandlongitudeto the consumer's own map or grid. - Branch on
event_type. Treatverified_txas verified mapping-test coverage. Treatpassive_rxas traffic heard by the wardriving radio, with optional confidence from the Watcher counts andquality.
Both event types may appear on the same map, but passive RX must not be relabelled as verified TX. A practical display priority is verified TX over passive RX, with passive RX allowed to fill an untested or negative area.
Verified TX coverage
An event appears only after an approved independent Watcher receives the exact tagged packet sent during a FreqMapper wardrive. The phone's mapping-test position is the coverage location.
The feed deliberately does not include:
- ordinary Meshtastic or MQTT Position Packets;
- a wardriver's route or background GPS samples;
- Watcher identities or locations;
- RSSI, SNR, signal grades, or individual Watcher measurements;
- failed, pending, simulated, or not-heard mapping tests.
Software-simulated phone locations and disqualified trips never appear, even if a packet receipt exists.
Make a verified TX request
The application makes an HTTPS GET request to:
/api/v1/integrations/verified-coverage
Every request must contain its bearer key:
GET /api/v1/integrations/verified-coverage?limit=500 HTTP/1.1
Host: freqmapper.net
Authorization: Bearer fm_live_REPLACE_WITH_THE_ISSUED_KEY
Accept: application/json
An absent, expired, or revoked key returns 401. An unknown or inactive region code returns 404; an existing active region outside the key's assigned scope returns 403. An invalid cursor or parameter returns 422. Only an allowed, active region with no matching events returns an empty 200 response.
Check both the HTTP status and the JSON response. A successful feed response has Content-Type: application/json, schema_version: 1, and an events array. HTML is not an API response, even if its status is 200. The production and documentation-only hostnames return JSON 404 for API paths until they support those routes.
Read the verified TX feed
curl --fail-with-body \
--header "Authorization: Bearer $FREQMAPPER_PARTNER_TOKEN" \
"https://freqmapper.net/api/v1/integrations/verified-coverage?limit=500"
An optional region_iata=SLC filter is allowed only when the key includes that region. limit may be 1–1000 and defaults to 500.
{
"schema_version": 1,
"generated_at": "2026-09-01T19:30:00Z",
"coverage_rule": "independent_watcher_verified",
"has_more": false,
"next_cursor": "opaque-value",
"events": [
{
"verification_id": "a6f33a9a-d94b-46a1-9285-7b121aa6e528",
"verified_at": "2026-09-01T19:28:14Z",
"mapping_test_sent_at": "2026-09-01T19:27:55Z",
"latitude": 40.76078,
"longitude": -111.89105,
"region_iata": "SLC",
"radio_node_id": "!a1b2c3d4",
"watcher_count": 2,
"verified": true
}
]
}
verification_id is stable and should be the partner's deduplication key when using this TX-only feed. radio_node_id identifies the wardriver's enrolled Meshtastic radio so an integration can associate the evidence with its own radio record. It is not a Watcher or MQTT gateway ID. watcher_count is the number of distinct approved Watchers that verified this mapping test; identities and individual measurements remain private.
Coordinates, not map cells
Each event is evidence at the reported mapping-test coordinate, not a claim that an entire cell has coverage. Apply the latitude and longitude to your application's own grid. This feed intentionally does not include FreqMapper map-cell indices or polygons; do not depend on the browser map endpoint as a substitute for this integration contract.
FreqMapper's detailed coverage display uses approximately 100 m cells; its overview uses approximately 300 m cells. Position Packet coverage remains a separate evidence source and is not included in this controlled-test feed. These coverage cells are display grids, not equal-area measurements: their fixed angular lattice is calibrated near latitude 40 degrees, so physical widths vary with latitude. These display choices do not change the coordinates returned by the API.
Passive RX coverage
The passive RX feed returns accepted Received here events from an active FreqMapper wardrive. The coordinates are where the driving phone/radio heard the packet, not the sender's reported position and not a Watcher's location.
FreqMapper accepts only eligible text, position, node-info, or telemetry packets heard on any configured channel the driving radio can decode, with a fresh, accurate, non-simulated phone location and plausible radio metadata. Mapping-test packets, self echoes, MQTT-path reports, unsupported packet types, stale locations, and disqualified trips are excluded.
Make an authenticated request with the same key:
curl --fail-with-body \
--header "Authorization: Bearer $FREQMAPPER_PARTNER_TOKEN" \
"https://freqmapper.net/api/v1/integrations/received-coverage?limit=500"
The optional region_iata and limit parameters behave exactly as they do on the verified TX feed.
{
"schema_version": 1,
"generated_at": "2026-09-07T19:30:00Z",
"coverage_rule": "driving_radio_received",
"watcher_comparison_rule": "same_sender_and_packet_id_within_30_seconds",
"comparison_window_seconds": 30,
"has_more": false,
"next_cursor": "opaque-rx-value",
"events": [
{
"reception_id": "f1df7296-12a0-4c32-a85b-262b6dd44bcb",
"received_at": "2026-09-07T19:28:14Z",
"location_at": "2026-09-07T19:28:13Z",
"location_accuracy_meters": 7.2,
"location_trust": "platform_unflagged",
"latitude": 40.76078,
"longitude": -111.89105,
"region_iata": "SLC",
"radio_node_id": "!a1b2c3d4",
"packet_type": "position",
"portnum": 3,
"rssi_dbm": -96.4,
"snr_db": 7.25,
"quality": "strong",
"hop_count": 0,
"path_classification": "direct",
"last_relay_node": null,
"watcher_corroborated": true,
"watcher_count": 3,
"same_region_watcher_count": 2,
"cross_region_watcher_count": 1,
"evidence_type": "device_receive",
"verified_coverage": false
}
]
}
RX field meanings
| Field | Meaning |
|---|---|
reception_id |
Stable deduplication ID for this accepted reception. It does not expose the trip or installation ID. |
received_at |
Time the connected driving radio reported receiving the packet. |
location_at |
Time of the phone location fix paired with the reception. |
location_accuracy_meters |
Reported phone horizontal accuracy. Accepted values are no greater than 50 m. |
location_trust |
Always platform_unflagged; software-simulated locations are rejected before storage. |
latitude, longitude |
Driving radio's reception location. These are not the sender's coordinates. |
radio_node_id |
Enrolled Meshtastic node used by the wardriver, suitable for associating evidence with the mapper. |
packet_type, portnum |
Eligible Meshtastic application type: text/1, position/3, node_info/4, or telemetry/67. Packet contents are never returned. |
rssi_dbm, snr_db |
Signal measured by the driving radio. snr_db can be null. These are not Watcher measurements. |
quality |
strong, fair, or weak, calculated from the weaker available RSSI/SNR band. It describes the received signal, not delivery probability. |
hop_count |
Header-derived hop estimate when available; otherwise null. |
path_classification |
direct, relayed, or unknown, derived only from hop_count. It is not a complete route. |
last_relay_node |
Optional one-byte last-relay header hint. It is not a full node identity and can be null. |
watcher_corroborated |
Whether at least one distinct approved Watcher reported the same sender/packet identity within the comparison window. |
watcher_count |
Total distinct corroborating approved Watchers. |
same_region_watcher_count |
Corroborating Watchers assigned to the reception's region. |
cross_region_watcher_count |
Corroborating Watchers assigned only to another active region. This can help sparse regions without changing the RX meaning. |
verified_coverage |
Always false. RX evidence must never be mixed into the verified TX feed or treated as independent mapping-test proof. |
The comparison says the driving radio and a Watcher reported the same packet identity near the same time. It does not prove a common RF path, the driver's distance from a Watcher, the sender's position, or that the Watcher heard the driving radio. Zero Watcher matches does not invalidate the reception; it means only that the RX event was not corroborated by an approved Watcher.
The partner feed deliberately omits sender IDs, packet IDs, message text or payloads, Watcher identities and measurements, trip and installation IDs, routes, channel keys, and raw packet bytes. Those identifiers may be used privately for matching but are not returned.
Cursor polling
For the preferred combined stream, save and return only its combined cursor:
https://freqmapper.net/api/v1/integrations/coverage-events?cursor=THE_SAVED_COMBINED_CURSOR&limit=500
The first request without a cursor reads the selected feed's existing history from oldest to newest. Process the events, save that feed's next_cursor, and request its next page:
https://freqmapper.net/api/v1/integrations/verified-coverage?cursor=THE_SAVED_CURSOR&limit=500
For passive RX, use its separately saved cursor:
https://freqmapper.net/api/v1/integrations/received-coverage?cursor=THE_SAVED_RX_CURSOR&limit=500
Continue while has_more is true, respecting the request budget below. When it becomes false, retain the returned cursor and use it on later polls. An empty page still returns a usable cursor. Polling every 30–60 seconds is normally enough for a live map.
Never derive a negative result from an empty response. It means only that no new events exist in that selected feed after its cursor.
Request limits and retries
Keys default to 120 requests per 60-second window, but your key may have a different assigned limit. Read X-RateLimit-Limit rather than hard-coding 120. The budget is shared across the key's regions and all processes using that key. The window starts with the first request after the previous window has expired. Bursts are allowed within that budget, including paginated history downloads. Each authenticated request consumes budget, including requests with invalid filters or parameters. Use one sequential pagination loop and up to limit=1000 events per request for large imports.
Successful responses and 429 responses include:
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
This key's current maximum requests per window (default 120). |
X-RateLimit-Remaining |
Remaining requests in the current window. |
X-RateLimit-Reset |
Window expiry as a Unix timestamp in seconds. |
If the budget is exhausted, the API returns JSON 429 Too Many Requests with Retry-After containing the number of seconds to wait. Wait at least that long, then retry the same URL and cursor. Do not reset or advance your saved cursor on an error. Rejected requests do not extend the window.
For network failures or temporary 5xx errors, retry the same request with increasing delays (for example 5, 10, 20, then 60 seconds). Save a new cursor only after successfully processing its page. Deduplicate verified TX with verification_id and passive RX with reception_id. Do not repeatedly retry 401, 403, 404, or 422 without correcting the key, host, region, or request.
When reporting a problem, include the response's X-Request-ID header if present, the time, and HTTP status. Never include your bearer key. Requests rejected before reaching the API may not include a request ID.
Type-specific feed compatibility
Older integrations can consume both type-specific feeds without confusing their evidence:
- A verified TX event means “independent Watcher-verified mapping-test coverage exists at this coordinate.” It can use
verification_idfor deduplication and apply its own display or processing rules. - A passive RX event means “this enrolled driving radio heard eligible mesh traffic here with this measured signal.” It can use
reception_idfor deduplication and the quality/corroboration fields for a separate RX map or mechanic.
Passive RX must not be promoted to verified TX, even when several Watchers corroborate it. Neither feed supports inferring continuous road coverage or a failed/red result from the absence of events.
New integrations that need both evidence types should normally use the combined endpoint above instead.
Last updated: 2026-09-08