A small Flask service that proxies Apple MapKit satellite tile requests.
It uses a headless Chrome/Chromium (via Selenium) to load Apple Maps and scrape a short‑lived MapKit accessKey from Chrome performance logs. That accessKey is then appended to upstream tile requests. This was developed and run using Ubuntu LTS 24, YMMV on windows or other operating systems.
- Starts a local HTTP server (Flask)
- Exposes a
/tileendpoint compatible with typical XYZ tile clients - For each incoming tile request:
- Adds
accessKey=<current access key> - Adds
v=<tile version> - Proxies the request to Apple’s tile CDN (
sat-cdn.apple-mapkit.com)
- Adds
proxy.py— The full implementationrequirements.txt— Pinned runtime dependencies
- Python 3.12 (recommended; should work on 3.10+)
- Google Chrome or Chromium installed
- A compatible
chromedriver
This project will try to auto-install a compatible chromedriver using chromedriver-autoinstaller. You can also provide a driver explicitly via the CHROMEDRIVER environment variable.
Create a virtual environment and install dependencies:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtStart the proxy:
python3 proxy.pyJan 08 15:25:43 systemd[1]: Started amp.service - APPLE MAPS PROXY.
Jan 08 15:25:43 python3[62498]: [2026-01-08 15:25:43,953] INFO in proxy: Fetching initial Apple Maps accessKey...
Jan 08 15:25:46 python3[62498]: * Serving Flask app 'proxy'
Jan 08 15:25:46 python3[62498]: * Debug mode: off
Jan 08 15:35:59 python3[62498]: [2026-01-08 15:35:59,291] INFO in proxy: Refreshing Apple Maps accessKey (force=False, reason=scheduled)...
Jan 08 15:36:01 python3[62498]: [2026-01-08 15:36:01,220] INFO in proxy: accessKey refreshed successfully in 1.9s
By default it listens on 127.0.0.1:8081.
QGIS: http://localhost:8081/tile?style=7&size=2&scale=1&z={z}&x={x}&y={y}
Returns basic health status and key refresh timing.
Response includes:
key_age_secondslast_refreshed_atnext_refresh_at
Proxies a tile request to Apple.
Example:
http://127.0.0.1:8081/tile?style=7&size=2&scale=1&z=12&x=656&y=1583
The server appends these query parameters when calling upstream:
accessKey=<scraped>v=<apple tile version>
The function _getAPIKey():
- Starts headless Chrome/Chromium with performance logging enabled.
- Loads
https://maps.apple.com/. - Polls Chrome’s
performancelog entries. - Extracts an
accessKey=...substring from logged request/response URLs.
Because Apple’s MapKit access keys expire, proxy.py will also refresh the key in the background periodically and retry once on 401/403 responses.
In the current proxy.py, the v query parameter is hardcoded via:
create_app(*, v: str = "10281", ...)serve(..., v: str = "10281", ...)/tilealways setsq["v"] = v
This v value represents an Apple Maps tile/version identifier and can change with Apple Maps/MapKit releases.
If Apple changes this version, requests may start failing until v is updated. A more robust approach is to parse v dynamically from the same Apple Maps network traffic where the accessKey is discovered.
CHROMEDRIVER— Absolute path to chromedriver (optional)LOG_LEVEL— Logging level (default:INFO)ACCESS_KEY_REFRESH_INTERVAL_SECONDS— Background refresh interval (default:600)ACCESS_KEY_REFRESH_COOLDOWN_SECONDS— Prevents redundant refreshes (default:90)
- Ensure Chrome/Chromium is installed.
- Ensure chromedriver matches your browser version.
- Optionally set
CHROMEDRIVER=/path/to/chromedriver.
This usually means the accessKey is expired or invalid. The proxy automatically refreshes and retries once.
--no-sandbox and --disable-dev-shm-usage are enabled for better compatibility in containerized environments.

