netmapper

API

Everything on the map is also a small JSON API under https://netmapper.app/v1: upload captures, search your networks, and read your stats, your group and where you stand on the leaderboards. This page covers how to authenticate, how to upload, every endpoint, what each status code means, and the limits.

Authentication

Every /v1 route needs a signed-in identity. From a script, that is an API key:

  1. Sign in, then open your account.
  2. Create a key. It is nm_ followed by 40 letters and digits, and it is shown exactly once: it is stored only as a hash, so copy it then or create another.
  3. Send it as a bearer token on every request:
curl -H "Authorization: Bearer $KEY" https://netmapper.app/v1/me

A browser signed in to this site is accepted as well, which is how the map and the upload page use the API. Keys work on the API and on MCP, not on the site's pages. Revoke a key on your account page and it stops working at once. Limits follow your account, not the key, so every key you create shares one allowance.

A request without a valid key or session is 401 with {"error":"unauthorized"} and WWW-Authenticate: Bearer. Everything you upload is private to you: the network routes only ever search your own uploads, and another account's upload or network is a 404, the same as one that does not exist.

Uploading

The easiest way is the Upload page: drop files on it and it follows each one through processing. With a key, post the file as multipart/form-data in a part named file:

curl -H "Authorization: Bearer $KEY" \
     -F file=@WigleWifi_20260920.csv \
     https://netmapper.app/v1/uploads

What is accepted

Inside a file, rows that cannot be used are skipped and counted rather than failing the upload: cell-tower rows, which have no MAC address, rows at latitude and longitude 0, rows with a position accuracy worse than 200 m, and rows whose time cannot be read. A file that is not in WiGLE's format fails as a whole, with the reason in error.

What happens next

The upload answers 202 as soon as the file is stored, with the new upload's id. Processing happens afterwards: poll GET /v1/uploads/{id} until status goes from queued through processing to done or failed. Along the way it counts rows_ok and rows_rejected, and networks_new, the networks nobody had uploaded before, which are your discoveries on the leaderboard.

curl -H "Authorization: Bearer $KEY" https://netmapper.app/v1/uploads/$ID

Duplicates and waiting uploads

Uploading the exact same bytes a second time stores nothing and answers 409, naming the earlier upload:

{
  "error": "duplicate upload",
  "upload_id": "1e2a2254-6c1f-4d6e-9d3a-3f0b8c7e5a91"
}

At most 10 of your uploads may be waiting to be processed at once. The next one is refused with 429 and Retry-After: 30 before any of its body is read, so wait and send it again; the Upload page does that for you. Processing is shared by everyone, and this keeps one long backlog from holding up everybody else's.

Upload answers

StatusWhen
202Stored and queued: {"id", "status"}.
400Not multipart/form-data, no part named file, or a file part without a filename. The error says which.
401No valid key or session.
409You uploaded these exact bytes before; upload_id names that upload.
413Over 200 MB.
429Over the upload rate limit, or too many uploads still waiting. Wait Retry-After seconds.
500, 502Something failed on our side, or storage did; nothing was recorded. Send it again later.

Endpoints

Every route below lives under https://netmapper.app, needs the authentication above, and answers JSON. Query parameters are checked before anything is read: a value outside what is listed is a 400 that names the parameter, except a limit out of range, which is clamped.

POST /v1/uploads

Uploads one capture as multipart/form-data with a part named file that carries a filename. The file is stored and queued, and the answer comes back straight away; processing happens afterwards (see Uploading).

Answers
202, 400, 401, 409, 413, 429, 500, 502
Rate limit
upload: 1 every 20 seconds, bursts of 30
Cache-Control
none
Example answer
{
  "id": "1e2a2254-6c1f-4d6e-9d3a-3f0b8c7e5a91",
  "status": "queued"
}

GET /v1/uploads/{id}

One of your uploads: its status, queued, processing, done or failed, and its counters. Somebody else's id is a 404, never a 403, so ids cannot be probed.

