A modern, type-safe Python client for the Apple Maps Server API.
I've always found Apple's documentation for the Maps Server API a bit opaque, especially when it comes to JWT management. I built this library to handle the heavy lifting: signing tokens, automatic refreshes, and providing a clean, type-safe interface for geocoding and search.
If you are building a server-side application that needs to interact with Apple's mapping services without the overhead of MapKit JS or a full mobile SDK, this is for you. It's built on top of Pydantic and httpx, so it's ready for modern Python workflows.
uv add apple-maps-apiFor optional Sentry integration:
uv add apple-maps-api[sentry]You'll need your Team ID, Key ID, and the private key from your Apple Developer account.
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-----"""
)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"Convert coordinates back to a structured address:
results = client.reverse_geocode((37.3346, -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", near="37.3346,-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"Provide search completions for a partial query:
results = client.autocomplete("1 Apple Park", country_code="US")
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..."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"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
cityfield onGeocodeResultmaps tostructuredAddress.locality, which may differ from the colloquial city name. For example, postal code90210returnscity="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.
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"# 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"Apple Park, Cupertino CA (37.3346, -122.0090):
place = client.reverse_geocode((37.3346, -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 => NoneTimes Square, NYC (40.7589, -73.9851):
place = client.reverse_geocode((40.7589, -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((34.1025226, -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"]geocode(query, country=None, lang=None)- Convert address to coordinatesreverse_geocode(lat, lon, lang=None)- Convert coordinates to addresssearch(query, at=None, bbox=None, country=None, lang=None)- Search for places and POIsautocomplete(query, at=None, bbox=None, country=None, lang=None)- Provide search completionscreate_token()- Generate a MapKit JS compatible access token
All responses are returned as Pydantic models for easy integration and validation:
PlaceResults- Response for geocoding and reverse geocodingSearchResponse- Response for search requestsSearchAutocompleteResponse- Response for autocomplete requestsGeocodeResult- A simplified, flattened representation of a geocoded locationPlace- Detailed information about a specific location or POIAddress- Structured address data (street, city, state, etc.)
- 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
httpxfor synchronous requests, with a structure that's ready for future async support.
This project was created from iloveitaly/python-package-template