Stop paying per request for WiFi geolocation. This Cloudflare Worker is a free, production-ready alternative to paid providers like Google Geolocation API.
It proxies Apple’s WiFi positioning service, accepts one or many BSSIDs, validates inputs, and can compute a weighted-centroid fix when you include RSSI values.
Add optional reverse geocoding and a smart auto-upgrade that retries broader lookups, and you get a fast, low-latency API that runs on the Cloudflare Free plan.
Why this project
If your app or device can observe nearby WiFi access points, you can approximate location without GPS.
Commercial APIs often charge per call and require keys, rate planning, and billing.
This Worker runs at the edge on Cloudflare, calling Apple’s WiFi database and returning coordinates with an optional human-readable address.
For multi-AP inputs with signal strengths, it computes a weighted centroid to estimate the device position more accurately.
You can use it in an MDM like as a Jamf Extension Attribute or script to trigger actions depending on where a device is etc.
Key features
- HTTP GET and POST for single or batched BSSID lookups
- Normalization and validation of BSSID and signal values before upstream queries
- Signal sample summarization when multiple RSSI readings are provided
- Weighted-centroid triangulation when you send several BSSIDs with RSSI
- Fallback to Cloudflare IP geolocation when Apple has no data
- Reverse geocoding via OpenStreetMap Nominatim (no keys), with smart throttling
- Smart auto-upgrade: if an exact BSSID match returns nothing, it retries with a broader query
- Zero server management thanks to Cloudflare Workers and Wrangler
Public repo
Code, examples, and configuration live here: wifi-geolocate-worker on GitHub
How it compares to paid geolocation APIs
This Worker is a cost-saving alternative to:
- Google Geolocation API (WiFi plus cell; paid, API key, per-request billing)
- HERE Positioning (WiFi positioning service; paid tiers)
- Skyhook (legacy pioneer of WiFi positioning; commercial plans)
- Unwired Labs (WiFi and cell geolocation; paid tiers)
- Mozilla Location Service MLS (community database, usage limits apply)
With Cloudflare’s generous free tier and this Worker’s design, many small to mid-scale workloads can run at negligible cost while remaining very fast.
Prerequisites
- Node.js 18 or newer to run Wrangler
- Wrangler CLI
- A Cloudflare account with Workers enabled
Quick start
Install Wrangler
# Local (recommended)
npm install -D wrangler@latest
# Or global
npm install -g wrangler@latest
# Verify
npx wrangler --version
Clone and setup
git clone https://github.com/gonzague/wifi-geolocate-worker.git
cd wifi-geolocate-worker
npm install
# Authenticate once
npx wrangler login
npx wrangler whoami
Run locally
npx wrangler dev
# Default local endpoint: http://127.0.0.1:8787
Deploy to Cloudflare
npx wrangler deploy
# wrangler.toml already points to worker/index.js and enables Smart Placement
API reference
GET /
Lookup a single access point by query string:
GET https://<your-worker>.workers.dev/?bssid=34:DB:FD:43:E3:A1&all=true&reverseGeocode=true
bssid
required, 12 hex chars, separators optionalall
optional, return all APs Apple responds withreverseGeocode
optional, add human-readable address
POST /
Batch query BSSIDs with optional RSSI in dBm:
{
"accessPoints": [
{ "bssid": "34:DB:FD:43:E3:A1", "signal": -52 },
{ "bssid": "34:DB:FD:43:E3:B2", "signal": -60 },
{ "bssid": "34:DB:FD:40:01:10", "signal": -70 }
],
"all": false,
"reverseGeocode": true
}
Response shape
{
"query": { "accessPoints": [{ "bssid": "34:db:fd:43:e3:a1", "signal": -52 }], "all": false },
"found": true,
"results": [
{
"bssid": "34:db:fd:43:e3:a1",
"latitude": 48.856613,
"longitude": 2.352222,
"mapUrl": "https://www.google.com/maps/place/48.856613,2.352222",
"signal": -52,
"signalCount": 1,
"signalMin": -52,
"signalMax": -52,
"address": {
"displayName": "Champs-Élysées, Paris, Île-de-France, France",
"address": {
"road": "Champs-Élysées",
"city": "Paris",
"state": "Île-de-France",
"country": "France"
}
}
}
],
"triangulated": {
"latitude": 48.8571,
"longitude": 2.3519,
"pointsUsed": 3,
"weightSum": 6.84,
"method": "weighted-centroid",
"signalWeightModel": "10^(dBm/10)"
}
}
When Apple returns nothing, the Worker adds a Cloudflare IP geolocation fallback:
{
"found": false,
"fallback": {
"latitude": 40.7128,
"longitude": -74.0060,
"accuracyRadius": 1000,
"country": "US",
"region": "NY",
"city": "New York",
"postalCode": "10001",
"timezone": "America/New_York",
"isp": "Cloudflare",
"asOrganization": "Cloudflare, Inc."
}
}
Smart auto-upgrade
If you request all=false
and Apple has no exact match, the Worker retries with all=true
and sets autoUpgraded: true
in the response.
GET /?bssid=f8:ab:05:03:e9:40&all=false
{
"query": { "accessPoints": [{"bssid": "f8:ab:05:03:e9:40", "signal": null}], "all": true },
"found": true,
"autoUpgraded": true,
"results": [ ... ]
}
Reverse geocoding strategy
- With triangulation from multiple BSSIDs, only the centroid is geocoded
- With a single BSSID, exact matches get geocoded
- When auto-upgraded results appear, the first result is geocoded
This respects Nominatim’s 1 req per second guideline, reduces latency, and avoids redundant calls when nearby APs share the same address.
Data handling and math
- BSSIDs normalized to lowercase colon-separated hex
- Signals interpreted as dBm RSSI and clamped between −120 and −5 for weighting
- Weighted centroid uses a power model
w = 10^(dBm/10)
then normalizes weights
Caveats
- Apple’s WiFi positioning is undocumented and can change
- Always respect local law, privacy rules, and Apple terms
- Reverse geocoding uses public OpenStreetMap Nominatim, rate limits apply
Credits and prior work
- Academic research by François-Xavier Aguessy and Côme Demoustier on smartphone geolocation
- Open source Apple BSSID Locator by Darko Sancanin
- This Worker adapts those ideas for a serverless HTTP API, focusing on WiFi only
Useful links