Answers
200, 400, 401, 404, 429
Rate limit
read: 5 per second, bursts of 60
Cache-Control
private, no-store
Example answer
{
  "id": "1e2a2254-6c1f-4d6e-9d3a-3f0b8c7e5a91",
  "filename": "WigleWifi_20260920.csv",
  "size_bytes": 482113,
  "format": "wigle-1.6",
  "status": "done",
  "rows_total": 4838,
  "rows_ok": 4821,
  "rows_rejected": 17,
  "networks_new": 903,
  "networks_updated": 1250,
  "error": null,
  "created_at": "2026-09-20T21:05:00Z",
  "started_at": "2026-09-20T21:05:02Z",
  "finished_at": "2026-09-20T21:05:41Z"
}

GET /v1/networks

Your networks, as JSON. Only networks you uploaded are searched. At least one of bbox, ssid or bssid is required.

ParameterMeaning
bboxwest,south,east,north in WGS 84 degrees. At most 0.25 square degrees; a bigger, inverted or malformed box is a 400
ssidCase-insensitive substring of the network name
bssidExact hardware address
limitNetworks returned: default 500, at most 2,000
include_mobile1 to include networks that move (left out by default)
Answers
200, 400, 401, 429
Rate limit
read: 5 per second, bursts of 60
Cache-Control
private, no-store
Example answer
{
  "count": 1,
  "networks": [
    {
      "bssid": "00:1a:2b:3c:4d:5e",
      "ssid": "Riverside Public",
      "type": "WIFI",
      "auth_mode": "[WPA2-PSK-CCMP][ESS]",
      "channel": 6,
      "frequency": 2437,
      "security": "wpa2",
      "band": "2.4",
      "randomized": false,
      "first_seen": "2026-03-14T09:12:01Z",
      "last_seen": "2026-09-20T21:05:00Z",
      "best_rssi": -52,
      "spread_m": 38.5,
      "mobile": false,
      "obs_count": 17,
      "lat": 39.101234,
      "lon": -94.58321
    }
  ]
}

GET /v1/networks.geojson

The same search as GET /v1/networks, shaped as a GeoJSON FeatureCollection of points, ready for a map. Same parameters.

Answers
200, 400, 401, 429
Rate limit
read: 5 per second, bursts of 60
Cache-Control
private, no-store
Example answer
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [
          -94.58321,
          39.101234
        ]
      },
      "properties": {
        "bssid": "00:1a:2b:3c:4d:5e",
        "ssid": "Riverside Public",
        "type": "WIFI",
        "auth_mode": "[WPA2-PSK-CCMP][ESS]",
        "channel": 6,
        "frequency": 2437,
        "security": "wpa2",
        "band": "2.4",
        "randomized": false,
        "first_seen": "2026-03-14T09:12:01Z",
        "last_seen": "2026-09-20T21:05:00Z",
        "best_rssi": -52,
        "spread_m": 38.5,
        "mobile": false,
        "obs_count": 17
      }
    }
  ]
}

GET /v1/networks/{bssid}

One of your durable network summaries, with observations: [] and observations_retained: false. Readings are deleted after processing. A network you have not uploaded is a 404, whoever else has.

Answers
200, 401, 404, 429
Rate limit
read: 5 per second, bursts of 60
Cache-Control
private, no-store
Example answer
{
  "network": {
    "bssid": "00:1a:2b:3c:4d:5e",
    "ssid": "Riverside Public",
    "type": "WIFI",
    "auth_mode": "[WPA2-PSK-CCMP][ESS]",
    "channel": 6,
    "frequency": 2437,
    "security": "wpa2",
    "band": "2.4",
    "randomized": false,
    "first_seen": "2026-03-14T09:12:01Z",
    "last_seen": "2026-09-20T21:05:00Z",
    "best_rssi": -52,
    "spread_m": 38.5,
    "mobile": false,
    "obs_count": 17,
    "lat": 39.101234,
    "lon": -94.58321
  },
  "observations": [],
  "observations_retained": false
}

GET /v1/coverage.geojson

Your fixed networks binned into hexagons, for a zoomed-out map: only non-empty hexagons, each with a count. edge_m is the hexagon edge in EPSG:3857 metres.

