Skip to main content

Release Notes Downloads GitHub CI Status License: MIT

Python Client for Apple Maps Server API

A modern, type-safe Python client for the Apple Maps Server API.

There's no python library for the Apple Maps API. Maps is super cheap compared to all of the alternatives, so with AI I figured it wouldn't be too hard to build a client library.

The goal of this library is to provide similar functionality to the radar python client. Here's the main goals:

  • Automatic JWT Management: Handles signing and periodic refresh of Apple's ES256 tokens.
  • Type-Safe: Built with Pydantic models for all API responses, giving you excellent IDE support.
  • Resilient: Automatic retries with exponential backoff for transient network and server errors.
  • Modern: Uses httpx for synchronous requests, with a structure that's ready for future async support.

Full-stack Example

See examples/full-stack/ for a runnable FastAPI + React/MapKit JS app that demonstrates:

  • Minting MapKit JS tokens server-side (create_mapkit_token) and rendering an interactive map
  • Address autocomplete via this client, proxied through a small FastAPI API

Installation

uv add apple-maps-api

Usage

Basic Setup

You'll need your Team ID, Key ID, and the private key from your Apple Developer account. Step-by-step portal setup (Maps ID, MapKit JS key, domains/localhost): docs/mapkit-js-credential-setup.md.

from apple_maps_api import AppleMapsClient

client = AppleMapsClient(
    team_id="YOUR_TEAM_ID",
    key_id="YOUR_KEY_ID",
    private_key="""-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----""",
)

Or load credentials from environment variables:

export APPLE_MAPS_TEAM_ID=...
export APPLE_MAPS_KEY_ID=...
export APPLE_MAPS_P8_KEY=...   # PEM or raw base64 DER
# optional: export APPLE_MAPS_ORIGIN=https://example.com

And then you can source the vars from the env:

from apple_maps_api import AppleMapsClient

client = AppleMapsClient.from_env()

Generating a Long-lived Frontend Token

MapKit JS needs a signed JWT (not the Server API access token). Use create_mapkit_token():

from apple_maps_api import AppleMapsClient

client = AppleMapsClient.from_env()

# default TTL is 1 hour — fine for production token endpoints
token = client.create_mapkit_token()

# long-lived token for local FE work (e.g. 30 days)
token = client.create_mapkit_token(ttl_seconds=30 * 24 * 60 * 60)
print(token)

Paste the printed JWT into MapKit JS / mapkit-react during development so you do not need a backend token endpoint.