ParameterMeaning
bboxRequired. west,south,east,north; no area cap, the hexagons grow to fit
zRequired. The map zoom, 0 to 11
Answers
200, 400, 401, 429
Rate limit
read: 5 per second, bursts of 60
Cache-Control
private, no-store
Example answer
{
  "type": "FeatureCollection",
  "edge_m": 1528.7,
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [
              -94.6034,
              39.0917
            ],
            [
              -94.5897,
              39.0917
            ],
            [
              -94.5829,
              39.1011
            ],
            [
              -94.5897,
              39.1104
            ],
            [
              -94.6034,
              39.1104
            ],
            [
              -94.6103,
              39.1011
            ],
            [
              -94.6034,
              39.0917
            ]
          ]
        ]
      },
      "properties": {
        "count": 37
      }
    }
  ]
}

GET /v1/stats

Platform-wide counts of networks, observations, uploads and accounts. The same for everyone, refreshed every 30 seconds.

Answers
200, 401, 429
Rate limit
read: 5 per second, bursts of 60
Cache-Control
private, max-age=30
Example answer
{
  "networks": 184220,
  "observations": 5120933,
  "uploads": 812,
  "users": 57
}

GET /v1/stats/platform

Platform totals, a daily series and breakdowns by type, security, band, channel, manufacturer, device and app. The same for everyone, recomputed at most every five minutes.

ParameterMeaning
daysLength of the daily series: 30, 90 (default) or 365
Answers
200, 400, 401, 429
Rate limit
read: 5 per second, bursts of 60
Cache-Control
private, max-age=30
Example answer
{
  "generated_at": "2026-09-20T21:05:00Z",
  "totals": {
    "networks": 184220,
    "wifi": 170311,
    "bluetooth": 13909,
    "discoveries": 184220,
    "observations": 5120933,
    "uploads": 812,
    "contributors": 57
  },
  "series": {
    "from": "2026-09-26",
    "to": "2026-09-28",
    "days": [
      {
        "day": "2026-09-26",
        "discoveries": 0,
        "new_networks": 0,
        "observations": 0
      },
      {
        "day": "2026-09-27",
        "discoveries": 41,
        "new_networks": 88,
        "observations": 2310
      },
      {
        "day": "2026-09-28",
        "discoveries": 12,
        "new_networks": 20,
        "observations": 604
      }
    ]
  },
  "breakdowns": {
    "type": [
      {
        "bucket": "WIFI",
        "label": "Wi-Fi",
        "count": 1987,
        "share": 0.923
      },
      {
        "bucket": "BLE",
        "label": "Bluetooth LE",
        "count": 166,
        "share": 0.077
      }
    ],
    "security": [
      {
        "bucket": "wpa2",
        "label": "WPA2",
        "count": 1520,
        "share": 0.765
      },
      {
        "bucket": "open",
        "label": "Open",
        "count": 467,
        "share": 0.235
      }
    ],
    "band": [
      {
        "bucket": "2.4",
        "label": "2.4 GHz",
        "count": 1290,
        "share": 0.649
      },
      {
        "bucket": "5",
        "label": "5 GHz",
        "count": 697,
        "share": 0.351
      }
    ],
    "channel": [
      {
        "bucket": "6",
        "label": "6",
        "count": 540,
        "share": 0.272
      },
      {
        "bucket": "other",
        "label": "Other",
        "count": 1447,
        "share": 0.728
      }
    ],
    "manufacturers": [
      {
        "bucket": "Netgear",
        "label": "Netgear",
        "count": 212,
        "share": 0.141
      }
    ],
    "devices": [
      {
        "bucket": "google|Pixel 7",
        "label": "Google Pixel 7",
        "count": 6,
        "share": 1
      }
    ],
    "apps": [
      {
        "bucket": "2.78",
        "label": "WiGLE 2.78",
        "count": 6,
        "share": 1
      }
    ]
  },
  "randomized_share": 0.18,
  "top_ssids": [
    {
      "ssid": "xfinitywifi",
      "networks": 1204
    }
  ],
  "k_threshold": 5
}

GET /v1/me

Everything about you in one read: profile; stats (totals, rank, area, series); your group with its members ranked against each other; standings, where you and your group stand on both boards for every period and category (null where there is nothing to stand on); and leaderboards, the top of both boards for the period and category asked for. A hidden account sees its private rank. The group's invite code is never included.

ParameterMeaning
daysLength of the daily series: 30, 90 (default) or 365
periodweek (the current ISO week, from Monday), month (the calendar month) or all (default). UTC
categoryall (default), wifi or bluetooth
limitRows of each board: default 10, 0 to 100; 0 leaves the rows out and keeps the standings
Answers
200, 400, 401, 404, 429
Rate limit
expensive: 1 every 2 seconds, bursts of 10
Cache-Control
private, no-store
Example answer
{
  "profile": {
    "handle": "driver-00c0de",
    "hidden": false,
    "member_since": "2026-03"
  },
  "stats": {
    "totals": {
      "networks": 2153,
      "wifi": 1987,
      "bluetooth": 166,
      "discoveries": 903,
      "observations": 48210,
      "uploads": 6
    },
    "rank": {
      "rank": 2,
      "of": 41,
      "discoveries": 903,
      "listed": true
    },
    "area_km2": 37,
    "series": {
      "from": "2026-09-26",
      "to": "2026-09-28",
      "days": [
        {
          "day": "2026-09-26",
          "discoveries": 0,
          "new_networks": 0,
          "observations": 0
        },
        {
          "day": "2026-09-27",
          "discoveries": 41,
          "new_networks": 88,
          "observations": 2310
        },
        {
          "day": "2026-09-28",
          "discoveries": 12,
          "new_networks": 20,
          "observations": 604
        }
      ]
    }
  },
  "group": {
    "id": 3,
    "name": "Night Owls",
    "role": "member",
    "joined": "2026-04",
    "members": 4,
    "hidden_members": 1,
    "totals": {
      "networks": 5120,
      "wifi": 4710,
      "bluetooth": 410,
      "discoveries": 2107,
      "observations": 120331,
      "uploads": 19
    },
    "ranked": [
      {
        "rank": 1,
        "handle": "driver-a1b2c3",
        "role": "owner",
        "discoveries": 1204,
        "you": false
      },
      {
        "rank": 2,
        "handle": "driver-00c0de",
        "role": "member",
        "discoveries": 903,
        "you": true
      },
      {
        "rank": 0,
        "handle": "driver-fresh1",
        "role": "member",
        "discoveries": 0,
        "you": false
      }
    ],
    "you": {
      "rank": 2,
      "of": 2,
      "discoveries": 903,
      "listed": true
    }
  },
  "standings": {
    "users": {
      "week": {
        "all": {
          "rank": 2,
          "of": 41,
          "discoveries": 903,
          "listed": true
        },
        "wifi": {
          "rank": 2,
          "of": 39,
          "discoveries": 850,
          "listed": true
        },
        "bluetooth": {
          "rank": 5,
          "of": 12,
          "discoveries": 53,
          "listed": true
        }
      },
      "month": {
        "all": {
          "rank": 2,
          "of": 41,
          "discoveries": 903,
          "listed": true
        },
        "wifi": {
          "rank": 2,
          "of": 39,
          "discoveries": 850,
          "listed": true
        },
        "bluetooth": {
          "rank": 5,
          "of": 12,
          "discoveries": 53,
          "listed": true
        }
      },
      "all": {
        "all": {
          "rank": 2,
          "of": 41,
          "discoveries": 903,
          "listed": true
        },
        "wifi": {
          "rank": 2,
          "of": 39,
          "discoveries": 850,
          "listed": true
        },
        "bluetooth": {
          "rank": 5,
          "of": 12,
          "discoveries": 53,
          "listed": true
        }
      }
    },
    "groups": {
      "week": {
        "all": null,
        "wifi": null,
        "bluetooth": null
      },
      "month": {
        "all": null,
        "wifi": null,
        "bluetooth": null
      },
      "all": {
        "all": {
          "rank": 1,
          "of": 7,
          "discoveries": 2107,
          "listed": true
        },
        "wifi": null,
        "bluetooth": null
      }
    }
  },
  "leaderboards": {
    "users": {
      "board": "users",
      "period": "week",
      "category": "all",
      "from": "2026-09-28",
      "users": [
        {
          "rank": 1,
          "handle": "driver-a1b2c3",
          "group": {
            "id": 3,
            "name": "Night Owls"
          },
          "discoveries": 1204,
          "you": false
        },
        {
          "rank": 2,
          "handle": "driver-00c0de",
          "group": {
            "id": 3,
            "name": "Night Owls"
          },
          "discoveries": 903,
          "you": true
        }
      ],
      "groups": null,
      "me": {
        "rank": 2,
        "of": 41,
        "discoveries": 903,
        "listed": true
      }
    },
    "groups": {
      "board": "groups",
      "period": "week",
      "category": "all",
      "from": "2026-09-28",
      "users": null,
      "groups": [
        {
          "rank": 1,
          "id": 3,
          "name": "Night Owls",
          "members": 4,
          "discoveries": 2107,
          "you": true
        },
        {
          "rank": 2,
          "id": 9,
          "name": "Ring Road",
          "members": 2,
          "discoveries": 380,
          "you": false
        }
      ],
      "me": {
        "rank": 1,
        "of": 7,
        "discoveries": 2107,
        "listed": true
      }
    }
  }
}