Optional domain lock: set origin on the client (or APPLE_MAPS_ORIGIN) so the token only works from that origin (e.g. http://localhost:5173). Omit origin for unrestricted local play.

Do not ship long-lived tokens in production frontend builds — mint short-lived tokens from your server instead (see examples/full-stack/).

Geocoding

Convert an address string to coordinates:

results = client.geocode("1 Apple Park Way, Cupertino, CA")

for place in results.results:
    print(f"{place.name}: {place.coordinate.latitude}, {place.coordinate.longitude}")

# place.name                              => "1 Apple Park Way"
# place.coordinate.latitude               => 37.334859
# place.coordinate.longitude              => -122.0090403
# place.formattedAddressLines             => ["1 Apple Park Way", "Cupertino, CA  95014", "United States"]
# place.structuredAddress.locality        => "Cupertino"

Reverse Geocoding

Convert coordinates back to a structured address:

results = client.reverse_geocode(lat=37.3346, lng=-122.0090)

if results.results:
    print(results.results[0].formattedAddressLines)

# place.formattedAddressLines      => ["Apple Park", "1 Apple Park Way", "Cupertino, CA  95014", "United States"]
# place.structuredAddress.locality => "Cupertino"
# place.structuredAddress.postCode => "95014"

Search for points of interest near a location:

results = client.search("pizza", lat=37.3346, lng=-122.0090)

for place in results.results:
    print(f"{place.name} - {place.formattedAddressLines}")

# results.results[0].name                  => "Pizza My Heart"
# results.results[0].formattedAddressLines => ["19409 Stevens Creek Blvd", "Cupertino, CA  95014", "United States"]
# results.results[1].name                  => "Mountain Mike's Pizza"
# results.results[2].name                  => "Pizz'a Chicago"

Address Autocomplete

Provide search completions for a partial query and filter by country:

results = client.autocomplete(
    "1 Apple Park",
    limit_to_countries=["US"],
    lat=37.3346,
    lng=-122.0090,
)

for completion in results.results:
    print(completion.displayLines)

# results.results[0].displayLines => ["1 Apple Park Way", "Cupertino, CA, United States"]
# results.results[1].displayLines => ["1 Apple Hill Dr", "Natick, MA, United States"]
# results.results[0].completionUrl => "/v1/search?q=1%20Apple%20Park%20Way..."

Multi-value filters (countries, categories, result types, …) accept a list; a single string still works.

Importance of Lat/Lng Hinting for Result Relevance

Always pass lat/lng. Autocomplete ranking is heavily biased by location — without it you get scattered results across the country for common street names. For server-side autocomplete, resolve the user's approximate location from their IP:

  1. Get the client public IP with fastapi-ipware
  2. Look up lat/lng via observabilitystack/geoip-api (ghcr.io/observabilitystack/geoip-api:latest)

Location Bias Options

Search, autocomplete, and geocode share the same location-hint shape:

  • lat / lng — app-defined search bias (Apple searchLocation). Prefer nearby results around this map point.
  • user_lat / user_lng — the user's current position (Apple userLocation). Used for ranking; if lat/lng are omitted, some endpoints may fall back to this.
  • search_region — bounding-box hint as a MapRegion (Apple searchRegion).
  • search_region_priority — SearchRegionPriority.default or .required (or the strings "default" / "required").
from apple_maps_api import MapRegion, SearchRegionPriority

region = MapRegion(
    northLatitude=37.5,
    eastLongitude=-121.7,
    southLatitude=37.1,
    westLongitude=-122.5,
)

results = client.search(
    "coffee",
    lat=37.3346,
    lng=-122.0090,
    user_lat=37.3317,
    user_lng=-122.0307,
    search_region=region,
    search_region_priority=SearchRegionPriority.required,
    categories=["Cafe"],
    limit_to_countries=["US"],
)

Helper Functions

The library includes high-level helpers for common geocoding operations:

from apple_maps_api import geocode_postal_code, geocode_coordinates

# Geocode a postal code
result = geocode_postal_code(client, postal_code="95014", country="US")
# result.lat              => 37.2895111
# result.lon              => -122.0811912
# result.city             => "Cupertino"
# result.state_code       => "CA"
# result.postal_code      => "95014"
# result.formatted_address => "Cupertino, CA  95014, United States"

# Reverse geocode coordinates
result = geocode_coordinates(client, lat=37.3346, lon=-122.0090)
# result.address1          => "1 Apple Park Way"
# result.postal_code       => "95014"
# result.city              => "Cupertino"
# result.state_code        => "CA"
# result.formatted_address => "Apple Park, 1 Apple Park Way, Cupertino, CA  95014, United States"

Example Payloads

These examples show the real data shapes returned by the Apple Maps API for common operations.

Note on city names: Apple Maps uses its own administrative boundaries. The city field on GeocodeResult maps to structuredAddress.locality, which may differ from the colloquial city name. For example, postal code 90210 returns city="Los Angeles" even though the formatted address reads "Beverly Hills" — Beverly Hills is an independent city but Apple Maps assigns it to the LA locality.

Geocoding a postal code

result = geocode_postal_code(client, postal_code="10001")
# result.lat             => 40.7504876
# result.lon             => -74.0025705
# result.city            => "New York"
# result.state_code      => "NY"
# result.postal_code     => "10001"
# result.formatted_address => "New York, NY  10001, United States"
result = geocode_postal_code(client, postal_code="90210")
# result.lat             => 34.1025226
# result.lon             => -118.4167959
# result.city            => "Los Angeles"   # NOT "Beverly Hills"
# result.state_code      => "CA"
# result.postal_code     => "90210"
# result.formatted_address => "Beverly Hills, CA  90210, United States"

Reverse geocoding coordinates

# Times Square, NYC
result = geocode_coordinates(client, lat=40.7589, lon=-73.9851)
# result.address1          => "1552–1568 Broadway"
# result.postal_code       => "10036"
# result.city              => "New York"
# result.state_code        => "NY"
# result.formatted_address => "Times Square, 1552–1568 Broadway, New York, NY  10036, United States"

Raw reverse_geocode response — full structuredAddress

Apple Park, Cupertino CA (37.3346, -122.0090):

place = client.reverse_geocode(lat=37.3346, lng=-122.0090).results[0]
# place.name                                      => "Apple Park"
# place.formattedAddressLines                     => ["Apple Park", "1 Apple Park Way", "Cupertino, CA  95014", "United States"]
# place.structuredAddress.administrativeArea      => "California"
# place.structuredAddress.administrativeAreaCode  => "CA"
# place.structuredAddress.subAdministrativeArea   => None
# place.structuredAddress.locality                => "Cupertino"
# place.structuredAddress.subLocality             => None
# place.structuredAddress.fullThoroughfare        => "1 Apple Park Way"
# place.structuredAddress.thoroughfare            => "Apple Park Way"
# place.structuredAddress.subThoroughfare         => "1"
# place.structuredAddress.postCode                => "95014"
# place.structuredAddress.areasOfInterest         => ["Apple Park"]
# place.structuredAddress.dependentLocalities     => None

Times Square, NYC (40.7589, -73.9851):

place = client.reverse_geocode(lat=40.7589, lng=-73.9851).results[0]
# place.name                                      => "Times Square"
# place.formattedAddressLines                     => ["Times Square", "1552–1568 Broadway", "New York, NY  10036", "United States"]
# place.structuredAddress.administrativeArea      => "New York"
# place.structuredAddress.administrativeAreaCode  => "NY"
# place.structuredAddress.subAdministrativeArea   => None
# place.structuredAddress.locality                => "New York"
# place.structuredAddress.subLocality             => "Manhattan"
# place.structuredAddress.fullThoroughfare        => "1552–1568 Broadway"
# place.structuredAddress.thoroughfare            => "Broadway"
# place.structuredAddress.subThoroughfare         => "1552–1568"
# place.structuredAddress.postCode                => "10036"
# place.structuredAddress.areasOfInterest         => ["Times Square", "Manhattan"]
# place.structuredAddress.dependentLocalities     => ["Broadway", "Times Square", "Theater District", "Midtown Manhattan", "Midtown", "North Hudson"]

Beverly Hills, CA (34.1025226, -118.4167959) — note locality vs formatted address mismatch:

place = client.reverse_geocode(lat=34.1025226, lng=-118.4167959).results[0]
# place.name                                      => "1731 N Franklin Canyon Dr"
# place.formattedAddressLines                     => ["1731 N Franklin Canyon Dr", "Beverly Hills, CA  90210", "United States"]
# place.structuredAddress.administrativeArea      => "California"
# place.structuredAddress.administrativeAreaCode  => "CA"
# place.structuredAddress.subAdministrativeArea   => None
# place.structuredAddress.locality                => "Los Angeles"   # NOT "Beverly Hills"
# place.structuredAddress.subLocality             => "Beverly Crest"
# place.structuredAddress.fullThoroughfare        => "1731 N Franklin Canyon Dr"
# place.structuredAddress.thoroughfare            => "N Franklin Canyon Dr"
# place.structuredAddress.subThoroughfare         => "1731"
# place.structuredAddress.postCode                => "90210"
# place.structuredAddress.areasOfInterest         => None
# place.structuredAddress.dependentLocalities     => ["Beverly Crest"]
  • fastapi-ipware — client IP extraction for FastAPI (useful for lat/lng hinting via GeoIP)
  • mapkit-react — React bindings for MapKit JS (used in the full-stack example)

MIT License


This project was created from iloveitaly/python-package-template

Metadata

Release files for apple-maps-api 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for apple-maps-api 0.2.1
File Size Uploaded
apple_maps_api-0.2.1.tar.gz 16.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for apple-maps-api 0.2.1
File Interpreter ABI Platform
apple_maps_api-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 34.7 kB

Release files / apple_maps_api-0.2.1.tar.gz

Download URL apple_maps_api-0.2.1.tar.gz
Size 16.4 kB
Tags Source
SHA-256 checksum
How to use checksums
adeeee97d9dfac5893e296e8538b30c67139030a856aa154c54be17db236f0ca
BLAKE2b-256 checksum
How to use checksums
743e3d9934d4c7e7684f61558a15e244b68c4520c7f52d70103f1124fca5bc34
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / apple_maps_api-0.2.1-py3-none-any.whl

Download URL apple_maps_api-0.2.1-py3-none-any.whl
Size 18.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b317f755f7f43ed81bdb3a3c2291cdff500c3c6b19c5b0ee8e5af3b988c00003
BLAKE2b-256 checksum
How to use checksums
68b68680107fa7900710430e027ecafd74e0d2d54699c7c40f9a359daf4d7839
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page