GET /v1/stats/me

Your own totals, all-time rank, area explored, daily series and breakdowns. Never cached, so a new upload shows at once.

ParameterMeaning
daysLength of the daily series: 30, 90 (default) or 365
Answers
200, 400, 401, 404, 429
Rate limit
expensive: 1 every 2 seconds, bursts of 10
Cache-Control
private, no-store
Example answer
{
  "totals": {
    "networks": 2153,
    "wifi": 1987,
    "bluetooth": 166,
    "discoveries": 903,
    "observations": 48210,
    "uploads": 6
  },
  "handle": "driver-00c0de",
  "hidden": false,
  "member_since": "2026-03",
  "group": {
    "id": 3,
    "name": "Night Owls"
  },
  "rank": {
    "rank": 2,
    "of": 41,
    "discoveries": 903,
    "listed": true
  },
  "area_km2": 37,
  "series": {
    "from": "2026-09-26",
    "to": "2026-09-28",
    "days": [
      {
        "day": "2026-09-26",
        "discoveries": 0,
        "new_networks": 0,
        "observations": 0
      },
      {
        "day": "2026-09-27",
        "discoveries": 41,
        "new_networks": 88,
        "observations": 2310
      },
      {
        "day": "2026-09-28",
        "discoveries": 12,
        "new_networks": 20,
        "observations": 604
      }
    ]
  },
  "breakdowns": {
    "type": [
      {
        "bucket": "WIFI",
        "label": "Wi-Fi",
        "count": 1987,
        "share": 0.923
      },
      {
        "bucket": "BLE",
        "label": "Bluetooth LE",
        "count": 166,
        "share": 0.077
      }
    ],
    "security": [
      {
        "bucket": "wpa2",
        "label": "WPA2",
        "count": 1520,
        "share": 0.765
      },
      {
        "bucket": "open",
        "label": "Open",
        "count": 467,
        "share": 0.235
      }
    ],
    "band": [
      {
        "bucket": "2.4",
        "label": "2.4 GHz",
        "count": 1290,
        "share": 0.649
      },
      {
        "bucket": "5",
        "label": "5 GHz",
        "count": 697,
        "share": 0.351
      }
    ],
    "channel": [
      {
        "bucket": "6",
        "label": "6",
        "count": 540,
        "share": 0.272
      },
      {
        "bucket": "other",
        "label": "Other",
        "count": 1447,
        "share": 0.728
      }
    ],
    "manufacturers": [
      {
        "bucket": "Netgear",
        "label": "Netgear",
        "count": 212,
        "share": 0.141
      }
    ],
    "devices": [
      {
        "bucket": "google|Pixel 7",
        "label": "Google Pixel 7",
        "count": 6,
        "share": 1
      }
    ],
    "apps": [
      {
        "bucket": "2.78",
        "label": "WiGLE 2.78",
        "count": 6,
        "share": 1
      }
    ]
  },
  "randomized_share": 0.21
}

GET /v1/leaderboard

One discovery board, with your own standing on it (me). A network is credited to the first account to upload it.

ParameterMeaning
boardusers (default) or groups
periodweek (the current ISO week, from Monday), month (the calendar month) or all (default). UTC
categoryall (default), wifi or bluetooth
limitRows: default 50, clamped to 1 to 100
Answers
200, 400, 401, 429
Rate limit
read: 5 per second, bursts of 60
Cache-Control
private, no-store
Example answer
{
  "board": "users",
  "period": "week",
  "category": "all",
  "from": "2026-09-28",
  "users": [
    {
      "rank": 1,
      "handle": "driver-a1b2c3",
      "group": {
        "id": 3,
        "name": "Night Owls"
      },
      "discoveries": 1204,
      "you": false
    },
    {
      "rank": 2,
      "handle": "driver-00c0de",
      "group": {
        "id": 3,
        "name": "Night Owls"
      },
      "discoveries": 903,
      "you": true
    }
  ],
  "groups": null,
  "me": {
    "rank": 2,
    "of": 41,
    "discoveries": 903,
    "listed": true
  }
}

GET /healthz

The one open route: no authentication. {"ok":true}, or 503 when the database cannot be reached.

Status codes and errors

Every error from /v1 is JSON of one shape, {"error":"…"}, with a message written for people; the 409 adds upload_id. The exception is a path or method the API does not have, which gets a plain-text 404 or 405.

StatusNameMeaning
200OKThe read worked.
202AcceptedThe upload is stored and queued: {"id", "status"}. Poll GET /v1/uploads/{id} for the outcome.
400Bad RequestA parameter or the upload body is malformed. The error names what to fix: days must be 30, 90 or 365, missing multipart part named "file".
401UnauthorizedNo valid API key or session. The body is {"error":"unauthorized"} with WWW-Authenticate: Bearer.
404Not FoundThe upload or network is not yours, or does not exist; the two are the same answer on purpose. An unknown path is a plain-text 404.
405Method Not AllowedA known path with the wrong method, such as GET /v1/uploads. Plain text, with an Allow header.
409ConflictYou already uploaded these exact bytes. Nothing is stored; upload_id names the earlier upload.
413Content Too LargeThe upload is over the size limit. Split the capture, or gzip or zip it.
429Too Many RequestsOver a rate limit, or too many of your uploads are still waiting to be processed. Wait Retry-After seconds and send the same request again.
500Internal Server ErrorSomething failed on our side. It is logged; try again later.
502Bad GatewayThe upload could not be written to storage. Nothing was recorded; send it again.
503Service Unavailable/healthz only: the database cannot be reached.

Rate limits

Limits are token buckets: each holds a burst of requests you may spend at once, and refills at a steady pace. Limits per account apply behind authentication, and follow the account whichever key or session it uses; limits per address apply to every request before it is authenticated.

BucketPerLimitCovers
Readaccount5 per second, bursts of 60Every /v1 GET not listed as expensive
Expensiveaccount1 every 2 seconds, bursts of 10GET /v1/me and GET /v1/stats/me, which run many queries per call
Uploadaccount1 every 20 seconds, bursts of 30POST /v1/uploads
MCPaccount5 per second, bursts of 60Every request to /mcp
Floodaddress20 per second, bursts of 200Every request of any kind, pages included
Bad API keysaddress1 every 30 seconds, bursts of 20Requests whose nm_ key does not exist

Every admitted /v1 and /mcp request carries where you stand, so you can pace yourself before you are refused:

Over a limit, the answer is 429 with Retry-After, in seconds, and Cache-Control: no-store:

HTTP/1.1 429 Too Many Requests
Retry-After: 2
Content-Type: application/json; charset=utf-8

{"error":"rate limit exceeded; retry in 2 seconds"}

Wait that long and send the same request again. An address that presents too many API keys that do not exist is refused for a while before any key it sends is checked.

Separately, at most 10 of your uploads may be waiting to be processed at once (see Uploading).

Two more bounds apply to everyone: a read gives up after 20 seconds, and a network search covers at most 0.25 square degrees and 2,000 networks.

Caching

Answers about you (your networks, coverage, uploads, stats, /v1/me and the leaderboard, which marks your row) are Cache-Control: private, no-store: a browser's cache is keyed by address, not by who is signed in, and must never hand one account's answer to the next. /v1/stats and /v1/stats/platform are the same for everyone and may be kept for 30 seconds. There is no cross-origin access: the API sends no Access-Control-Allow-Origin.

MCP

https://netmapper.app/mcp speaks the Model Context Protocol over Streamable HTTP, with the same API keys, so an assistant can search your networks and check your uploads. Its tools are read-only: search_networks, get_network, upload_status, list_my_uploads and stats. To add it to Claude Code:

claude mcp add --transport http netmapper https://netmapper.app/mcp \
  --header "Authorization: Bearer $KEY"